Skip to content

Commit 0d7ed5a

Browse files
fix(spec): os migrate meta guidance for the driver-*, kernel-* and system-* migration entries states each lesson in words, not tracker numbers (stage 3) (#20384)
Part of #20233 Clause-②: no **Stage 3 of a staged card.** The card stays open for later stages; this PR carries no closing keyword. Text only: no entry id, `surface`, `from` / `to`, conversion or matching logic moves, and the chain rewrites exactly what it rewrote before. ## 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 the parent card sets the shape: the lesson in words, and no number, dead or alive. This stage covers the next three families by site count, `driver-`, `kernel-` and `system-`: **132 sites → 0** in the three prose fields. None of the 26 entries carries a tracker id in `surface` (ruling A of the stage-1 ACCEPT, `5858839916`, is checked and has nothing to do here). 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 stage-1 pin now holds `engine-`, `ui-`, `plugin-`, `driver-`, `kernel-` and `system-`. ## Census — tracker ids in the author-shown fields **Instrument.** The stage-2 AST instrument, unchanged: a TypeScript-AST walk over every `packages/spec/src/migrations/entries/**/*.ts`. For each `entry` object literal it evaluates the string value of `replacement`, `reason`, `acceptanceCriteria` and (counted separately) `surface`, joining string literals with `+`, then counts `#` followed by 4 or 5 digits at a word boundary. **Validated first** by reproducing the stage-1 readings on the stage-1 tree (`443b2f4fdc`, extracted with `git archive`): `driver-` 7 entries / 44 sites (0 / 44 / 0, 25 distinct), `kernel-` 9 / 44 (1 / 41 / 2, 11 distinct), `system-` 10 / 44 (0 / 42 / 2, 6 distinct), `engine-` 5 / 67, whole tree 266 entries / 1,016 sites / 9 `surface` sites — every figure equal to the stage-1 census. **Tree measured:** `objectstack-ai/objectstack` at `569d4d2dbf` (this branch's base). Unevaluable fields: 0. **Controls, same run.** - **Lit:** `17.aggregation-node-distinct-retired.ts` reads 7 sites (replacement 1, reason 6), the reading stages 1 and 2 took. - **Dark (comment lines):** 794 `//` lines in entry files carry a tracker id, and none is counted. Comment lines belong to the sibling card, and ⛔ this PR touches none (794 before and after). - **Dark (field boundary):** the 7 `surface` sites left in the tree (other families) count 0 in the three-field total and 7 in the `surface` column. **Re-measured on the base, matching the stage-1 census:** `driver-` 7 entries, **44** sites (replacement 0 / reason 44 / acceptanceCriteria 0), 25 distinct ids; `kernel-` 9 entries, **44** (1 / 41 / 2), 11 distinct; `system-` 10 entries, **44** (0 / 42 / 2), 6 distinct. 37 distinct ids across the three (the families share `#14478`, `#15939`, `#17635` and `#3733`). `surface`: 0 in all three. Whole tree: 300 entries, **843** sites, 7 `surface` sites. **After this PR:** `driver-` 0, `kernel-` 0, `system-` 0; `engine-`, `ui-`, `plugin-` still 0; whole tree **843 → 711** sites; `surface` 7 (unchanged, other families). | entry | sites (replacement / reason / acceptanceCriteria) | |---|---| | `17.driver-aggregate-undeclared-key-aliases-removed` | 6 (0 / 6 / 0) | | `17.driver-capabilities-inert-bits-removed` | 4 (0 / 4 / 0) | | `18.driver-options-timeout-to-timeout-ms` | 1 (0 / 1 / 0) | | `17.driver-sql-distinct-bare-filter-typed` | 9 (0 / 9 / 0) | | `18.driver-sql-unresolvable-where-column-refused` | 14 (0 / 14 / 0) | | `18.driver-sql-upsert-cross-row-identity-merge-refused` | 9 (0 / 9 / 0) | | `18.driver-turso-config-local-path-wasm-retired` | 1 (0 / 1 / 0) | | `18.kernel-compatibility-matrix-estimated-migration-time-unit-in-key` | 5 (0 / 5 / 0) | | `18.kernel-context-preview-mode-retired` | 5 (1 / 4 / 0) | | `18.kernel-event-bus-retention-unit-in-key` | 3 (0 / 3 / 0) | | `18.kernel-health-check-and-hot-reload-durations-unit-in-key` | 7 (0 / 5 / 2) | | `18.kernel-package-lifecycle-durations-unit-in-key` | 3 (0 / 3 / 0) | | `18.kernel-plugin-health-report-durations-unit-in-key` | 3 (0 / 3 / 0) | | `18.kernel-plugin-security-durations-unit-in-key` | 4 (0 / 4 / 0) | | `18.kernel-runtime-config-timeout-unit-in-key` | 11 (0 / 11 / 0) | | `18.kernel-startup-orchestrator-durations-unit-in-key` | 3 (0 / 3 / 0) | | `18.system-cache-durations-unit-in-key` | 3 (0 / 3 / 0) | | `18.system-collaboration-durations-unit-in-key` | 4 (0 / 4 / 0) | | `18.system-failover-health-check-interval-unit-in-key` | 3 (0 / 3 / 0) | | `18.system-metrics-jsdoc-durations-unit-in-key` | 12 (0 / 12 / 0) | | `18.system-metrics-window-durations-unit-in-key` | 5 (0 / 3 / 2) | | `18.system-object-storage-durations-unit-in-key` | 3 (0 / 3 / 0) | | `18.system-registry-config-durations-unit-in-key` | 3 (0 / 3 / 0) | | `18.system-tracing-otel-exporter-durations-unit-in-key` | 5 (0 / 5 / 0) | | `18.system-tracing-span-duration-unit-in-key` | 3 (0 / 3 / 0) | | `18.system-worker-queue-rate-limit-duration-unit-in-key` | 3 (0 / 3 / 0) | | **total, 26 entries** | **132 (1 / 127 / 4)** | ## Every citation read, and what the text now says I read each cited issue or PR myself with single-card REST reads: the body, and the comments where a ruling or a measurement lives. Ids are in code spans so this body posts no cross-references. All 36 bare ids were resolved against this repository, because every sentence that cites one is about this repository's code; the one cross-repo id is `cloud#1651`. | cited | what it decided (read) | how the text now carries it | |---|---|---| | `#3733` | The pruned `cached` field key: measured, the parse succeeded and the removed key was dropped without a word; the orphan schema was deleted. | "an earlier field-key prune measured exactly that — the parse succeeded and the removed key was dropped without a word" (health-check, OTel exporter) | | `#3821` | The sharing-rule page: an unsortable query fell through to an empty page, and the driver fix made an unsortable query lose its ORDER BY, not its rows. | "the unknown-column recovery ladder (an unsortable query loses its ORDER BY, not its rows)"; "the ladder's own premise — rows matter more than their order"; "the ladder's recoveries" | | `#4484` | `IDataDriver.findStream` removed: no production caller, two of three implementations buffered the whole set, and no tombstone because nothing parses a driver object. | "Retiring `IDataDriver.findStream` (it had no production caller, and two of its three implementations read the whole result set into memory …)"; "(`IDataDriver.findStream`, removed with no tombstone because nothing parses a driver object)"; by entry id in the `distinct` entry | | `#4583` | The datasource ledger's dead keys removed; `capabilities.*` went as a whole block (11 of 11 unread). | "was retired separately, as a whole block nothing read" | | `#4634` | Audit of all 34 `DriverCapabilities` bits: 3 live, 31 dead and tombstoned. | the entry already states the audit ("the follow-up audit checked every bit"); the trailing id is dropped | | `#4914` | Maintainer, 2026-08-04: remove `manifest.loading` and `PluginHotReloadSchema`; keep `HotReloadConfigSchema`, the side with an implementation (`HotReloadManager`), as the start point. | "kept twice: as the hot-reload vocabulary that had an implementation when the manifest-side copy was removed, …" | | `#4984` | An org-axis red-line gate read only aliases the schema rejects while its own fixtures spelt them: tests green, rule dead. | "the family of the org-axis red-line gate that read only rejected aliases while its own fixtures spelt them, so its tests stayed green and the rule stayed dead" | | `#5181` | Narrow the query parameter of `IDataDriver`'s methods (`DriverQuery`, no redundant `object`). | "neither the narrowing of `IDataDriver`'s query parameters to `DriverQuery` nor the follow-through …" | | `#5499` | Maintainer, 2026-08-05: freeze investment in `driver-memory` / `driver-mongodb`; fully lifted 2026-08-11 (comments `5249019855`, `5252526378`). | "the maintainer's 2026-08-05 investment freeze on driver-memory, which was lifted on 2026-08-11" | | `#5540` | Remove `IStorageService.list(prefix)`: zero consumers, and the two adapters answered differently and both incompletely. | "(the zero-consumer `IStorageService.list`, whose two adapters answered differently and both incompletely)" | | `#6011` | Maintainer: close the `ctx.user` `roles` alias now. | "(the `ctx.user` `roles` alias, closed at once on the maintainer's word rather than given a window)" | | `#6075` | **404** — see Acceptance notes. | "the follow-through that brought five drivers' implementations in line" | | `#6320` | `distinct`'s third argument meant different things on memory and sql; the sql half was dispatched, the memory half held under the freeze. | "(the measurement that found the two drivers reading this argument differently split the fix: the sql half is this entry, and the memory half was held back by that freeze)" | | `#6321` | `query.aggregate` / `agg.func` are undeclared aliases whose only writers are driver fixtures; order: re-spell the fixtures, delete the aliases, then narrow the signature. | "The removal ran in a fixed order — the fixtures re-spelt first, the two alias branches deleted second, the parameter narrowed to `DriverQuery` last — because the reverse order yields red nobody can explain." | | `#6404` (PR) | Executed that order and narrowed `aggregate`'s query parameter to `DriverQuery`. | the same sentence | | `#7929` | Maintainer, 2026-08-12, ruling B: `driver-sql`'s filter refusal stops echoing `$field` operands, for every caller; the full diagnostic goes to the server log. | "the same predicate-text disclosure shape the driver's field-reference filter refusals had already been made to stop echoing (the full diagnostic goes to the server log, never the response)"; "that disclosure shape closed on the last dialect" | | `#8371` | Ruled option 2: a dotted filter key whose head is a relation, a formula or a scalar is refused at both doors; a structured head stays unjudged. | "the axis owned by the dotted-filter verdict, which refuses a dotted key whose head is a relation, a formula or a plain column at the protocol and engine doors" | | `#8592` | Measured on live MySQL: knex compiles the named conflict target away. | stated by the entry ("knex drops the named keys before the statement leaves the process"); the trailing id is dropped | | `#8621` | Option A: a pre-flight refusal when no unique index backs the caller-named conflict target. | "Two earlier pre-flight refusals closed the half where no unique index backed a caller-named target …" | | `#8622` | `id` becomes insert-only on the merge path: a merge on a non-primary conflict key was measured rewriting the existing row's primary key. | "`id` is insert-only on the merge path (made so once a merge on a non-primary conflict key was measured rewriting the existing row's primary key)" | | `#8755` | Ruling option A: a pre-flight refusal when a second unique key could absorb a backed, caller-named target. | "… and the half where a rival unique key could absorb a caller-named one" | | `#8790` | Maintainer, 2026-08-15: refuse both halves with `INVALID_FILTER` / 400, naming the column. | "Ruled by the maintainer on 2026-08-15: refuse BOTH halves …"; "Recover-both was excluded by the ruling's own argument" | | `#8807` | Maintainer, 2026-08-15: an upsert must never modify a row whose identity the caller did not supply and whose conflict key it did not name; enforcement delegated, blanket refusal excluded. | "Ruled by the maintainer on 2026-08-15, as a contract principle …" (the principle itself was already quoted verbatim) | | `#8926` | Maintainer, 2026-08-16, option A: MySQL's spelling joins the one shared predicate (envelope and recoveries together). | "Addendum 2026-08-16." — the paragraph already states option A | | `#9061` (PR) | Implemented that option A. | the same | | `#11825` | Maintainer, 2026-08-25: retire the declarative `AdvancedPluginLifecycleConfig` container; the classes stay a host-driven library. | "… and as a host-driven library when the declarative lifecycle config container was retired" | | `#11846` | **404** — see Acceptance notes. | "maintainer ruling 2026-08-27 (Option A: remove)"; "(as the removal ruling recorded)" | | `#14478` | Ruling B, 2026-09-02: a no-baseline gate plus an ADR-0087 rename of every offender (`DriverOptions.timeout` among the seven named); ruling B again, 2026-09-05: the population is every authored and every runtime-emitted duration, minus exemptions declared on the schema. | "Maintainer ruling B on duration units (2026-09-02, its population widened on 2026-09-05 to every authored and every runtime-emitted duration, bar the exemptions a schema declares on the key itself)"; "the duration-unit rule (…)" | | `#14519` | The two tenant timeouts published a describe naming no unit (the unit sat in the JSDoc only); folded into the rename. | "the unit-nowhere shape (no unit in the name or in the published describe, first measured on two tenant timeouts)" | | `#15626` (PR) | Landed the gate and the seven founding renames, the tenant `idleTimeout` → `idleTimeoutSeconds` among them. | "The tenant half was already renamed, in the same change that landed the duration gate itself" | | `#15678` | `kernel/`: the 14 remaining duration keys carry their unit in the key name. | "the kernel-directory duration renames" / "the kernel-directory round"; trailing ids dropped | | `#15679` | `system/`: the 15 remaining duration keys carry their unit in the key name; `size` got an honest name. | "the system-directory duration round"; trailing ids dropped | | `#15939` | The gate did not read JSDoc. Ruled 2026-09-07: refuse the JSDoc / describe divergence. Ruled A 2026-09-11: remediate the population per file first, land the widened gate last. | "Director-seat ruling A of 2026-09-11 on the JSDoc-channel finding … a duration key whose JSDoc names a unit its describe does not is refused, and the keys in that shape are remediated per file before that refusal lands" | | `#16024` | Maintainer, 2026-09-06, per key: forward `timeout`, remove `localPath` and `wasm`. | "ruled per key by the maintainer on 2026-09-06, once all three of this package's unread config keys had been measured" | | `#17635` (PR) | The widened gate: refuse a duration key whose JSDoc names a unit its describe does not; landed last. | "lands that widened gate last, into a tree already clean" | | `#18669` | Ruling A, 2026-09-17: rename `FileValue.duration` and `estimatedMigrationTime`, each with an ADR-0087 entry; no new closed type, no narrowing of stored data. | "Maintainer ruling A of 2026-09-17 on the last two duration keys no closed duration type could express …" | | `cloud#1651` | **Not readable from this session** — see Acceptance notes. | "cloud — a census closed 2026-08-26: OS_PREVIEW_MODE there is a routing-only switch …" | No call-shaped token moves: a `name(` census over `registry.ts` is identical before and after (297 distinct tokens), so textual call-spelling ratchets read the same. ## Pin — `packages/cli/test/migrate-meta-engine-guidance.test.ts`, widened `COVERED_PREFIXES` is now `engine-`, `ui-`, `plugin-`, `driver-`, `kernel-`, `system-`. The pin still spawns the real CLI (`os migrate meta --from 16 --to 18`) once, locates each covered block **verbatim** in stdout, and asserts the printed block — `surface` included — carries no `#` plus 4 or 5 digits. Anti-vacuity: - the `REWRITTEN` floor rises from 29 to **55** ids: the 26 entries of this stage (7 `driver-`, 9 `kernel-`, 10 `system-`) are added, and every covered prefix must still select at least one entry; - presence in stdout is asserted before cleanliness (the `driver-sql-unresolvable-where-column-refused` reason carries two blank-line paragraph breaks, and its block is found verbatim); - the detector is exercised on both sides first (lit on 4 and 5 digits, dark on 3, 6 and `ADR-0112`). The file keeps its stage-1 name; the header lists the six covered families. ## Ablation — the widened pin can fail on a `driver-` block From committed state, HEAD `1c0dc7ad54`, with `scripts/ablation-replace.mjs` in wrap mode (it owns the restore trap) 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 `driver-sql-upsert-cross-row-identity-merge-refused`: anchor `pre-flight refusals closed the half` → `pre-flight refusals (#8621) closed the half`. The tool read anchor 1 → 0 and replacement 0 → 1, blob `b41e1d44` → `8f8d226e`. - **Mutate leg** (one lock turn: build, preflight, pin). Spec build exit 0. Preflight: marker present in 4 built files. Pin: **red**, `1 failed | 2 passed` — `driver-sql-upsert-cross-row-identity-merge-refused: the printed guidance cites a tracker id: expected '#8621' to be undefined`. - **Restore.** Tool-proven: blob `b41e1d44` == HEAD, `git diff HEAD` empty. - **Restore leg.** One lock turn, taken on the second try (the first waited out its 540 s budget, exit 99, NOT MEASURED, the tree already restored). Spec build exit 0. The `--absent` preflight found the marker in none of 222 built files, with the working tree clean against HEAD. Pin: **green**, `3 passed`. ## Verification Final head **`1c0dc7ad54`** for every line below; each heavy run went through `scripts/pm/os-verify-lock.sh` (one turn, `VERDICT command-exit 0`, per-step exits recorded separately). - **Build:** `pnpm exec turbo run build --concurrency=2 --filter='@objectstack/cli^...'` gives `Tasks: 55 successful, 55 total`. - **Pin and 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 pre-existing `skipIf`). - **Spec tests that read these entries or the registry:** `pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2 src/migrations src/kernel/preview-mode-retirement.test.ts scripts/build-schemas-check-mode.test.ts` plus the 19 other spec test files that read `MIGRATIONS_BY_MAJOR`, the registry or an entry file: `Test Files 24 passed`, `Tests 696 passed`. - **CLI unit:** `test/vitest-tiers-partition.test.ts` and `src/utils/spec-release-changes.test.ts`: `Test Files 2 passed`, `Tests 28 passed`. - **The call-spelling census that reads `registry.ts`:** `pnpm --filter @objectstack/driver-sql exec vitest run --maxWorkers=2 src/sql-driver-query-signature.test.ts` gives 15 passed. - **Typecheck:** `pnpm --filter @objectstack/spec typecheck` exits 0 (test layer: 53 files / 255 errors held in its ledger); `pnpm --filter @objectstack/cli typecheck` exits 0 (test layer: 3 files / 28 errors held, unchanged). - **Gate families:** `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` derives **89** families at `1c0dc7ad54` (after `git fetch origin main`). `--ran` over the recorded exit codes reads **89 derived, 89 run, 0 NOT-MEASURED, 0 UNRUN**, all exit 0. They include `check:doc-authoring` ("16466 customer-facing string(s) across 1135 spec sources clean"), `check:issue-citations`, `check:migration-registry` ("registry.ts is current (300 semantic, 219 retired-key, 199 retired-def)"), `check:spec-changes`, `check:upgrade-guide`, `check:generated` ("All 15 generated artifacts are up to date"), `check:duration-unit-keys`, `check:nul-bytes`, `check:adr-0087-registration` and `check:changeset-no-major`. - `check:dual-build-cjs-loads` refused first with `PREREQUISITE NOT MET` (exit 3: twelve packages outside the CLI closure had no `dist/`). Those `dist/` directories were written later in the same pass (04:48–04:49Z, inside the `check:type-check-debt` run, whose re-measure builds them); re-run at the same head it exits 0 (104 entries / 66 packages / 659 CJS files). The reconciled list takes that latest run. - **Lint (a proven narrowing, not the repo-wide run, which is CI's):** `eslint --no-inline-config --format json` over the 28 changed `.ts` files reports 28 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 28 are in it (no file-ignored warning). - 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:** see Acceptance notes (driver-free `merge-tree` against `862b6ce869` exits 0). ## Acceptance notes - **Two dead ids, rewritten from the code on `main`.** `#6075` and `#11846` answer 404 on both the issues and the pulls endpoint, re-probed with a 200 control (`#14478`). - `#6075` (`distinct`'s "never reached it" sentence): `packages/drivers/driver-sql/CHANGELOG.md` (commit `d367f03`) and `sql-driver-query-signature.test.ts` record what it did — the five drivers' implementations followed `IDataDriver`'s `DriverQuery` narrowing. The sentence now says exactly that. - `#11846` (preview mode): `packages/spec/CHANGELOG.md` (commit `0c2334f`), `packages/spec/src/kernel/context.zod.ts` and `preview-mode-retirement.test.ts` record the 2026-08-27 ruling (Option A: remove), the three-repo zero-consumer measurement and the re-declare-fresh condition. Dropped because `main` does not state them: "decision-inbox batch 2" and "all four decision facets pointed the same way". "The #11846 card records the measurement" (objectui leg) now reads "zero consumers, measured when the removal was ruled", which is what the changelog and the test header say. - **One cross-repo id this session cannot read.** `cloud#1651` answers 403 here: `objectstack-ai/cloud` is not attached to this session (`add_repo` refused: no access). It is neither confirmed nor refuted, so its sentence was rewritten from what `main` records about it — `packages/spec/CHANGELOG.md` (`0c2334f`: closed 2026-08-26 with positive controls, `RuntimeMode` zero hits, `ArtifactKernelFactory` 20+ hits and never touching `previewMode`) and `context.zod.ts` (`OS_PREVIEW_MODE` there is routing-only). The cloud-side detail `main` does not state — `previewMode` "only as a local variable" whose effect is adding wildcards to "CSRF" trusted origins — is dropped; the parenthesis that replaces it describes this repository's own `serve.ts` (the one reader of `OS_PREVIEW_MODE` here only widens better-auth's trusted origins to preview-domain wildcards), which is measured on `main`. - **Decision-batch numbers went too (invisible to the regex).** Nineteen sites cited a decision batch as `#` plus two or three digits (`#43` ×13, `#115` ×4, `#151` ×1, `#158` ×1). They are numbers an author is shown and cannot follow, so each is dropped. One consequence worth naming: 13 entries said `Maintainer ruling B on #14478 (2026-09-02, decision batch #43)`, which fused two rulings on the same card — B of 2026-09-02 (the gate and the no-baseline rename) and B of 2026-09-05, decided in that batch (the population: every authored and every runtime-emitted duration, minus schema-declared exemptions). The sentence now names both dates. The `#158` sentence (the agreement shape ruled an offence on 2026-09-18) is corroborated by `.changeset/18075-agreement-shape-is-an-offence.md` on `main`. - **"issue NNNN" / "PR NNNN" spellings, checked by hand.** No bare-number spelling exists in these 26 entries' author-shown text; the two `PR` citations (`PR #6404`, `PR #9061`) were `#`-spelled, so the instrument saw them and they are gone. The only `#` left in these 26 files is on `//` comment lines (sibling card's surface), including a `Prime Directive #13` reference. - **A citation whose page says something narrower than the text.** `kernel-health-check-and-hot-reload-durations-unit-in-key` called `shutdownTimeout`'s shape "the #14519 unit-nowhere shape". `#14519`'s keys carried their unit in the JSDoc; "unit nowhere" is the gate's name for it (`check-duration-unit-keys.ts` header: "no unit ANYWHERE (the #14519 shape)"), because the gate did not read JSDoc. The sentence now says what the shape is — no unit in the name or in the published describe — and that it was first measured on two tenant timeouts. - **A comment that my text edit makes slightly stale.** `18.system-metrics-window-durations-unit-in-key.ts` carries a `//` comment saying its acceptanceCriteria sentence "is #15679's, left word for word". That sentence now says "that JSDoc-channel gap is filed as a finding of its own" where it said "is #15939": same content, no number. The comment is the sibling card's surface (comment lines), so it is untouched here. - **Three "card" references re-anchored.** Removing an id left "the same card" in the Turso entry pointing at nothing; it now says "the same measurement". The kernel entries' "renamed by this same card" carry no number and were not otherwise rewritten, so they are left. - **Cross-PR check: no open PR adds or edits a `driver-`, `kernel-` or `system-` semantic entry.** Read at 2026-09-28T04:0xZ: the 18 open PRs' file lists (`GET /pulls/{n}/files`) carry 0 files matching `migrations/entries/semantic/NN.(driver|kernel|system)-*`. The Version Packages PR (`#17076`) lists more than 1,000 files; the 1,100 rows read carry no entry file, and it is the bot-generated release PR. Nothing in flight will be held by the widened pin on arrival. - **`main` moved 4 commits past the base** (`862b6ce869`: `#20364`, `#20341`, `#20366`, `#20352`); none touches `packages/spec/src/migrations/` or the pin. A driver-free bare-clone `merge-tree --write-tree` of this head against `862b6ce869` exits 0 with no conflicted path, so `registry.ts` needs no merge, and `main` was not merged in. - **Generated projections** (`spec-changes.json`, `docs/protocol-upgrade-guide.md`) are regenerated, as in stages 1 and 2; their `--check` legs are green. Only the three `driver-` entries registered at protocol 17 appear in them, which is why those diffs are small. - **No other test pins these entries' text.** A `git grep` of test files for the 26 entry ids finds one (`preview-mode-retirement.test.ts`), which names the entry in a comment and reads no prose; a grep of tests for the 37 cited numbers finds only comment lines. So no test needed re-pinning this stage (stage 2's `migrations.test.ts` case has no counterpart here). ## Line budget Entry files: **352 changed lines** (+228 / −124) across 26 files, against the stage-1 ≈400 budget. The whole diff is **776 lines** (+516 / −260) in 31 files. Of the rest, `registry.ts` is 352, the two projections are 18 (`spec-changes.json` 12, the upgrade guide 6), the widened pin is 33 and the changeset is 21. --- _Generated by [Claude Code](https://claude.ai/code/session_01QcAS3qiYYZNezaxZxaUdMV)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 15bf186 commit 0d7ed5a

31 files changed

Lines changed: 516 additions & 260 deletions

File tree

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
---
2+
'@objectstack/spec': patch
3+
---
4+
5+
fix(spec): `os migrate meta` guidance for the `driver-*`, `kernel-*` and `system-*` 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 `driver-*` family (the driver query-argument
10+
narrowings, the inert capability bits, the SQL driver's unresolvable-column and
11+
cross-row upsert refusals, and the retired Turso config keys), the `kernel-*` family
12+
(preview mode, and the kernel duration keys that now carry their unit in the key name)
13+
and the `system-*` family (the system duration keys renamed under the same rule) are
14+
printed by `os migrate meta` as the header, `why:` and `verify:` lines of a manual
15+
change. Their text sent the reader to issue-tracker and decision-batch numbers — some
16+
of which no longer resolve — for what a ruling, measurement or fix had decided; it now
17+
says what was decided, in the sentence being read. ADR ids are kept.
18+
19+
Text only: no entry id, `surface`, `from` / `to`, conversion or matching logic changes,
20+
and the chain rewrites exactly what it rewrote before. The generated migration registry,
21+
`spec-changes.json` and the protocol upgrade guide carry the same text.

‎docs/protocol-upgrade-guide.md‎

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

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

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,9 @@
22

33
/**
44
* `os migrate meta` — the guidance it prints for the ADR-0087 semantic entries
5-
* of the COVERED families (`engine-*`, `ui-*`, `plugin-*`) states each lesson
6-
* in words and carries no tracker number.
5+
* of the COVERED families (`engine-*`, `ui-*`, `plugin-*`, `driver-*`,
6+
* `kernel-*`, `system-*`) states each lesson in words and carries no tracker
7+
* number.
78
*
89
* ## What this pins
910
*
@@ -72,19 +73,35 @@ const TSX = resolve(HERE, '../../../node_modules/.bin/tsx');
7273
const TRACKER_ID = /#\d{4,5}\b/;
7374

7475
/** The families this pin holds, selected by entry-id prefix. */
75-
const COVERED_PREFIXES = ['engine-', 'ui-', 'plugin-'];
76+
const COVERED_PREFIXES = ['engine-', 'ui-', 'plugin-', 'driver-', 'kernel-', 'system-'];
7677

7778
/**
7879
* The entries rewritten when each family was brought to this line — the
7980
* anti-vacuity floor. A covered entry that carried no tracker id to begin with
8081
* is held by its prefix and needs no row here.
8182
*/
8283
const REWRITTEN = [
84+
'driver-aggregate-undeclared-key-aliases-removed',
85+
'driver-capabilities-inert-bits-removed',
86+
'driver-options-timeout-to-timeout-ms',
87+
'driver-sql-distinct-bare-filter-typed',
88+
'driver-sql-unresolvable-where-column-refused',
89+
'driver-sql-upsert-cross-row-identity-merge-refused',
90+
'driver-turso-config-local-path-wasm-retired',
8391
'engine-dotted-filter-refused',
8492
'engine-dotted-projection-refused',
8593
'engine-find-formula-filter-refused',
8694
'engine-find-formula-order-by-refused',
8795
'engine-update-upsert-retired',
96+
'kernel-compatibility-matrix-estimated-migration-time-unit-in-key',
97+
'kernel-context-preview-mode-retired',
98+
'kernel-event-bus-retention-unit-in-key',
99+
'kernel-health-check-and-hot-reload-durations-unit-in-key',
100+
'kernel-package-lifecycle-durations-unit-in-key',
101+
'kernel-plugin-health-report-durations-unit-in-key',
102+
'kernel-plugin-security-durations-unit-in-key',
103+
'kernel-runtime-config-timeout-unit-in-key',
104+
'kernel-startup-orchestrator-durations-unit-in-key',
88105
'plugin-activation-events-retired',
89106
'plugin-auto-restart-never-reinitialised',
90107
'plugin-manifest-contributes-dead-members-retired',
@@ -95,6 +112,16 @@ const REWRITTEN = [
95112
'plugin-runtime-family-retired',
96113
'plugin-security-scan-result-surface-retired',
97114
'plugin-security-scanner-retired',
115+
'system-cache-durations-unit-in-key',
116+
'system-collaboration-durations-unit-in-key',
117+
'system-failover-health-check-interval-unit-in-key',
118+
'system-metrics-jsdoc-durations-unit-in-key',
119+
'system-metrics-window-durations-unit-in-key',
120+
'system-object-storage-durations-unit-in-key',
121+
'system-registry-config-durations-unit-in-key',
122+
'system-tracing-otel-exporter-durations-unit-in-key',
123+
'system-tracing-span-duration-unit-in-key',
124+
'system-worker-queue-rate-limit-duration-unit-in-key',
98125
'ui-cloud-connection-widgets-unknown-keys-refused',
99126
'ui-form-field-length-malformed-refused',
100127
'ui-form-field-precision-scale-integer-refused',

‎packages/spec/spec-changes.json‎

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

‎packages/spec/src/migrations/entries/semantic/17.driver-aggregate-undeclared-key-aliases-removed.ts‎

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,11 @@ export const entry: SemanticMigration = {
2020
+ 'upgrade guide or release note. So this entry does not record a declared surface '
2121
+ 'being withdrawn; it records a LENIENCY being withdrawn, which is why it is here '
2222
+ 'rather than behind a tombstone. The only writers in this repository were the two '
23-
+ 'driver packages\' own fixtures — #4984\'s family, where a fixture spelling the alias '
24-
+ 'keeps the tolerant limb green forever and no test in existence can go red on its '
25-
+ 'deletion — so ADR-0049 enforce-or-remove applies once those are re-spelt. ⚠️ Do '
23+
+ 'driver packages\' own fixtures — the family of the org-axis red-line gate that read '
24+
+ 'only rejected aliases while its own fixtures spelt them, so its tests stayed green '
25+
+ 'and the rule stayed dead: a fixture spelling the alias keeps the tolerant limb green '
26+
+ 'forever and no test in existence can go red on its deletion — so ADR-0049 '
27+
+ 'enforce-or-remove applies once those are re-spelt. ⚠️ Do '
2628
+ 'NOT read this across to `dashboard`/`page` measures: `aggregate` IS the canonical '
2729
+ 'key there and `func` IS a declared, loudly-suggesting alias (`DatasetMeasureSchema`, '
2830
+ 'ui/dataset.zod.ts). That neighbouring vocabulary is untouched, and it is the most '
@@ -35,8 +37,14 @@ export const entry: SemanticMigration = {
3537
+ 'no enforced channel at all, which is exactly why this ledger entry has to exist: '
3638
+ 'the generated upgrade guide is the only way such a reader learns of the rename. '
3739
+ 'Same disposition, and the same reason, as `data-driver-find-stream-retired` '
38-
+ '(#4484), `storage-service-list-retired` (#5540) and `actor-user-roles-to-positions` '
39-
+ '(#6011). ADR-0049 / ADR-0087, #6321 (PR #6404).',
40+
+ '(`IDataDriver.findStream`, removed with no tombstone because nothing parses a driver '
41+
+ 'object), `storage-service-list-retired` (the zero-consumer `IStorageService.list`, '
42+
+ 'whose two adapters answered differently and both incompletely) and '
43+
+ '`actor-user-roles-to-positions` (the `ctx.user` `roles` alias, closed at once on the '
44+
+ 'maintainer\'s word rather than given a window). The removal ran in a fixed order — '
45+
+ 'the fixtures re-spelt first, the two alias branches deleted second, the parameter '
46+
+ 'narrowed to `DriverQuery` last — because the reverse order yields red nobody can '
47+
+ 'explain. ADR-0049 / ADR-0087.',
4048
acceptanceCriteria:
4149
'No caller passes `aggregate:` to a driver\'s `aggregate()`, and no aggregation entry '
4250
+ 'spells its function `func:`; both are written `aggregations:` / `function:`. An '

‎packages/spec/src/migrations/entries/semantic/17.driver-capabilities-inert-bits-removed.ts‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,10 @@ export const entry: SemanticMigration = {
2727
+ 'presence cannot carry the signal are `queryDateGranularity`, `autonumber` and '
2828
+ '`batchSchemaSync`)',
2929
reason:
30-
'The #4484 findStream close-out found `DriverCapabilities.streaming` pointing at a '
31-
+ 'capability the contract no longer declares, and the follow-up audit (#4634) checked '
30+
'Retiring `IDataDriver.findStream` (it had no production caller, and two of its three '
31+
+ 'implementations read the whole result set into memory before yielding a row) left '
32+
+ '`DriverCapabilities.streaming` pointing at a capability the contract no longer '
33+
+ 'declares, and the follow-up audit checked '
3234
+ 'every bit in the record the same way, across objectstack and cloud (objectui '
3335
+ 'confirmed clean): of 34 declared bits, THREE have a decision-making reader — '
3436
+ '`queryDateGranularity` (engine aggregate dispatch + checkDateBucketParity), '
@@ -47,14 +49,15 @@ export const entry: SemanticMigration = {
4749
+ 'stack metadata — `supports` literals live in driver classes and '
4850
+ '`DriverConfig.capabilities` is plugin TS configuration, neither ever a '
4951
+ '`sys_metadata` shape (the stack-tree neighbour, `datasource.capabilities`, was '
50-
+ 'retired separately in #4583) — so there is no source for the D2 chain to rewrite '
52+
+ 'retired separately, as a whole block nothing read) — so there is no source for the '
53+
+ 'D2 chain to rewrite '
5154
+ 'and this entry is the D3 record. The keys are tombstoned rather than deleted '
5255
+ 'because `DriverCapabilitiesSchema` is not `.strict()` and IS parsed '
5356
+ '(DriverConfigSchema / SQLDriverConfigSchema / NoSQLDriverConfigSchema embed it): '
5457
+ 'a plain delete would silently strip a vendor\'s authored bit, replacing one '
5558
+ 'silent no-op with another. `batchSchemaSync` also drops its `.default(false)` '
5659
+ 'for `.optional()` — absence already meant false at both readers, and the default '
57-
+ 'forced every capability object to spell out 30+ bits. ADR-0049 / ADR-0078, #4634.',
60+
+ 'forced every capability object to spell out 30+ bits. ADR-0049 / ADR-0078.',
5861
acceptanceCriteria:
5962
'No `supports` literal or `DriverConfig.capabilities` object authors any of the 31 '
6063
+ 'retired bits — a driver class that still writes one fails tsc against '

‎packages/spec/src/migrations/entries/semantic/17.driver-sql-distinct-bare-filter-typed.ts‎

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ export const entry: SemanticMigration = {
1313
reason:
1414
'This entry records a TYPE being added, not a surface being withdrawn, and it says '
1515
+ 'so up front because the distinction decides who has to do anything. `distinct` is '
16-
+ 'not declared on `IDataDriver`, so #5181 / #6075 never reached it and it kept '
16+
+ 'not declared on `IDataDriver`, so neither the narrowing of `IDataDriver`\'s query '
17+
+ 'parameters to `DriverQuery` nor the follow-through that brought five drivers\' '
18+
+ 'implementations in line ever reached it, and it kept '
1719
+ '`filters?: any` while its body said something far more specific — '
1820
+ '`applyFilters(builder, filters)` is handed the ARGUMENT ITSELF, never a `.where` '
1921
+ 'off it. ⚠️ RUNTIME BEHAVIOUR IS UNCHANGED by this entry\'s change: not one '
@@ -26,9 +28,9 @@ export const entry: SemanticMigration = {
2628
+ 'orders" answered with EVERY product, silently. That spelling is now TS2345 at the '
2729
+ 'call site. This is a driver CALL ARGUMENT — code, never stack metadata — so there '
2830
+ 'is no source for the D2 chain to rewrite and deliberately no schema tombstone, the '
29-
+ 'disposition `data-driver-find-stream-retired` (#4484), `storage-service-list-retired` '
30-
+ '(#5540), `actor-user-roles-to-positions` (#6011) and '
31-
+ '`driver-aggregate-undeclared-key-aliases-removed` (#6321) already carry. ⚠️ It '
31+
+ 'disposition `data-driver-find-stream-retired`, `storage-service-list-retired`, '
32+
+ '`actor-user-roles-to-positions` and '
33+
+ '`driver-aggregate-undeclared-key-aliases-removed` already carry. ⚠️ It '
3234
+ 'differs from those four in ONE measured way a reader should not have to infer: '
3335
+ 'because nothing changed at run time, an untyped JS caller is not affected BY THE '
3436
+ 'UPGRADE at all. The entry is here for a different reason — such a caller is exactly '
@@ -41,9 +43,11 @@ export const entry: SemanticMigration = {
4143
+ 'valid filter — one constraining columns named `object` and `where` — and so is a '
4244
+ 'FilterArray. Both reach `distinct` type-checked and are refused at run time, '
4345
+ 'loudly, with INVALID_FILTER / 400. `driver-memory`\'s opposite half — where the '
44-
+ 'BARE spelling returns the unfiltered set in silence — stayed open under the #5499 '
45-
+ 'freeze, which was lifted on 2026-08-11; it is still open, now unexcused rather than '
46-
+ 'deferred (#6320). ADR-0087, #6320.',
46+
+ 'BARE spelling returns the unfiltered set in silence — stayed open under the '
47+
+ 'maintainer\'s 2026-08-05 investment freeze on driver-memory, which was lifted on '
48+
+ '2026-08-11; it is still open, now unexcused rather than deferred (the measurement '
49+
+ 'that found the two drivers reading this argument differently split the fix: the sql '
50+
+ 'half is this entry, and the memory half was held back by that freeze). ADR-0087.',
4751
acceptanceCriteria:
4852
'No caller passes a non-object to `distinct()`\'s third argument. A scalar there is '
4953
+ 'now a compile error (`TS2345: Argument of type \'string\' is not assignable to '

‎packages/spec/src/migrations/entries/semantic/18.driver-options-timeout-to-timeout-ms.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ export const entry: SemanticMigration = {
77
surface: '`DriverOptions.timeout` (data/driver.zod.ts) — the per-call options argument of every `IDataDriver` method',
88
replacement: '`DriverOptions.timeoutMs` (milliseconds) — rename the key; the value is unchanged',
99
reason:
10-
'Maintainer ruling 2026-09-02 on #14478 (ruled B — no grandfathered baseline): the unit of a '
10+
'Maintainer ruling 2026-09-02 on duration units (ruled B — no grandfathered baseline): the unit of a '
1111
+ 'duration-shaped `z.number()` key lives in the key NAME, never only in the description. '
1212
+ '`timeout` said "Timeout in ms" in prose and nothing else. Tombstoned with retiredKey '
1313
+ '(`DriverOptionsSchema` is not strict, so a bare deletion would strip the old key in '

‎packages/spec/src/migrations/entries/semantic/18.driver-sql-unresolvable-where-column-refused.ts‎

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,9 @@ export const entry: SemanticMigration = {
1616
+ 'is no spelling of an unresolvable column that means "match nothing", which is '
1717
+ 'exactly what the old empty list was mistaken for',
1818
reason:
19-
'One predicate had two answers. `SqlDriver.findRows()` carries the #3821 unknown-'
20-
+ 'column recovery ladder, whose rungs are all built from `buildBase()` — and '
19+
'One predicate had two answers. `SqlDriver.findRows()` carries the unknown-column '
20+
+ 'recovery ladder (an unsortable query loses its ORDER BY, not its rows), whose rungs '
21+
+ 'are all built from `buildBase()` — and '
2122
+ '`buildBase()` always re-applies `query.where`. So the ladder can drop a projection '
2223
+ 'and can drop an ORDER BY, but it can never drop the clause that failed when the '
2324
+ 'unresolvable column is in the WHERE: both rungs raise the same error and the method '
@@ -33,25 +34,27 @@ export const entry: SemanticMigration = {
3334
+ 'agent reads "no matching records" and writes its next query on that belief. The '
3435
+ "thrown half was no better — the dialect's own `code`, no `status` (an unclassified "
3536
+ '5xx at the REST boundary rather than a caller mistake), and the statement\'s bound '
36-
+ 'literals inlined in the message, the same predicate-text disclosure shape #7929 '
37-
+ 'redacted elsewhere.\n\n'
38-
+ 'Ruled 2026-08-15 on #8790: refuse BOTH halves with `INVALID_FILTER` / 400, naming '
39-
+ 'the column. The envelope is not minted here — it is what every sibling refusal on '
37+
+ 'literals inlined in the message, the same predicate-text disclosure shape the '
38+
+ 'driver\'s field-reference filter refusals had already been made to stop echoing '
39+
+ '(the full diagnostic goes to the server log, never the response).\n\n'
40+
+ 'Ruled by the maintainer on 2026-08-15: refuse BOTH halves with `INVALID_FILTER` / '
41+
+ '400, naming the column. The envelope is not minted here — it is what every sibling '
42+
+ 'refusal on '
4043
+ 'this path already answers, required on both SQL drivers by '
4144
+ '`cross-field-conformance-cases.ts` and pinned by `sql-driver-boolean-identity.test.ts` '
4245
+ 'and `sql-driver-cross-field-conformance.test.ts` — so what closes is a declared-vs-'
43-
+ 'enforced gap, not a new posture. Recover-both was excluded by the card\'s own '
44-
+ 'argument: dropping a WHERE returns rows the caller explicitly excluded, and #3821\'s '
45-
+ '"rows matter more than their order" is an argument about how rows are PRESENTED, '
46-
+ 'which does not transfer to a predicate. The ladder KEEPS both of its recoveries — '
46+
+ 'enforced gap, not a new posture. Recover-both was excluded by the ruling\'s own '
47+
+ 'argument: dropping a WHERE returns rows the caller explicitly excluded, and the '
48+
+ 'ladder\'s own premise — rows matter more than their order — is an argument about '
49+
+ 'how rows are PRESENTED, which does not transfer to a predicate. The ladder KEEPS both of its recoveries — '
4750
+ 'only the WHERE-failure terminal became a refusal.\n\n'
4851
+ 'Reach, stated rather than assumed: the refusal fires on the wordings the ladder has '
4952
+ 'always recognised — SQLite (`no such column: x`) and Postgres (`column "x" does not '
5053
+ "exist`). MySQL spells it `Unknown column 'x' in 'where clause'`, which neither arm "
5154
+ 'matches, so on MySQL this condition still travels out as the raw dialect error; '
52-
+ 'widening that predicate would also hand MySQL the #3821 recoveries it has never had, '
55+
+ 'widening that predicate would also hand MySQL the ladder\'s recoveries it has never had, '
5356
+ 'which is an accept-set change in the opposite direction and is filed separately.\n\n'
54-
+ 'Addendum 2026-08-16 (#8926, landed by PR #9061). The paragraph above is kept as the '
57+
+ 'Addendum 2026-08-16. The paragraph above is kept as the '
5558
+ 'state at registration; this amends it. MySQL joined the one shared predicate, so the '
5659
+ 'reach is now all three dialects this driver speaks, and a MySQL reader must NOT '
5760
+ 'conclude the migration does not apply — it applies exactly as it does on SQLite and '
@@ -61,9 +64,9 @@ export const entry: SemanticMigration = {
6164
+ 'recoveries — was considered and refused). (1) THE ENVELOPE: an unresolvable WHERE '
6265
+ 'column now refuses with the same `INVALID_FILTER` / 400 naming the column, instead of '
6366
+ "travelling out as the raw `ER_BAD_FIELD_ERROR` with the statement's bound literals "
64-
+ 'inlined — the #7929 disclosure shape closed on the last dialect that still had it. '
65-
+ '(2) THE RECOVERIES: MySQL also gained the #3821 projection and ORDER-BY recoveries it '
66-
+ 'had never had, so an unresolvable column in a projection or an ORDER BY now returns '
67+
+ 'inlined — that disclosure shape closed on the last dialect that still had it. '
68+
+ '(2) THE RECOVERIES: MySQL also gained the ladder\'s projection and ORDER-BY '
69+
+ 'recoveries it had never had, so an unresolvable column in a projection or an ORDER BY now returns '
6770
+ 'recovered rows where it used to throw. The two halves arrive together because '
6871
+ '`ER_BAD_FIELD_ERROR` spells every clause position with one sentence — '
6972
+ "`Unknown column 'x' in 'where clause'` / `'field list'` / `'order clause'` — so all "
@@ -72,15 +75,17 @@ export const entry: SemanticMigration = {
7275
+ 'The widening can never drop a predicate: every ladder rung is rebuilt from '
7376
+ '`buildBase()`, which unconditionally re-applies `query.where`. Unchanged by the '
7477
+ 'ruling: a DOTTED filter key is still classified per dialect (Postgres raises '
75-
+ 'undefined_table, which neither arm matches), the axis #8371 owns. The entry id, '
78+
+ 'undefined_table, which neither arm matches), the axis owned by the dotted-filter '
79+
+ 'verdict, which refuses a dotted key whose head is a relation, a formula or a plain '
80+
+ 'column at the protocol and engine doors. The entry id, '
7681
+ 'surface and prescription are unchanged — this is a text amendment, not a new '
7782
+ 'migration.\n\n'
7883
+ 'This is a CODE-path API, not stored metadata, so — like '
7984
+ '`engine-dotted-projection-refused` and `engine-find-formula-filter-refused` — there '
8085
+ 'is no `sys_metadata` row for the D2 chain to rewrite and this entry is the '
8186
+ 'notification channel. No mechanical rewrite exists: the platform cannot know which '
8287
+ 'real column a mistyped filter key meant, and guessing one would answer with rows the '
83-
+ 'caller never asked for. #8790, #3821, #7929, #8371, ADR-0112.',
88+
+ 'caller never asked for. ADR-0112.',
8489
acceptanceCriteria:
8590
'No saved report `query.filter`, flow condition, sharing/permission rule or hook '
8691
+ 'filters on a name the queried object has no column for. Reads and counts complete '

0 commit comments

Comments
 (0)