Skip to content

Commit 0e1afe8

Browse files
fix(spec): os migrate meta guidance for the field-*, export-*, api-*, dataset-*, hook-* and metadata-* migration entries states each lesson in words, not tracker numbers (stage 5) (#20522)
Part of #20233 Stage 5: the field-, export-, api-, dataset-, hook- and metadata- families. Clause-②: no **Stage 5 of a staged card.** The card stays open for later stages; this PR carries no closing keyword. Text only: no entry id, `from` / `to`, conversion or matching logic moves, and the chain rewrites exactly what it rewrote before. One `surface` moves, under ruling A of the stage-1 ACCEPT (`5858839916`): it carried two tracker numbers. ## What this does `os migrate meta` prints every ADR-0087 semantic entry it crosses as one block: `⚠ [protocol N] SURFACE → REPLACEMENT`, then `why:` (the entry's `reason`) and `verify:` (its `acceptanceCriteria`). AGENTS.md's runtime-string rule applies to all of it: 「Runtime strings — refusal prose, prescriptions, anything an author is shown — carry no tracker number (`pnpm check:doc-authoring`): the lesson goes into the text.」 Form **D** of ruling C+D on #19123 (`5749154545`) sets the shape: the lesson in words, and no number, dead or alive; a cross-repo number is still a tracker number. This stage covers the next six families, `field-`, `export-`, `api-`, `dataset-`, `hook-` and `metadata-`: **110 sites → 0** in the three prose fields and **2 → 0** in `surface`, across 25 entry files. Each site now says what the cited ruling, measurement or fix decided. ADR ids stay. `registry.ts`, `spec-changes.json` and `docs/protocol-upgrade-guide.md` are regenerated from the entries (`gen:migration-registry`, `gen:spec-changes`, `gen:upgrade-guide`), never hand-edited. The pin now holds seventeen families. ## Census — tracker ids in the author-shown fields **Instrument.** Stage 4's TypeScript-AST census, the same script: for each entry object literal under `packages/spec/src/migrations/entries/**` it evaluates `replacement`, `reason`, `acceptanceCriteria` and (separately) `surface`, joining string literals with `+`, and counts `#` followed by 4 or 5 digits at a word boundary. On base `9e9bb464` it reads the whole tree at **571** sites / 7 `surface` / 61 short, which is stage 4's recorded after-count. Unevaluable fields: 0. **Controls, same run.** - **Lit:** `17.aggregation-node-distinct-retired.ts` reads 7 sites (replacement 1, reason 6), before and after. - **Dark (comment lines):** 823 `//` / docblock lines in entry files carry a tracker id, and none is counted; 823 before and after. Comment lines are #20234's surface, and this PR touches none (proved below). **Base `9e9bb464`:** `field-` 10 entries, **28** sites (5 / 20 / 3); `export-` 3, **21** (0 / 21 / 0); `hook-` 4, **17** (0 / 17 / 0); `api-` 5, **16** (0 / 14 / 2); `metadata-` 7, **16** (1 / 15 / 0); `dataset-` 3, **12** (2 / 8 / 2), plus **2** in `surface`. **110** sites (8 / 95 / 7) in 24 of the 32 entries; 75 distinct ids (71 bare, 1 spelled `framework#`, 3 `objectui#`). Short numbers: 14. **After this PR:** all six families **0**, `surface` 0; the eleven earlier families still 0; whole tree **571 → 461**, `surface` **7 → 5**, short **61 → 50**. The PM's rough line count (126 sites, 25 files) is a wider instrument; the AST reading is 110 in 24 files, and the 25th file carries only short decision-batch numbers and ruling-record ids. | entry | sites (replacement / reason / acceptanceCriteria) | surface | short numbers | |---|---|---|---| | `17.api-runtime-create-withdrawn` | 9 (0 / 7 / 2) | | | | `17.export-axis-opt-in` | 7 (0 / 7 / 0) | | | | `17.export-field-meta-constraints-retired` | 8 (0 / 8 / 0) | | | | `17.field-runtime-create-withdrawn` | 9 (0 / 6 / 3) | | | | `17.hook-context-session-roles-retired` | 6 (0 / 6 / 0) | | | | `17.hook-register-empty-object-target-refused` | 8 (0 / 8 / 0) | | | | `18.api-assembled-entry-split` | 1 (0 / 1 / 0) | | 1 | | `18.api-error-retry-after-unit-in-key` | 3 (0 / 3 / 0) | | 1 | | `18.api-runtime-config-durations-unit-in-key` | 3 (0 / 3 / 0) | | 1 | | `18.dataset-filter-nested-relation-equality-array-refused-at-save` | 2 (0 / 2 / 0) | | | | `18.dataset-measure-aggregate-field-type-refused` | 5 (1 / 2 / 2) | 2 | 2 (1 kept) | | `18.dataset-measure-selecting-aggregate-field-type-refused` | 5 (1 / 4 / 0) | | 3 (1 kept) | | `18.export-job-family-retired` | 6 (0 / 6 / 0) | | 2 | | `18.field-currency-scale-refused` | 0 | | 2 | | `18.field-max-length-malformed-or-misplaced-refused` | 8 (3 / 5 / 0) | | | | `18.field-min-length-malformed-or-misplaced-refused` | 3 (1 / 2 / 0) | | | | `18.field-multiple-non-capable-type-refused` | 4 (0 / 4 / 0) | | 1 | | `18.field-predicate-reference-traversal-refused` | 2 (0 / 2 / 0) | | | | `18.field-scale-precision-integer-refused` | 2 (1 / 1 / 0) | | 1 (kept) | | `18.hook-register-undispatched-lifecycle-event-refused` | 3 (0 / 3 / 0) | | | | `18.metadata-customization-protocol-retired` | 4 (0 / 4 / 0) | | | | `18.metadata-endpoints-switch-radius-repartitioned` | 3 (0 / 3 / 0) | | | | `18.metadata-manager-config-cache-ttl-unit-in-key` | 3 (1 / 2 / 0) | | | | `18.metadata-manager-config-inert-cache-keys-retired` | 3 (0 / 3 / 0) | | | | `18.metadata-plugin-additional-types-retired` | 3 (0 / 3 / 0) | | | | **total, 25 entries** | **110 (8 / 95 / 7)** | **2** | **14 (3 kept)** | The seven entries of these families that carried no number are untouched: `api-endpoint-cache-ttl-unit-in-key`, `field-inline-and-related-list-columns-closed`, `field-master-detail-set-null-refused`, `field-reference-to-spelling-retired`, `hook-timeout-unit-in-key`, `metadata-changed-event-payload-retired`, `metadata-item-name-grammar-enforced`. ## Text only — proved by a base-vs-head AST comparison For every entry file this PR changes, both versions (`9e9bb464` and the head) are parsed and compared: every import declaration; every property other than the three prose fields, by evaluated value (so `id`, `from` / `to` and any matcher); every comment token in the file; and the code skeleton, token by token with each run of joined string literals collapsed to one. `surface` is allowed to differ only where the base value carried a tracker id and the head value carries none. **25 files compared, 0 with a non-prose change**; one note, the ruling-A `surface` of `18.dataset-measure-aggregate-field-type-refused`. The instrument is shown able to fail first: on an in-memory copy it reports DETECTED for a mutated `id`, a mutated comment, a mutated `surface` whose base carried no tracker id, a mutated code token and a mutated import, and stays dark on a prose-only mutation. So none of #20234's comment lines moved, and no entry's identity or matching moved. ## Every citation read, and what the text now says Each cited id was read with a single-card REST read (body plus the ruling, measurement or landing comments), resolved against the repository its sentence names: 72 in this repository (one spelled `framework#`, the repository's old directory name) and 3 in `objectstack-ai/objectui`. Ids are in code spans so this body posts no cross-references. **4 ids answer 404** on both the issues and the pulls endpoint (re-probed with a 200 control, `14478`); those sentences are rewritten from what `main` records, listed in Acceptance notes. **`api-` (11 ids)** | cited | what it decided (read) | how the text now carries it | |---|---|---| | `5488` | Maintainer, 2026-08-07: flip `api` to `allowRuntimeCreate: false` and refuse at the write inlet (remove, not converge the read path); re-entry only with a real consumption path. | the measurement and the ruling were already in the sentence; the id is dropped | | `5040` | The declarative endpoint executor project; its acceptance step moved showcase's endpoints to the artifact route, live. | "showcase uses the artifact route, and its declared endpoints serve live" | | `4052` | `BatchOptions.validateOnly`, retired the same way (a runtime verdict, no D2). | named by the key, which the sentence already carried | | `5279` (PR), `5189`, `5203` (PR) | The publish gate for `api` drafts; `publishPackage` and load-time `buildEndpointIndex` running the endpoint gate. | named by the functions the sentence already carried | | `2657` | Studio metadata coverage: Part B asks which un-typed concepts (`apis` among them) become registered types. | "if the Studio metadata-coverage work promotes `apis` to a registered type WITH A REAL CONSUMPTION PATH" | | `5311` | The direct-active `saveMetaItem` write was a third path past the namespace and duplicate gates; closed as subsumed by the `5488` ruling. | "The same refusal closes the direct-active write too, which had been a third path past the endpoint namespace and duplicate-path gates." | | `18576` | Maintainer, 2026-09-17, option B: narrow the `./api` entry, rather than add a bundle-weight rule (A) or accept the weight (C). | "Maintainer ruling of 2026-09-17, option B (narrow the entry, rather than add a bundle-weight rule to the browser-reachability ledger or accept the weight as it stood)" | | `14478`, `15677` | Maintainer ruling B of 2026-09-02: a duration's unit lives in the key name, no offender grandfathered; the `api/` stack of that ruling. | "Maintainer ruling B of 2026-09-02 on duration-shaped keys"; trailing ids dropped | **`export-` (15 ids)** | cited | what it decided (read) | how the text now carries it | |---|---|---| | `6350` | The stock reconciliation of the v17 train's breaking changesets against the ledger, which backfilled this entry. | "Registered (backfilled) by the stock reconciliation that compared the breaking changesets already on the v17 release train against this ledger" | | `3544`, `3710` | The user-level export axis, and its extension to the CSV attachments scheduled reports mail out. | "the export axis, and its extension to the CSV attachments scheduled reports mail out" | | `6148` | **404** — see Acceptance notes. | "the gate that makes a breaking changeset state its ADR-0087 disposition" | | `3956` (spelled `framework#`) | The import dry run skipped the field-level validation the real write ran; the hand-copied pre-check mirror was added to close it. | "added when the dry run was found skipping the field-level validation the real write ran" | | `4633`, `6532` (PR) | Maintainer, 2026-08-06, ruling D: a validate-only protocol operation, so the dry run's prediction is the engine's verdict; the mirror retired. | "the maintainer's 2026-08-06 ruling D (a validate-only protocol operation, so the dry run's prediction is the engine's verdict by construction)" | | `4484`, `5540`, `6011` | The `findStream`, `IStorageService.list` and `actor-user-roles-to-positions` retirements. | named by their surfaces, which the sentence already carried | | `6536` | The eight keys left read by nothing after the mirror retired, deferred to their own sweep. | "this is the removal the dry-run change deliberately deferred to a sweep of its own" | | `17158` | Maintainer, 2026-09-12, ruling A: retire the family, `IExportService` and `ScheduleExportInput`; `ScheduleState` with it unless a live consumer is measured. Landing route A, 2026-09-24; scope note, 2026-09-25. | "maintainer ruling A of 2026-09-12 (…), the landing route the maintainer ruled on 2026-09-24 (route A: …), and a scope note the maintainer agreed on 2026-09-25" | | `objectui#10247` | The console retires its own async-export path first. | "objectui retires its own side of the unimplemented async-export path first"; "which carries objectui's own retirement" | | `19543` | Three sibling list doors declared `limit` / `cursor` and never read them; the export-job list's door folded into this retirement. | "one of three sibling list doors found declaring them and never reading them" | | `16320` | The seven cron-typed positions nothing read, retired (three on these defs). | "once the retirement of the cron-typed positions nothing read had deleted theirs"; "Those earlier cron-position deletions" | **`field-` (20 ids)** | cited | what it decided (read) | how the text now carries it | |---|---|---| | `7893` | Maintainer, 2026-08-12: retire the runtime `field` write channel rather than build a read path. | the measurement and the ruling were already in the sentence; the id is dropped | | `5488`, `4052` | The `api` and `validateOnly` withdrawals. | "the `api` withdrawal's rationale reused (`api-runtime-create-withdrawn`)"; named by key | | `7743`, `7894` | The field overlay lock (`NOT_OVERRIDABLE`); the plural `/meta/fields/` spelling folded onto the singular. | "The field overlay refusal"; the plural door named by its path | | `8169` | The `_diagnostics` envelope asserts well-formedness only; it has no "in effect" axis. | "(the envelope has no "in effect" axis)" | | `11566`, `11989` (PR), `11950` | Maintainer, 2026-08-24: tighten both halves of `maxLength` (value shape and applicable types); shipped on 17.x; registered in a follow-up. | "Maintainer ruling of 2026-08-24, tightening both halves — the value's shape and the types the key applies to"; "registration was deferred to a follow-up" | | `11875` | Maintainer, 2026-08-25, option 1: the write seam enforces `maxLength` for `signature` / `qrcode`, then both join the bounded-string set. | "which joined once the write seam enforced a declared bound on them" | | `11431` | The SQL driver stops reading a malformed bound as authoritative (the `varchar(0)` plan). | "until it was taught to stop reading a malformed bound as authoritative" | | `8321` | `scale` / `precision` refused as non-integer or negative — the house pattern. | "the house pattern the `precision`/`scale` integer refusal set" | | `11949` | Maintainer, 2026-08-25, option B: `minLength` is `int().min(1)`, zero refused, the `maxLength` template in full. | "Maintainer ruling of 2026-08-25 (option B, the lower bound at 1) … the defect pair the 2026-08-24 ruling closed for `maxLength`" | | `17469`, `11437` | Maintainer, 2026-09-13, option 1′: `multiple: true` refused outside the multi-capable types — the earlier `radio` rule generalised; the driver derives its JSON column from the spec predicate. | "Maintainer ruling of 2026-09-13, option 1′ (the earlier rule refusing an authored `radio` with `multiple: true`, generalised)" | | `objectui#8886`, `objectui#8937` | The console's related list shaped its parent filter from the spec predicate; its follow-up recorded the driver half as owed. | "the console's related list pinned the divergence on the consumer side when it began shaping that filter from the spec predicate, and its follow-up recorded the driver half as owed and not filed" | | `20078` | Triage, 2026-09-25, remedy A: refuse the traversal at authoring with a prescription; hydrating the field level is a capability of its own. | "Triage routed this on 2026-09-25 to remedy A: refuse the traversal at authoring, with a prescription." | | `18682` | A validation rule or visibility predicate reads one hop through a lookup. | "is served, one hop deep, and stays accepted" | | `7501` | `scale` enforced at write time: an over-scale write refused, never rounded. | "when `scale` was made enforced at write time (an over-scale write refused, never rounded)"; "the write-time `scale` enforcement" | **`hook-` (12 ids)** | cited | what it decided (read) | how the text now carries it | |---|---|---| | `4839`, `5049` (PR) | Both `session.roles` admin exemptions removed; the record lock and the delegation guard back on the one permission vocabulary. | "An earlier fix removed both readers, returning the record lock and the delegation guard to the one permission vocabulary" | | `4579`, `4657` | The `openApi31` and `activationEvents` retirements. | named by their surfaces, which the sentence already carried | | `3733` | Measured: a key removed from a non-strict schema parses clean and is silently dropped. | "as a removed field key was measured to be" | | `5050` | This retirement's own card. | trailing id dropped | | `4281` (PR) | An empty hook target is not "no target": closed at the two metadata doors. | "An earlier breaking fix established that an empty hook target is not "no target""; "that fix's headline failure mode" | | `5928` | The `excludeObjects` face (global except named objects); it declined to change the matcher's read in passing. | "The later `excludeObjects` face (a hook global except for the objects it names)"; "the `excludeObjects` change declined to do it in passing" | | `6573` | **404** — see Acceptance notes. | trailing id dropped; the entry states the change | | `4001` | The unknown-key strictness campaign (ADR-0078). | trailing id dropped (ADR-0078 kept) | | `3195` | The hook taxonomy collapsed from 18 events to the 8 dispatched; a registration guard warns on the rest. | "the change that collapsed the hook taxonomy to the eight dispatched events made this branch a warn" | | `17713` | This refusal's own card. | trailing id dropped | **`metadata-` (11 ids)** | cited | what it decided (read) | how the text now carries it | |---|---|---| | `12057` | Maintainer, 2026-08-29: retirement adopted, re-scope rejected (the card's ruling comment is no longer on it; `main` records it). | "the maintainer's ruling of 2026-08-29 adopted retirement and rejected a re-scope" | | `13135` | **404** — see Acceptance notes. | "executed widened to the full coupling set the fork report on that ruling measured" | | `11513` | **404** — see Acceptance notes. | "the 2026-08-24 lock-and-clone ruling (lock the packaged base, customize a clone) left deliberately unchartered" | | `15542`, `15854` | Maintainer, 2026-09-06, ruled together (2 + A): every `endpoints.*` switch gates exactly the face its name states; the whole-store family gets its own key. | "The maintainer ruled the two together on 2026-09-06 as one principle: …" | | `15543` | No shipped boot path constructs a `RestServerConfig`. | the measurement was already in the sentence; the id is dropped | | `15624` | The outer cache keys read by nothing, retired on their own (the owning seat's ruling). | "(the owning seat's ruling, conditioned on the measurement below and re-taken on the merged ref)"; "retired on its own under ADR-0049" | | `14478` | Maintainer ruling B of 2026-09-02 on duration units. | "Maintainer ruling B of 2026-09-02 on duration-shaped keys"; "the duration-unit rename" | | `8586`, `8421` | Maintainer, 2026-08-14, jointly: remove `additionalTypes`, and refuse unknown `/meta` types by the static registry. | "maintainer ruling of 2026-08-14: remove the key, jointly with refusing unknown types at the `/meta` boundary by the static registry" | | `4212` | Four of five declared plugin lifecycle hooks were never invoked, `onInstall` among them. | "the plugin lifecycle's `onInstall` (a documented hook with no invocation site)" | **`dataset-` (9 ids)** | cited | what it decided (read) | how the text now carries it | |---|---|---| | `19889` | Ruling A of 2026-09-24: the schema door refuses what the compile face refuses; a field spec with no `$` key stays undescended. | "ruling A of 2026-09-24, which made the schema door refuse what the compile face refuses, drew the line there" | | `20080` | Triage, 2026-09-25, remedy A: refine the two analytics carriers; remedy B (stop the analytics door descending) changes what a nested list means. | "Triage on 2026-09-25 routed the fix to the two analytics carriers instead, rather than stop the analytics door descending, which would change what a nested list means" | | `16737`, `16099` | The measured defect: `AVG()` over a datetime is an average year on SQLite and an error on Postgres. | the measurement was already in the sentence; the ids are dropped | | `16353` | The aggregate × field-type table, declared in the spec. | named by the table, which the sentence already carried | | `16099` (in `surface` and `acceptanceCriteria`) | The widening of the refusal to `sum` / `avg` over every field class, registered `not-required` against this entry. | "followed in a later change"; "widened by a later change" | | `17560` | Director ruling B of 2026-09-13: the compile door enforces the table for every aggregate. | "Director ruling B of 2026-09-13: …"; the sibling entry named by id | | `15768`, `16236` | `measureResultType` typing `min` / `max` over strings and over a `formula` return type. | the sentence already states both | | `17513` | Closed as a duplicate with zero rulings on it. | "the card it cited is closed as a duplicate with zero rulings on it" | ## Pin — `packages/cli/test/migrate-meta-engine-guidance.test.ts`, widened `COVERED_PREFIXES` gains `field-`, `export-`, `api-`, `dataset-`, `hook-` and `metadata-` (17 prefixes; `data-` still selects neither `datasource-` nor `dataset-`, and `api-` does not select `apimethod-`: the match is `startsWith`). The `REWRITTEN` floor rises from **88 to 113** ids: the 25 entries this stage rewrote. The three `it` blocks are textually unchanged. The file keeps its stage-1 name; the header lists the seventeen covered families. ## Ablation — the widened pin can fail on a new-family block From committed state, HEAD `d24a253bd0`, in one lock turn, with `scripts/ablation-replace.mjs` in wrap mode (it owns the mutation's restore; the leg script adds its own `trap … EXIT INT TERM` that restores `registry.ts` from `HEAD` by absolute path and checks the blob) and `scripts/ablation-dist-preflight.mjs` gating each leg. The bundle is built from the generated `registry.ts`, so that is the file mutated. - **Mutation.** In `registry.ts`, the `reason` of `hook-register-undispatched-lifecycle-event-refused`: anchor `made this branch a warn — ` → `made this branch a warn (#3195) — `. The tool read anchor 1 → 0 and replacement 0 → 1, blob `94bc4938` → `b045dbfd`. - **Mutate leg.** Spec build exit 0. Preflight: marker present in 4 built files. Pin: **red**, `1 failed | 2 passed` — `hook-register-undispatched-lifecycle-event-refused: the printed guidance cites a tracker id: expected '#3195' to be undefined`. - **Restore.** Tool-proven: blob `94bc4938` == HEAD, `git diff HEAD` empty. - **Restore leg.** Spec build exit 0. The `--absent` preflight found the marker in none of 224 built files, with the working tree clean against HEAD. Pin: **green**, `3 passed`. Whole tree afterwards: 0 dirty paths. ## Verification Final head **`d24a253bd0`**; every reading below was taken there. Every heavy run went through `scripts/pm/os-verify-lock.sh`, with per-step exit codes recorded separately. - **Build:** `pnpm exec turbo run build --concurrency=2 --filter='@objectstack/cli^...'` gives `Tasks: 58 successful, 58 total`; the ten packages outside that closure (for `check:dual-build-cjs-loads`) give `Tasks: 68 successful, 68 total`. - **Pin with its neighbour:** `pnpm --filter @objectstack/cli exec vitest run --project integration --maxWorkers=2 test/migrate-meta-engine-guidance.test.ts test/migrate-meta-default-range.test.ts` gives `Test Files 2 passed`, `Tests 10 passed | 1 skipped` (the skip is the default-range file's own `skipIf`). - **CLI unit:** `test/vitest-tiers-partition.test.ts` and `src/utils/spec-release-changes.test.ts` give `Test Files 2 passed`, `Tests 28 passed`. - **Spec, the whole `local` project:** `pnpm --filter @objectstack/spec exec vitest run --project local --maxWorkers=2` gives `Test Files 573 passed (573)`, `Tests 16801 passed | 1 todo`. **The `repo` project:** `Test Files 38 passed`, `Tests 690 passed`. - **Typecheck:** `pnpm --filter @objectstack/spec typecheck` exits 0 (test layer: 53 files / 251 errors held in its ledger); `pnpm --filter @objectstack/cli typecheck` exits 0 (3 files / 28 errors held). - **Gate families:** `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` derives **89** families. `--ran` over the recorded exit codes reads **89 derived, 89 run, 0 NOT-MEASURED, 0 UNRUN**, all exit 0. They include `check:doc-authoring` ("16735 customer-facing string(s) across 1168 spec sources clean"), `check:issue-citations`, `check:migration-registry` ("registry.ts is current (311 semantic, 230 retired-key, 206 retired-def)"), `check:spec-changes`, `check:upgrade-guide`, `check:generated` ("All 15 generated artifacts are up to date"), `check:org-identifier` ("no removed session.tenantId alias"), `check:nul-bytes`, `check:dual-build-cjs-loads` (104 require entry points across 66 packages load), `check:type-check-debt`, `check:adr-0087-registration` and `check:changeset-no-major`. `check:dual-build-cjs-loads` first exited 3 (`PREREQUISITE NOT MET`: ten packages had no `dist/`); after the ten were built it exits 0, and that is the reading recorded. - **Lint (a proven narrowing, not the repo-wide run, which is CI's):** `eslint --no-inline-config --format json` over the 27 changed `.ts` files reports 27 files, 0 errors, 0 warnings. - The population is read from `eslint.config.mjs`: `**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}` minus `NEVER_LINTED`, and all 27 are in it (no file-ignored notice). - Invariance: the config enables no type-aware linting (no `parserOptions.project`, no typed rules), so a text edit cannot move the verdict on a file it does not touch. - **Mergeability:** `main` moved one commit past the base, to `0bbe4005`: the landed `20511` (a `qa-` entry, a retired key and `registry.ts`). A driver-free bare-clone `merge-tree --write-tree` of `d24a253bd0` against `0bbe4005` exits 0 with no conflicted path, and the census over that merged tree reads 0 sites in all six families (whole tree 461), so `main` was not merged in; CI's merge ref runs the registry gates on the merged tree. ## Acceptance notes - **Four dead ids, rewritten from what `main` records.** Each answers 404 on both the issues and the pulls endpoint, with a 200 control (`14478`). - `6148`: `scripts/check-adr-0087-registration.mjs`'s header (a declared-breaking changeset must state its ADR-0087 disposition in writing) — the same reading stage 4 used. - `6573`: the objectql CHANGELOG entry "`engine.registerHook` refuses an empty `object` target and a scope whose two faces cancel out", and `engine.ts`'s refusal docblocks. It is this entry's own change, so the trailing id is dropped; the entry already states the change. - `13135`: `packages/metadata/CHANGELOG.md` ("retire the paper metadata-customization protocol with its full coupling set … re-charter of" the `12057` ruling, "the maintainer adopted retirement … 2026-08-29 … and" it "charters the full coupling set the fork report measured"). - `11513`: ADR-0126 names it the lock-and-clone ruling of 2026-08-24 (Salesforce-style: lock the packaged base, customize a clone), and its section 9 records the per-field overlay layer as explicitly not chartered. - **One ruling read from `main`, not from its card.** `12057` answers 200 but carries only its triage comment; the 2026-08-29 ruling the entry names is recorded in `packages/metadata/CHANGELOG.md`, and the sentence says only what that record says. - **Short numbers, ruling-record ids and acknowledgements went too (invisible to the regex).** Eleven decision-batch numbers (`#43` ×2, `#59` ×2, `#122`, `#127`, `#128`, `#145`, `#215`, `#218`, `#221`) and five ruling-record comment ids (in `field-currency-scale-refused`, `field-predicate-reference-traversal-refused` and `dataset-filter-nested-relation-equality-array-refused-at-save`) are numbers an author is shown and cannot follow, so each is dropped. So are five maintainer acknowledgements (「同意,其他也同意」 in `api-assembled-entry-split`, 「同意」 ×3 in `export-job-family-retired`, 「同意」 in `metadata-customization-protocol-retired`): they record only that a batch was approved, and each sentence now states the ruling's date and content instead. The two quotes that carry the lesson stay verbatim: "both legs, table in spec" and 「`min`/`max` numeric plus `date`/`datetime`; everything else refused」. - **Three `Prime Directive #12` / `PD #12` spellings are kept.** They name a rule in this repository's AGENTS.md, not a tracker item, like the ADR ids; the earlier stages kept the same spellings in the `ui-`, `plugin-` and `system-` families. - **`surface`, per ruling A.** `18.dataset-measure-aggregate-field-type-refused` was the only entry of these families whose `surface` carried tracker ids (two). Its header now names the later widening and the sibling entry in words, and the AST comparison shows nothing else in it moved. No test or tool reads that `surface`: outside the migration tree, the pin and the generated projections, the id appears only in three earlier changesets' ADR-0087 disposition markers (and this PR's changeset). - **"issue NNNN" / "PR NNNN" spellings, checked by hand.** A scan of the six families' evaluated prose for any run of three or more digits and for `issue` / `card` / `PR` / `batch` / `record` / `summon` / `item` plus a number now finds only HTTP statuses, ports, byte counts, durations, dates, commit shas, SQLSTATE and TS error codes, ADR ids and example values. - **No test pinned a removed tracker number of these entries.** A search of test files for the 25 entry ids finds only the pin, `export-job-family-retirement.test.ts` (it asserts `not a D2 conversion` and the backtick-free `surface`, both unchanged) and comment lines; a search for the 75 cited numbers in `toMatch` / `toContain` assertions finds only unrelated digit runs and runtime strings outside this card (`api-endpoint-step.test.ts` and `endpoint-executor.test.ts` assert a `5040` hint, which is #20513's surface). - **No open PR touches these six families.** Read twice: at the start of this stage, 10 open PRs and 634 file rows; again just before opening this one, 13 open PRs and 633 rows (the Version Packages PR `17076` included both times). None carries a `migrations/entries/semantic/NN.(field|export|api|dataset|hook|metadata)-*` file. PR `20512` adds a retired-key file `18.api__RestApiConfig__documentation.version.ts`; a retired key has no id and is not in `step.semantic`, so the widened `api-` prefix does not select it. PRs `20512`, `20504`, `20460` and `20458` add entries in other families (`rest-`, `turso-`, `stack-`, `cube-`) and regenerate `registry.ts`: ordinary concurrency, regenerate on merge. - **Generated projections** (`spec-changes.json`, `docs/protocol-upgrade-guide.md`) are regenerated, as in stages 1–4; only the protocol-17 entries appear in them. - **What later stages pick up** (whole tree at this head, same instrument): **461** prose-field sites in the other families, **50** short numbers, **5** `surface` sites. ## Line budget Entry files: **265 changed lines** (+147 / −118) across 25 files, against the stage-1 ≈400 budget. The whole diff is **631 lines** (+372 / −259) in 30 files. Of the rest, `registry.ts` is 265, the two projections are 44 (`spec-changes.json` 24, the upgrade guide 20), the widened pin is 29 and the changeset 28. --- _Generated by [Claude Code](https://claude.ai/code/session_01ARcDurZ5j34RdqsGgc4jgH)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 397572e commit 0e1afe8

30 files changed

Lines changed: 372 additions & 259 deletions
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
fix(spec): `os migrate meta` guidance for the `field-*`, `export-*`, `api-*`, `dataset-*`, `hook-*` and `metadata-*` migration entries states each lesson in words instead of citing tracker numbers
6+
7+
Clause-②: no
8+
9+
The ADR-0087 semantic entries of the `field-*` family (the runtime `field` write door, the
10+
`maxLength` / `minLength` / `scale` / `precision` refusals, `scale` on a currency field,
11+
`multiple` on a type that holds one value, and predicates that read through a reference),
12+
the `export-*` family (the export permission axis, the eight constraint keys retired from
13+
`ExportFieldMeta` and the retired export-job API family), the `api-*` family (the runtime `api` write door,
14+
the split API entry and two duration keys renamed with their unit), the `dataset-*` family
15+
(the aggregate × field-type refusals and the nested-relation list refused at save), the
16+
`hook-*` family (the retired hook-session `roles` and the two `registerHook` refusals) and
17+
the `metadata-*` family (the retired customization protocol, the re-partitioned endpoint
18+
switches, the metadata-manager cache keys and the retired `additionalTypes`) are printed by
19+
`os migrate meta` as the header, `why:` and `verify:` lines of a manual change. Their text
20+
sent the reader to issue-tracker, decision-batch and ruling-record numbers — some of which
21+
no longer resolve, and some in another repository — for what a ruling, measurement or fix
22+
had decided; it now says what was decided, in the sentence being read. ADR ids are kept.
23+
24+
Text only: no entry id, `from` / `to`, conversion or matching logic changes, and the chain
25+
rewrites exactly what it rewrote before. One entry's `surface` (the header line of
26+
`dataset-measure-aggregate-field-type-refused`) drops the two tracker numbers it carried and
27+
names nothing else differently. The generated migration registry, `spec-changes.json` and
28+
the protocol upgrade guide carry the same text.

‎docs/protocol-upgrade-guide.md‎

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

‎packages/cli/test/migrate-meta-engine-guidance.test.ts‎

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
* `os migrate meta` — the guidance it prints for the ADR-0087 semantic entries
55
* of the COVERED families (`engine-*`, `ui-*`, `plugin-*`, `driver-*`,
66
* `kernel-*`, `system-*`, `datasource-*`, `filter-*`, `action-*`, `data-*`,
7-
* `element-*`) states each lesson in words and carries no tracker number.
7+
* `element-*`, `field-*`, `export-*`, `api-*`, `dataset-*`, `hook-*`,
8+
* `metadata-*`) states each lesson in words and carries no tracker number.
89
*
910
* ## What this pins
1011
*
@@ -76,6 +77,7 @@ const TRACKER_ID = /#\d{4,5}\b/;
7677
const COVERED_PREFIXES = [
7778
'engine-', 'ui-', 'plugin-', 'driver-', 'kernel-', 'system-',
7879
'datasource-', 'filter-', 'action-', 'data-', 'element-',
80+
'field-', 'export-', 'api-', 'dataset-', 'hook-', 'metadata-',
7981
];
8082

8183
/**
@@ -89,12 +91,19 @@ const REWRITTEN = [
8991
'action-descriptor-resume-authority-default-flip',
9092
'action-engine-facade-find-query-envelope',
9193
'action-session-roles-to-positions',
94+
'api-assembled-entry-split',
95+
'api-error-retry-after-unit-in-key',
96+
'api-runtime-config-durations-unit-in-key',
97+
'api-runtime-create-withdrawn',
9298
'data-driver-find-stream-retired',
9399
'data-driver-query-omit-object',
94100
'data-engine-batch-retired',
95101
'data-field-changed-event-retired',
96102
'data-file-value-duration-unit-in-key',
97103
'data-nosql-query-options-timeout-unit-in-key',
104+
'dataset-filter-nested-relation-equality-array-refused-at-save',
105+
'dataset-measure-aggregate-field-type-refused',
106+
'dataset-measure-selecting-aggregate-field-type-refused',
98107
'datasource-config-inline-credential-refused',
99108
'datasource-config-mongo-options-credential-refused',
100109
'datasource-config-placeholder-refused',
@@ -118,6 +127,16 @@ const REWRITTEN = [
118127
'engine-find-formula-filter-refused',
119128
'engine-find-formula-order-by-refused',
120129
'engine-update-upsert-retired',
130+
'export-axis-opt-in',
131+
'export-field-meta-constraints-retired',
132+
'export-job-family-retired',
133+
'field-currency-scale-refused',
134+
'field-max-length-malformed-or-misplaced-refused',
135+
'field-min-length-malformed-or-misplaced-refused',
136+
'field-multiple-non-capable-type-refused',
137+
'field-predicate-reference-traversal-refused',
138+
'field-runtime-create-withdrawn',
139+
'field-scale-precision-integer-refused',
121140
'filter-between-blank-endpoint-refused',
122141
'filter-between-field-reference-endpoint-refused',
123142
'filter-comparand-types-and-widget-nested-slots-refused-at-save',
@@ -129,6 +148,9 @@ const REWRITTEN = [
129148
'filter-query-face-comparands-refused-at-save',
130149
'filter-regex-options-retired',
131150
'filter-text-operator-declared-type-refused',
151+
'hook-context-session-roles-retired',
152+
'hook-register-empty-object-target-refused',
153+
'hook-register-undispatched-lifecycle-event-refused',
132154
'kernel-compatibility-matrix-estimated-migration-time-unit-in-key',
133155
'kernel-context-preview-mode-retired',
134156
'kernel-event-bus-retention-unit-in-key',
@@ -138,6 +160,11 @@ const REWRITTEN = [
138160
'kernel-plugin-security-durations-unit-in-key',
139161
'kernel-runtime-config-timeout-unit-in-key',
140162
'kernel-startup-orchestrator-durations-unit-in-key',
163+
'metadata-customization-protocol-retired',
164+
'metadata-endpoints-switch-radius-repartitioned',
165+
'metadata-manager-config-cache-ttl-unit-in-key',
166+
'metadata-manager-config-inert-cache-keys-retired',
167+
'metadata-plugin-additional-types-retired',
141168
'plugin-activation-events-retired',
142169
'plugin-auto-restart-never-reinitialised',
143170
'plugin-manifest-contributes-dead-members-retired',

‎packages/spec/spec-changes.json‎

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

‎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 '

0 commit comments

Comments
 (0)