Skip to content

Commit cf21c71

Browse files
committed
fix(spec): the field-, export-, api-, dataset-, hook- and metadata- migration entries state each lesson in words, not tracker numbers
Stage 5 of the staged sweep: the reason / replacement / acceptanceCriteria text (and one surface) of 25 ADR-0087 semantic entries in these six families no longer cites a tracker id. Each site now says what the cited ruling, measurement or fix decided; ADR ids stay. Text only. Claude-Session: https://claude.ai/code/session_01ARcDurZ5j34RdqsGgc4jgH Co-authored-by: Claude <noreply@anthropic.com>
1 parent 9e9bb46 commit cf21c71

25 files changed

Lines changed: 147 additions & 118 deletions

‎packages/spec/src/migrations/entries/semantic/17.api-runtime-create-withdrawn.ts‎

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ export const entry: SemanticMigration = {
1010
+ 'and ship it through `publishPackage`',
1111
reason:
1212
'The `api` registry entry declared `allowRuntimeCreate: true` and the runtime never '
13-
+ 'honoured it. Measured on a real showcase boot (#5488): `PUT /api/v1/meta/api/'
13+
+ 'honoured it. Measured on a real showcase boot: `PUT /api/v1/meta/api/'
1414
+ 'e8_backdoor` answered 200 with `{"success":true,…,"message":"Saved …"}`, and the '
1515
+ 'declared route then answered 404 forever — with NO `[EndpointMatcher] … EXCLUDED` '
1616
+ 'line, because the endpoint was never in the index to be excluded from. The serving '
@@ -24,19 +24,22 @@ export const entry: SemanticMigration = {
2424
+ 'making the matcher read `sys_metadata` re-opens cache, invalidation, tenancy and '
2525
+ "the ADR-0110 D3 miss-vs-outage distinction on a new read path, and there is no "
2626
+ 'business pull for Studio-authored endpoints today (zero `.api.*` artifacts author '
27-
+ 'them at runtime; showcase uses the artifact route, #5040 E8 LIVE). '
27+
+ 'them at runtime; showcase uses the artifact route, and its declared endpoints serve '
28+
+ 'live). '
2829
+ 'There is NO D2 conversion, for the reason this list exists: nothing in an authored '
2930
+ 'source spells this key. `allowRuntimeCreate` is a PLATFORM registry value, not an '
3031
+ 'authorable one, and the artifact route it points authors toward is untouched — a '
3132
+ '`**/*.api.ts` file valid before this change is valid after it, byte for byte. What '
3233
+ 'changed is a runtime HTTP verdict, so it is one semantic TODO for operators and '
3334
+ 'Studio callers rather than a stack conversion — the same disposition '
34-
+ '`BatchOptions.validateOnly` (#4052) takes. Consequently `gateApiDraftsForPublish` '
35-
+ '(PR #5279) is retired with it: it gated a promotion into a state the matcher can '
35+
+ '`BatchOptions.validateOnly` takes. Consequently `gateApiDraftsForPublish` '
36+
+ 'is retired with it: it gated a promotion into a state the matcher can '
3637
+ 'never read, and with the inlet closed no `api` draft can exist for it to judge. '
37-
+ 'Re-entry is recorded in the ruling: if #2657 Part B promotes `apis` to a registered '
38-
+ 'type WITH A REAL CONSUMPTION PATH, the flag flips back then — implementation first, '
39-
+ 'declaration second. ADR-0049 / ADR-0121, #5488 (subsumes #5311).',
38+
+ 'Re-entry is recorded in the ruling: if the Studio metadata-coverage work promotes `apis` '
39+
+ 'to a registered type WITH A REAL CONSUMPTION PATH, the flag flips back then — '
40+
+ 'implementation first, '
41+
+ 'declaration second. The same refusal closes the direct-active write too, which had been a '
42+
+ 'third path past the endpoint namespace and duplicate-path gates. ADR-0049 / ADR-0121.',
4043
acceptanceCriteria:
4144
'No caller creates or updates an `api` item through the runtime metadata API. '
4245
+ '`PUT /api/v1/meta/api/{name}` answers 403 with `code: "NOT_CREATABLE"` and a body '
@@ -45,8 +48,8 @@ export const entry: SemanticMigration = {
4548
+ 'as well as direct-active, because the gate runs before the draft/publish branch and '
4649
+ 'does not read `mode`. ⚠️ Verify the artifact route is UNAFFECTED, which is the whole '
4750
+ 'point of the change: a stack declaring `apis:` still compiles, still passes '
48-
+ '`validateApiEndpointDeclarations` at publish (`publishPackage`, #5189) and at load '
49-
+ '(`buildEndpointIndex`, PR #5203), and its endpoints still SERVE — that route was '
51+
+ '`validateApiEndpointDeclarations` at publish (`publishPackage`) and at load '
52+
+ '(`buildEndpointIndex`), and its endpoints still SERVE — that route was '
5053
+ 'always the only one that served. An operator who genuinely needs the runtime door '
5154
+ 'back on one deployment sets `OS_METADATA_WRITABLE=api`, the same single escape '
5255
+ 'hatch `job` / `agent` / `capability` use; note that this unlocks the WRITE only, and '

‎packages/spec/src/migrations/entries/semantic/17.export-axis-opt-in.ts‎

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,8 +39,10 @@ export const entry: SemanticMigration = {
3939
+ 'and `false` is authoring intent rather than a veto, because permission sets are '
4040
+ 'additive capability containers (ADR-0090). The super-user bits no longer confer it: '
4141
+ '`viewAllRecords` / `modifyAllRecords` are "may see all data", not "may take a bulk '
42-
+ 'copy". Registered by the #6350 stock reconciliation; #3544 / #3710 predate the #6148 '
43-
+ 'completeness gate. ADR-0087, #3544 / #3710 (backfilled #6350).',
42+
+ 'copy". Registered (backfilled) by the stock reconciliation that compared the breaking '
43+
+ 'changesets already on the v17 release train against this ledger: the export axis, and its '
44+
+ 'extension to the CSV attachments scheduled reports mail out, both predate the gate that makes '
45+
+ 'a breaking changeset state its ADR-0087 disposition. ADR-0087.',
4446
acceptanceCriteria:
4547
'Every environment-authored permission set has been READ and decided, not just parsed: '
4648
+ 'each object entry whose holders should keep exporting carries `allowExport: true`, and '

‎packages/spec/src/migrations/entries/semantic/17.export-field-meta-constraints-retired.ts‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,9 @@ export const entry: SemanticMigration = {
1919
+ 'passed in, so the map carried a second copy of facts the caller already held. '
2020
+ "They existed for exactly one consumer — the import dry run's hand-copied "
2121
+ 'pre-check mirror (`firstMissingRequiredField` / `firstConstraintViolation`, '
22-
+ 'framework#3956) — and #4633 ruling D retired that mirror (PR #6532): the dry run '
22+
+ 'added when the dry run was found skipping the field-level validation the real write ran) '
23+
+ '— and the maintainer\'s 2026-08-06 ruling D (a validate-only protocol operation, so the '
24+
+ 'dry run\'s prediction is the engine\'s verdict by construction) retired that mirror: the dry run '
2325
+ "now asks `DataProtocol.validateData` for the engine's verdict, which reads the "
2426
+ "object's own schema. That left all eight computed on every import and read by "
2527
+ 'NOTHING, which is the declared-and-unread shape ADR-0049 exists for; a constraint '
@@ -30,8 +32,8 @@ export const entry: SemanticMigration = {
3032
+ "verify, plugin-auth, plugin-dev) and the `objectui` sibling; plugin-auth's "
3133
+ 'identity import forwards `prepared.metaMap` into `runImport` but reads only the '
3234
+ 'presentation keys through `coerceRow`. '
33-
+ 'Why this needs a ledger entry despite that sweep: it is the `findStream` (#4484) / '
34-
+ '`IStorageService.list` (#5540) / `actor-user-roles-to-positions` (#6011) '
35+
+ 'Why this needs a ledger entry despite that sweep: it is the `findStream` / '
36+
+ '`IStorageService.list` / `actor-user-roles-to-positions` '
3537
+ 'disposition — a published TS surface with NO spec schema, so there is no '
3638
+ '`retiredKey()` tombstone and no parse rejection that could carry a prescription, '
3739
+ 'and the ledger is the only channel that reaches an upgrader. It is if anything '
@@ -45,7 +47,8 @@ export const entry: SemanticMigration = {
4547
+ 'remain fully authorable on a field definition and fully enforced by the engine, '
4648
+ 'which is where they always lived. The only place these eight are ever spelled is '
4749
+ "inside a consumer's own TypeScript, so no `objectstack migrate meta` transform can "
48-
+ 'reach them. ADR-0049 / ADR-0087, #6536 (the sweep PR #6532 deliberately deferred).',
50+
+ 'reach them. ADR-0049 / ADR-0087; this is the removal the dry-run change deliberately '
51+
+ 'deferred to a sweep of its own.',
4952
acceptanceCriteria:
5053
'No code of yours reads any of the eight off a `buildFieldMetaMap` / '
5154
+ '`prepareImportRequest` result. Grep your sources for `.required` / `.hasDefault` / '

‎packages/spec/src/migrations/entries/semantic/17.field-runtime-create-withdrawn.ts‎

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ export const entry: SemanticMigration = {
1212
reason:
1313
'The `field` registry entry declared `allowRuntimeCreate: true` and the platform never '
1414
+ 'built a read path for it. Measured end-to-end through the real HttpDispatcher -> '
15-
+ 'ObjectStackProtocolImplementation -> SysMetadataRepository (#7893): '
15+
+ 'ObjectStackProtocolImplementation -> SysMetadataRepository: '
1616
+ "`PUT /api/v1/meta/field/showcase_task.zz_probe` answered 200 with "
1717
+ '{"success":true,"state":"active","message":"Saved field …"}, the row persisted, and '
1818
+ '`GET /api/v1/meta/object/showcase_task` then listed fields = [title, status] with '
@@ -29,7 +29,8 @@ export const entry: SemanticMigration = {
2929
+ '(a composition step that does not exist, ~20 `gate.fields` call sites, physical '
3030
+ 'schema/migrations, and cold boot via `loadMetaFromDb`); if ever wanted it is a '
3131
+ 'separate card — implementation first, declaration second. '
32-
+ '⚠️ This is NOT the #5488 (`api`) rationale reused: that ruling rested on "zero '
32+
+ '⚠️ This is NOT the `api` withdrawal\'s rationale reused (`api-runtime-create-withdrawn`): '
33+
+ 'that ruling rested on "zero '
3334
+ 'business pull", and "add a field" is the opposite — a core Studio/CRM operation. The '
3435
+ 'justification here is that the operation REMAINS AVAILABLE on the route that actually '
3536
+ 'composes: `object` keeps `allowRuntimeCreate: true`, so what is withdrawn is a second, '
@@ -39,30 +40,30 @@ export const entry: SemanticMigration = {
3940
+ 'authorable one, and no authored source changes — an `**/*.object.ts` file valid before '
4041
+ 'this change is valid after it, byte for byte. What changed is a runtime HTTP verdict, '
4142
+ 'so it is one semantic TODO for operators and Studio callers rather than a stack '
42-
+ 'conversion — the same disposition `api` (#5488) and `BatchOptions.validateOnly` '
43-
+ '(#4052) take. ADR-0049 / ADR-0087, #7893 (split from #7743).',
43+
+ 'conversion — the same disposition `api` and `BatchOptions.validateOnly` take. '
44+
+ 'ADR-0049 / ADR-0087.',
4445
acceptanceCriteria:
4546
'No caller creates a standalone `field` item through the runtime metadata API. '
4647
+ '`PUT /api/v1/meta/field/{object}.{name}` answers 403 with `code: "NOT_CREATABLE"` and '
4748
+ 'a body naming both flags (`allowRuntimeCreate=false, allowOrgOverride=false`) and the '
4849
+ 'prescription `PUT /api/v1/meta/object/:object with the new field in `fields``. The '
4950
+ 'plural spelling `PUT /api/v1/meta/fields/{object}.{name}` folds onto the singular '
50-
+ '(#7894) and earns the same refusal — verify it, because it was a separate door until '
51+
+ 'and earns the same refusal — verify it, because it was a separate door until '
5152
+ '2026-08-12. ⚠️ Verify the OBJECT route is UNAFFECTED, which is the whole point of the '
5253
+ 'change: `PUT /api/v1/meta/object/{name}` with a new entry in `fields` still answers '
5354
+ '200, and `GET /api/v1/meta/object/{name}` READS THE NEW FIELD BACK (assert on '
5455
+ '`body.data.item.fields`, not `body.item`, which is undefined and makes an empty read '
5556
+ 'look like a pass). Assert a DECLARED field is present in the same response, so a dead '
5657
+ 'read cannot be what makes the check pass. '
57-
+ '⚠️ #7743\'s overlay refusal is untouched and must stay: overwriting a field a code '
58+
+ '⚠️ The field overlay refusal is untouched and must stay: overwriting a field a code '
5859
+ 'package ships is still 403 `NOT_OVERRIDABLE`, a different gate for a different '
5960
+ 'question — making field OVERRIDES legal was never part of this decision. '
6061
+ 'DISPOSITION OF EXISTING ROWS: `field` rows already written through the retired channel '
6162
+ 'stay in `sys_metadata` and are INERT — they were inert before this change too, since '
6263
+ 'no read path ever composed them into an object, so nothing that used to work stops '
6364
+ 'working and no data is silently reinterpreted. They remain self-readable by name and '
6465
+ 'still report `_diagnostics.valid: true`, which asserts only that the isolated document '
65-
+ 'is well-formed (see #8169 — the envelope has no "in effect" axis). They may be deleted '
66+
+ 'is well-formed (the envelope has no "in effect" axis). They may be deleted '
6667
+ 'at leisure: `deleteMetaItem` is deliberately NOT gated by this refusal, so repair stays '
6768
+ 'possible. An operator who needs the write door back on one deployment sets '
6869
+ '`OS_METADATA_WRITABLE=field`; note this unlocks the WRITE only — the field still will '

‎packages/spec/src/migrations/entries/semantic/17.hook-context-session-roles-retired.ts‎

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,18 @@ export const entry: SemanticMigration = {
2020
+ 'different untyped object that does carry `roles`, tracked apart and unaffected). '
2121
+ 'Both branches were therefore dead on '
2222
+ 'every real engine path — an authorization decision in shape only, and a second admin '
23-
+ 'dialect competing with the one ADR-0090 D3 / ADR-0095 D3 sanction. #4839 (PR #5049) '
24-
+ 'removed the readers; this removes the declaration, per ADR-0049 enforce-or-remove. '
23+
+ 'dialect competing with the one ADR-0090 D3 / ADR-0095 D3 sanction. An earlier fix removed '
24+
+ 'both readers, returning the record lock and the delegation guard to the one permission '
25+
+ 'vocabulary; this removes the declaration, per ADR-0049 enforce-or-remove. '
2526
+ 'This is a RUNTIME context, not stored metadata: the engine builds a HookContext per '
2627
+ 'operation and nothing persists one, so no `sys_metadata` row, example or template '
2728
+ 'can carry the key and there is no source for the D2 chain to rewrite — the '
28-
+ '`openApi31` (#4579) / `activationEvents` (#4657) shape, one semantic TODO rather '
29+
+ '`openApi31` / `activationEvents` shape, one semantic TODO rather '
2930
+ 'than a stack conversion. The key IS tombstoned (`HookContextSchema` is deliberately '
30-
+ 'not `.strict()` — a plain delete would strip it silently, #3733 / ADR-0104), so a '
31+
+ 'not `.strict()` — a plain delete would strip it silently, as a removed field key was '
32+
+ 'measured to be, ADR-0104), so a '
3133
+ 'consumer that parses a context it was handed still meets the prescription. '
32-
+ 'ADR-0049, #5050.',
34+
+ 'ADR-0049.',
3335
acceptanceCriteria:
3436
'No hook reads `ctx.session.roles`; caller gating uses `ctx.session.userId` / '
3537
+ '`ctx.session.isSystem`, and privilege comes from the security service '

‎packages/spec/src/migrations/entries/semantic/17.hook-register-empty-object-target-refused.ts‎

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,16 @@ export const entry: SemanticMigration = {
1212
+ "a global hook, `object: '*'` or no `object` key at all; for a cancelled scope, widen "
1313
+ '`object` or drop the overlapping names from `excludeObjects`',
1414
reason:
15-
'#4281 ruled that an empty hook target is not "no target" and closed the shape at the '
15+
'An earlier breaking fix established that an empty hook target is not "no target" and '
16+
+ 'closed the shape at the '
1617
+ "two METADATA doors — `HookSchema.object`'s refine and `hook-binder.ts`'s "
1718
+ '`normalizeObjects`. `engine.registerHook`, the CODE door, goes through neither, so '
1819
+ 'all three spellings still registered, each producing a defect the author did not '
1920
+ "write: `''` is FALSY, so the allow face was skipped entirely and the entry became a "
20-
+ "GLOBAL hook (#4281's headline failure mode — blank intent taking the broadest "
21+
+ "GLOBAL hook (that fix's headline failure mode — blank intent taking the broadest "
2122
+ "possible blast radius); `[]` and `['']` are truthy but admit no object name, so the "
22-
+ 'entry could never fire. #5928 then added the `excludeObjects` face, which brought a '
23+
+ 'entry could never fire. The later `excludeObjects` face (a hook global except for the '
24+
+ 'objects it names) then brought a '
2325
+ 'fourth shape reached by arithmetic rather than by one bad name: an `object` list '
2426
+ 'every member of which is also excluded admits nothing, so that entry can never fire '
2527
+ 'either. All four are ADR-0078 silently-inert declarations, and all four are now '
@@ -31,7 +33,7 @@ export const entry: SemanticMigration = {
3133
+ "the MATCHING read be changed instead: teaching the matcher that `''` is an "
3234
+ 'unmatchable name would silently convert a hook firing on every object into one '
3335
+ 'firing on none — the same class of defect pointing the other way, which is why '
34-
+ '#5928 declined to do it in passing.\n\n'
36+
+ 'the `excludeObjects` change declined to do it in passing.\n\n'
3537
+ 'This is a RUNTIME registration API, not stored metadata, so — like '
3638
+ '`hook-context-session-roles-retired` at this step — there is no `sys_metadata` row '
3739
+ 'for the D2 chain to rewrite and the ledger entry is the notification channel. One '
@@ -41,8 +43,7 @@ export const entry: SemanticMigration = {
4143
+ 'a blank `objectName` used to bind a trigger to EVERY object in the tenant. It now '
4244
+ "fails to bind instead, loudly — the automation engine's per-flow bind guard warns "
4345
+ 'and the `kernel:bootstrapped` binding audit re-reports it — which is the correct '
44-
+ 'end state, but it is an observable change for that flow. #6573, #4281, #4001, '
45-
+ '#5928, ADR-0078.',
46+
+ 'end state, but it is an observable change for that flow. ADR-0078.',
4647
acceptanceCriteria:
4748
'No `registerHook` call site passes an empty `object` target, and none passes an '
4849
+ '`excludeObjects` list covering every name in its `object` list. Every `record-change` '

‎packages/spec/src/migrations/entries/semantic/18.api-assembled-entry-split.ts‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@ export const entry: SemanticMigration = {
2020
+ 'the request schemas of both read doors, the install / uninstall / upgrade / rollback shapes, '
2121
+ '`PackageApiErrorCode` — stays on `@objectstack/spec/api`.',
2222
reason:
23-
'Maintainer ruling on #18576 (batch #145 item 1, letter B, 「同意,其他也同意」): split the API entry '
23+
'Maintainer ruling of 2026-09-17, option B (narrow the entry, rather than add a bundle-weight '
24+
+ 'rule to the browser-reachability ledger or accept the weight as it stood): split the API entry '
2425
+ 'so its browser-facing half no longer carries the assembled-package declarations. Those five embed '
2526
+ 'the ASSEMBLED package body, which reaches the whole metadata vocabulary and, behind it, the '
2627
+ 'datasource declaration and the driver-config validators; declared inside `@objectstack/spec/api`, '

‎packages/spec/src/migrations/entries/semantic/18.api-error-retry-after-unit-in-key.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export const entry: SemanticMigration = {
77
surface: 'EnhancedApiError.retryAfter (api/errors.zod.ts) — the ADR-0112 error envelope on the wire',
88
replacement: 'retryAfterSeconds — rename the key; the value (seconds) is unchanged',
99
reason:
10-
'Maintainer ruling B on #14478 (2026-09-02, decision batch #43): the unit of a duration-shaped z.number() lives in the key NAME or in a unit-carrying value, never only in the describe prose, and no existing offender is grandfathered. '
10+
'Maintainer ruling B of 2026-09-02 on duration-shaped keys: the unit of a duration-shaped z.number() lives in the key NAME or in a unit-carrying value, never only in the describe prose, and no existing offender is grandfathered. '
1111
+ 'BREAKING ON THE WIRE, and ruled in deliberately: the ruling puts the ~16 runtime-emitted '
1212
+ 'measurements in scope because they are read by humans and agents even if nobody authors '
1313
+ 'them, and names ApiError.retryAfter explicitly, with its own BREAKING note. The ambiguity '
@@ -21,7 +21,7 @@ export const entry: SemanticMigration = {
2121
+ 'green grep for `retry-after` in transport code as leftover work. A SEMANTIC entry rather '
2222
+ 'than a D2 conversion because an error envelope is emitted, never stored: it is not a stack '
2323
+ 'collection member and never a sys_metadata row, so the conversion chain has no seam that '
24-
+ 'would see one. #15677, #14478, ADR-0087, ADR-0112.',
24+
+ 'would see one. ADR-0087, ADR-0112.',
2525
acceptanceCriteria:
2626
'No producer emits `retryAfter` on an ApiError envelope and no consumer reads it; the old '
2727
+ 'spelling is a retiredKey() tombstone that fails tsc at the construction site and fails the '

0 commit comments

Comments
 (0)