11 KiB
Compatibility policy
This document defines how lore evolves without breaking existing user projects. It is the contract between current users and future maintainers. Any change to .lore/ structure, file formats, Python scripts, mirror templates, or this skill's reference docs must conform to these rules.
Three principles
- Add, never subtract. New fields, scripts, sections, and reference docs always use new names. Removal happens only after the deprecation cycle (below).
- Readers are forward-compatible. An older skill reading a newer
.lore/ignores unknown fields, unknown files, and unknown tags. It never errors on unfamiliar content. - Writers are backward-compatible (during transition). A newer skill detecting an older
.lore/runs a migration step before writing. It never overwrites old data with new defaults.
Layer-specific rules
Layer 1: .lore/.config.json schema
schema_versionis required (integer). Seereferences/config.mdfor handling missing/newer/older values.- Adding a new field = bump
schema_versionto N+1; old fields stay; the field is added with its default value at first read. - Removing a field requires the deprecation cycle (one schema version's worth of warnings before hard removal).
- Renaming a field: write the new field, copy the value, mark the old one with
_deprecated: "reason". The migration tool handles this inmigrate.py.
Layer 2: Entry format
- [ARCH-2026-07-10-a3f2] Entry text; reason. #added:2026-07-10 #verified:2026-07-15
- IDs (
LAYER-DATE-HASH) are stable as long as the entry text is unchanged. Editing an entry produces a new ID; old ID stays in history (via git) forhistoryqueries. - Tag set is a closed set today:
#added,#verified,#stale,#archived. Adding a new tag is allowed; old skills' tag parsers (which match(added|verified|stale|archived)) silently ignore unknown tags. - Never make a tag required. Required tags break every old entry in every old
.lore/.
Layer 3: .lore/ directory structure
Current canonical layout:
.lore/
├── SUMMARY.md
├── _global/
├── scopes/
├── draft/ (init only — temporary)
├── audit/ (audit only)
└── archive/ (referenced in spec; reserved for future)
Rules:
- Adding a new top-level directory (e.g.,
rejected/for rejected entries) is non-breaking. - Renaming an existing directory is breaking — every reference in
references/*.md, every script, and every user's project breaks. - Removing a directory is breaking unless that directory was never actually written (e.g., removing
archive/today is non-breaking because nothing writes there yet).
Layer 4: Python scripts
Current scripts: id_hash.py, list_entries.py, find_stale.py, find_duplicates.py, history.py, plus planned migrate.py.
Rules:
- Renaming is breaking. All names are part of the public surface; they're referenced from
SKILL.md,references/*.md, and downstream tooling. Don't rename; deprecate and add a new one if needed. - Removing is breaking. A deprecated script stays on disk with a clear "Deprecated: use
migrate.pyinstead" header for one schema version. - Adding a new script is non-breaking. Reference it from
SKILL.mdreference index on introduction. - Changing output format is breaking for
--jsonconsumers. Add--v2-outputor a new flag; old flag keeps old behavior forever.
Layer 5: Platform mirror files
Mirror files (CLAUDE.md, .cursorrules, AGENTS.md, etc.) follow this contract:
## Lore (auto-managed)
... lore content ...
---
## My notes (free edit)
... user content (preserved verbatim) ...
Rules:
## Lore (auto-managed)is a contract string. Never rename; never remove. Mirror detection regexes depend on it.## My notes (free edit)is a contract string. Never rename; never remove. User-written content depends on it.- The content between
## Loreand---is lore's domain; content after---is the user's. Respect the boundary on every regeneration. - Adding a new auto-managed section (e.g.,
## Sync history (auto-managed)) is allowed; insert before---. Old skills ignore it. - Changing the index template body (e.g., adding a "Last mirror:" line) is non-breaking: content-based dedup means unchanged mirrors are not rewritten, so old mirrors stay valid.
- Backward-write safety: if the existing mirror has no
## My notes (free edit)section (e.g., a legacy single-section mirror from a pre-v1 project), the firstlore mirrorrun must append an empty My notes section rather than overwriting the file.
Layer 6: reference docs
Current docs: entry-format.md, summary-template.md, audit-template.md, monorepo-detection.md, stale-new-markers.md, platform-mirrors.md, config.md, history-command.md, compatibility.md (this file).
Rules:
- Renaming a reference doc is breaking. Every external link (issue trackers, blog posts, README badges) breaks. Add a redirect stub instead.
- Splitting a doc (e.g.,
platform-mirrors.md→mirror-index.md+mirror-takeover.md) requires a stub at the old path that points to the new location. UpdateSKILL.mdreference index on the same commit. - Removing a doc is breaking. Mark it
<!-- DEPRECATED: see new-location.md -->for one schema version, then move toarchive/(inreferences/, not in.lore/). - Adding a doc is non-breaking. Add to
SKILL.mdreference index on introduction.
Migration tool
scripts/migrate.py does not exist in v1. It will be added on the first schema_version bump.
Triggers that will ship the first migration:
- A new required field is added to
.lore/.config.json. - A field is renamed or its accepted values are tightened.
- The entry ID algorithm changes.
Until any of these happen, schema_version: 1 is the only version and no migration is possible or needed.
Until the script ships, list_entries.py emits a one-time warning to stderr if .lore/.config.json is missing the schema_version field. This nudges users to add the field manually before the first breaking change ships, so future upgrades can detect the version mismatch correctly.
Template — to be implemented when the first migration is needed:
#!/usr/bin/env python3
"""Migrate .lore/.config.json from version N to N+1.
Idempotent: running twice produces no further changes.
Refuses to run if schema_version > EXPECTED_FROM.
"""
import json, sys
from pathlib import Path
EXPECTED_FROM = 1 # versions this script can read
TARGET = 2 # current schema version after migration
def migrate(config: dict) -> tuple[dict, list[str]]:
msgs = []
# v1 → v2 transformations go here. Example:
# if config.get("mirror_mode") == "summary":
# config.pop("mirror_mode", None)
# msgs.append('removed deprecated "mirror_mode": "summary"')
config["schema_version"] = TARGET
return config, msgs
def main() -> int:
cfg_path = Path(".lore/.config.json")
if not cfg_path.exists():
print(f"error: {cfg_path} not found", file=sys.stderr)
return 1
cfg = json.loads(cfg_path.read_text(encoding="utf-8"))
current = cfg.get("schema_version", 1)
if current > EXPECTED_FROM:
print(
f"error: schema_version={current} is newer than this skill "
f"can read (max: {EXPECTED_FROM}). Pull latest lore.",
file=sys.stderr,
)
return 2
if current < TARGET:
print(f"migrating v{current} → v{TARGET}...")
cfg, msgs = migrate(cfg)
for m in msgs:
print(f" - {m}")
cfg_path.write_text(json.dumps(cfg, indent=2) + "\n", encoding="utf-8")
print("done.")
return 0
if __name__ == "__main__":
sys.exit(main())
Deprecation workflow
Any capability slated for removal follows a three-stage cycle. The minimum cycle is two schema versions (typically 6–12 months).
| Stage | Schema version | Behavior |
|---|---|---|
| Announce | N | Add an entry to references/deprecations.md with the feature name, replacement, and removal target version. The skill prints a one-line notice when the feature is used. |
| Warn | N+1 | The skill prints a louder warning (with remediation steps) and writes a _deprecation_warnings_shown array to .lore/.config.json so the warning doesn't repeat. |
| Remove | N+2 | Hard delete. Old .lore/ data is migrated by migrate.py to the new format. Users who skipped migrations will see explicit errors pointing at migrate.py. |
Skipping a stage is allowed only for security fixes or unreleased features that were never shipped.
CI enforcement
A compatibility CI job (planned for .github/workflows/compat.yml) verifies:
- New skill reads old
.lore/: checkout a fixture project fromfixtures/v0-project/, runlist_entries.py,history.py,find_stale.pyon it. Pass = no exceptions, correct counts. - Old skill reads new
.lore/: build a fixture with the latest schema, check out the previous release's scripts, run them. Pass = no exceptions on known fields; unknown fields silently ignored. - No rename or delete in
scripts/orreferences/: PR diff againstscripts/andreferences/filenames; any removed file = failure.
At least one fixture project must be checked in at fixtures/v0-project/ and re-pinned to a known old schema version after each major release.
Examples
Compatible change (additive)
Adding a new compress_thresholds.max_entries_per_scope field:
- Bump
schema_versionto 2. migrate.pyv1 → v2: add the new field with default100if absent.- Old skill reads v2 config: sees only the fields it knows; ignores
max_entries_per_scope. - New skill reads v1 config: detects
schema_versionmismatch, refuses to write until migration runs.
Incompatible change (avoid)
Renaming mirror_mode to render_mode:
- Every existing
.lore/.config.jsonwould silently lose itsmirror_mode: "index"setting on next migration (old field dropped, new field absent → defaults kick in). - Bad. Instead: add
render_modeas the new canonical field; markmirror_modeas_deprecated; keep both for one schema version; eventually removemirror_modevia the deprecation cycle.
Breaking change (deprecation cycle required)
Removing support for mirror_mode: "full":
- v1: ship
mirror_mode: "full"as deprecated; full mode still works. - v2: print warning when
"full"is set; suggest migrating to"index". - v3: hard reject
"full";migrate.pyauto-converts to"index". - Each version's release notes link to the deprecation entry in
references/deprecations.md.
Decision checklist
Before merging any change to lore, answer these questions:
- Does this change add, modify, or remove anything in
.lore/? - Does this change add, modify, or remove any script in
scripts/? - Does this change add, modify, or remove any contract string (
## Lore (auto-managed),## My notes (free edit), etc.)? - Does this change add, modify, or remove any reference doc filename?
- Does this change add, modify, or remove any entry tag?
If any answer is "modify" or "remove", the change requires either:
- A migration step in
migrate.py(for schema changes) - A deprecation cycle (for removals)
- An entry in
references/deprecations.md
If all answers are "add" or "no", the change is non-breaking and can ship as a minor or patch release.