# 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 1. **Add, never subtract.** New fields, scripts, sections, and reference docs always use new names. Removal happens only after the deprecation cycle (below). 2. **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. 3. **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_version` is **required** (integer). See `references/config.md` for handling missing/newer/older values. - Adding a new field = bump `schema_version` to 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 in `migrate.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) for `history` queries. - 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.py` instead" header for one schema version. - **Adding a new script is non-breaking.** Reference it from `SKILL.md` reference index on introduction. - **Changing output format is breaking for `--json` consumers.** Add `--v2-output` or 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: ```markdown ## 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 `## Lore` and `---` 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 first `lore mirror` run 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. Update `SKILL.md` reference index on the same commit. - **Removing a doc** is breaking. Mark it `` for one schema version, then move to `archive/` (in `references/`, not in `.lore/`). - **Adding a doc** is non-breaking. Add to `SKILL.md` reference 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:** ```python #!/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: 1. **New skill reads old `.lore/`**: checkout a fixture project from `fixtures/v0-project/`, run `list_entries.py`, `history.py`, `find_stale.py` on it. Pass = no exceptions, correct counts. 2. **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. 3. **No rename or delete in `scripts/` or `references/`**: PR diff against `scripts/` and `references/` 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_version` to 2. - `migrate.py` v1 → v2: add the new field with default `100` if absent. - Old skill reads v2 config: sees only the fields it knows; ignores `max_entries_per_scope`. - New skill reads v1 config: detects `schema_version` mismatch, refuses to write until migration runs. ### Incompatible change (avoid) Renaming `mirror_mode` to `render_mode`: - Every existing `.lore/.config.json` would silently lose its `mirror_mode: "index"` setting on next migration (old field dropped, new field absent → defaults kick in). - Bad. Instead: add `render_mode` as the new canonical field; mark `mirror_mode` as `_deprecated`; keep both for one schema version; eventually remove `mirror_mode` via 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.py` auto-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: 1. Does this change add, modify, or remove anything in `.lore/`? 2. Does this change add, modify, or remove any script in `scripts/`? 3. Does this change add, modify, or remove any contract string (`## Lore (auto-managed)`, `## My notes (free edit)`, etc.)? 4. Does this change add, modify, or remove any reference doc filename? 5. 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.