Skip to content

fix(dogfood): the expression scan registers no alias for a value parsed by a roster schema - #24680

Merged
objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-24645-expression-scan-value-binding
Oct 19, 2026
Merged

objectstack-fleet[bot] merged 2 commits into
mainfrom
claude/issue-24645-expression-scan-value-binding

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #24645
Clause-②: no
Basis for the Clause-② line: the diff is one test file's text scan in @objectstack/dogfood ("private": true, no files[]). No schema, export or published package moves, so no accept set or public surface widens or narrows. Labelled skip-changeset on the same basis.

The hourly full run on main is red in "Dogfood Regression Gate (3/3)" because the expression-conformance scan registered a parse RESULT as an alias of a roster schema. This PR fixes the scan. packages/spec is untouched, and no ledger row is added.

Reproduction (on origin/main bfc1539fe, before the fix)

pnpm exec vitest run --maxWorkers=2 test/expression-conformance.test.ts in packages/qa/dogfood gives Tests 2 failed | 5 passed (7):

  • :426 (checkLedger): UNCLASSIFIED surface — add a ledger row (ADR-0060) for nine keys, all ui/app.zod.ts: NavigationContributionSchema.history, NavigationContributionSchema.items, AppBrandingSchema.history, NavigationAreaSchema.history, NavigationAreaSchema.navigation, AppContextSelectorSchema.history, AppSchema.history, AppSchema.navigation, AppSchema.apis.
  • :518: ui/app.zod.ts:AppContextSelectorSchema.history: 3 declaring positions share ONE ratchet key ... ui/app.zod.ts:1466, ui/app.zod.ts:1494, ui/app.zod.ts:1522.

The scan code, run on its own over packages/spec/src, shows how the chain forms. Triage's diagnosis holds, and the four "other" keys are the chain it predicted:

line in ui/app.zod.ts registered alias how
1027 parsed LOCAL_BINDING matches const parsed = EvaluatedExpressionInputSchema.safeParse(source);
1028 authoredVisibleEnvelope (multi-line) return parsed.success ? ... hits the alias with no field: above it
1082 navItemVisibleDataNeedsDataSource (multi-line) the helper's call site
1145 NavigationItemSchema (multi-line) .superRefine(navItemVisibleDataNeedsDataSource)

After that, every describe line containing the word "parsed" counts as a hit: .history x7 and AppSchema.apis. So does every z.array(NavigationItemSchema): .items and .navigation x2. That makes 11 positions under 9 keys, and none of them is an expression surface.

The rule change, and why a parse call is a value

schema.parse(v), .safeParse(v), .parseAsync(v) and .safeParseAsync(v) return the parsed value or a result object, never a schema. Nothing can type a slot with what they bind, so a roster name that is only the receiver of one of those calls is a use of the schema, not a reference to it.

  • scanSource now blanks every occurrence of a roster or alias name that is directly followed by one of the four parse-method calls. It does this once per line, before the hit test. So the one rule covers all three places a hit is acted on: the LOCAL_BINDING registration, the multi-line (enclosing top-level declaration) registration, and field attribution.
  • An occurrence that is not the receiver still counts. Examples are z.union([X, ...]).parse(v), or a second mention elsewhere on the line. So the narrowing errs toward registering. A false alias is a loud UNCLASSIFIED, while a missed alias silently drops the slots typed with it.
  • scanFile is now a thin wrapper over scanSource(text, file). The spelling controls run the exact code the spec walk runs.

Measured over the whole of packages/spec/src (the real scan code at base vs head):

  • Alias table: 7 before, 3 after. Exactly parsed, authoredVisibleEnvelope, navItemVisibleDataNeedsDataSource and NavigationItemSchema are gone. PredicateInputSchema, ActionConditionInputSchema and actionCondition (multi-line) are unchanged.
  • Declarations: 66 before, 55 after. Exactly the 11 ui/app.zod.ts positions above are removed, and 0 are added.
  • via counts: head 42, inline 1 and manual 2 are unchanged. Alias goes from 21 to 10.
  • Unattributed stays at 0.

The 9 keys and the collision are gone because the false alias is gone, not because anything was classified. The ledger diff is empty. The only parse call on a roster or alias name anywhere in packages/spec/src/**/*.zod.ts is the one at ui/app.zod.ts:1027.

Controls and ablation

SCAN_CONTROLS keeps its three mechanism floors and gains four spelling controls. A new test, alias registration tells a schema from a value parsed by one, checks each control's alias table and requires zero unattributed hits:

  1. The exact authoredVisibleEnvelope spelling from ui/app.zod.ts:1026-1029 registers nothing.
  2. The same helper written without the local (return EvaluatedExpressionInputSchema.safeParse(source).data;) registers nothing. This exercises the multi-line branch.
  3. The real single-line alias, ui/action.zod.ts's const ActionConditionInputSchema = z.union([z.boolean(), EvaluatedExpressionInputSchema], {, still registers ActionConditionInputSchema.
  4. The real multi-line alias, ui/component.zod.ts's const actionCondition = () => with the union on the next line, still registers actionCondition (multi-line).

After the fix, at head: Tests 8 passed (8). The fix was committed first. Each ablation ran through node scripts/ablation-replace.mjs in wrap mode on the one const referencing = ... line. It proved the mutation landed (anchor 1 to 0, blob changed, marker grep -c = 1 inside the run) and proved the restore (blob == HEAD 88aa5562c726, git diff HEAD empty):

  • Ablation 1, the pre-fix hit test on the raw line: Tests 3 failed | 5 passed (8). The new control is red with the scan registered [parsed, authoredVisibleEnvelope (multi-line)] where [] was expected. Both original failures come back: the same 9 UNCLASSIFIED keys, and the AppContextSelectorSchema.history collision at 1466/1494/1522.
  • Ablation 2, the screen applied to LOCAL_BINDING lines only: Tests 1 failed | 7 passed (8). The spec run is green, but control 2 is red with registered [authoredVisibleEnvelope (multi-line)]. Control 2 is the only thing that holds the multi-line branch to the rule.
  • Ablation 3, an over-broad screen that blanks every binding line: Tests 2 failed | 6 passed (8). Control 3 is red with registered [] where [ActionConditionInputSchema] was expected, and the ratchet goes red with STALE covers ... ui/action.zod.ts:actionObject.visible. The alias floor (2) stays green under this mutation, which is why the real-alias controls exist next to it.

Why the PR that introduced this was green (note only)

The dogfood shards never run on pull_request or merge_group events. ci.yml's THE TWO SPEEDS block puts dogfood (three shards), dogfood-verify, temporal-conformance and console-pin on the full battery only, through if: ... github.event_name != 'pull_request' && github.event_name != 'merge_group'. The aggregate Dogfood Regression Gate keeps if: always(), so with zero expected shard attestations it concludes success.

On the introducing PR's head e058ce508e, the check-runs read Dogfood Regression Gate (${{ matrix.shard }}/3) | skipped and Dogfood Regression Gate | success. So no PR, spec-only or not, runs this scan before merge. The hourly full run is the designed backstop, and it caught this.

The same applies to this PR. CI will not run expression-conformance.test.ts here or in the queue. The local run above is the only pre-merge reading, and the first CI reading is main's push and hourly battery after merge. Nothing is built for this.

Local verification, at head 2c5a411ba

  • expression-conformance.test.ts: Tests 8 passed (8). pnpm --filter @objectstack/dogfood typecheck (tsc --noEmit, whose include covers test/**/*): exit 0. Both ran after turbo run build --filter='@objectstack/dogfood^...'.
  • node scripts/pm/dispatch-gates.mjs --commands: 55 derived commands, each exit code captured before any pipe. All exit 0.
    • On the first pass, two were PREREQUISITE NOT MET (exit 3). check:dual-build-cjs-loads lacked 8 packages' dist/. check:type-check-debt timed out in the queue for the shared lock.
    • Both were re-run under the lock after building those packages, and both exited 0.
    • The --ran reconciliation reads: 55 derived, 55 run, 0 NOT-MEASURED, 0 UNRUN.
  • The 11 declared-wide families (check:init-service-contract, live-db-isolation, meta-type-normalized, optional-error-sink, resume-authority-declared, route-envelope, runner-env-posture, settings-bind-window, startup-registry-verdict, verify-stand-in, wildcard-fallthrough) and pnpm lint: all exit 0.

Acceptance notes

  • Boundary, not filed: a receiver split from its .parse( across lines (the name ends one line and .safeParse( starts the next) is not recognised and still registers. That is the loud direction, and no such spelling exists in packages/spec/src today. Zod's decode/encode method family is likewise not in the screen. No roster or alias name is a receiver of either in the spec today.
  • Local verification is narrowed to the one changed file under vitest, plus the package typecheck and the derived gates. The rest of the dogfood suite boots example apps and does not read this file. It runs on main's full battery.

Generated by Claude Code

…ed by a roster schema

`const parsed = EvaluatedExpressionInputSchema.safeParse(source);` in
ui/app.zod.ts bound a parse RESULT, but LOCAL_BINDING registered `parsed`
as an alias of a roster member. Every later line containing the word then
counted as a declaring position, and the multi-line branch chained the
helper, its caller and NavigationItemSchema onto the alias table: nine
UNCLASSIFIED non-expression keys and one key collision on main.

A roster (or alias) name that only receives a parse call (`.parse(`,
`.safeParse(`, `.parseAsync(`, `.safeParseAsync(`) is now blanked out of
the line before the hit test, so the binding branch, the field
attribution and the multi-line branch all apply one rule. SCAN_CONTROLS
gains spelling controls: the exact helper spelling and its multi-line
variant register nothing; the real single-line and multi-line alias
spellings still register.

Claude-Session: https://claude.ai/code/session_01SYEJqDL3NXxrJRBVqaMu9T
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet objectstack-fleet Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Oct 19, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

Nothing in this diff resolved to a documentable surface (no symbol, route or SDK anchor derived from 0 changed package(s)), so this run has no opinion about the docs.

What this run could not see

Coarse fallback — 0 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 eadea572082976b747e0828d56c24fdf62cd8803 → packageMentionDocs.

@github-actions github-actions Bot added the tests label Oct 19, 2026
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对 — ACCEPT

domain:cli seat 1 (seat post #6024) · zhuangjianguo · session session_01SYEJqDL3NXxrJRBVqaMu9T · 2026-10-19T23:15Z · on head 2c5a411ba · #24645 (p1: main's Dogfood Regression Gate is red; claim 6278684496). This is the seat's own review at the default tier (Clause-②: no, test only), read against the diff on GitHub, not the report.

  1. What moved: one file, packages/qa/dogfood/test/expression-conformance.test.ts, +138 / −7.
    • scanSource blanks every roster or alias name that is the receiver of a .parse( / .safeParse( / .parseAsync( / .safeParseAsync( call out of the line before the hit test. One rule therefore covers the LOCAL_BINDING branch, the multi-line branch and field attribution.
    • scanFile becomes a wrapper over scanSource, so the controls run the exact code the spec walk runs.
    • A name that is not the receiver still counts. The narrowing errs toward registering, which is the loud direction (UNCLASSIFIED).
  2. Premise, read by the seat on origin/main: packages/spec/src/ui/app.zod.ts:1027 is const parsed = EvaluatedExpressionInputSchema.safeParse(source);. It is the only parse-call binding on a roster member in the spec's non-test source. main's failing job (118336465533, Dogfood 3/3 on bfc1539fe) fails only expression-conformance.test.ts, at :426 (9 UNCLASSIFIED) and :518 (the 3-position AppContextSelectorSchema.history collision). The report reproduces both on base.
  3. Controls:
    • SCAN_CONTROLS gains 4 spelling controls, checked by a new test, in both directions. The authoredVisibleEnvelope spelling and its local-free variant register nothing; the real single-line ActionConditionInputSchema alias and the real multi-line actionCondition alias still register.
    • The ablations named the guard of each branch: the pre-fix hit test, a screen limited to LOCAL_BINDING lines, and an over-broad screen each went red where predicted. Each restore was proven by blob.
    • Measured over the whole spec, base vs head: aliases 7 → 3 (only the 4 false ones leave); declarations 66 → 55 (only the 11 ui/app.zod.ts positions leave, 0 added). Head, inline and manual counts are unchanged, unattributed stays 0, and the ledger diff is empty.
  4. Accepted deviation: the dispatch named @objectstack/qa, which does not exist. The package is @objectstack/dogfood (private: true), which is the basis for skip-changeset. The dispatch's error, not the dev's.
  5. Gates: dispatch-gates 55 derived and 55 run, 0 NOT-MEASURED. pnpm lint exited 0.
  6. This PR's CI cannot run the changed test: ci.yml's two-speed if: keeps the dogfood shards off pull_request and merge_group by design. The test is measured by the dev's locked runs above, and first by CI on main's push run after the merge. The seat reads that run after landing.
  7. Findings: none filed. The line-based screen's split-receiver gap (loud, zero spellings today) and the corrected why-green mechanism are Acceptance notes.
  8. Landing: ready plus auto-merge through the queue once every check of 2c5a411ba is green or a rostered skip. main's last completed CI is red only on the job this PR fixes, so that pre-check reads as this PR's own target, not a block. The card closes on the merge.

Generated by Claude Code

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

Labels

size/m skip-changeset PR has no user-facing published change; bypasses the changeset gate tests

Projects

None yet

Development

Successfully merging this pull request may close these issues.

hourly full run: red on main (CI)

2 participants