Skip to content

Commit c0efe5d

Browse files
os-zhuangclaude
andauthored
feat(spec): retired-key tombstones + ship CHANGELOG in the npm artifact (#2543)
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 <noreply@anthropic.com>
1 parent 1e406a5 commit c0efe5d

6 files changed

Lines changed: 111 additions & 1 deletion

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
---
2+
'@objectstack/spec': minor
3+
---
4+
5+
Upgrade path for retired spec keys — the error IS the guide:
6+
7+
- **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.
8+
- **`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`.
9+
- **`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.

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,7 @@ export default {
261261
never `gh pr merge --auto`). A finished task = a merged PR, not a dirty
262262
working tree.
263263
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.
264+
**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.
264265
4. Update `CHANGELOG.md` / `ROADMAP.md` if user-facing or architectural.
265266
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.
266267

packages/spec/llms.txt

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,3 +198,20 @@ function registerObject(rawConfig: unknown) {
198198
- **Bundled schema**: `json-schema/objectstack.json` for IDE autocomplete
199199
- **OpenAPI 3.1**: Auto-generated from REST API protocol (`json-schema/openapi.json`)
200200
- **Schema versioning**: `x-spec-version` field in all generated schemas
201+
202+
---
203+
204+
## 9. Upgrading Across Spec Versions
205+
206+
When a dependency bump makes `ObjectSchema.create()` (or `objectstack validate`)
207+
reject a key that used to work:
208+
209+
1. **Read the error first** — retired keys carry a tombstone message naming the
210+
replacement key and the version/decision that removed it. The fix is in the
211+
error text; no external lookup needed.
212+
2. **`CHANGELOG.md` ships inside this package** (`node_modules/@objectstack/spec/CHANGELOG.md`).
213+
It is version-ordered and every breaking entry includes its migration notes.
214+
Grep it for the rejected key to see the full context of the change.
215+
3. Do NOT re-add rejected keys or downgrade to make errors disappear — the keys
216+
were removed deliberately (enforce-or-remove, ADR-0049); renaming/migrating
217+
per the tombstone is always a small, mechanical edit.

packages/spec/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -176,7 +176,8 @@
176176
"prompts",
177177
"llms.txt",
178178
"README.md",
179-
"src/**/*.zod.ts"
179+
"src/**/*.zod.ts",
180+
"CHANGELOG.md"
180181
],
181182
"scripts": {
182183
"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",

packages/spec/src/data/object.test.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -779,6 +779,60 @@ describe('ObjectSchema.create()', () => {
779779
expect(message).toContain('#1535');
780780
});
781781

782+
// Tombstones: a RETIRED key's rejection must carry the upgrade
783+
// prescription — the compile/validation error is the one channel every
784+
// upgrading consumer (human or agent) is guaranteed to hit.
785+
it('tombstone: retired compactLayout names its replacement and versions', () => {
786+
let message = '';
787+
try {
788+
ObjectSchema.create({
789+
name: 'demo',
790+
fields: {},
791+
// @ts-expect-error — compactLayout was retired (#2536)
792+
compactLayout: ['name'],
793+
});
794+
} catch (e) {
795+
message = (e as Error).message;
796+
}
797+
expect(message).toContain('highlightFields');
798+
expect(message).toContain('11.7.0');
799+
expect(message).toContain('#2536');
800+
});
801+
802+
it('tombstone: removed detail block routes each job to its semantic role', () => {
803+
let message = '';
804+
try {
805+
ObjectSchema.create({
806+
name: 'demo',
807+
fields: {},
808+
// @ts-expect-error — the detail block was removed (ADR-0085)
809+
detail: { stageField: 'status' },
810+
});
811+
} catch (e) {
812+
message = (e as Error).message;
813+
}
814+
expect(message).toContain('stageField');
815+
expect(message).toContain('highlightFields');
816+
expect(message).toContain('fieldGroups');
817+
expect(message).toContain('ADR-0085');
818+
});
819+
820+
it('tombstone: object-level views dialect points at semantic roles + listViews', () => {
821+
let message = '';
822+
try {
823+
ObjectSchema.create({
824+
name: 'demo',
825+
fields: {},
826+
// @ts-expect-error — object-level views.* was never a spec key
827+
views: { form: { sections: [] } },
828+
});
829+
} catch (e) {
830+
message = (e as Error).message;
831+
}
832+
expect(message).toContain('listViews');
833+
expect(message).toContain('ADR-0085');
834+
});
835+
782836
it('suggests the intended key on a typo (`validation` → `validations`)', () => {
783837
expect(() => ObjectSchema.create({
784838
name: 'demo',

packages/spec/src/data/object.zod.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -800,6 +800,34 @@ const UNKNOWN_KEY_GUIDANCE: Record<string, string> = {
800800
triggers:
801801
'`triggers` is not an ObjectSchema field. Use a lifecycle hook ' +
802802
'(`src/objects/<name>.hook.ts`) or a top-level `record_change` flow.',
803+
804+
// ── Tombstones for RETIRED keys (upgrade prescriptions) ────────────────
805+
// A retired key's error must carry the fix: the compile/validation error is
806+
// the one upgrade channel every consumer is guaranteed to hit — an agent
807+
// bumping @objectstack/spec sees THIS message, not our docs site. Each entry
808+
// names what replaced the key and the version/decision that removed it.
809+
// Tombstones age out too: drop an entry ~two majors after the removal
810+
// (by then it's archaeology, not an upgrade; see CHANGELOG.md for history).
811+
compactLayout:
812+
'`compactLayout` was renamed to `highlightFields` in @objectstack/spec 11.7.0 ' +
813+
'(ADR-0085 semantic roles) and the alias was retired in 11.9.1 (#2536). ' +
814+
'Rename the key — the value shape (ordered field-name list) is unchanged.',
815+
detail:
816+
'The `detail` UI-hints block was removed by ADR-0085 (spec 11.7.0). Its ' +
817+
'jobs moved to top-level semantic roles: `detail.stageField` → `stageField` ' +
818+
'(string | false), `detail.highlightFields` → `highlightFields`, section ' +
819+
'layout → `fieldGroups` + `Field.group`. Whole-page customization is done ' +
820+
'by assigning a custom Page schema instead of per-page hint keys.',
821+
views:
822+
'`views` is not an ObjectSchema field: the object-level `views.form/*` and ' +
823+
'`views.detail/*` UI-hint dialect was never part of the spec and its ' +
824+
'renderer support was removed (ADR-0085). Use the semantic roles ' +
825+
'(`highlightFields`, `stageField`, `fieldGroups`) for hints and `listViews` ' +
826+
'for named list views.',
827+
defaultDetailForm:
828+
'`defaultDetailForm` was never implemented and was removed from the spec ' +
829+
'(#2402). Curate the record page by assigning a custom Page schema; form ' +
830+
'layout derives from `fieldGroups` + `Field.group`.',
803831
};
804832

805833
/** Levenshtein edit distance — backs the "did you mean" hint for typo'd keys. */

0 commit comments

Comments
 (0)