Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .changeset/retired-key-tombstones.md
Original file line number Diff line number Diff line change
@@ -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.
1 change: 1 addition & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
17 changes: 17 additions & 0 deletions packages/spec/llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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.
3 changes: 2 additions & 1 deletion packages/spec/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
54 changes: 54 additions & 0 deletions packages/spec/src/data/object.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
28 changes: 28 additions & 0 deletions packages/spec/src/data/object.zod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -800,6 +800,34 @@ const UNKNOWN_KEY_GUIDANCE: Record<string, string> = {
triggers:
'`triggers` is not an ObjectSchema field. Use a lifecycle hook ' +
'(`src/objects/<name>.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. */
Expand Down