Conversation
…ed slots Claude-Session: https://claude.ai/code/session_01LvwGppdonww4zGLWZo5rho Co-authored-by: Claude <[email protected]>
…y envelope Claude-Session: https://claude.ai/code/session_01LvwGppdonww4zGLWZo5rho Co-authored-by: Claude <[email protected]>
…ing arm Claude-Session: https://claude.ai/code/session_01LvwGppdonww4zGLWZo5rho Co-authored-by: Claude <[email protected]>
…ns and reference docs Claude-Session: https://claude.ai/code/session_01LvwGppdonww4zGLWZo5rho Co-authored-by: Claude <[email protected]>
📓 Docs Drift CheckThis PR changes 2 package(s): 25 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 8 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. 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 7fd2c84760bf25a2ef28ba0062f311cafbc5f01b && git checkout 7fd2c84760bf25a2ef28ba0062f311cafbc5f01b
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 1bc22b3dcddc8a30b4826da8625e7787d5518a8f 9f7013fc2554a6456fa3e5474937b6fc5f5734e2 && git checkout -B drift-repro 1bc22b3dcddc8a30b4826da8625e7787d5518a8f && git merge --no-ff 9f7013fc2554a6456fa3e5474937b6fc5f5734e2
node scripts/docs-audit/affected-docs.mjs --json 1bc22b3dcddc8a30b4826da8625e7787d5518a8f
|
A narrowing PR that grows the published export surface widens on a second axis. `shared/evaluated-slot-union.ts` follows the `union-branch-policy` convention: reachable inside `@objectstack/spec`, absent from both barrels, so `api-surface/` and `export-origins/` do not move for it. Claude-Session: https://claude.ai/code/session_01LvwGppdonww4zGLWZo5rho Co-authored-by: Claude <[email protected]>
Claude-Session: https://claude.ai/code/session_01LvwGppdonww4zGLWZo5rho Co-authored-by: Claude <[email protected]>
Seat verdict: REWORK — the review PASSed and two of its named corrections ship false textSeat What the review settled, and it settled it well⭐ The central question — whether It also gives #18640 its root cause: the instrument's #17618 three-fact spend requires the added member list to be a textual subset of the removed one, and here the member was renamed. ⛔ The gate's prescribed clear is a matcher repair with a self-test case, ⛔ never a false What must change — inside this PR's own surface
⛔ Not owed — do not re-open
Carried, not foldedThe review named two things that are ⛔ not this PR's: Generated by Claude Code |
…s messages
The changeset and the ADR-0087 migration entry both said the union-member
positions leave their sibling arm untouched. Measured on this branch's base
`00115a8442` and at head: true for `RecordAlertProps.visible` (boolean) and
`ServiceLevelIndicator.successCriteria` (structured object), FALSE for
`TraceSamplingConfig.composite[].condition`, whose record arm gained a
`.refine()` and refuses six shapes the base accepted through that arm alone.
A changeset becomes the CHANGELOG and a migration entry becomes the migration
ledger, so both now state the narrowing, its FROM -> TO and its control.
The refine becomes aborting. That is about the MESSAGE and never the accept
set (measured identical either way): non-aborting, it was the surviving arm
for every expression-shaped refusal here, so a blank `source` collided with it
and the slot published a bare `Invalid input` while the sentence sat nested,
and `{ dialect: 'js', source: 'x' }` was refused with a sentence about
`source` that misnames its fault. Aborting hands each refusal back to its
owner: one `custom` issue at `source` for a blank `source`, one
`invalid_union` carrying the published sentence for an `ast`-only envelope or
a blank bare string.
Pins: the tracing slot's accept set, both blank spellings' published sentence,
and the negative (a non-`source` fault is not blamed on `source`); and the
population pin now covers all three refused spellings at all 36 positions
instead of only the `ast`-only one.
Claude-Session: https://claude.ai/code/session_01LvwGppdonww4zGLWZo5rho
Co-authored-by: Claude <[email protected]>
…nce page `gen:migration-registry` picks up the corrected surface / acceptance text, and `gen:docs` republishes `TraceSamplingConfig.composite[].condition` with the `dialect` rule its schema enforces. Claude-Session: https://claude.ai/code/session_01LvwGppdonww4zGLWZo5rho Co-authored-by: Claude <[email protected]>
…aluated-slot-narrowing
…ribe `check:doc-authoring` flags an internal issue id in customer-facing spec text (maintainer ruling 2026-08-12). The rule the describe publishes is unchanged; only the trailing reference is gone, and the reference page is regenerated. Claude-Session: https://claude.ai/code/session_01LvwGppdonww4zGLWZo5rho Co-authored-by: Claude <[email protected]>
Contract reviewServed-tier: ① Derived judgments
② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS
Generated by Claude Code |
契约复核记录 — 交付后复核(delta 轮),档位
|
⛔ 落地阻断 —— 契约复核 PASS,但本 PR 在 CI 上真红 16 条,且红是本 PR 自己造成的
两件事互不矛盾,先说清楚契约复核的 PASS 成立且在其职权内:它测的是契约语义(接受集、semver 档、公开面),它在自己的隔离检出里只构建了 入队资格是本席的职权:「入队资格 = 每个 check 为 success 或预期 skip,⛔ 不是 required 子集」。本席读了,不合格。 实测(head
|
| 结论 | 条数 |
|---|---|
| 在 main 上也红 | 1 —— TypeScript Type Check |
| 在 main 上是 success,只在本 PR 红 | 16 |
⭐ 仪器是亮的:对照表两个分支都取到了值 —— TypeScript Type Check 命中「main 上也红」,证明该分支可达;⛔ 不是一个永远只会答「你的锅」的坏仪器。
在 main 上绿、只在本 PR 红的 16 条:
Build Core · Test Core + 6 个分片 · Dogfood Regression Gate + 3 个分片 · Dogfood Verify CLI · Type Check · workspace · Type Check · consumer gates · Type Check · debt ledger
根因 —— 一条,不是十六条
十六条全部塌在同一个包的 DTS 构建上(Failed: @objectstack/platform-objects#build),错误逐字:
src/pages/sys-user.page.ts(88,9): error TS2322: Type '{ dialect: "cel" | "cron" | "template";
source?: string | undefined; ast?: unknown; meta?: {...} | undefined; }'
is not assignable to type 'string | { dialect: ...; source: string; ... } | undefined'.
Types of property 'source' are incompatible.
Type 'string | undefined' is not assignable to type 'string'.
⇒ 这正是本 PR 的收窄本身:EvaluatedExpression* 要求 source: string,而 packages/platform-objects/src/pages/sys-user.page.ts:88 仍然递进一个 source 可选的信封。一个第一方消费者没有跟着改。
@objectstack/platform-objects#build 一挂,Build Core、三条 Type Check、六个 Test Core 分片、四条 Dogfood 全部连带塌掉。修好这一处,十六条应当一起回绿 —— 这是预测,⛔ 不是读数,由补丁轮实测。
这恰恰是收窄该有的样子,⛔ 不是复核失职
一次契约收窄本来就该把不合规的调用点照出来。复核测的是「收窄是否正确且已声明」——它是;CI 测的是「谁在用旧形状」——sys-user.page.ts:88 在用。两个读数都对,合起来才是完整判断。本 PR 声明了破坏性变更却没有修自己仓内的消费者。
处置
- 契约复核 PASS 照记(
5715671761),⛔ 不撤、⛔ 不改。 - 本 PR 进补丁轮,原因是 CI 红,⛔ 不是契约 FAIL。
needs:contract-review继续挂着,⛔ 不在此刻摘除。理由说明白以便审计:规则写「PASS ⇒ 剥标、ready、auto-merge」,那是一个以「PR 可落地」为前提的动作单元;本 PR 不可落地,且补丁轮若动到契约面就欠一次 delta 复核。轮次未完,载体不摘。--pair 18638仍读 exit 4(C5)。复核已认定两条 tell 为假、修矫正器归 [finding]check-widening-tells.mjsreports T1 and T2 on lines added only because a zod options object was appended to an existing union — the clause-② enqueue gate refuses a diff that adds no key and no arm #18640、本 PR ⛔ 不得翻Clause-②: yes。⚠️ 本席补记一条给接 [finding]check-widening-tells.mjsreports T1 and T2 on lines added only because a zod options object was appended to an existing union — the clause-② enqueue gate refuses a diff that adds no key and no arm #18640 的人:该卡目前是未定级的finding(只有finding一个标签),按派发规则未定级的 finding 不可派发,归分诊席。⇒ 在 [finding]check-widening-tells.mjsreports T1 and T2 on lines added only because a zod options object was appended to an existing union — the clause-② enqueue gate refuses a diff that adds no key and no arm #18640 被定级并落地之前,本 PR 的--pair读数不会自己变 0;这是本 PR 之后还会撞上的第二道门,现在记下来,免得补丁轮回绿之后再被它挡一次。
Generated by Claude Code
…aluated-slot-narrowing Conflict resolved by hand in packages/spec/src/system/metrics.zod.ts: both intents stack — main's new DurationSeconds import plus this branch's swap of ExpressionInputSchema for EvaluatedExpressionInputSchema and the union refusal helper. ExpressionInputSchema has no remaining use in the merged file. The two both-sides-edited os-regen artifacts take main's side in this commit; the regeneration follows as its own commit. Claude-Session: https://claude.ai/code/session_01LvwGppdonww4zGLWZo5rho Co-authored-by: Claude <[email protected]>
Discharges the os-regen deferral the merge commit recorded. Both pages carry main's incoming content plus this branch's narrowing: the evaluated-slot envelope now prints `source: string` instead of `source?: string`, and metrics.mdx keeps main's `window.durationSeconds` rename. Claude-Session: https://claude.ai/code/session_01LvwGppdonww4zGLWZo5rho Co-authored-by: Claude <[email protected]>
`expression()`, `cel`, `tmpl` and `cron` each assign a `string` to `source`
unconditionally, so the `Expression` return type they carried described none of
them — it was slop that cost nothing until an evaluated slot started requiring
`source`. With the slot narrowed, `visibleWhen: P` stopped type-checking at the
spelling this file's own docblock tells authors to use, and
`@objectstack/platform-objects` failed its DTS build on exactly that:
src/pages/sys-user.page.ts(88,9): error TS2322: Type '{ dialect: "cel" |
"cron" | "template"; source?: string | undefined; ... }' is not assignable to
type 'string | { dialect: ...; source: string; ... } | undefined'.
Types of property 'source' are incompatible.
The producer is fixed, not the call site: the value always had a `source`, only
its declared type did not say so. `EvaluatedExpression` is assignable to
`Expression`, so persistence-contract slots keep accepting these values
unchanged; what the narrower return type adds is that evaluated slots accept
them too.
Claude-Session: https://claude.ai/code/session_01LvwGppdonww4zGLWZo5rho
Co-authored-by: Claude <[email protected]>
The changeset already carries the `RowCrudPredicates` narrowing as a published TypeScript change; the four expression constructors are the same shape and were missing. Semver level, BREAKING banner and the ADR-0087 disposition are untouched. Claude-Session: https://claude.ai/code/session_01LvwGppdonww4zGLWZo5rho Co-authored-by: Claude <[email protected]>
Patch round — the 16 CI failures had one cause, and it was one line of slop in this PR's own fileDispatched by the The real breakage list is exactly one fileTurbo stops at the first failing package, so the block report could only name one. I built the 23 failing packages, one non-cascade error among them. Every other package failed on TS7016 The one real error, and why the fix is at the producerLine 88 is export function cel(strings: TemplateStringsArray, ...values: unknown[]): Expression {
return { dialect: 'cel', source: renderTemplate(strings, values) };
}
So
No (b)-class site anywhereNothing in the workspace, the five example apps, or the downstream-contract package holds an Measurements — every exit code read from
|
| what | command | reading |
|---|---|---|
| Full workspace build | turbo run build --filter='!@objectstack/docs' --continue --concurrency=2 |
73 successful, 73 total · VERDICT command-exit 0 |
platform-objects DTS |
(inside the above) | check-dts-emitted: 11/11 declared declaration file(s) present |
| Workspace type check (CI's lane 2 spelling) | turbo run typecheck --filter='./packages/*' --filter='./packages/*/*' --filter='./apps/*' --continue |
137 successful, 137 total · VERDICT command-exit 0 |
| Example apps (CI's consumer-gates lane) | pnpm --filter './examples/*' run typecheck |
EXAMPLES_EXIT=0, 5 of 82 projects in scope, all Done |
| Downstream consumer contract | pnpm --filter @objectstack/downstream-contract run typecheck |
DOWNSTREAM_EXIT=0 |
@objectstack/spec tests |
pnpm --filter @objectstack/spec test |
486 files, 14034 passed |
@objectstack/platform-objects tests |
pnpm --filter @objectstack/platform-objects test |
40 files, 575 passed |
@objectstack/formula tests |
pnpm --filter @objectstack/formula test |
30 files, 871 passed |
| Spec generated artifacts | pnpm --filter @objectstack/spec check:generated |
15/15 up to date, exit 0 |
| Spec source audits | 11 of them, check:empty-state … check:objectui-pin-citations |
all exit 0 |
| Consumer / ledger gates | check:dts-closure, check:published-readme-exports, check:type-check-debt, check:type-check-coverage |
all exit 0; dts-closure swept 165/165 declaration files across 73 packages |
| Changeset gates | check-adr-0087-registration, check-changeset-no-major, check-empty-changeset, check-changeset-fixed |
all exit 0 |
| Repo lint | pnpm lint (eslint . --no-inline-config) |
exit 0 — the whole population, no narrowing claimed |
| Byte hygiene | check:nul-bytes plus a direct control-character sweep of the three changed source files |
exit 0 / no match |
The three test suites ran joined with &&, so the VERDICT command-exit 0 covers all three rather
than only the last.
Merge, not rebase
origin/main f6c2eb7c86 merged through scripts/pm/os-regen-merge.sh. One hand-written conflict,
in packages/spec/src/system/metrics.zod.ts, resolved as a semantic merge with both intents
stacked: main's new DurationSeconds import plus this branch's swap of ExpressionInputSchema
for EvaluatedExpressionInputSchema and the union-refusal helper. ExpressionInputSchema has no
remaining use in the merged file, so main's import of it was dropped rather than kept dead.
Two merge=os-regen artifacts were edited on both sides
(content/docs/references/system/metrics.mdx, .../kernel/plugin-security-advanced.mdx): main's
side was taken in the worktree, the merge was committed first, and the regeneration landed as its
own commit, which discharged the pre-commit deferral. The regenerated diff is exactly this branch's
intent on top of main's content — the envelope now prints source: string, and metrics.mdx keeps
main's window.durationSeconds rename.
What this round did NOT touch, and did NOT measure
- The contract review PASS (
5715671761) stands. Nothing in the narrowing was redesigned,
reverted or loosened. - The changeset's semver level, BREAKING banner and ADR-0087 disposition are byte-unchanged. One
descriptive paragraph was appended recording the constructor return-type narrowing, matching the
paragraph the changeset already carried forRowCrudPredicates. All four changeset gates re-run
green after it. Clause-②: noin the PR body is untouched, and the body was not edited at all this round.
check:pm-widening-tells --pair 18638was NOT MEASURED here: the script requires
--declaration yes|noand refuses to decide on a diff alone, and that reading is the seat's, not
the executor's. The blocker named above ([finding]check-widening-tells.mjsreports T1 and T2 on lines added only because a zod options object was appended to an existing union — the clause-② enqueue gate refuses a diff that adds no key and no arm #18640 being an unprioritisedfinding) is unchanged.needs:contract-reviewis still on the PR — not removed, not re-applied.- CI itself is NOT MEASURED. Everything above is local. The 16 checks turning green is a
prediction from the root cause being repaired, not a reading; CI on9f7013fc25is the reading. - Not run locally, by design: the remaining families of the 110
dispatch-gates --commands
list,Temporal Conformance(needs live PG + MySQL), the Dogfood lanes and the six
path-scheduled CI jobs, whichdispatch-gatesitself marks as having no local invocation.
Generated by Claude Code
席位处置 —— 补丁轮收下,含对本席自己上一条读数的更正,以及围栏的裁定
① 更正:本席评论
|
Contract reviewServed-tier: Scope: delta Delta graph (first-parent): merge ① Derived judgments1. Four return types (a) Bodies always assign (b) Covariance: wrote a positive probe into Contravariant / invariant hunt (tree,
2. "No accept set, no parse behaviour" — established. Zero schema/refine/transform lines moved (reading 1a). 3. Hand-resolved 4. os-regen path — regenerated artefacts match a fresh regeneration; no side dropped. Merge commit took main's bytes for both 5. Semver — no export added; the delta does not change the answer. Export name set of the fenced file before Not measured: full-monorepo ② Semver level
③ Boundary flags
Implemented-by: VERDICT: PASS Generated by Claude Code |
⏸️ 全绿、两轮达档复核 PASS、仍不入队 —— 停在 C5 上,阻塞卡是 #18640
已完成的每一步
卡住的那一步
两条 tell 都已被达档复核判定为假(两处都是
闸门自己也写死了这一点:「⛔ An explanation in the claim moves no exit code — nothing reads one.」 所以本条评论不试图移动那个 exit code,它只说明为什么本 PR 停在这里。 解冻条件与当前阻塞#18640 已被分诊定级( ⇒ 三张在飞卡任意一张交付腾出槽位,#18640 即为本席的下一张取卡,优先于取序里的其它卡 —— 因为它解锁的是一张已经全绿、已经付过两轮达档复核的 PR。
Generated by Claude Code |
落地窗口更新 —— 矫正器修复已并入 main,本 PR 从 2 条 tell 降到 1 条,仍停在 C5
#18724 已 MERGED( 本席在并入后的 main 上复跑(完整 worktree,
⇒ 残留这一条不会被"再修一次"解决 —— 它在等一次裁决
卡 #18640 已进决策箱( ⭐ 施工席推荐 A 为当下、C 另立卡,并明确点名不是 B —— 理由是 B 的直接受益者正是本席这张停着的 PR。⛔ 本席收下该拒绝,⛔ 不申辩、⛔ 不重新派发去争取 B,⛔ 也不替维护者选。 本 PR 的其余部分保持就绪CI 全绿(33 success / 2 预期 skip / 0 fail)、 Generated by Claude Code |
Fixes #15811
Clause-②: no
Rework round — the seat's three items, measured on this head
Seat verdict #18638 (comment) (REWORK on a PASSed contract review). Three items, nothing else re-opened.
1. The changeset and the ADR-0087 entry said something this diff makes false
Both claimed the three union-member positions leave their sibling arm untouched. Re-measured here, base
00115a8442vs head, parsing each value AS MOUNTED throughTraceSamplingConfigSchema:RecordAlertProps.visiblez.boolean()true/falseacceptedServiceLevelIndicator.successCriteria{ threshold, operator, percentile? }dialectkeyTraceSamplingConfig.composite[].conditionz.record(z.string(), z.unknown())At the tracing slot, six shapes the base accepted through that arm alone — measured: the base's
ExpressionInputSchemarefuses all six, so the record arm was the only thing admitting them — are refused at head:condition{ dialect: 'cel' }{ dialect: 'js', source: 'x' }{ dialect: 'nope', source: 'x' }{ dialect: 'cel', source: 5 }{ dialect: 'cel', source: 'x', meta: { rationale: 5 } }{ dialect: 'zzz', foo: 1 }Control that HITS: a structured filter carrying no
dialectkey —{},{ service: 'api' },{ attributes: { 'http.route': '/v1/orders' } }— is accepted at base and at head alike. Without it the sixrefuseds would be a schema that refuses everything.⭐ The narrowing is correct and load-bearing (it is what makes the ruled change non-inert at that slot) and is not removed. What changed is the description: the changeset now carries the table and its FROM → TO, and the migration entry's
surfaceandacceptanceCriteriaboth name the wider sweep that slot needs — flag everyconditionobject carrying adialectkey, not only the two spellings. A changeset becomes the CHANGELOG and an ADR-0087 entry becomes the migration ledger; neither may ship a false sentence.2. The published reference page
.refine()has no JSON Schema projection — measured against zod 4.4.3:z.toJSONSchemareturns byte-identical output for the plain record, the refined record and the aborting refined record ({"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}). So regenerating alone could never move that TYPE cell, and hand-editing the page is forbidden and would be reverted. The fix is at source: the slot's.describe()now states the rule, andgen:docsrepublishes it.content/docs/references/system/tracing.mdxnow reads:.refine()in the spec, not only this one. Making the page contradict the artifact beside it would be worse. Reported as an out-of-scope finding rather than repaired here.3. The two unpinned message cells — the fix was in the schema, not only in a test
Measured at the slot, on head as it arrived:
{ dialect: 'cel', source: '' }→ one top-levelinvalid_unionwith the bareInvalid input; the published sentence appeared only inside nested arm issues;{ dialect: 'js', source: 'x' }→ refused with the 「needs a non-blanksource」 sentence, which misnames the fault: that value'ssourceis fine, its dialect is not.Root cause, measured: zod 4.4 reports the ONE arm that did not abort, else
invalid_union. The record arm's.refine()was non-aborting, so it was the surviving arm for every expression-shaped refusal here and answered for all of them — and it answered with the other arm's sentence.⇒ The repair is in the schema, not only in a test: the refine becomes aborting, and its message becomes the arm's own rule (module-local, ⛔ not a new published export). Ablation of the accept set: the refused set is identical with and without
abort— both measured over the ten-value corpus above, so this is a message change and not a second narrowing. After it, the slot answers exactly what the other 35 answer, and exactly what the migration entry's own acceptance criteria promise:condition{ dialect: 'cel', source: '' }/' 'invalid_union@ slot,Invalid inputcustomissue @…condition.source, the published sentence{ dialect: 'cel', ast: … }custom@ slot, published sentenceinvalid_union@ slot, published sentence''/' '(bare)invalid_union@ slot, published sentence{ dialect: 'js', source: 'x' }custom@ slot, publishedsourcesentenceinvalid_union@ slot,Invalid input— no longer blamessourcePins, in
packages/spec/src/system/tracing.test.ts: the accept set (six refusals + the accepting control), both blank spellings' published sentence and its exactcode/path, theast-only and bare-string cells, the negative (a non-sourcefault is not answered with thesourcesentence), and the.describe()the reference page renders. Andevaluated-slot-population.test.ts's published-sentence pin now runs all three refused spellings at all 36 positions instead of only theast-only one — 108 cases, all green. That is what would have caught this slot in the first place.Not re-opened
Clause-②staysno(strict subtype, measured over 28 inputs by the review; the two tells are false positives whose matcher repair is card #18640).minor+ BREAKING banner + ADR-0087 disposition stay.printCelAst, the package-internal helper and the 36-position census stay.packages/spec/api-surface/shared.jsonandexport-origins/shared.jsonare still hash-identical to base (git hash-object:cf260910f1…/0429ff67a6…), andgit diff --stat 00115a8442..HEAD -- packages/spec/api-surface packages/spec/export-originsis empty.Gates, re-derived on this head
node scripts/pm/dispatch-gates.mjs --commandson the merged head, every exit code recorded as it ran, reconciled with--ran: 110 derived, 104 run, 6 NOT MEASURED, 0 unrun (--ranexit 0).pnpm --filter @objectstack/spec build && test && typecheckgreen — 486 files / 14016 tests;@objectstack/formula30 files / 871 tests, typecheck green.check:generated: all 15 artifacts up to date after theorigin/mainmerge and the final rebuild.Non-zero exits, all declared:
check:doc-formula-expressions,check:doc-security-posture,check:docs-transcript-drift,check:dual-build-cjs-loads,check:lean-entry-closure,check:type-check-debt) — each refuses an unbuilt workspace closure and says so. NOT MEASURED, not findings;check:skill-examplesexit 1 — the same class in exit-1 clothing (packages/client-react/distholds no.d.ts);check:react-declaration-parityexit 1 — run as CI runs it,MANIFEST="$PWD/sdui.manifest.json" … --strict:111 spec-only divergences, 1 blocks missing from the registry. Control: the identical command at base00115a8442prints the same two numbers, so it is pre-existing and this PR moves neither.One gate went red on this round's own work and is fixed:
check:doc-authoringrefused an internal issue id in customer-facing spec text — the(#15811)this round put in the tracing.describe(). Removed, page regenerated, gate green; the same gate at base is green, so the id was the only offender.node scripts/pm/check-clause2-carriers.mjs --pair 18638— exit 4, and the dual-carrier row C1 is gone: only C5 remains, with the same two false tells (ui/action.zod.ts:833T2,ui/component.zod.ts:1595T1). ⛔ Reported, not acted on; the matcher repair is #18640's.Rework round authored by the
domain:specexecution seat, sessionsession_01LvwGppdonww4zGLWZo5rho.Decision batch #122 item 2 generalised the evaluated-slot rule:
EvaluatedExpressionInputSchemanow composes into every slot an engine evaluates, whileExpressionSchema/ExpressionInputSchemastay the persistence contract (sourceORast) by item 2 of the same ruling. Anast-only envelope and asourcethat is blank after trimming — through the envelope key or the bare-string shorthand — are refused at the door instead of parsing, registering, and faulting at run time.The population was re-derived, not inherited
The census in the card is six days old and
shared/expression.zod.tsmoved after the ruling, so the 36 figure was treated as a premise. Re-derived by identity on this branch's base00115a8442— a negative lookaround on identifier characters, because the bare substring also fires insideCronExpressionInputSchema,TemplateExpressionInputSchemaandEvaluatedExpressionInputSchema, which is the trap that inflated triage's own reading on this card (32 files, five of them Cron-only):ZzzNoSuchSchemaThat 7-versus-17 gap in one file is the trap itself, in miniature. Identical to the measured census (#15811 (comment)), position for position. Two aliases:
ui/action.zod.tsActionConditionInputSchema(mountsvisible+disabled) andsystem/settings-manifest.zod.tsSettingsVisibilityInputSchema(mounts the specifier and manifestvisible). Three positions reach the schema as a union member rather than head-of-declaration.PredicateInputSchemais a plain alias ofExpressionInputSchemawith zero slot users; it stays wide with the schema it aliases.Two defects found while measuring, both fixed here
1. The narrowing was INERT at
TraceSamplingConfig.composite[].condition. That slot isz.union([z.record(z.string(), z.unknown()), …]), and a bare record arm accepts{ dialect: 'cel', ast }as an ordinary record — so swapping the other arm changed nothing. Measured: after the swap and before this fix the slot still answeredsuccess: trueon theast-only envelope, while its 35 siblings answeredfalse. The structured-filter arm now declines an object carrying adialectkey, which is an expression attempt whatever it got wrong. Shipping the swap alone would have been a declared-but-unenforced narrowing.2. Four positions refused with zod's bare
Invalid input. Where the declaration wraps the evaluated schema in a WIDER union — a boolean beside it onaction.visible/action.disabled/RecordAlertProps.visible, a structured object beside it onServiceLevelIndicator.successCriteria— the outer union reportsinvalid_unionat the slot and the inner union's sentence never surfaces.evaluatedExpressionUnionRefusalgives those unions the published sentence. It is deliberately stricter than the inner map it complements: it answers only for a blank string or an object carryingdialect, so a malformed threshold object is not blamed onsource. It lives inshared/evaluated-slot-union.ts, package-internal and absent from both barrels, on theunion-branch-policyconvention: a narrowing PR that grows the published export surface widens on a second axis, soapi-surface/andexport-origins/do not move for it.Item 3 — the printer path is real, and measured
The ruling asked for the lossless direction 「where the dialect has a printer」 before falling back to a structured TODO. Measured rather than assumed:
@marcbachmann/cel-jsshipsserialize, andcel-engine.tsalready uses it for its own scope rewrites. So@objectstack/formulagainsprintCelAst(ast), the inverse of the existingparseCelToAst, and the migration entry prescribes it by name instead of describing a capability nobody can call.Measured round-trip, six sources, each re-evaluated on the same scope:
Lossless about MEANING, not bytes — the printer re-renders from the parse tree, so quote style normalises. Dark controls, all four throwing rather than inventing a source:
{},null,{ type: 'nope' }and a plain string each raiseUnknown AST operation.printCelAstconverts that intonulland additionally requires the printed text to parse back through the platform's own boundedparseCelToAst, so it can never widen what this platform evaluates.Where the printer answers
null, and for every blanksource, the ADR-0087 D3 entryevaluated-expression-slots-source-requiredis the structured TODO — naming the object, the field and the slot, and splitting the judgment by fail policy, because removing a key is safe on the fail-soft half of the population and a silent disclosure on the fail-closed half.Why this is a D3 entry and not a D2 conversion, now that a printer exists. The conversion layer lives in
packages/spec, which is dependency-free by Prime Directive #2 and carries no engine —packages/formula/src/normalize.tsstates the same boundary from the other side. A conversion that had to call the CEL printer could not live where conversions live, and one that guessed without a printer would be the platform inventing a predicate.minor, and the ruling saidmajorItem 3 ordered a 「
majorchangeset」.scripts/check-changeset-no-major.mjsforbids amajormarker during the launch window, because the fixed group versions in lockstep and onemajorpromotes all ~70 packages to a whole-stack major — which is a release act reserved to the maintainer. The guard's own header names the two carriers the convention uses instead, and both are present: the BREAKING banner in the changeset body and the ADR-0087 disposition line. The ruling's substance ships; only the marker differs, and it differs because a repo gate forbids the marker. Flagged rather than chosen silently.Item 4 — the mechanical acceptance surface
#17630 is closed and its widening is live on this base: discovery in
packages/qa/dogfood/test/expression-conformance.test.tsmatches a roster name by identity anywhere on a line, attributes it to thefield:it mounts, and resolves file-local aliases. BothExpressionInputSchemaandEvaluatedExpressionInputSchemaare on that roster, so every one of the 36 positions stays discovered across the swap, the ledger'sfile:Schema.fieldcover keys are unchanged, and theSCAN_CONTROLSfloors (head 37 / inline 3 / alias 2) are unaffected — the swap changes the identifier, never the syntactic shape. No ledger row'sfailPolicymoves: the column records what the EVALUATOR does with a bad expression, and no evaluator changed.Clause-② carrier readings, reported rather than acted on
node scripts/pm/check-clause2-carriers.mjs --pair 18638— exit 4, two rows at the time of writing (re-read on the rework head: C1 has cleared, C5 stands — see the rework section above). ⛔ Neither carrier is touched from here; this is the reading, not a verdict.C1 — the dual carrier is split.
needs:contract-reviewis on card spec: the evaluated-slot rule of #15430 reaches only the flow-node ledger — every otherExpressionInputSchemaslot an engine evaluates (formulaexpression, validation / hook / sharingcondition,visibleWhen…) still accepts anast-only or blank-sourceenvelope #15811 and NOT on this PR. That is the state as found; the seat that owns the gate hangs or clears both sides in one stroke.C5 — three widening tells against
Clause-②: no. One was real and is gone: the new published exportevaluatedExpressionUnionRefusalinapi-surface/shared.json, removed by moving the helper package-internal (above), so the published surface is byte-unchanged by this PR. The remaining two are false, and both for the same reason — the matcher fires on an ADDED LINE that has the shape of a widening, and these two lines were added because an options object was appended to a union that gained no member:ui/action.zod.tsActionConditionInputSchema— read as T2 「a new member of a closed set」. The union has the same two members before and after; what is new on the line is, { error: … }.ui/component.zod.tsRecordAlertProps.visible— read as T1 「a new key on a Zod object schema」.visibleexisted before this PR; the line moved for the same options object.Per the gate's own instruction a false tell is repaired in the matcher (
scripts/pm/check-widening-tells.mjs, with a--self-testcase pinning the shape) or filed as its own card. Repairing ascripts/pm/**matcher is outside this card's surface, so it is filed rather than done here — see the report'sout_of_scope_findings.Tests
packages/spec/src/shared/evaluated-slot-population.test.tsis the new pin, in two halves because either alone is a green that proves nothing:packages/spec/srcstill mounts the persistence schema on a code line, with a lit control (the scan does find the name in the definition file and the barrel), a dark control, and an explicit assertion that the Cron / Template / Evaluated siblings do not leak in as substrings;ExpressionSchema/ExpressionInputSchema/PredicateInputSchemastill ACCEPT both shapes, and a healthy predicate still parses at all 36 (the settings pair gets the predicate its own closed grammar accepts).packages/formula/src/print-cel-ast.test.tspins the printer's two claims, including seven dark-control inputs.Three existing pins were rewritten rather than relaxed — each pinned exactly the arm this PR deletes:
system/settings-manifest.test.ts「anast-only envelope is opaque at this layer」 now pins the refusal, and asserts the settings GRAMMAR message is not the one raised, so the two refusals stay independent;ui/action.test.ts「rejects composition with an AST-only visible loudly (ADR-0078)」 — ADR-0078's promise is unchanged, but the refusal moved from therequiresFeaturelowering to the slot, so it now holds with and without the flag. A second case pins that spec/kernel:requiresFeaturecomposed with a blank-sourcevisiblebuilds a predicate that can never parse — ADR-0078's no-silently-inert guard produces the shape it exists to reject #17631's shape (a blanksourcecomposed into( ) && features.admin == true) can no longer reach the lowering at all;ui/view-form-features-root.test.ts「documented boundary」 now asserts the refusal comes from the evaluated-slot rule and not from the features-root scanner this file is about.Repo census for the migration: zero authored occurrences of either refused spelling outside
packages/spec's own refusal fixtures, acrosspackages/,examples/,content/andskills/, against a lit control that hits. Nothing in this repository needs rewriting.Acceptance notes
PredicateInputSchema(shared/expression.zod.ts) remains a plain value alias ofExpressionInputSchemawith zero slot users. Left wide deliberately — it aliases the persistence contract. Noted, not filed; carrier is the ledger's own limit 2, already written up there.celEngine.evaluateon{ dialect: 'cel', source: '' }answers with the AST-only message rather than an empty-source one. Message accuracy only; the verdict is correct. Unchanged here, still uncarried.Authored by the
domain:specexecution seat, sessionsession_01LvwGppdonww4zGLWZo5rho, under the dispatch claim #15811 (comment).Generated by Claude Code