Skip to content

Commit d0e68a9

Browse files
committed
fix(spec): os migrate meta guidance for the actor-, hot-, external-, query-, delete-, etl-, storage-, apimethod-, dashboard-, notification-, record-, runtime-, rls- and scim- migration entries states each lesson in words (stage 7)
Every tracker-id site in the reason / replacement / acceptanceCriteria text of the fourteen families is rewritten to say what the cited ruling, measurement or fix decided. ADR ids stay. registry.ts, spec-changes.json and the upgrade guide are regenerated by their generators. Also: rest-api-endpoint-handler-status-retired names the API skill, whose factual sweep corrected the handlerStatus sentence. Claude-Session: https://claude.ai/code/session_01Sfe5YjBLwB9J3y8fvm2xq1 Co-authored-by: Claude <noreply@anthropic.com>
1 parent 288611e commit d0e68a9

31 files changed

Lines changed: 455 additions & 317 deletions

‎docs/protocol-upgrade-guide.md‎

Lines changed: 25 additions & 25 deletions
Large diffs are not rendered by default.

‎packages/spec/spec-changes.json‎

Lines changed: 40 additions & 40 deletions
Large diffs are not rendered by default.

‎packages/spec/src/migrations/entries/semantic/17.actor-user-roles-to-positions.ts‎

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,33 +17,34 @@ export const entry: SemanticMigration = {
1717
+ 'SINGLE assignment (`roles: core.positions`), so the two keys were verbatim identical '
1818
+ 'on every dispatch — a second spelling of the vocabulary ADR-0090 D3 reserves and bans, '
1919
+ 'published straight into author-written code. The maintainer ruled it closed IMMEDIATELY '
20-
+ '(2026-08-06 14:49Z, #6011): no deprecation window, no dual-emit, the alias simply gone '
21-
+ 'in 17 (PR #6048). '
20+
+ '(2026-08-06 14:49Z, on the finding that this alias had no closing date): no deprecation '
21+
+ 'window, no dual-emit, the alias simply gone in 17. '
2222
+ '⚠️ Do not read this entry across to its sibling `action-session-roles-to-positions`: '
2323
+ '`action-session-roles-to-positions` governs `ctx.session`, a DIFFERENT object reached '
24-
+ 'through the same `ctx`, and that one KEEPS its one-window dual-emit (#5613). Same word, '
24+
+ 'through the same `ctx`, and that one KEEPS its one-window dual-emit. Same word, '
2525
+ 'same dispatch, two faces, two schedules — `ctx.user.roles` is absent in 17 while '
2626
+ '`ctx.session.roles` still answers for the length of its window. '
2727
+ 'What makes this entry different in KIND from both session-side siblings: `ctx.user` has '
2828
+ 'no spec schema and never had one. It is a runtime TS interface, so unlike '
29-
+ '`HookContext.session.roles` (tombstoned on a deliberately non-strict `HookContextSchema`, '
30-
+ '#5050) and unlike `ActionSessionSchema` (declared contract-first at #5697 precisely so '
31-
+ 'its key could be renamed), there is no schema key here to tombstone and no '
29+
+ '`HookContext.session.roles` (tombstoned on a deliberately non-strict `HookContextSchema` '
30+
+ 'once it had no producer and no consumer left) and unlike `ActionSessionSchema` (declared '
31+
+ 'contract-first, as it stood, as the first stage of the session rename, precisely so its '
32+
+ 'key could be renamed), there is no schema key here to tombstone and no '
3233
+ '`retiredKey()` prescription that could reach anybody — nothing ever ran an `ActorUser` '
3334
+ 'through a `.parse()`, so a prescription there would have no one to reach. The enforced '
3435
+ 'channel is tsc, and it reports at the READ site inside the author\'s own body; for an '
3536
+ 'untyped or sandboxed body there is no enforced channel at all, which is exactly why '
3637
+ 'this ledger entry has to exist — `spec-changes.json` and the generated upgrade guide '
37-
+ 'are the ONLY way such a reader learns of the rename. It is the `findStream` (#4484) / '
38-
+ '`IStorageService.list` (#5540) disposition — a TS/API contract, no stored source, no '
38+
+ 'are the ONLY way such a reader learns of the rename. It is the `findStream` (no caller) / '
39+
+ '`IStorageService.list` (no consumer) disposition — a TS/API contract, no stored source, no '
3940
+ 'tombstone, tsc at the call site — applied to a surface that lives one layer further '
4041
+ 'out than either: those two are at least DECLARED in `packages/spec/src/contracts`, '
4142
+ 'this one only in `packages/runtime`. '
4243
+ 'Why it is a D3 semantic TODO and not a D2 conversion, on the same two independent '
4344
+ 'grounds as its session sibling: FIRST, there is no source to convert — an `ActorUser` '
4445
+ 'is constructed per dispatch and never persisted, so no `sys_metadata` row, example or '
45-
+ 'template can carry the key (the `openApi31` (#4579) / `activationEvents` (#4657) / '
46-
+ '`hook-context-session-roles-retired` (#5050) shape). SECOND, the only place the key is '
46+
+ 'template can carry the key (the `openApi31` / `activationEvents` / '
47+
+ '`hook-context-session-roles-retired` shape). SECOND, the only place the key is '
4748
+ 'ever SPELLED is inside an action body or an AI route handler: author-written JS/TS, or '
4849
+ 'a sandboxed script. A declarative transform cannot safely rewrite an identifier inside '
4950
+ 'free-form code — the same reason the ADR-0090 wave delegated `current_user.roles` to '
@@ -53,12 +54,12 @@ export const entry: SemanticMigration = {
5354
+ 'here because the ledger is where an upgrading consumer meets it: the declaration\'s own '
5455
+ 'comment claimed the alias was "kept for the REST/AI shapes", and that claim was '
5556
+ 'DISPROVEN face by face against `origin/main` — repo-wide `user.roles` was 4 hits, all '
56-
+ 'of them in the pins PR #6048 flipped; the four `ActorUser` construction sites build '
57+
+ 'of them in the pins the removal flipped; the four `ActorUser` construction sites build '
5758
+ 'server-side envelopes that never enter a response body; objectui\'s `.roles` reads '
5859
+ 'belong to two unrelated producers (the better-auth session, and the '
5960
+ '`/auth/me/permissions` payload). The `cloud` repo was NOT reachable in that session and '
6061
+ 'is the one consumer face left unverified — this entry, and the changeset\'s FROM/TO '
61-
+ 'prescription, are its disposition. ADR-0090 D3 / ADR-0049 / ADR-0087, #6011 (PR #6048).',
62+
+ 'prescription, are its disposition. ADR-0090 D3 / ADR-0049 / ADR-0087.',
6263
acceptanceCriteria:
6364
'No action body reads `ctx.user.roles` and no AI route handler reads `req.user.roles`; '
6465
+ 'every such read is `.positions` and observes the SAME array — the value was '

‎packages/spec/src/migrations/entries/semantic/17.apimethod-enum-shrink.ts‎

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ export const entry: SemanticMigration = {
1818
+ 'fact. The FROM → TO is a table rather than a rename: `upsert` → `create` + `update`; '
1919
+ '`import` → `create` + `update`; `export`, `aggregate` and `search` → `list`; '
2020
+ '`history` → `get`; and `restore` / `purge` map to NOTHING — they never derived, '
21-
+ 'because `enable.trash` was retired in #2377, so the value is deleted outright. That '
21+
+ 'because `enable.trash` was retired with the other dead `enable.*` flags in the 11.0 '
22+
+ 'ADR-0049 removal of dead author-facing properties, so the value is deleted outright. That '
2223
+ 'last row is why this is a semantic entry and not a mechanical conversion, and the '
2324
+ 'reason is a security one: the mapping WIDENS. An allowlist naming `history` was '
2425
+ 'granting read of one record\'s audit trail; rewritten to `get` it grants ordinary '
@@ -30,8 +31,11 @@ export const entry: SemanticMigration = {
3031
+ 'replacement per site, and FLAGS the allowlists the mapping would widen so the edit '
3132
+ 'stays reviewable — it reports, it does not rewrite. Stored metadata keeps parsing '
3233
+ '(permanent tolerance, narrowing only), so nothing breaks at rest; what changes is what '
33-
+ 'an author may newly write. Registered by the #6350 stock reconciliation; #3543 (P2 of '
34-
+ '#3391) predates the #6148 completeness gate. ADR-0087, #3543 (backfilled #6350).',
34+
+ 'an author may newly write. Registered late, by the stock reconciliation that compared '
35+
+ 'the breaking changesets already on the v17 release train against this ledger: the enum '
36+
+ 'shrink (phase 2 of the programme that made UI action buttons agree with the '
37+
+ '`apiMethods` allowlist) predates the gate that makes a breaking changeset state its '
38+
+ 'ledger disposition. ADR-0087.',
3539
acceptanceCriteria:
3640
'No authored `enable.apiMethods` array names a legacy value; `objectstack validate` '
3741
+ 'passes. Run the reporter codemod first and read its widening flags before applying '
@@ -42,6 +46,6 @@ export const entry: SemanticMigration = {
4246
+ 'operation. Where the six primitives are all present, prefer deleting the key: that is '
4347
+ 'equivalent to default-open and it tracks future primitives, whereas a hand-listed six '
4448
+ 'silently stops granting anything added later. `restore` / `purge` are deleted with no '
45-
+ 'replacement — if trash-like behaviour was being relied on, that capability left in '
46-
+ '#2377 and this entry is not where it returns.',
49+
+ 'replacement — if trash-like behaviour was being relied on, that capability left in the '
50+
+ '11.0 dead-property removal and this entry is not where it returns.',
4751
};

‎packages/spec/src/migrations/entries/semantic/17.dashboard-widget-compareto-offset.ts‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ export const entry: SemanticMigration = {
1111
+ '`{ kind, dimension? }`, with no `offset` concept in it at all. On the ADR-0021 dataset '
1212
+ 'path — the spec\'s single author-facing analytics shape — `{ offset }` was forwarded '
1313
+ 'verbatim into that contract and threw `compareTo requires a timeDimension "undefined"`, '
14-
+ 'taking the widget down; the arm ever only ran on the legacy inline chart path (#5011). '
14+
+ 'taking the widget down; the arm ever only ran on the legacy inline chart path (measured '
15+
+ 'when all three declared arms were found dead on the dataset path: two silently dropped, '
16+
+ 'this one throwing). '
1517
+ "The conversion rewrites `{ offset: '1y' }`, which IS `previousYear` by definition. Every "
1618
+ 'other duration has NO faithful target: `previousPeriod` shifts by the length of whatever '
1719
+ "window the widget's filter resolves to, which equals `7d` only when that window happens "

‎packages/spec/src/migrations/entries/semantic/17.delete-by-id-before-hook-repoint-retired.ts‎

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,19 @@ export const entry: SemanticMigration = {
1616
'The by-id target of an `update()` or `delete()` is now IMMUTABLE inside a `before*` '
1717
+ 'handler, on both verbs, cleared or rebound. `delete()` was the last cell of that table '
1818
+ 'still answering differently: it HONOURED a repoint, re-resolving the new target by '
19-
+ "re-reading its pre-image and rebinding `previous` (#5272), so `afterDelete` and the "
20-
+ 'roll-up recompute saw the row actually deleted. It now refuses with '
19+
+ "re-reading its pre-image and rebinding `previous` (the fix that first made a single-row "
20+
+ "delete bind `previous` at all), so `afterDelete` and the roll-up recompute saw the row "
21+
+ 'actually deleted. It now refuses with '
2122
+ '`HookTargetRebindError` / `ERR_HOOK_TARGET_REBIND`, `path: \'by-id\'`, exactly as the '
2223
+ '`update()` twin and both per-row paths (ADR-0058 Amendment II.1 / D4) already did.\n\n'
2324
+ 'Read this as a RULING, not a defect report — that distinction is the reason the entry '
24-
+ 'is worth its length. #5272\'s re-resolution was internally CORRECT and nothing stale '
25+
+ 'is worth its length. That re-resolution was internally CORRECT and nothing stale '
2526
+ 'ever leaked from it; the case that retires a rebind on `update()` (the write landing on '
2627
+ 'a row whose pre-image, `readonlyWhen` locks and validation rules were never evaluated) '
27-
+ 'simply did not apply to it. #5574\'s engine half (PR #6697) therefore left the asymmetry '
28-
+ 'standing on purpose rather than folding a behaviour removal into an ordering change, and '
29-
+ 'filed it as #6752. The 2026-08-09 maintainer ruling on that card closed it on three '
28+
+ 'simply did not apply to it. The engine change that dispatches `before*` hooks per matched '
29+
+ 'row on a bulk write therefore left the asymmetry standing on purpose rather than folding '
30+
+ 'a behaviour removal into an ordering change, and filed it as a finding of its own. The '
31+
+ '2026-08-09 maintainer ruling on that finding closed it on three '
3032
+ 'measured axes instead: compatibility cost zero (a repository-wide grep for assignments '
3133
+ "into a hook's `input.id`, re-run on the implementing PR's base, found six sites and ALL "
3234
+ 'SIX are this family\'s own pins — no consumer anywhere repoints); one rule across both '
@@ -35,8 +37,9 @@ export const entry: SemanticMigration = {
3537
+ 'silently redirects which row gets deleted" is a top-grade footgun for authored — '
3638
+ 'especially AI-authored — handlers however correctly the redirect is implemented. '
3739
+ 'Correctness of a mechanism does not justify the surface it exposes. Aligning the other '
38-
+ 'way, by building `update()` the same re-resolution, stays excluded by #5574\'s own '
39-
+ 'recorded ruling ("do not silently pick re-resolution instead").\n\n'
40+
+ 'way, by building `update()` the same re-resolution, stays excluded by the recorded '
41+
+ 'ruling that extended per-row hook semantics to `before*` hooks on bulk writes ("do not '
42+
+ 'silently pick re-resolution instead").\n\n'
4043
+ 'Why this is a D3 semantic TODO and not a D2 conversion, on the same two grounds as '
4144
+ '`hook-register-empty-object-target-refused` and `hook-context-session-roles-retired` at '
4245
+ 'this step: FIRST, there is no source to convert — a `HookContext` is constructed per '
@@ -51,8 +54,7 @@ export const entry: SemanticMigration = {
5154
+ 'throws before anything is written and its message NAMES the retired capability and the '
5255
+ 'three replacement routes, so a handler that still repoints fails loudly and self-'
5356
+ 'describingly on its first execution rather than going quiet. This ledger entry is the '
54-
+ 'channel that reaches an upgrader BEFORE that first execution. #6752, #5272, #5574, '
55-
+ 'PR #6697, ADR-0058 Amendment II.2.',
57+
+ 'channel that reaches an upgrader BEFORE that first execution. ADR-0058 Amendment II.2.',
5658
acceptanceCriteria:
5759
'No `beforeDelete` handler assigns `ctx.input.id` anything but the id it arrived with — '
5860
+ 'grep handler bodies for assignments into `input.id` and rewrite each into an explicit '

‎packages/spec/src/migrations/entries/semantic/17.etl-pipeline-layer-retired.ts‎

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@ export const entry: SemanticMigration = {
1717
+ '`AutomationEngine.registerConnector` runs `ConnectorSchema.parse` and stores the '
1818
+ 'parsed definition; nothing reads `syncConfig` back off it, and the key has no '
1919
+ 'reader outside `packages/spec` at all — the same measurement that retired '
20-
+ '`syncConfig.schedule` in 18 under ADR-0049 (#16320). What the platform DOES '
20+
+ '`syncConfig.schedule` in 18 under ADR-0049, with the other cron-typed positions '
21+
+ 'nothing reads. What the platform DOES '
2122
+ 'execute on a connector is its `actions`: a flow\'s `connector_action` node '
2223
+ 'resolves the registered handler and awaits it, so an author who needs data '
2324
+ 'actually moved drives it from there. Per-field value '
@@ -28,7 +29,8 @@ export const entry: SemanticMigration = {
2829
+ 'because it never had an implementation either. It returns through the ENFORCE route: '
2930
+ 'the engine first, the vocabulary second)',
3031
reason:
31-
'The reading #4738 used to retire L1 `DataSyncConfig`, re-measured one layer up and '
32+
'The reading the spec dual-source cleanup used to retire L1 `DataSyncConfig` (its '
33+
+ 'automation copy deleted as dead), re-measured one layer up and '
3234
+ 'identical: narrative-only. No engine ever parsed, scheduled or executed an '
3335
+ '`ETLPipeline`. Measured on origin/main immediately before the removal: the only '
3436
+ 'non-spec references in this repo are two fumadocs-generated documentation sources '
@@ -37,26 +39,29 @@ export const entry: SemanticMigration = {
3739
+ 'on it — while the same file family\'s EXECUTED half does have one '
3840
+ '(`liveness/mapping.json`), which is the contrast that makes the absence meaningful '
3941
+ 'rather than an oversight. The `etl` string in this registry was the one untested '
40-
+ 'link the finding named, and it is not a loader path: it was the id of the #4962 '
41-
+ 'retry-vocabulary entry, absorbed here. '
42+
+ 'link the finding named, and it is not a loader path: it was the id of the '
43+
+ 'retry-vocabulary entry for `ETLPipeline.retry` (a third retry-policy vocabulary the '
44+
+ 'retry convergence had not covered), absorbed here. '
4245
+ 'The layer was ADR-0078\'s asymmetry in its purest form — an author could write a '
4346
+ 'complete ten-stage pipeline, get no error, and get no execution. It was also '
4447
+ 'advertised: `packages/spec/docs/SYNC_ARCHITECTURE.md` named `ETLPipeline` as the '
45-
+ 'recommended destination for authors displaced by the L1 retirement (#4738) and '
48+
+ 'recommended destination for authors displaced by the L1 retirement and '
4649
+ 'listed ten transformation types with copyable examples down to '
4750
+ '`script | Custom JavaScript/Python`. That document is rewritten in the same change; '
4851
+ 'a retirement whose own doc still recommends the retired layer is self-contradictory, '
4952
+ 'and forwarding L1\'s authors to a second layer with no executor was the defect '
5053
+ 'compounding rather than closing. '
51-
+ '⚠️ `etl-retry-converged-onto-retry-policy` (#4962) is SUBSUMED here, the '
52-
+ '#4657/#4834/#5055 way: both land in the unreleased protocol 17, so composed, a '
54+
+ '⚠️ `etl-retry-converged-onto-retry-policy` is SUBSUMED here, the way the '
55+
+ '`activationEvents`, dynamic plugin-loading and widget / i18n retirements each let an '
56+
+ 'earlier tombstone go with the shape that carried it: both land in the unreleased '
57+
+ 'protocol 17, so composed, a '
5358
+ 'rename of `retry.maxAttempts` on a shape that does not survive the major has no '
5459
+ 'observable effect — and keeping both would tell an upgrader to rewrite a key on a '
5560
+ 'schema the same upgrade deletes. The `maxAttempts` `retiredKey()` tombstone goes '
5661
+ 'with the shape that carried it, which is strictly stronger than the tombstone: there '
5762
+ 'is no longer a `retry` block to author the key into. Route 3 — no carrier key, no '
5863
+ 'parse site, so no D2 conversion and no tombstone; RETIRED_DEFS_BY_MAJOR plus this '
59-
+ 'entry are the declaration. ADR-0049, ADR-0078, #6414.',
64+
+ 'entry are the declaration. ADR-0049, ADR-0078.',
6065
acceptanceCriteria:
6166
'No source imports `ETLPipeline`, `ETLPipelineParsed`, `ETLPipelineSchema`, '
6267
+ '`ETLPipelineRun(Schema)`, `ETLSource(Schema)`, `ETLDestination(Schema)`, '

0 commit comments

Comments
 (0)