docs(spec,formula): state the Expression contract in the present tense — drop the M9.1 / M9.2 phase language - #18557
Conversation
The M9.1 / M9.2 phase language is dropped from the six citation sites in `packages/spec/src/shared/expression.zod.ts` and from the two in `packages/formula`. `source` is the canonical persisted form; `ast` is an optional opaque structured value with no promise of becoming required; an evaluated slot requires `source`. No accept-set change. `EVALUATED_EXPRESSION_SOURCE_REQUIRED` loses the four words "of phase M9.1" and nothing else, which is exactly the wording the two pending changesets that quote it verbatim already carry. Claude-Session: https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3 Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift Check2 anchor(s) derived from 2 changed package(s); no hand-written page names any of them. What this run could not see
Coarse fallback — 137 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 1357dbd8e17560cd04f76edd79ccd818e8431a56 && git checkout 1357dbd8e17560cd04f76edd79ccd818e8431a56
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 79a046f8cdf085d95200826ee9bb2fa6584bc3d5 62ca2d6c5e408d5f597743ca1eb4c13c6fb9797b && git checkout -B drift-repro 79a046f8cdf085d95200826ee9bb2fa6584bc3d5 && git merge --no-ff 62ca2d6c5e408d5f597743ca1eb4c13c6fb9797b
node scripts/docs-audit/affected-docs.mjs --json 79a046f8cdf085d95200826ee9bb2fa6584bc3d5 |
…red M9.2 promise (objectstack-ai#18569) Part of objectstack-ai#17849 Clause-②: no The second half of objectstack-ai#17849, split out of PR objectstack-ai#18557 because AGENTS.md's Documentation Guardrails row for `packages/*/CHANGELOG.md` requires it: an already-published entry is amended **in a dedicated docs-only PR**, ⛔ never as a rider on code changes. This diff is exactly one file and adds no source, no schema and no export. objectstack-ai#17323's ruling item 2 orders it: 「`packages/spec/CHANGELOG.md`: an erratum line under the entry that promised M9.2 (the objectstack-ai#17026 shape — correct the published text, note the date), ⛔ not a rewrite of history.」 ## What was corrected, and where One entry: `ef3a138` (`feat(spec)!: an evaluated expression slot requires a non-blank source`) under the already-published `## 17.4.0`. Line numbers re-derived on this tree at `79a046f8c`, ⛔ not carried over from the card or from the earlier round: | line | published text | disposition | |--:|:--|:--| | 1096 | the blockquote quoting `EVALUATED_EXPRESSION_SOURCE_REQUIRED`, parenthetical `(the canonical persisted form of phase M9.1)` | **left exactly as shipped** — it is a faithful quote of what 17.4.0 published. A paragraph under it says what the constant reads now. | | 1103 | 「its docblock declares that `ast` becomes required in build output at phase M9.2」 | corrected in place, old words kept as a marked quotation | | 1119 | 「has no evaluable form under M9.1」 | corrected in place, old words kept as a marked quotation | One adjacent clause falsified by the same ruling is corrected in the same stroke and **named here rather than smuggled**: the bullet also read 「when AST-only evaluation lands」, which presupposes the retired promise. It now reads 「if AST-only evaluation is ever chartered」 — the wording the two pending changesets already carry. Leaving it would have left a 「when it lands」 sitting beside 「no promise of becoming required」 in one bullet. One dated erratum line closes the entry, carrying the in-repo tail this repository already uses in five places (`packages/spec/CHANGELOG.md` ×3 at `:2457`, `:2882`, `:5277`, `packages/lint/CHANGELOG.md:1214`, `packages/metadata-protocol/CHANGELOG.md:134`): ``` *Erratum, 2026-09-17 — the M9.1 / M9.2 phase promise this entry restated was retired by the ruling on objectstack-ai#17323 (2026-09-12) … (Corrected after publication, objectstack-ai#17849.)* ``` ⛔ No new entry at the top, ⛔ no version heading added (`git diff -U0 | grep -c '^+## '` → 0), ⛔ nothing this release published is changed. ##⚠️ The card's grep criterion cannot be met, and the ruling is why The card sets `git grep -l 'M9\.[12]' origin/main -- packages` → 0. After this PR that file still carries **four** hits, and every one of them is required by the ruling's own 「⛔ not a rewrite of history」: ``` :1096 the as-shipped blockquote (deliberately untouched) :1109 "…becomes required in build output at phase M9.2" inside `As published, that sentence continued "…"` :1130 "no evaluable form under M9.1" inside `As published that clause read "…"` :1142 the erratum line itself, naming the retired promise ``` Not one of them is a live assertion of the promise — they are the quotation marks the objectstack-ai#17026 shape puts around it, plus the erratum that retires it. A zero would require deleting the published words, which is precisely the rewrite the ruling forbids. ⇒ the criterion and the ruling are not jointly satisfiable, and the ruling governs. Reported rather than forced. ##⚠️ `skip-changeset` — checked against objectstack-ai#18375 before relying on it The label is applied. Before applying it I re-read the refusal objectstack-ai#18375 is about, `scripts/check-empty-changeset.mjs`, and it does **not** reach this PR: - that gate has two rules and **both take the `.changeset/` diff and nothing else** — rule 1 fires on a newly ADDED empty-frontmatter changeset, rule 2 (`scanForeign()`) on a MODIFIED or DELETED changeset that exists on the merge base; - the DELIBERATE-CORRECTION text 「no label and no diff shape makes that safe」 is scoped, in its own words, to 「the note you rewrote describes behaviour THIS PR changed」 — a **pending release note**, i.e. a `.changeset/*.md`; - **this diff contains no `.changeset/` path at all** (`git diff --name-only origin/main...HEAD` → one line, `packages/spec/CHANGELOG.md`). So the label suppresses no refusal that could have fired here, and the finding's hazard is absent rather than accepted. What the label IS doing is the documented job: `Check Changeset` requires an added changeset from **every** PR, with no path filter, so `skip-changeset` is the only instrument for a diff that releases nothing of its own — the same instrument the objectstack-ai#17026 ruling named (item 3) and PR objectstack-ai#17896 spent for the same shape. ## Verification Exit codes landed to disk before reading, ⛔ never through a pipe. | run | verdict | |:--|:--| | derived gate families for this one path | 55 derived · **51 run green** · 4 NOT MEASURED | | `--ran` reconciliation with per-family exit codes | `55 derived famil(ies) accounted for — 51 run, 4 NOT-MEASURED (4 DERIVED from a recorded exit 3)` | | `check-release-section-coverage` (plain · `--self-test` · `--strict`) | exit 0 ×3 — 7 published minors across 2 GA majors, every one still covered | | `pnpm check:release-notes`, `pnpm check:release-page-status` | exit 0 | | `pnpm lint` (`eslint . --no-inline-config`, whole repo) | exit 0 | | `pnpm check:nul-bytes` + a direct control-character sweep of the file | exit 0 / no hits | The 4 NOT MEASURED are `check:dts-closure`, `check:dual-build-cjs-loads`, `check:lean-entry-closure` and `check:sourcemap-no-sources-content` — all exit **3 = PREREQUISITE NOT MET** (they read the `dist/` of packages this tree never built). ⛔ Neither a pass nor a failure; CI's `Build Core` runs them. **No test is owed and that is measured, not assumed.** Four test files name a `CHANGELOG.md` path and **every one of them excludes it**: `compliance-families-retirement.test.ts` skips `CHANGELOG.md`, both `action-owner-key-single-source.test.ts` files list it under `covers` as 「a published CHANGELOG is the record of the removal itself」, and `template-consistency.test.ts` passes `:(exclude)**/CHANGELOG.md`. No test reads the bytes this PR moves. **The `changeset version` survival question is already answered and is not re-run here.** objectstack-ai#17026's round measured it with a lit control: a manual edit inside an already-compiled section SURVIVES `changeset version` (marker present before and after, while the tool provably re-wrote both files — a new version section prepended, the package version advanced, 250 pending changesets consumed). That reading stands; re-running it would buy nothing. ## Acceptance notes Confirming reading for the finding recorded on objectstack-ai#17849: `pnpm check:cross-package-test-inputs` is **green (exit 0)** on this tree, which has no `packages/spec/dist` because a markdown-only diff needs no build. Same gate, same commit base, opposite verdict from the built tree — a third independent leg for the same cause. ⛔ Not filed again; it is already objectstack-ai#18353 and objectstack-ai#18440. Noted, not filed: nothing else. Successor for the `M9.9b` / `M9.5` / `M9.7` ROADMAP citations elsewhere in the tree: none — no PR or person is routed to those files by this work, and the ruling names only the M9.1 / M9.2 phase promise. --- _Generated by [Claude Code](https://claude.ai/code/session_01JbZnqu8bt6YqfJsr9vaFb3)_ Co-authored-by: Claude <noreply@anthropic.com>
…L `visible` (objectstack-ai#17631) (objectstack-ai#18575) Fixes objectstack-ai#17631 Clause-②: no `lowerRequiresFeature` lowers the `requiresFeature: 'FLAG_NAME'` sugar into the canonical `visible` CEL predicate, and its own docblock states the ADR-0078 rule it enforces: a composition that could never take effect is a loud parse error, not a silent one. The guard enforcing that rule tested the **type** of `source` — `typeof existing.source !== 'string'` — so a whitespace-only `source`, legal on `ExpressionSchema` (the persistence contract, whose `min(1)` whitespace clears), passed it and the feature gate was composed **around a blank operand**. The guard produced exactly the shape it exists to reject. ## Measured, this branch, on `f962be9d08` The card's four-row probe on `ActionSchema`, reproduced before touching anything — all four rows came out as filed: | case | before (`f962be9d08`) | after | |:---|:---|:---| | ast-only `visible`, NO `requiresFeature` | `parses=true`, envelope verbatim | unchanged | | ast-only `visible`, WITH `requiresFeature` | `parses=false` (AST-only refusal) | unchanged | | blank-src `visible`, NO `requiresFeature` | `parses=true`, `{"dialect":"cel","source":" "}` | **unchanged — the persistence contract is not narrowed** | | blank-src `visible`, WITH `requiresFeature` | `parses=true`, `source: "( ) && features.organization != false"` | **`parses=false`, loud refusal at `requiresFeature`** | And the composed predicate, through `@objectstack/formula` `celEngine.evaluate`, with a control leg differing only in the source: ``` "( ) && record.x == 1" ok=false kind=parse "Unexpected token: RPAREN" "(record.x == 1) && record.x == 1" ok=true value=true # CONTROL, identical composition, real source ``` So at render the gate faults instead of gating: fail-soft surfaces show the element regardless of the flag, fail-closed surfaces hide it regardless of the flag. Either way the flag decides nothing. ## The repair, and why it is this one and not the wider one Two repairs were on the table and they differ in blast radius. This PR takes the guard, **on the merits**, not because it is the smaller diff: - **The defective string is produced here.** `( ) && features.X != false` is built by the composition step in `lowerRequiresFeature`. Contract-first says refuse at the producer; this is the producer. - **The invariant being restored is this function's own**, stated in its docblock and cited to ADR-0078 one bullet above the leg that was missing. - **It covers every slot that composes the sugar at once** — `ActionSchema.visible` and `ActionParamSchema.visible` today, and whatever composes it next — because the rule lives in the shared lowering rather than in either slot's declaration. - **The wider repair would narrow something this card does not own.** Composing `visible` onto `EvaluatedExpressionInputSchema` also refuses a blank-`source` `visible` with **no** `requiresFeature` beside it — row 3 above, which parses clean today by design. objectstack-ai#17631's own filing says that is one of the per-slot decisions objectstack-ai#15811's census enumerates and "may want to travel with that family rather than alone". - **And the `expression.zod.ts` spelling of it is refused by that file's own declaration**, not merely by scope: `EvaluatedExpressionSchema`'s docblock states "`ExpressionSchema` itself is NOT narrowed: it is the persistence contract". The evaluated-slot rule already exists there as `EvaluatedExpressionInputSchema`; nothing in that file needed to move, and nothing in it did. It was declared read-only for this card (in flight as objectstack-ai#17849 / PR objectstack-ai#18557) and was read, never edited. ## What changed - `packages/spec/src/kernel/public-auth-features.ts` — the lowering refuses a `source` that is blank after trimming, on the same leg as the AST-only refusal above it. The notion of blank is `source.trim()`, the one the engine's own helpers apply, so a `source` merely **padded** around real text still composes verbatim. The refusal names the composition it would have produced and both exits (drop the blank `visible` and the sugar emits the gate alone; or put the predicate the gate should compose with in `source`). - `packages/spec/src/kernel/public-auth-features.test.ts` — the new refusal is pinned in the same shape as the AST-only pin beside it (issue **kind** and **subject**, never the wording), across three blank spellings, plus the assertion that no envelope is produced. A second pin fixes the boundary the refusal must not cross: a padded-but-authored source still composes. - `.changeset/17631-requires-feature-blank-source.md` — `patch`. ## Reverse verification Committed first, then mutated, then restored — the mutation proven on disk by occurrence count and by blob hash, the restore proven by blob hash equality with `HEAD` plus an empty `git diff HEAD`, under a `trap ... EXIT INT TERM` with absolute paths. The test resolves its subject through a **relative `./public-auth-features` import**, not through the package `exports`, so no rebuild sits between the edit and the verdict. Mutation: `if (existing.source.trim().length === 0) {` -> `if (false) {` (the pre-fix behaviour). Predicted direction: red. ``` anchor occurrences BEFORE mutation: 1 anchor occurrences AFTER mutation: old=0 new=1 MUTATED_BLOB=81814bc9d6e408eae96650bac701fda5ee38841f (differs from HEAD: yes) FAIL src/kernel/public-auth-features.test.ts > lowerRequiresFeature > rejects a CEL visible whose source is blank after trimming (ADR-0078) AssertionError: expected [] to have a length of 1 but got +0 Test Files 1 failed (1) Tests 1 failed | 23 passed (24) RESTORED ok (blob 59e27e1, git diff HEAD empty) ``` Exactly the new pin went red and the other 23 stayed green, so the mutation is targeted and the pin is discriminating. ## One gate finding this change caused, and where it was fixed The refusal was first written interpolating the composed gate. That put `featureGatePredicate` — and through it the whole `PUBLIC_AUTH_FEATURES` registry — in a customer-facing `message:` position, and `check:doc-authoring`'s per-module fixed point then swept that registry's **internal** `notes` / `exempt.reason` prose as customer-facing text, flagging three pre-existing strings (`:197`, `:220`, `:230`). Measured with a control leg rather than assumed: the same gate is **green** with `packages/spec/src/kernel/public-auth-features.ts` reverted to `f962be9d08` (blob equality asserted both ways), and red with the interpolation in. So the red was this diff's, not a pre-existing one. Fixed at the cause — the sentence now names the composed shape without interpolating the gate, which is also how the two refusals beside it are written — rather than by editing three unrelated strings in the registry. `pnpm check:doc-authoring` is green again: 15502 customer-facing strings across 993 spec sources clean. ## Acceptance notes - **Noted, not filed** — `check:doc-authoring`'s hoisted-const fixed point is a per-module over-approximation: one field of a const reaching a `message:` position (`semantics`, here) makes every string in that const customer-facing, including registry notes no customer can ever see. The gate's own output declares this spelling ("hoisted text-sink consts (fixed-point, per module)"), so it is designed behaviour rather than a contract violation, and it is cheap to work around at the call site. Successor: none identified — no queued card touches this gate's climb. - **Declared gap, not a deviation hidden** — the AST-only leg is pinned twice, in `kernel/public-auth-features.test.ts` and in `ui/action.test.ts`. This PR adds the first of those two. `ui/action.test.ts` is declared read-only for this card, so the schema-level sibling pin is not in this diff; the schema-level behaviour is measured in the table above instead. A seat holding that file can add it in one `it` block. ## Gates Derived for the actual changed paths with `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands` (three-dot against the merge base; committed + working tree + untracked), then reconciled with `--ran` carrying every exit code. Each exit code landed to disk before being read — never through a pipe. ``` Run reconciliation — 83 derived, 83 run, 0 NOT-MEASURED, 0 UNRUN. ✓ dispatch-gates --ran: 83 derived famil(ies) accounted for — 83 run, 0 NOT-MEASURED (a DERIVED zero — all 83 recorded an exit code and none of them is 3). ``` - **82 of 83 green**, including `check:api-surface`, `check:authorable-surface`, `check:docs`, `check:liveness`, `check:adr-0087-registration`, `check:changeset-no-major`, `check:nul-bytes`, `check:doc-authoring`, `check:type-check-coverage`, `check:type-check-debt`, `check:test-source-alias`, `check:dispatcher-error-vocabulary`, `check:where-matcher`. - Four of them first answered **exit 3 (PREREQUISITE NOT MET — not a finding)** because they read built output: `check:doc-formula-expressions`, `check:dual-build-cjs-loads`, `check:lean-entry-closure`, `check:type-check-debt`. The workspace closure was built (`turbo run build --filter='./packages/*' --filter='./packages/*/*'`, 72/72 successful) and all four then exited 0. They are measured, not declared away. - `pnpm --filter @objectstack/spec typecheck` green; `pnpm --filter @objectstack/spec test` green — **483 files, 13775 tests** — both re-run at the final head, after the refusal sentence changed. - `pnpm --filter @objectstack/spec check:generated` — **all 15 generated artifacts up to date**, nothing to regenerate. - `pnpm lint` (`eslint . --no-inline-config`, the whole repo, no narrowing) green at `ec0eaed2d2`, the final commit. - `node scripts/pm/check-clause2-carriers.mjs --pair 18575` — **exit 0**: the clause-② declaration is readable in the fixed spelling, both carriers agree, and the diff carries no widening tell. No `needs:contract-review` label is on this PR; hanging or clearing it is the seat's, not this PR's. - The one red: `pnpm check:cross-package-test-inputs` **exit 1**, signature `@objectstack/cli descends a directory tree from packages/spec/dist/`. That is the known defect already carried by **objectstack-ai#18353** and **objectstack-ai#18440** — it exits 1 on any tree where `packages/spec` has been built and 0 when it has not, ablated both ways by two devs this session. Not filed again, and unrelated to this diff. ## 维护者速读(草稿) **改了什么** —— `requiresFeature` 这个语法糖在和一个 `source` 只有空白字符的 `visible` 组合时,会生成 `( ) && features.X != false` 这种任何作用域下都解析不了的谓词。现在它当场报错拒绝,而不是悄悄合成一个永远跑不起来的门。 **为什么改** —— 这正是 `lowerRequiresFeature` 自己文档里援引 ADR-0078 要拒绝的形态:声明解析通过、却什么都不改变。作者以为加了特性开关,实际上渲染时谓词直接解析失败 —— 容错的界面把元素照常显示出来(开关形同虚设),严格的界面无论开关如何都把它藏起来。两种结局都是开关不起作用。 **风险与代价(含回滚)** —— 这是一次接受集收窄:今天能解析通过的一种写法,以后会被拒绝。但被拒绝的那种写法在运行时本来就必然失败,所以没有任何能正常工作的写法被破坏,也就没有迁移负担 —— 拒绝信息本身就是处方。`ExpressionSchema` / `ExpressionInputSchema`(持久化契约)一个字节没动,不带 `requiresFeature` 的空白 `source` 照旧解析通过。回滚 = 撤销这个 PR,没有数据迁移、没有生成物、没有已发布的注册表条目参与。 **席位意见** —— **你要做的** —— 无需操作。这是一个 `patch` 级修复,不触及受管面,按常规队列落地即可。 --- _Generated by [Claude Code](https://claude.ai/code)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Part of #17849
Clause-②: no
The spec-seat half of #17323, ruled B by the director seat (comment
5644710461, decision batch #123 item 1, 2026-09-12; maintainer verbatim 「同意」). The M9.1 / M9.2 phase language is dropped from the citation sites inpackages/specandpackages/formula, and the contract each of them describes is restated in the present tense. ⛔ No accept-set change: no key is added, renamed, re-typed or retired, and every envelope that parsed before parses byte-identically.The docs half (PR #17851,
Part of #17323) merged on 2026-09-12T13:17:02Z, socontent/docs/data-modeling/formulas.mdxand the two pending changesets already carry the new wording. This PR is the other half the ruling's item 4 assigns to this seat —packages/formulaincluded, which the domain table callsdomain:engineand the ruling puts here verbatim.What the text says now
sourceis the canonical persisted form — it is what the engine evaluates.astis accepted beside it as an optional opaque structured value (each engine validates its own shape), and it carries no promise of becoming required.source;EvaluatedExpressionSchemais where that is spelled out.M9\.[12]hits at79a046fpackages/spec/src/shared/expression.zod.tspackages/formula/src/cel-engine.tspackages/formula/src/normalize.tsLit control for the same probe:
ExpressionSchemainexpression.zod.tsreads 19, so the zero is a reading and not a broken path.cel-engine.ts's AST-only arm also carried anM9.7promise ("revisit AST-only execution in M9.7 when we cut the spec persistence over") inside the same sentence as itsM9.1hit. It goes with the sentence rather than being left as a phase promise standing beside a de-phased one; it is named here rather than smuggled.The verbatim-quote constraint, and the proof it holds
Coordination note
5645945565: the two pending changesets quote the exported constantEVALUATED_EXPRESSION_SOURCE_REQUIREDVERBATIM inside a blockquote, and the docs half already edited them to read 「(the canonical persisted form)」. The constant loses exactly the four wordsof phase M9.1and nothing else, so the quote stays a quote. Proved mechanically rather than by eye — the constant's runtime value was reassembled from the source and compared with each changeset's blockquote, joined:No consumer parses that sentence: every call site imports the constant by name, and the message assertions in
packages/lint,packages/services/service-automationandpackages/specread substrings that never contained the phase id — the non-blank-source clause and the cannot-evaluate-ast clause.packages/spec/CHANGELOG.mderratum is NOT in this PRThe card's work items and the ruling's item 2 also name an erratum line under the already-published
## 17.4.0entry that promised M9.2 (3 hits at:1096,:1103,:1119). It is deliberately not carried here, because AGENTS.md's Documentation Guardrails row forpackages/*/CHANGELOG.mdis categorical about the packaging:This PR is a code PR: it moves a published string and owes a
patchchangeset. The ruling names 「the #17026 shape」 for the erratum, and that shape — read off the card #17026 and PR #17896 that executed it — is a dedicated docs-only PR withskip-changeset, whose executing seat quoted this same AGENTS.md row verbatim as its authorization chain. So the ruling and the guardrail agree; only the card's file-surface bundling puts them in one PR.⇒ The erratum is owed as a separate docs-only PR, and #17849 stays open for it — which is why this body reads
Part of #17849and not the closing form the dispatch asked for. The residual after this PR, measured on this branch:Nothing under
content/remains; the docs half cleared it.Verification
Everything below is a real local run, exit codes landed to disk before reading (⛔ never through a pipe).
pnpm --filter @objectstack/spec --filter @objectstack/formula buildVERDICT command-exit 0(shared verify lock)pnpm --filter @objectstack/spec check:generated.describe()moved)pnpm --filter @objectstack/spec --filter @objectstack/formula typecheckpnpm --filter @objectstack/spec --filter @objectstack/formula testpnpm lint(eslint . --no-inline-config, whole repo)dispatch-gates.mjs --repo objectstack-ai/objectstack --commands)--ranreconciliation with exit codes recorded per family:78 derived famil(ies) accounted for — 76 run, 2 NOT-MEASURED (2 DERIVED from a recorded exit 3). The two arecheck:dual-build-cjs-loadsandcheck:lean-entry-closure, both exit 3 = PREREQUISITE NOT MET (they read thedist/of ~80 packages this tree never built) — ⛔ neither a pass nor a failure; CI'sBuild Coreis where they run.Two families needed a prerequisite before they could answer, and both are green once given it:
check:doc-formula-expressions(exit 3 until@objectstack/lintwas built, then exit 0) andcheck-plugin-teardown-shape --self-test(exit 1 on this shallow clone until its pinned fixture commit was fetched, then exit 0 / 47 cases).Acceptance notes
pnpm check:cross-package-test-inputs(exit 1), flagging@objectstack/cli descends a directory tree from packages/spec/dist/, rooted inpackages/cli/test/init-created-files-summary.e2e.test.ts. Ablated to the cause on a pristineorigin/mainworktree with zero diff:⇒ the verdict turns on whether the gitignored
packages/spec/distexists, not on any source change — and buildingpackages/specis exactly what AGENTS.md requires before the spec artifact gates run. CI cannot see it:Lint & Repo Gatesinstalls and never builds, so the gate is green there. Filed as a finding for the dispatching seat rather than fixed here —packages/cliandscripts/check-cross-package-test-inputs.mjsare both outside this card's file surface.Noted, not filed: nothing else. The three source files were read whole and carry no other stale phase promise; the remaining
M9.xmentions in the tree (M9.9bindefault-value-shape.ts,action.zod.ts,objectql/engine.ts;M9.5/M9.7inobjectqland the docs page) are ROADMAP section references outside the ruling's seven citation sites, and the ruling names only the M9.1 / M9.2 phase promise.Generated by Claude Code