Skip to content

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

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

os-litant wants to merge 13 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 551139bb78785441c2fee72acd87f0ce835f683b.

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 551139bb78785441c2fee72acd87f0ce835f683bpackageMentionDocs.

Which tree this was computed on

This run read content/docs from 6a2531be3b454244746c208bbc2a0d89f8c10e00 — the merge of head 115de33525f3670d943c958fac501fb4112bc226 into base 551139bb78785441c2fee72acd87f0ce835f683b, 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 6a2531be3b454244746c208bbc2a0d89f8c10e00 && git checkout 6a2531be3b454244746c208bbc2a0d89f8c10e00
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 551139bb78785441c2fee72acd87f0ce835f683b 115de33525f3670d943c958fac501fb4112bc226 && git checkout -B drift-repro 551139bb78785441c2fee72acd87f0ce835f683b && git merge --no-ff 115de33525f3670d943c958fac501fb4112bc226

node scripts/docs-audit/affected-docs.mjs --json 551139bb78785441c2fee72acd87f0ce835f683b

⚠️ 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 551139bb78785441c2fee72acd87f0ce835f683b → 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 <noreply@anthropic.com>

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 <noreply@anthropic.com>
…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 <noreply@anthropic.com>
…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 <noreply@anthropic.com>

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 <noreply@anthropic.com>
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 <noreply@anthropic.com>
`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 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment