From 2b84ebb298c137be96df488f108a611862b2287e Mon Sep 17 00:00:00 2001 From: os-zhuang Date: Fri, 3 Jul 2026 20:57:38 +0800 Subject: [PATCH] feat(spec): retired-key tombstones + ship CHANGELOG in the npm artifact MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit An agent upgrading a downstream app meets exactly one guaranteed channel: the create() rejection. Today it dead-ends ('not assignable to never' / bare unknown-key); the docs site is off-artifact and version-skewed, and the changesets' migration notes never left the monorepo (CHANGELOG.md was not in the files allowlist — verified absent from a real consumer's node_modules). - UNKNOWN_KEY_GUIDANCE tombstones for compactLayout / detail / views / defaultDetailForm: each rejection names the replacement + version + decision. Entries age out ~two majors after removal. - CHANGELOG.md added to the files allowlist (216KB, grep-target). - llms.txt: 'Upgrading Across Spec Versions' — tombstone first, then grep the shipped CHANGELOG; never re-add keys or downgrade to pass. - AGENTS.md: breaking changesets must carry FROM→TO migration notes, and removing an authorable key requires a tombstone. spec object tests 75/75 green (3 new tombstone assertions); npm pack --dry-run confirms CHANGELOG.md ships. Co-Authored-By: Claude Fable 5 --- .changeset/retired-key-tombstones.md | 9 +++++ AGENTS.md | 1 + packages/spec/llms.txt | 17 +++++++++ packages/spec/package.json | 3 +- packages/spec/src/data/object.test.ts | 54 +++++++++++++++++++++++++++ packages/spec/src/data/object.zod.ts | 28 ++++++++++++++ 6 files changed, 111 insertions(+), 1 deletion(-) create mode 100644 .changeset/retired-key-tombstones.md diff --git a/.changeset/retired-key-tombstones.md b/.changeset/retired-key-tombstones.md new file mode 100644 index 0000000000..e4c0c641fd --- /dev/null +++ b/.changeset/retired-key-tombstones.md @@ -0,0 +1,9 @@ +--- +'@objectstack/spec': minor +--- + +Upgrade path for retired spec keys — the error IS the guide: + +- **Tombstone entries** in `UNKNOWN_KEY_GUIDANCE`: `create()` rejecting a retired key (`compactLayout`, the `detail` block, object-level `views`, `defaultDetailForm`) now names the replacement, the version/decision that removed it, and the one-line fix — instead of a bare unknown-key error. Tombstones age out ~two majors after the removal. +- **`CHANGELOG.md` now ships inside the npm package** (`files` allowlist): every breaking entry's migration notes travel with the exact version installed, greppable offline from `node_modules/@objectstack/spec/CHANGELOG.md`. +- **`llms.txt` gains an "Upgrading Across Spec Versions" section** teaching agents the two-step protocol: read the tombstone, then grep the shipped CHANGELOG — and never to re-add rejected keys or downgrade to silence errors. diff --git a/AGENTS.md b/AGENTS.md index cdd2f514c9..8f16312753 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -261,6 +261,7 @@ export default { never `gh pr merge --auto`). A finished task = a merged PR, not a dirty working tree. 3. **Add a changeset for feature work.** When the change is a feature or functional improvement, run `pnpm changeset` (or add a `.changeset/*.md` entry) describing it before committing. Pure bug fixes do **not** require a changeset. + **Breaking changesets must carry their migration.** If the change removes or renames anything an author can write (a spec key, an export, a config field), the changeset body must state the FROM → TO mapping and the one-line fix — this text ships to consumers as `CHANGELOG.md` inside the npm package and is what an upgrading agent greps after the tombstone error. Removing an authorable spec key also requires a tombstone entry in the relevant `UNKNOWN_KEY_GUIDANCE` map (see `object.zod.ts`) so the rejection itself carries the prescription. 4. Update `CHANGELOG.md` / `ROADMAP.md` if user-facing or architectural. 5. **Delete temporary artifacts** — screenshots, traces, scratch logs, `.playwright-mcp/`, throwaway `tmp*.ts`, ad-hoc scripts. Repo must look identical to before, minus intended changes. diff --git a/packages/spec/llms.txt b/packages/spec/llms.txt index a5a703e7d5..711aa8b603 100644 --- a/packages/spec/llms.txt +++ b/packages/spec/llms.txt @@ -198,3 +198,20 @@ function registerObject(rawConfig: unknown) { - **Bundled schema**: `json-schema/objectstack.json` for IDE autocomplete - **OpenAPI 3.1**: Auto-generated from REST API protocol (`json-schema/openapi.json`) - **Schema versioning**: `x-spec-version` field in all generated schemas + +--- + +## 9. Upgrading Across Spec Versions + +When a dependency bump makes `ObjectSchema.create()` (or `objectstack validate`) +reject a key that used to work: + +1. **Read the error first** — retired keys carry a tombstone message naming the + replacement key and the version/decision that removed it. The fix is in the + error text; no external lookup needed. +2. **`CHANGELOG.md` ships inside this package** (`node_modules/@objectstack/spec/CHANGELOG.md`). + It is version-ordered and every breaking entry includes its migration notes. + Grep it for the rejected key to see the full context of the change. +3. Do NOT re-add rejected keys or downgrade to make errors disappear — the keys + were removed deliberately (enforce-or-remove, ADR-0049); renaming/migrating + per the tombstone is always a small, mechanical edit. diff --git a/packages/spec/package.json b/packages/spec/package.json index 91a7648e68..6a702313b7 100644 --- a/packages/spec/package.json +++ b/packages/spec/package.json @@ -176,7 +176,8 @@ "prompts", "llms.txt", "README.md", - "src/**/*.zod.ts" + "src/**/*.zod.ts", + "CHANGELOG.md" ], "scripts": { "build": "pnpm gen:schema && pnpm gen:openapi && tsup && if [ -z \"$OS_SKIP_DTS\" ]; then NODE_OPTIONS=\"--max-old-space-size=12288\" BUILD_DTS=true tsup; fi", diff --git a/packages/spec/src/data/object.test.ts b/packages/spec/src/data/object.test.ts index 1b8f17937d..19b7cf2ce3 100644 --- a/packages/spec/src/data/object.test.ts +++ b/packages/spec/src/data/object.test.ts @@ -779,6 +779,60 @@ describe('ObjectSchema.create()', () => { expect(message).toContain('#1535'); }); + // Tombstones: a RETIRED key's rejection must carry the upgrade + // prescription — the compile/validation error is the one channel every + // upgrading consumer (human or agent) is guaranteed to hit. + it('tombstone: retired compactLayout names its replacement and versions', () => { + let message = ''; + try { + ObjectSchema.create({ + name: 'demo', + fields: {}, + // @ts-expect-error — compactLayout was retired (#2536) + compactLayout: ['name'], + }); + } catch (e) { + message = (e as Error).message; + } + expect(message).toContain('highlightFields'); + expect(message).toContain('11.7.0'); + expect(message).toContain('#2536'); + }); + + it('tombstone: removed detail block routes each job to its semantic role', () => { + let message = ''; + try { + ObjectSchema.create({ + name: 'demo', + fields: {}, + // @ts-expect-error — the detail block was removed (ADR-0085) + detail: { stageField: 'status' }, + }); + } catch (e) { + message = (e as Error).message; + } + expect(message).toContain('stageField'); + expect(message).toContain('highlightFields'); + expect(message).toContain('fieldGroups'); + expect(message).toContain('ADR-0085'); + }); + + it('tombstone: object-level views dialect points at semantic roles + listViews', () => { + let message = ''; + try { + ObjectSchema.create({ + name: 'demo', + fields: {}, + // @ts-expect-error — object-level views.* was never a spec key + views: { form: { sections: [] } }, + }); + } catch (e) { + message = (e as Error).message; + } + expect(message).toContain('listViews'); + expect(message).toContain('ADR-0085'); + }); + it('suggests the intended key on a typo (`validation` → `validations`)', () => { expect(() => ObjectSchema.create({ name: 'demo', diff --git a/packages/spec/src/data/object.zod.ts b/packages/spec/src/data/object.zod.ts index 09abf0bc0c..0c12e9c7d2 100644 --- a/packages/spec/src/data/object.zod.ts +++ b/packages/spec/src/data/object.zod.ts @@ -800,6 +800,34 @@ const UNKNOWN_KEY_GUIDANCE: Record = { triggers: '`triggers` is not an ObjectSchema field. Use a lifecycle hook ' + '(`src/objects/.hook.ts`) or a top-level `record_change` flow.', + + // ── Tombstones for RETIRED keys (upgrade prescriptions) ──────────────── + // A retired key's error must carry the fix: the compile/validation error is + // the one upgrade channel every consumer is guaranteed to hit — an agent + // bumping @objectstack/spec sees THIS message, not our docs site. Each entry + // names what replaced the key and the version/decision that removed it. + // Tombstones age out too: drop an entry ~two majors after the removal + // (by then it's archaeology, not an upgrade; see CHANGELOG.md for history). + compactLayout: + '`compactLayout` was renamed to `highlightFields` in @objectstack/spec 11.7.0 ' + + '(ADR-0085 semantic roles) and the alias was retired in 11.9.1 (#2536). ' + + 'Rename the key — the value shape (ordered field-name list) is unchanged.', + detail: + 'The `detail` UI-hints block was removed by ADR-0085 (spec 11.7.0). Its ' + + 'jobs moved to top-level semantic roles: `detail.stageField` → `stageField` ' + + '(string | false), `detail.highlightFields` → `highlightFields`, section ' + + 'layout → `fieldGroups` + `Field.group`. Whole-page customization is done ' + + 'by assigning a custom Page schema instead of per-page hint keys.', + views: + '`views` is not an ObjectSchema field: the object-level `views.form/*` and ' + + '`views.detail/*` UI-hint dialect was never part of the spec and its ' + + 'renderer support was removed (ADR-0085). Use the semantic roles ' + + '(`highlightFields`, `stageField`, `fieldGroups`) for hints and `listViews` ' + + 'for named list views.', + defaultDetailForm: + '`defaultDetailForm` was never implemented and was removed from the spec ' + + '(#2402). Curate the record page by assigning a custom Page schema; form ' + + 'layout derives from `fieldGroups` + `Field.group`.', }; /** Levenshtein edit distance — backs the "did you mean" hint for typo'd keys. */