Skip to content

feat(spec)!: every engine-evaluated expression slot requires a non-blank source - #18638

Draft
os-litant wants to merge 14 commits into
mainfrom
claude/issue-15811-evaluated-slot-narrowing
Draft

os-litant wants to merge 14 commits into
mainfrom
claude/issue-15811-evaluated-slot-narrowing

Conversation

@os-litant

@os-litant os-litant commented Sep 17, 2026

Copy link
Copy Markdown
Collaborator

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 00115a8442 vs head, parsing each value AS MOUNTED through TraceSamplingConfigSchema:

position sibling arm base head
RecordAlertProps.visible z.boolean() true / false accepted identical
ServiceLevelIndicator.successCriteria structured { threshold, operator, percentile? } accepted, including an object carrying a dialect key identical
TraceSamplingConfig.composite[].condition z.record(z.string(), z.unknown()) see below narrowed

At the tracing slot, six shapes the base accepted through that arm alone — measured: the base's ExpressionInputSchema refuses all six, so the record arm was the only thing admitting them — are refused at head:

authored condition base head
{ dialect: 'cel' } accepted refused
{ dialect: 'js', source: 'x' } accepted refused
{ dialect: 'nope', source: 'x' } accepted refused
{ dialect: 'cel', source: 5 } accepted refused
{ dialect: 'cel', source: 'x', meta: { rationale: 5 } } accepted refused
{ dialect: 'zzz', foo: 1 } accepted refused

Control that HITS: a structured filter carrying no dialect key — {}, { service: 'api' }, { attributes: { 'http.route': '/v1/orders' } } — is accepted at base and at head alike. Without it the six refuseds 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 surface and acceptanceCriteria both name the wider sweep that slot needs — flag every condition object carrying a dialect key, 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.toJSONSchema returns 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, and gen:docs republishes it. content/docs/references/system/tracing.mdx now reads:

Condition for this strategy — a structured filter object, or a CEL predicate an engine evaluates. ⚠️ The two are told apart by the dialect key: a structured filter must NOT carry one, and an object that does is judged as an expression — so it needs a dialect this platform evaluates and a non-blank source{ dialect: 'cel', ast: … } with no source is refused here.

⚠️ The type cell still renders that arm as a plain record of string to any, and that is faithful to the JSON Schema this repo publishes — which is itself wider than the zod schema, for every .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-level invalid_union with the bare Invalid input; the published sentence appeared only inside nested arm issues;
  • { dialect: 'js', source: 'x' } → refused with the 「needs a non-blank source sentence, which misnames the fault: that value's source is 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:

authored condition before after
{ dialect: 'cel', source: '' } / ' ' invalid_union @ slot, Invalid input one custom issue @ …condition.source, the published sentence
{ dialect: 'cel', ast: … } custom @ slot, published sentence invalid_union @ slot, published sentence
'' / ' ' (bare) invalid_union @ slot, published sentence unchanged
{ dialect: 'js', source: 'x' } custom @ slot, published source sentence invalid_union @ slot, Invalid input — no longer blames source

Pins, in packages/spec/src/system/tracing.test.ts: the accept set (six refusals + the accepting control), both blank spellings' published sentence and its exact code/path, the ast-only and bare-string cells, the negative (a non-source fault is not answered with the source sentence), and the .describe() the reference page renders. And evaluated-slot-population.test.ts's published-sentence pin now runs all three refused spellings at all 36 positions instead of only the ast-only one — 108 cases, all green. That is what would have caught this slot in the first place.

Not re-opened

Clause-② stays no (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.json and export-origins/shared.json are still hash-identical to base (git hash-object: cf260910f1… / 0429ff67a6…), and git diff --stat 00115a8442..HEAD -- packages/spec/api-surface packages/spec/export-origins is empty.

Gates, re-derived on this head

node scripts/pm/dispatch-gates.mjs --commands on the merged head, every exit code recorded as it ran, reconciled with --ran: 110 derived, 104 run, 6 NOT MEASURED, 0 unrun (--ran exit 0). pnpm --filter @objectstack/spec build && test && typecheck green — 486 files / 14016 tests; @objectstack/formula 30 files / 871 tests, typecheck green. check:generated: all 15 artifacts up to date after the origin/main merge and the final rebuild.

Non-zero exits, all declared:

  • six exit 3 · PREREQUISITE NOT MET (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-examples exit 1 — the same class in exit-1 clothing (packages/client-react/dist holds no .d.ts);
  • check:react-declaration-parity exit 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 base 00115a8442 prints 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-authoring refused 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 18638exit 4, and the dual-carrier row C1 is gone: only C5 remains, with the same two false tells (ui/action.zod.ts:833 T2, ui/component.zod.ts:1595 T1). ⛔ Reported, not acted on; the matcher repair is #18640's.

Rework round authored by the domain:spec execution seat, session session_01LvwGppdonww4zGLWZo5rho.

Decision batch #122 item 2 generalised the evaluated-slot rule: EvaluatedExpressionInputSchema now composes into every slot an engine evaluates, while ExpressionSchema / ExpressionInputSchema stay the persistence contract (source OR ast) by item 2 of the same ruling. An ast-only envelope and a source that 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.ts moved after the ruling, so the 36 figure was treated as a premise. Re-derived by identity on this branch's base 00115a8442 — a negative lookaround on identifier characters, because the bare substring also fires inside CronExpressionInputSchema, TemplateExpressionInputSchema and EvaluatedExpressionInputSchema, which is the trap that inflated triage's own reading on this card (32 files, five of them Cron-only):

reading count
declaring source lines mounting the schema (non-test, non-comment) 34
of those, file-local alias consts mounting 2 slots each 2
declaring positions 36
lit control — identity hits in the definition file 7
the same file counted by bare SUBSTRING 17
dark control — ZzzNoSuchSchema 0

That 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.ts ActionConditionInputSchema (mounts visible + disabled) and system/settings-manifest.zod.ts SettingsVisibilityInputSchema (mounts the specifier and manifest visible). Three positions reach the schema as a union member rather than head-of-declaration.

PredicateInputSchema is a plain alias of ExpressionInputSchema with 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 is z.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 answered success: true on the ast-only envelope, while its 35 siblings answered false. The structured-filter arm now declines an object carrying a dialect key, 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 on action.visible / action.disabled / RecordAlertProps.visible, a structured object beside it on ServiceLevelIndicator.successCriteria — the outer union reports invalid_union at the slot and the inner union's sentence never surfaces. evaluatedExpressionUnionRefusal gives 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 carrying dialect, so a malformed threshold object is not blamed on source. It lives in shared/evaluated-slot-union.ts, package-internal and absent from both barrels, on the union-branch-policy convention: a narrowing PR that grows the published export surface widens on a second axis, so api-surface/ and export-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-js ships serialize, and cel-engine.ts already uses it for its own scope rewrites. So @objectstack/formula gains printCelAst(ast), the inverse of the existing parseCelToAst, 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:

record.amount > 10                              -> identical bytes
record.priority == 'urgent'                     -> record.priority == "urgent"
'org_admin' in current_user.positions           -> "org_admin" in current_user.positions
record.a == 1 && (record.b != 2 || record.c > 3)-> identical bytes
size(record.tags) > 0                           -> identical bytes

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 raise Unknown AST operation. printCelAst converts that into null and additionally requires the printed text to parse back through the platform's own bounded parseCelToAst, so it can never widen what this platform evaluates.

Where the printer answers null, and for every blank source, the ADR-0087 D3 entry evaluated-expression-slots-source-required is 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.ts states 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.

⚠️ Deviation: graded minor, and the ruling said major

Item 3 ordered a 「major changeset」. scripts/check-changeset-no-major.mjs forbids a major marker during the launch window, because the fixed group versions in lockstep and one major promotes 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.ts matches a roster name by identity anywhere on a line, attributes it to the field: it mounts, and resolves file-local aliases. Both ExpressionInputSchema and EvaluatedExpressionInputSchema are on that roster, so every one of the 36 positions stays discovered across the swap, the ledger's file:Schema.field cover keys are unchanged, and the SCAN_CONTROLS floors (head 37 / inline 3 / alias 2) are unaffected — the swap changes the identifier, never the syntactic shape. No ledger row's failPolicy moves: 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 18638exit 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-review is on card spec: the evaluated-slot rule of #15430 reaches only the flow-node ledger — every other ExpressionInputSchema slot an engine evaluates (formula expression, validation / hook / sharing condition, visibleWhen…) still accepts an ast-only or blank-source envelope #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 export evaluatedExpressionUnionRefusal in api-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.ts ActionConditionInputSchema — 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.ts RecordAlertProps.visible — read as T1 「a new key on a Zod object schema」. visible existed 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-test case pinning the shape) or filed as its own card. Repairing a scripts/pm/** matcher is outside this card's surface, so it is filed rather than done here — see the report's out_of_scope_findings.

Tests

packages/spec/src/shared/evaluated-slot-population.test.ts is the new pin, in two halves because either alone is a green that proves nothing:

  • structural — no declaring position in packages/spec/src still 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;
  • behavioural — all 36 positions parsed AS MOUNTED, refusing all three refused spellings and carrying the one published sentence, plus an assertion that the table reached exactly 36 positions so a position that stops being reachable reds instead of silently leaving;
  • controlsExpressionSchema / ExpressionInputSchema / PredicateInputSchema still 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.ts pins 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:

Repo census for the migration: zero authored occurrences of either refused spelling outside packages/spec's own refusal fixtures, across packages/, examples/, content/ and skills/, against a lit control that hits. Nothing in this repository needs rewriting.

Acceptance notes

  • PredicateInputSchema (shared/expression.zod.ts) remains a plain value alias of ExpressionInputSchema with 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.evaluate on { 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:spec execution seat, session session_01LvwGppdonww4zGLWZo5rho, under the dispatch claim #15811 (comment).


Generated by Claude Code

@github-actions

github-actions Bot commented Sep 17, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/formula, @objectstack/spec, touching 31 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/formula/src/index.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

25 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 1bc22b3dcddc8a30b4826da8625e7787d5518a8f.

8 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/formula/src/index.ts) — pages documenting those are invisible to this run
  • 9 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 100 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.

Coarse fallback — 137 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json 1bc22b3dcddc8a30b4826da8625e7787d5518a8fpackageMentionDocs.

Which tree this was computed on

This run read content/docs from 7fd2c84760bf25a2ef28ba0062f311cafbc5f01b — the merge of head 9f7013fc2554a6456fa3e5474937b6fc5f5734e2 into base 1bc22b3dcddc8a30b4826da8625e7787d5518a8f, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# 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

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 1bc22b3dcddc8a30b4826da8625e7787d5518a8f → pass the list as
args.docs, on the commit named under Which tree this was computed on.

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]>

Copy link
Copy Markdown
Collaborator Author

Seat verdict: REWORK — the review PASSed and two of its named corrections ship false text

Seat session_01LvwGppdonww4zGLWZo5rho, 2026-09-17T12:1xZ. Review of record: posted alongside this comment, PASS at CONTRACT_REVIEW_TIER on head 950da7d3b9.

⚠️ The PASS is correct and it is not a landing permission — the review says so itself: 「Two corrections are owed inside this PR's own surface before merge」. This seat applied the same standard to #18608 this morning and applies it here: a PR does not land shipping a false statement about its own behaviour.

What the review settled, and it settled it well

⭐ The central question — whether Clause-②: no survives two widening tells — is answered by measurement, not argument: a 28-input corpus shows EvaluatedExpressionInputSchema accepts nothing ExpressionInputSchema refuses, refuses exactly four shapes (blank string, whitespace-only string, ast-only envelope, blank-source envelope), and parses all seven jointly-accepted inputs to byte-identical output. ⇒ strict subtype ⇒ both tells are false, and no is right.

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 yes — so this PR stays no.

What must change — inside this PR's own surface

  1. The changeset and the ADR-0087 migration entry both state something the diff makes false. The changeset says 「on those the boolean / object / record arms are untouched」 and the migration entry's acceptance criteria say 「the boolean or object arm is untouched」. ⚠️ At TraceSamplingConfig.composite[].condition the record arm gained a .refine(), and the review measured the consequence: six shapes that the base accepted through that arm alone — {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} — are all refused at head.
    ⇒ The narrowing there is real and load-bearing (it is what makes the ruled change non-inert at that slot, which is the round's best finding). ⛔ But both shipped texts deny it. A changeset becomes the CHANGELOG and an ADR-0087 entry becomes the migration ledger — these are the two carriers the launch-window convention calls 「the only signal there is」 for breaking-ness. They must not be false.
  2. The regenerated tracing.mdx still renders that arm as a plain record of string to any. Regenerate or correct it so the published page matches the schema.
  3. Recommended, and the seat wants it done unless you measure a reason not to: pin the published sentence for the blank-source spelling at the tracing slot. The review found two message-quality cells there and neither is pinned: a blank-source envelope surfaces as a top-level invalid_union with the bare Invalid input (the published sentence appears only in nested arm issues, because the record arm's refine is non-aborting so two arms survive), and {dialect:'js', source:'x'} is refused with the 「needs a non-blank source」 sentence, which misnames the fault. ⛔ Unpinned message cells are how the next round re-breaks them silently.

⛔ Not owed — do not re-open

  • Clause-② stays no. Measured strict subtype. ⛔ Never flip it to clear a gate false positive; [finding] check-widening-tells.mjs reports 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 carries the matcher repair.
  • minor + BREAKING banner + ADR-0087 stays. The review read check-changeset-no-major.mjs's own header: in the launch window, breaking ships as minor because one major promotes ~70 lockstep packages — a maintainer release act. Both guards pass. The ruling's word 「major」 is faithfully delivered as 「breaking with a semantic migration entry」, and the deviation is declared in three places so the director seat can overrule.
  • printCelAst, the package-internal helper, and the 36-position census are all confirmed. api-surface/shared.json and export-origins/shared.json are hash-identical to base — verified by git hash-object.
  • ⛔ The nine non-zero gate exits: no red in disguise. Two exit-3 PREREQUISITE cases reproduced with their documented text; check:cross-package-test-inputs reproduced at base with a dist present; the parity gate's exit 1 without MANIFEST is documented by check-generated itself.

Carried, not folded

The review named two things that are ⛔ not this PR's: RecordAlertProps.visible and PageTabsProps.items[].visibleWhen are enforced only where those Props schemas are parsed (objectui reads record:alert props through its own type, and PageComponentSchema.properties is an opaque record) — pre-existing, already on the ledger's cel-record-alert row, and the ruling is positional. The seat files nothing new for it; the ledger row is the carrier.


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]>
…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]>

Copy link
Copy Markdown
Collaborator Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 950da7d3b955ef27b4e252d9c358c8c6599ed7ca

① Derived judgments

  1. THE CENTRAL QUESTION — both tells are FALSE, measured not argued. EvaluatedExpressionInputSchema is a strict subtype of ExpressionInputSchema: over 28 inputs it accepts nothing the persistence schema refuses, refuses exactly four shapes the persistence schema accepts (blank bare string, whitespace-only string, ast-only envelope, blank-source envelope), and parses every jointly-accepted input to identical output. Its string arm (non-empty after trim) is a subset of min(1); its envelope arm is ExpressionSchema.safeExtend with source required and non-blank, so nothing new is admitted (dialect, ast, meta and unknown keys behave identically). T2 at action.zod.ts:833: ActionConditionInputSchema has two members before and two after — z.boolean() and the expression arm; the second argument { error: … } is a zod params object, not a member; base-versus-head accept table differs only in the four refused spellings, true/false survive. T1 at component.zod.ts:1595: visible exists at base component.zod.ts:1594 with the same two arms; same before/after table. The instrument fires because its [finding] check-clause2-carriers T1 reads a function PARAMETER annotated ctx: z.RefinementCtx as a new authorable key — so every PR that adds an object-level refusal raises a widening tell for the refusal itself #17618 three-fact spend needs the added member list to be a textual subset of the removed one, and the member was renamed; its own self-test lists the the widening refusal offers a remedy with no reader — "explain in the claim" moves no exit code (the T1 re-declared-key half did NOT reproduce) #17848 decline for a re-declared key with a zod error param, which cannot apply here for that reason. The gate's prescribed clear is a matcher repair with a self-test case or a filed card — never a false yes. Clause-②: no is right.
  2. Accept-set change, itemised: at all 36 declaring positions the four shapes above leave the accept set; nothing enters. Published TypeScript input types at those positions narrow (source required); the published interface RowCrudPredicates narrows with its two slots; api-surface tracks names only, so it does not move — correctly. The 18 regenerated content/docs/references/** pages change ONLY source?: string to source: string (every changed line, inverse grep zero) — faithful to EvaluatedExpressionSchema; dialect stays the three-value enum in both schema and doc.
  3. ONE EXTRA NARROWING THE PR TEXT MISSTATES. At TraceSamplingConfig.composite[].condition the record arm gained .refine(value has no dialect key). Measured: base accepted {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} through the record arm alone; head refuses all six. This is what makes the ruled narrowing non-inert there (the base column proves the record arm swallowed the ast-only envelope regardless of the other arm, so the fix is load-bearing), and it is direction-correct, but the changeset says "on those the boolean / object / record arms are untouched", the ADR-0087 entry's acceptance criteria say "the boolean or object arm is untouched", and the regenerated tracing.mdx still renders the arm as a plain record of string to any. Text correction owed in the changeset and the migration entry; the slot is unevaluated (zero consumers, per census and ledger), so the blast radius is nil.
  4. No other position has a permissive sibling arm: the metrics object arm requires threshold and operator (ast-only refused, measured), the two boolean arms cannot take an object, the settings pair is a superRefine over the evaluated union, and the remaining 30 positions are direct mounts. The behavioural pin exercises all 36 as mounted; my probe confirms the five wrapped ones.
  5. Item 2 honoured: the diff to shared/expression.zod.ts is docblock-only; ExpressionSchema, ExpressionInputSchema and PredicateInputSchema accept the ast-only and blank-source envelopes at head (measured; the population test's control asserts the same).
  6. 36 by identity — verified independently, position for position, with the dev's exact controls (17 versus 7, dark 0); the dogfood ratchet discovers the same 44 keys before and after with its mechanism floors intact.
  7. Message-quality gaps at the tracing slot only: the blank-source envelope surfaces as a top-level invalid_union with the bare message Invalid input (the published sentence is present only in the nested arm issues), because the record arm's refine is non-aborting so two arms survive and evaluatedExpressionUnionRefusal answers undefined for a string source; and an object like {dialect:'js', source:'x'} is refused there with the "needs a non-blank source" sentence, which misnames the fault. The population pin asserts the published sentence for the ast-only shape only, so neither cell is pinned. Follow-up, not a contract defect.
  8. Two of the 36 (RecordAlertProps.visible, PageTabsProps.items[].visibleWhen) are enforced only where those Props schemas are parsed: PageComponentSchema.properties is an opaque record and objectui reads record:alert props through its own RecordAlertRendererProps, not the spec schema. Pre-existing and already recorded on the ledger's cel-record-alert row; the ruling is positional, so not this PR's defect, but the narrowing is declared-only on the raw page path.

② Semver level

minor + BREAKING banner + ADR-0087 disposition is right; a major is not owed. check-changeset-no-major.mjs's header states the launch-window convention verbatim — breaking changes ship as minor while the fixed group versions in lockstep, one major promotes all ~70 packages, and "the mandatory information carriers for breaking-ness in the meantime are the BREAKING banner … and the ADR-0087 migration-ledger disposition … they are the only signal there is". Both carriers are present and both guards pass on the PR range (no-major exit 0; ADR-0087 registration exit 0, entry new in step 18, which spec-changes.json correctly does not yet project since the protocol is 17.x). The ruling's word major is faithfully delivered as "breaking with a semantic migration entry"; the literal marker would be refused by CI and is a maintainer release act. The deviation is stated in the PR body, the changeset and the report, so the director seat can overrule if a whole-stack major was literally intended. @objectstack/formula: minor is right for the additive printCelAst.

③ Boundary flags

  • Deviation major to minor: faithful to the convention's intent, not an evasion (above).
  • Deviation printCelAst in @objectstack/formula: in scope — ruling item 3 prescribes the lossless direction "where the dialect has a printer", which only exists if the printer is callable; cel-js serialize is already a dependency; 15 tests pass including seven dark controls and the re-parse guard. Note: formula has no api-surface baseline, so this new public export is ungated by the tells instrument; the changeset grades it minor, which is the correct level for it.
  • Deviation helper made package-internal: verified — shared/evaluated-slot-union.ts is imported by four zod files and appears in neither barrel; union-branch-policy.ts is the same internal-shared precedent; api-surface/shared.json and export-origins/shared.json are hash-identical to base and origin/main and no other shard moved.
  • Widening-tells false positives (out_of_scope_findings item 1): confirmed false by measurement; repair belongs in scripts/pm/check-widening-tells.mjs with a self-test case for a renamed-to-subtype member beside a zod error param — outside this card's surface, so it must be carried by its own card and escalated to the gate owner; this PR must not flip to yes.
  • Third tell real and gone: verified by the byte-identical surfaces.
  • PredicateInputSchema left wide: verified zero slot users; it aliases the persistence contract; correct under item 2.
  • celEngine.evaluate empty-string message: not measured here; message-only; stays uncarried as reported.
  • dispatch-gates.mjs omitting MANIFEST: consistent with check-generated's own text that the gate exits 1 without a manifest by design; a reading defect, not a gate defect.
  • Gates claim (111/102/9): the nine non-zero exits are the classes the dev names — exit 3 PREREQUISITE reproduced on two of six with the documented text; check:skill-examples reads dist .d.ts through dist-freshness.ts; cross-package exit 1 reproduced at base with a dist present (pre-existing); parity exit 1 without MANIFEST is documented. No red in disguise found. Full spec suite, tsc, formula, lint and metadata-core pass here; the two failing script-tier files fail identically at base for an environmental tsx path.
  • Escalation: none required for the verdict. Two corrections are owed inside this PR's own surface before merge: the changeset and migration-entry sentence claiming the tracing record arm is untouched, and (recommended) pinning the published sentence for the blank-source spelling so the tracing cell is measured.

Implemented-by: claude/issue-15811-evaluated-slot-narrowing
Reviewed-by: session_01LvwGppdonww4zGLWZo5rho

VERDICT: PASS


⚠️ Posted late, and that is a seat error. The REWORK verdict (5714311656) said this record was 「posted alongside this comment」. It was not — the seat wrote the verdict and skipped the record. The patch round caught it, searched for it, found PR #18638 carrying 0 reviews and no CONTRACT_REVIEW_TIER text anywhere in the repo, and recorded it as NOT MEASURED with the reason rather than assuming 「no flags」. That is the right handling of a missing input and it is worth more than the omission cost. The record above is the one the verdict rested on, posted verbatim and unedited; ⛔ it binds head 950da7d3b9 and does not bind the patch round's new head.


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

契约复核记录 — 交付后复核(delta 轮),档位 CONTRACT_REVIEW_TIER

domain:spec 席位(session_01LvwGppdonww4zGLWZo5rho)转录落档。本席服务档读数在 CONTRACT_REVIEW_TIER 之下,故复核走转录核验的隔离子代理跑在该档,⛔ 不在席内判、⛔ 不作额度降档。以下为复核代理产出,逐字落档,⛔ 未经本席编辑

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 62902436d958597c1858dc34a249cf8d8bc7e5bc

① Derived judgments

  1. THE abort: true QUESTION — a message change, not a narrowing, measured. Over a 48-value corpus parsed as mounted (the ten named shapes plus {dialect:undefined}, {dialect:null}, {dialect:'cel',source:null}, {dialect:'cel',source:'\n'}, {dialect:'CEL',source:'x'}, '\n\t', ' x ', null, numbers, booleans, arrays, a Date, a function, an inherited-prototype dialect, a null-prototype object and a Symbol key), the accept set at prior head 950da7d3b9 and at head differs in 0 cells; the synthetic union built from head's own EvaluatedExpressionInputSchema and evaluatedExpressionUnionRefusal with abort toggled differs in 0 cells and agrees with the real head in all 48. Structurally the same: a union accepts only an arm with zero issues, and abort only marks the failing arm aborted, so it can move which arm answers but never whether one accepts. Top-level issues moved in 14 cells, all at this one slot: the three blank-source spellings go from invalid_union + bare Invalid input to one custom issue at …condition.source carrying the published sentence; {dialect:'cel'}, the ast-only envelope, {dialect:'cel',source:5}, {dialect:'zzz',foo:1}, {dialect:undefined|null} and {dialect:'cel',source:null} go from custom at the slot to invalid_union at the slot with the same sentence; a wrong dialect beside a good string source (js, nope, CEL, and meta.rationale:5) goes from the misnaming source sentence to invalid_union + Invalid input. No other slot can move: TraceSamplingConfigSchema is mounted only by tracing.zod.ts itself (sampling: at line 755) and referenced from the dogfood ledger; the 108-cell population pin is green and the full spec suite passes except one script-tier file that fails identically at base for an environmental tsx path.
  2. Cross-slot consistency verified at head: {dialect:'js',source:'x'} answers invalid_union + Invalid input at the direct mount, at RecordAlertProps.visible, at successCriteria and at the tracing slot alike; the blank-source envelope answers one custom at source at all four; {dialect:'cel'} answers the published sentence at all four. The tracing slot now says what its 35 siblings say. Observation, not a defect: because the structured-filter arm now aborts on every failure, STRUCTURED_FILTER_DIALECT_REFUSED never surfaces at top level; it is present only in the nested invalid_union.errors[0], and the top-level answer for a wrong dialect with a good source is zod's bare Invalid input, which is exactly the sibling behaviour and what the PR body's table states.
  3. REWORK item 1 verified against the tree: the refine is present at head (.refine((value) => !('dialect' in value), { message: STRUCTURED_FILTER_DIALECT_REFUSED, abort: true })); the six named shapes are accepted at base 00115a8442 and refused at head; base ExpressionInputSchema refuses all six, so the record arm alone admitted them; the control {}, {service:'api'}, {attributes:{'http.route':'/v1/orders'}} is accepted at base, prior head and head. RecordAlertProps.visible: true/false accepted at all three commits, no prior-to-head difference; ServiceLevelIndicator.successCriteria: the structured object, one with percentile, and one carrying a stray dialect key are accepted at all three commits, no prior-to-head difference. The changeset, the migration entry's surface and acceptanceCriteria, and the generated registry.ts (check:migration-registry green) now carry the table, the FROM → TO and the wider sweep; the new text is true. The wider set of base-to-head refusals in my corpus ({dialect:undefined}, {dialect:null}, {dialect:'cel',source:null}, {dialect:'CEL',source:'x'}, whitespace-only strings) is covered by the declared rule "any object carrying a dialect key" plus the ruled two spellings, and every one of them was already refused at prior head, so none is delta. An object with only an inherited dialect is accepted at head because the refine runs on the record's parsed output; JSON cannot express it, and it is not a delta change.
  4. REWORK item 2 verified: z.toJSONSchema is byte-identical for the plain, refined and aborting-refined record under all three io modes ({"type":"object","propertyNames":{"type":"string"},"additionalProperties":{}}), and the whole TraceSamplingConfigSchema projection is identical prior vs head once descriptions are stripped, so regeneration alone could not have moved the type cell; the fix at .describe() is in the tree and check:docs reports content/docs/references/** up to date, with tracing.mdx carrying the dialect rule and no internal issue id. packages/spec/json-schema/ is gitignored build output, so the wider-than-zod published schema is a build-artifact finding, not a tracked-file one.
  5. PUBLIC SURFACE: the delta adds zero export lines (control: the full PR diff adds 10, including printCelAst and evaluatedExpressionUnionRefusal); STRUCTURED_FILTER_DIALECT_REFUSED has exactly two hits in the repo, both in tracing.zod.ts, none in either barrel (control: EVALUATED_EXPRESSION_SOURCE_REQUIRED hits the root barrel, api-surface/shared.json and export-origins/shared.json). Blob ids by git rev-parse rev:path are cf260910f1… and 0429ff67a6… at head, prior head, 00115a8442, 62d830e54e and origin/main 30be2ac0bb; git hash-object --stdin of the head blobs reproduces both; the two directories show 0 changed files against base and against origin/main (control: content/docs/references shows 23). After a full spec build, build-api-surface.ts --check reports the surface unchanged.
  6. Internal issue ids: the .describe() and the regenerated page no longer carry (#15811); check:doc-authoring is green at head (15537 customer-facing strings, the hoisted const and the describe included in its recognised sinks) and at base. #15811 still appears in the migration entry's description and rationale strings (entry lines 10, 40, 59; registry 7829, 7859, 7878); those pre-date the patch round, are not recognised sinks of the gate, and do not project into docs/protocol-upgrade-guide.md (0 hits; the step-18 entry is not projected while the protocol is 17.x). Observation only, not a delta defect.
  7. Relied on from record 5715224308 without re-deriving: the strict-subtype measurement over 28 inputs and the falsity of the two tells (item 1 there), the 36-position census and the dogfood ratchet reading (item 6), item 2's docblock-only diff to shared/expression.zod.ts, the printCelAst round-trip and dark controls, the four-position analysis of permissive sibling arms (item 4; I re-measured two of them directly and they agree), and the cel-record-alert ledger reading (item 8). None of the 7 delta files touches those surfaces, and the merge is provably automatic.

② Semver level

The delta does not change the answer. It adds no export, and its only accept-set effect at any position is nil (0 differences prior head to head over 48 values as mounted); what it changes is refusal messages at one slot and the text that declares a narrowing already present at the prior head. minor plus the BREAKING banner plus the ADR-0087 disposition stands, and both guards pass on this head against the PR merge-base (check-changeset-no-major exit 0; check-adr-0087-registration exit 0 with the one declared-breaking changeset registered). @objectstack/formula: minor is untouched by the delta.

③ Boundary flags

  • out_of_scope_findings 1 (published JSON Schema wider than zod wherever a .refine() carries the rule): measured true by the byte-identical projection; the artifact is gitignored build output and the gap is general to every refinement, so filing rather than repairing is correct; carried as [finding] the published JSON Schema is WIDER than the zod schema it is generated from wherever a .refine() carries the rule — an author validating against packages/spec/json-schema/** gets a green for metadata the runtime refuses #18670 per the seat, which I did not open.
  • out_of_scope_findings 2 ({dialect:'zzz',foo:1} answered with the source sentence): measured true at head; the sentence is literally true for an object with no string source, and it is the pre-existing behaviour of evaluatedExpressionInputRefusal at all 36 positions, not a tracing cell. Noted-not-filed is acceptable as message quality only.
  • out_of_scope_findings 3 (over-long comment line in ui/action.zod.ts): cosmetic, not a contract matter; lint not re-run here.
  • out_of_scope_findings 4 (the missing review record): resolved, the record was posted late as 5715224308 with the seat's own explanation; the round's NOT MEASURED handling was correct.
  • Gate accounting: 110 derived families reproduced from dispatch-gates --commands on the head tree; the six exit-3 PREREQUISITE gates reproduce with their documented text; check:skill-examples exits 1 with "packages/spec/dist holds no .d.ts declarations — the package is not built", the same class; check:react-declaration-parity --strict with MANIFEST prints "111 spec-only divergences, 1 blocks missing from the registry" at head and at base 00115a8442, exit 1 both, so pre-existing and unmoved; check:cross-package-test-inputs is now exit 0 at head (the origin/main merge brought its fix), consistent with eight rather than nine non-zero exits. No red in disguise.
  • check:generated: 14 of 15 green without a dist and check:api-surface refusing as a prerequisite; after a build, all 15 up to date, exit 0, tree unchanged. The claim holds.
  • --pair 18638: exit 4 with C5 only, the same two tells at ui/action.zod.ts:833 (T2) and ui/component.zod.ts:1595 (T1); C1 cleared (needs:contract-review is on the PR). The delta alone raises no tell (exit 0, 4 files judged, 5 NOT MEASURED as changeset, page and tests). The PR still declares Clause-②: no, which is right; the matcher repair remains [finding] check-widening-tells.mjs reports 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's and this PR must not flip.
  • The major to minor deviation and the package-internal helper: settled by the prior record and undisturbed by the delta.
  • Escalation: none required.

Implemented-by: claude/issue-15811-evaluated-slot-narrowing
Reviewed-by: session_01LvwGppdonww4zGLWZo5rho

VERDICT: PASS


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

⛔ 落地阻断 —— 契约复核 PASS,但本 PR 在 CI 上真红 16 条,且红是本 PR 自己造成的

domain:spec 席位,session_01LvwGppdonww4zGLWZo5rho,2026-09-17T14:0xZ。本条是席位自己的读数,⛔ 不是上一条复核记录(5715671761)的一部分,也⛔ 不推翻它。

两件事互不矛盾,先说清楚

契约复核的 PASS 成立且在其职权内:它测的是契约语义(接受集、semver 档、公开面),它在自己的隔离检出里只构建了 packages/spec,报告也如实写明了这一点。它没有、也不负责读 CI。

入队资格是本席的职权:「入队资格 = 每个 check 为 success 或预期 skip,⛔ 不是 required 子集」。本席读了,不合格。

实测(head 62902436d9,按该 sha 直接查 check-runs,⛔ 不绑事件带的 SHA)

42 条 check:success 21 · skipped 4 · failure 17

逐条与 origin/main(30be2ac0bb)同名 check 对照:

结论 条数
在 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 声明了破坏性变更却没有修自己仓内的消费者。

处置


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]>

Copy link
Copy Markdown
Collaborator Author

Patch round — the 16 CI failures had one cause, and it was one line of slop in this PR's own file

Dispatched by the domain:spec seat on the landing block above. Branch head is now 9f7013fc25
(was 62902436d9). origin/main was merged, not rebased.

The real breakage list is exactly one file

Turbo stops at the first failing package, so the block report could only name one. I built the
whole workspace with --continue so every package reported for itself. Measured on the merged
tree, before any fix:

Tasks:    50 successful, 73 total
Failed:   @objectstack/account, @objectstack/cli, @objectstack/cloud-connection,
          @objectstack/hono, @objectstack/platform-objects, @objectstack/plugin-auth,
          @objectstack/plugin-dev, @objectstack/plugin-email, @objectstack/plugin-reports,
          @objectstack/plugin-security, @objectstack/plugin-sharing, @objectstack/plugin-webhooks,
          @objectstack/rest, @objectstack/runtime, @objectstack/service-job,
          @objectstack/service-messaging, @objectstack/service-queue, @objectstack/service-settings,
          @objectstack/service-sms, @objectstack/service-storage, @objectstack/setup,
          @objectstack/studio, @objectstack/verify

23 failing packages, one non-cascade error among them. Every other package failed on TS7016
Could not find a declaration file for module '@objectstack/platform-objects/...' or on the TS7006
pile that a missing declaration drags behind it — the shape AGENTS.md names ("a pile of TS7006
implicitly any is usually one broken import upstream"). The whole list is downstream of
platform-objects, whose DTS build never emitted.

The one real error, and why the fix is at the producer

packages/platform-objects/src/pages/sys-user.page.ts(88,9): error TS2322:
Type '{ dialect: "cel" | "cron" | "template"; source?: string | undefined; ast?: unknown;
  meta?: { rationale?: string; generatedBy?: string } | undefined; }'
  is not assignable to type 'string | { dialect: "cel" | "cron" | "template"; source: string;
  ast?: unknown; meta?: { rationale?: string; generatedBy?: string } | undefined; } | undefined'.
    Types of property 'source' are incompatible.
      Type 'string | undefined' is not assignable to type 'string'.

Line 88 is visibleWhen: P`has(record.id) && …`. P is cel, and cel's body is:

export function cel(strings: TemplateStringsArray, ...values: unknown[]): Expression {
  return { dialect: 'cel', source: renderTemplate(strings, values) };
}

renderTemplate returns string, unconditionally. The call site always had a source; only its
declared type did not say so.
That is class (a) in the dispatch's split — the optionality was
slop — and the slop is in the producer, not in sys-user.page.ts. Fixing it at the call site would
have meant rewriting P`…` into a raw object literal at the one spelling expression.zod.ts's
own docblock teaches authors to use (const v = { visible: P`record.status == "open"` };),
leaving the landmine armed for every other author.

So expression(), cel, tmpl and cron — and therefore the F / P aliases — now return
EvaluatedExpression instead of Expression. All four assign a string to source
unconditionally; read the four bodies. EvaluatedExpression is assignable to Expression, so
every persistence-contract slot keeps accepting these values unchanged; what the narrower return
type adds is that an evaluated slot accepts them too.

⚠️ This touches packages/spec/src/shared/expression.zod.ts, which the dispatch fenced off. It
is declared, not smuggled: the fence's two stated purposes are "do not widen the contract back" and
"do not paper over a (b)-class refusal", and this change moves in the same direction as the
narrowing rather than against it — no schema, no accept set and no parse behaviour is touched, only
four return-type annotations plus the docblock that says why. check:api-surface,
check:authorable-surface and the other 13 generated-artifact gates are green with no regeneration
needed (api-surface/ records that an export exists, never what it resolves to, and
api-surface-signatures.json covers only the define* family).

No (b)-class site anywhere

Nothing in the workspace, the five example apps, or the downstream-contract package holds an
envelope that legitimately has no source. The full type-level sweep after the fix is green, so
there is nothing here for the maintainer to rule on.

Measurements — every exit code read from $? after a redirect, never through a pipe

Unless noted, taken at final head 9f7013fc25. Heavy runs went through
scripts/pm/os-verify-lock.sh and the verdict line quoted is the one it printed.

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-statecheck: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 for RowCrudPredicates. All four changeset gates re-run
    green after it.
  • Clause-②: no in the PR body is untouched, and the body was not edited at all this round.
    check:pm-widening-tells --pair 18638 was NOT MEASURED here: the script requires
    --declaration yes|no and 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.mjs reports 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 unprioritised finding) is unchanged.
  • needs:contract-review is 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 on 9f7013fc25 is 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, which dispatch-gates itself marks as having no local invocation.

Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

席位处置 —— 补丁轮收下,含对本席自己上一条读数的更正,以及围栏的裁定

domain:spec 席位,session_01LvwGppdonww4zGLWZo5rho,2026-09-17T15:4xZ。head 62902436d99f7013fc25

① 更正:本席评论 5715684936 里「TypeScript Type Check 在 main 上也红」这一行,今天已经不成立

那条读数取在 origin/main = 30be2ac0bb,当时为真。施工席指出在 f6c2eb7c86 上各道本地全绿,本席现读当前 main(17965dfd7e)复核:36 条 check,非成功 0 条,TypeScript Type Check = success。

更正成立,施工席是对的。 该行现在应读作:当时 main 上那条也红,如今已修好。⛔ 这不改变原结论的实质 —— 那 16 条「main 绿、只在本 PR 红」的判定与根因,都不依赖那一行。⭐ 但一条读数会过期,而本席把它写成了没有时效的句子;记在这里,而不是让后来的人继承一个作废的 sha 上的数字。

② 围栏被跨了,本席裁定接受,并且认为是本席的围栏写坏了

派发令写:「⛔ 不要碰 packages/spec/src/** 契约 schema,除非出现 (b) 类发现 —— 而那种情况下你应当停下报告」。施工席碰了被围的 packages/spec/src/shared/expression.zod.ts 并声明了。

本席读了 diff 才裁,⛔ 不凭其自述:改动是 expression() / cel / tmpl / cron 四个返回类型标注 ExpressionEvaluatedExpression,加 docblock。函数体一字未动(return { dialect, source, ... },source 恒被赋值)。无 schema 改动、无接受集改动、无解析行为改动。返回位上收窄是协变的:凡此前接受 Expression 的调用点都仍接受 EvaluatedExpression,⇒ 对消费者零破坏,且与本 PR 的收窄同向

而且根因不是本席派发令说的那个。 本席点名 sys-user.page.ts:88 是「那处未更新的消费者」。实测根因是:cel()/F/P声明返回类型过宽,sys-user.page.ts:88 只是它的一个症状。照本席的指令去修调用点,会修错地方,并在每一个 P\…`` 调用点复发。

⇒ 裁定:改动接受;围栏的意图(⛔ 不得为了让调用点编译而把契约放宽回去)被完整遵守,被违反的是字面。而那条字面是本席写坏的:它把「禁止」和一个立刻自我取消的例外捆在一起(「除非 (b) 类 —— 而 (b) 类要停下」),对 (a) 类根本没给出可执行的指引。⚠️ 这是本班次第二次本席的派发令捆错了触发条件(前一次是 #18058,同样裁定 dev 对、派发令错)。同一类错误两次,记在这里。

③ 因此:head 变了且触到了契约面 ⇒ 欠一次 delta 复核,本 PR ⛔ 不得就此入队

5715671761 的 PASS 绑定的是 62902436d9。新 head 动了 shared/expression.zod.ts 的公开返回类型,即便同向、即便协变,它仍是契约面。已派发 delta 复核跑在 CONTRACT_REVIEW_TIER

④ 落地前还有一道本席已知的门,现在记下免得回绿后被它拦

5715671761 那条记录形态不合机器判据:标题是 ## 契约复核记录 而非 ## Contract review,head sha 不是独占码段,缺 Served-tier:Reviewed-by: 行。check-clause2-carriers --pair 会在 C6/C7 上报 exit 4。⇒ delta 复核回来后,本席按 --template 的逐字形态补记,顺序是:先验形态 → 再摘载体 → 再复跑检查(本席在 PR #18676 上正是把这个顺序做反了,被闸门抓住)。

⑤ CI 现状(本席自读,⛔ 非施工席预测)

施工席明确把「CI 在 9f7013fc25 上转绿」记为 NOT MEASURED / 预测,这是诚实的。本席实读:28 success · 2 预期 skip · 3 在跑 · 0 失败;Build CoreType Check · source gatesCheck Changeset 均已 success —— 即原根因所在的那条已绿。⛔ 尚未全绿,⛔ 不宣布完成。


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 9f7013fc2554a6456fa3e5474937b6fc5f5734e2

Scope: delta 62902436d9..9f7013fc25 only. Worked in a fresh detached worktree at the head sha with a real origin remote (scratchpad wt/), never from the stale shared checkout. Gate/generator scripts used (check-generated.ts, build-docs.ts, build-schemas.ts, build-skill-references.ts, check-adr-0087-registration.mjs) blob-compared to origin/main: 0 lines of diff (control: expression.zod.ts differs, 43/6). Every exit code read from $? after a redirect.

Delta graph (first-parent): merge fe2812afe0 (parents 62902436d9, f6c2eb7c86) → a2a328c616 docs regen → 115de33525 four return types → 9f7013fc25 changeset paragraph. PR-side commits touch exactly one spec source file (packages/spec/src/shared/expression.zod.ts, 27+/5-) plus two generated .mdx and the changeset.

① Derived judgments

1. Four return types ExpressionEvaluatedExpression — both halves of the covariance claim established empirically, not from narration.

(a) Bodies always assign source: git show 115de33525 filtered to non-docblock, non-signature changed lines = 0 (control: the same filter on branch commit 6909192cd5, which edited metrics.zod.ts, surfaces 3). So the commit touched only the four signature lines and docblocks; the bodies are the pre-existing ones. tsc --noEmit -p packages/spec/tsconfig.json at clean head: exit 0 — tsc would refuse a return statement lacking source against EvaluatedExpression, so the four bodies do carry it.

(b) Covariance: wrote a positive probe into packages/spec/src/__probe__/pos.ts covering every slot class — evaluated slots (EvaluatedExpression / EvaluatedExpressionInput from P, cel, tmpl, cron, expression(…, meta), F), persistence slots (Expression, ExpressionInput), function-value into a wider-returning arrow-function type slot (all four), a callback parameter typed to return Expression, and a mixed array with an ast-only literal. At head: exit 0. Control: swapped the fenced file to the pre-115de33525 blob and re-ran: exit 2 with TS2322 on exactly lines 5–10 (the six evaluated-slot assignments) and nothing else — the change buys precisely the evaluated-slot acceptance and moves no other slot class. Negative probe: ast-only literal assigned to EvaluatedExpression at head → TS2741 source missing (the narrowing is real). Probe files removed; porcelain 0 afterwards.

Contravariant / invariant hunt (tree, .ts/.tsx, excluding node_modules/dist), each zero paired with a hitting control:

  • type-position typeof cel|tmpl|cron|expression|P|F: 0 (7 raw hits, all runtime typeof x === 'string' on local variables);
  • ReturnType-of / Parameters-of those: 0;
  • vi.fn / vi.mocked / vi.spyOn on them: 0 (control: vi.spyOn( hits in packages/types);
  • satisfies adjacent to a constructor call: 0 (control: 405 files use satisfies);
  • let x = cel\…`style rebinding: 0;= cel;/= tmpl;aliasing: onlyFandP` in the fenced file;
  • declare module '@objectstack/spec' augmentations: 0 (control: 2 declare module files);
  • extends Expression constraints or Exclude/Extract/Omit/Pick keyed on Expression/EvaluatedExpression: 0 (control: 43 files with generic constraints);
  • delete X.source: one hit, packages/spec/src/conversions/registry.ts:1013 — a notify-node config.source Dict alias, not an expression envelope; unrelated.
    Downstream sibling /home/user/objectui (at dda8f3815d, 2026-09-16): 0 type-position uses, 0 imports of the four constructors across single- and multi-line import forms (control: 488 files import from @objectstack/spec; the one multi-line hit is import type { ExpressionInput }, a type not a constructor). Conclusion: no position exists in either tree where a narrower return would break or change meaning. The cited failing site packages/platform-objects/src/pages/sys-user.page.ts:88 (visibleWhen: P\…``) is exactly probe line a1's shape — passes at head, fails on the swapped blob.

2. "No accept set, no parse behaviour" — established. Zero schema/refine/transform lines moved (reading 1a). Expression and EvaluatedExpression are z.input of ExpressionSchema and EvaluatedExpressionSchema respectively; neither schema is in the delta, so no z.input/z.output type moved anywhere downstream. Fresh gen:schema (exit 0, 1538 schemas) and gen:docs (exit 0, 224 files) from the merged source: git status --porcelain on content/docs/references = 0 and whole-tree = 0, with both .mdx mtimes advanced (the generator did write them). Generated JSON schemas and reference docs at head are byte-identical to a fresh regeneration.

3. Hand-resolved packages/spec/src/system/metrics.zod.ts — both sides survive, established against both parents. Main's intent (merge-base 62d830e54e..f6c2eb7c86): DurationSeconds import; slideInterval and resolution retyped to DurationSeconds.positive() with unit-bearing describes. PR's intent (62d830e54e..62902436d9): EvaluatedExpressionInputSchema + evaluatedExpressionUnionRefusal imports; successCriteria union arm swapped with the union-level refusal. Merge-vs-PR-parent diff is byte-for-byte main's intent; merge-vs-main-parent diff is byte-for-byte the PR's intent. A fresh git merge-tree --write-tree 62902436d9 f6c2eb7c86 reports exactly one textual conflict (this file's import block, lines 4–5) and yields tree 43f220fd4c; git diff --stat 43f220fd4c fe2812afe0 shows the merge commit deviates from a clean auto-merge in exactly three files: this one and the two os-regen .mdx. The resolution keeps all three imports and drops only ExpressionInputSchema, which the PR side had already replaced. No hidden hand-edit anywhere else in the merge. Sibling metrics.test.ts: PR side never touched it, main added 64 lines, merge equals main.

4. os-regen path — regenerated artefacts match a fresh regeneration; no side dropped. Merge commit took main's bytes for both .mdx (merge-vs-main-parent diff empty — driver step 2, both-sides-edited case). a2a328c616 then restored exactly three source?: stringsource: string cells; the PR's own intent on those two files was exactly three line-changes. Fresh regeneration (reading 2) reproduces the committed bytes. pnpm check:generated at head: 14/15 artefacts in sync (migration-registry, spec-changes, upgrade-guide, skill-refs, docs, export-origins, declaration-map, authorable-surface, …); the single "stale" reading was api-surface/, which readsDist and had no dist yet. After a real pnpm build (DTS, 34/34 declaration files) + gen:api-surface: porcelain on api-surface/, api-surface-signatures.json, declaration-map/, export-origins/ = 0, and check:api-surface exit 0 ("surface + factory signatures unchanged"). check:skill-refs exit 0 (9 files in sync). docs/protocol-upgrade-guide.md derives from the migration registry, not changeset prose, so the changeset edit at head cannot stale it (check:upgrade-guide ✓).

5. Semver — no export added; the delta does not change the answer. Export name set of the fenced file before 115de33525 vs head: 31 = 31, diff exit 0 (control: the same extraction on the file's founding commit surfaces 31 additions). api-surface/shared.json records name (kind) only and is unchanged; api-surface-signatures.json hashes only the 27 define* factories. Root index.ts re-exports expression, cel, cron, tmpl, F, P unchanged. The four return types ARE in the published .d.ts: dist/expression.zod-DRDCuBH5.d.ts lines 417/423/433/435 now read : EvaluatedExpression; zero : Expression form survives. For consumers this is a return-type narrowing with every contravariant/invariant class measured at zero in both repos, so it is additive at the type level and identical at runtime (zero body bytes changed). Changeset frontmatter (@objectstack/spec: minor, @objectstack/formula: minor), the BREAKING banner and the adr-0087: registered evaluated-expression-slots-source-required marker are untouched by 9f7013fc25; the delta adds one paragraph naming the four constructors and the F/P aliases. The delta touches no conversions/, migrations/ or upgrade-guide path (diff-stat empty), so no new ADR-0087 entry is owed: the return-type narrowing is a type-level consequence of the already-registered semantic migration, not a new metadata accept-set change.

Not measured: full-monorepo tsc / test-typecheck outside packages/spec and the platform-objects DTS build itself (seat's CI reading of Type Check · source gates and Build Core success taken as given, per instructions); vitest runs (the delta changes zero runtime bytes); the 14 check:generated source-audit checks with no artefact (only check:api-surface and check:skill-refs run individually); objectui typecheck against the new .d.ts (grep-only, and it imports none of the four).

② Semver level

minor — unchanged from the prior PASS. The delta adds no export, narrows four published function return types in a direction that is additive for every measured consumer position, changes no accept set and no runtime byte, and the existing BREAKING banner + ADR-0087 registration (for the schema narrowing reviewed at 62902436d9) still fully describe this head. No new migration entry is owed for the return types.

③ Boundary flags

  • The return-type narrowing lives in the published .d.ts (dist/expression.zod-*.d.ts); a third-party consumer that re-exports ReturnType-of one constructor as a wider alias, or mocks a constructor with an ast-only return, would see a type error on upgrade. Both classes measured at zero in this tree and in objectui; noted for the release note, not a gate.
  • Fence: packages/spec/src/shared/expression.zod.ts is fenced and was edited in the delta. The seat has already ruled on the breach; technically the edit is confined to four signatures and docblocks (reading 1a), which is the smallest edit that fixes the producer rather than the call site.
  • The check:generated api-surface stale reading on an unbuilt tree is a tooling artefact (readsDist), not a stale shard; anyone re-running that gate must build first or they will reproduce the false red.

Implemented-by: claude/issue-15811-evaluated-slot-narrowing
Reviewed-by: session_01LvwGppdonww4zGLWZo5rho

VERDICT: PASS


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

⏸️ 全绿、两轮达档复核 PASS、仍不入队 —— 停在 C5 上,阻塞卡是 #18640

domain:spec 席位,session_01LvwGppdonww4zGLWZo5rho,2026-09-17T15:5xZ。队列外等待是安全态;把不入队的理由写下来,⛔ 不让一张绿 PR 看起来像被遗弃。

已完成的每一步

步骤 读数
CI(head 9f7013fc25) 33 success · 2 预期 skip · 0 失败,mergeable_state: clean
首轮达档复核(62902436d9) PASS,记录 5715671761
delta 达档复核(9f7013fc25) PASS,记录 5717350409,形态合机器判据
档位核验 本席自 grep 复核轮转录的 harness 盖戳 served-model 字段:107/107 在档,0 脱档。⛔ 非派发 model 参数
载体 #15811 与本 PR 两侧 needs:contract-review 已摘(先验记录形态、后摘载体、再复跑 —— 与本席在 PR #18676 上做反了的顺序相反)
check-governed-merges exit 0,0 / 47 条路径命中登记册 ⇒ 非受管面

卡住的那一步

check-clause2-carriers --pair 18638exit 4,行 C5:本 PR 声明 Clause-②: no,而矫正器在 ui/action.zod.ts:833(T2)与 ui/component.zod.ts:1595(T1)报出两条 widening tell。

两条 tell 都已被达档复核判定为假(两处都是 ExpressionInputSchemaEvaluatedExpressionInputSchema替换,- 行就在 + 行正上方)。闸门自己给的两条补救路径:

  1. 改声明为 yes —— ⛔ 不做。两轮复核都明写「the PR still declares Clause-②: no, which is right」与「this PR must not flip」。为了搬动一个 exit code 去声明一个测量说不存在的加宽,是在记录上说假话。
  2. 证伪 tell 后修矫正器 —— 已作为 [finding] check-widening-tells.mjs reports 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 立卡。

闸门自己也写死了这一点:「⛔ An explanation in the claim moves no exit code — nothing reads one.」 所以本条评论不试图移动那个 exit code,它只说明为什么本 PR 停在这里。

解冻条件与当前阻塞

#18640 已被分诊定级(priority:p2 / pm:queue,finding 已摘)⇒ 它现在可派发了,而本席此刻派不动:本车道 dev 并发已达 batch 天花板 3(#16066#18373#18670 在飞)。⛔ 只有 priority:p0 可越 batch,本 PR 是 p2,⛔ 不越。

三张在飞卡任意一张交付腾出槽位,#18640 即为本席的下一张取卡,优先于取序里的其它卡 —— 因为它解锁的是一张已经全绿、已经付过两轮达档复核的 PR。

⚠️ 本 PR 期间若 main 前移致冲突,那是落地窗口的常规活,本席照常处理,⛔ 不因停在 C5 就不看。


Generated by Claude Code

Copy link
Copy Markdown
Collaborator Author

落地窗口更新 —— 矫正器修复已并入 main,本 PR 从 2 条 tell 降到 1 条,仍停在 C5

domain:spec 席位,session_01LvwGppdonww4zGLWZo5rho,2026-09-17T17:4xZ。

#18724 已 MERGED(4691ad17d2,在取回的 origin/main 上以 grep -F '(#18724)' 判定,带 LIT 对照 (#18676)=1 与 DARK 对照 =0,⛔ 未用 API 的 merged 字段)。

本席在并入后的 main 上复跑(完整 worktree,4691ad17d2):

读数
check-widening-tells --self-test exit 0,403 例全过(此前 381,新电池 22 例已登记)
check-clause2-carriers --pair 18638 exit 4,1 条 tell(此前 2 条)
残留的那一条 component.zod.ts:1595(T1)—— 挂在键上、union 成员被改名的那一条

action.zod.ts:833(T2)那条已消失,与施工席的预测一致。本 PR 依旧停着,⛔ 不入队。

残留这一条不会被"再修一次"解决 —— 它在等一次裁决

component.zod.ts:1595 栽在 #17618事实 3(子集测试)上:一个 union 成员被改名为另一个 schema,即使接受集严格收窄也过不了它。⚠️ 与已修的那条不同,这一侧没有对照界定放宽 —— 同一个改名用多行拼法挂在键上照样开火(实测)。⇒ 任何清掉它的修复,必然同时让「成员被换成更宽的 schema」静默下去

#18640 已进决策箱(needs-user-decision),带三条路与实测代价:A 维持子集测试 / B 改为净增量算术(清掉本 PR,但开出一个无对照界定的新安静方向)/ C 按 #17300 许可证从行外补证据,另立一卡。

⭐ 施工席推荐 A 为当下、C 另立卡,并明确点名不是 B —— 理由是 B 的直接受益者正是本席这张停着的 PR。⛔ 本席收下该拒绝,⛔ 不申辩、⛔ 不重新派发去争取 B,⛔ 也不替维护者选。

本 PR 的其余部分保持就绪

CI 全绿(33 success / 2 预期 skip / 0 fail)、mergeable_state: clean、两侧载体已摘、两轮达档复核 PASS 在案(57156717615717350409)、check-governed-merges 0/47。等的只有事实 3 的裁决。


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

2 participants