Repository navigation
fix(dogfood): the expression scan registers no alias for a value parsed by a roster schema - #24680
Conversation
…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>
…creen's note Claude-Session: https://claude.ai/code/session_01SYEJqDL3NXxrJRBVqaMu9T Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckNothing 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): |
席位核对 — ACCEPT
Generated by Claude Code |
Fixes #24645
Clause-②: no
Basis for the Clause-② line: the diff is one test file's text scan in
@objectstack/dogfood("private": true, nofiles[]). No schema, export or published package moves, so no accept set or public surface widens or narrows. Labelledskip-changeseton the same basis.The hourly full run on
mainis 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/specis untouched, and no ledger row is added.Reproduction (on
origin/mainbfc1539fe, before the fix)pnpm exec vitest run --maxWorkers=2 test/expression-conformance.test.tsinpackages/qa/dogfoodgivesTests 2 failed | 5 passed (7)::426(checkLedger):UNCLASSIFIED surface — add a ledger row (ADR-0060)for nine keys, allui/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:ui/app.zod.tsparsedLOCAL_BINDINGmatchesconst parsed = EvaluatedExpressionInputSchema.safeParse(source);authoredVisibleEnvelope(multi-line)return parsed.success ? ...hits the alias with nofield:above itnavItemVisibleDataNeedsDataSource(multi-line)NavigationItemSchema(multi-line).superRefine(navItemVisibleDataNeedsDataSource)After that, every describe line containing the word "parsed" counts as a hit:
.historyx7 andAppSchema.apis. So does everyz.array(NavigationItemSchema):.itemsand.navigationx2. 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.scanSourcenow 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: theLOCAL_BINDINGregistration, the multi-line (enclosing top-level declaration) registration, and field attribution.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.scanFileis now a thin wrapper overscanSource(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):parsed,authoredVisibleEnvelope,navItemVisibleDataNeedsDataSourceandNavigationItemSchemaare gone.PredicateInputSchema,ActionConditionInputSchemaandactionCondition(multi-line) are unchanged.ui/app.zod.tspositions above are removed, and 0 are added.viacounts: head 42, inline 1 and manual 2 are unchanged. Alias goes from 21 to 10.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.tsis the one atui/app.zod.ts:1027.Controls and ablation
SCAN_CONTROLSkeeps 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:authoredVisibleEnvelopespelling fromui/app.zod.ts:1026-1029registers nothing.return EvaluatedExpressionInputSchema.safeParse(source).data;) registers nothing. This exercises the multi-line branch.ui/action.zod.ts'sconst ActionConditionInputSchema = z.union([z.boolean(), EvaluatedExpressionInputSchema], {, still registersActionConditionInputSchema.ui/component.zod.ts'sconst actionCondition = () =>with the union on the next line, still registersactionCondition (multi-line).After the fix, at head:
Tests 8 passed (8). The fix was committed first. Each ablation ran throughnode scripts/ablation-replace.mjsin wrap mode on the oneconst referencing = ...line. It proved the mutation landed (anchor 1 to 0, blob changed, markergrep -c= 1 inside the run) and proved the restore (blob == HEAD88aa5562c726,git diff HEADempty):Tests 3 failed | 5 passed (8). The new control is red withthe scan registered [parsed, authoredVisibleEnvelope (multi-line)] where [] was expected. Both original failures come back: the same 9 UNCLASSIFIED keys, and theAppContextSelectorSchema.historycollision at 1466/1494/1522.LOCAL_BINDINGlines only:Tests 1 failed | 7 passed (8). The spec run is green, but control 2 is red withregistered [authoredVisibleEnvelope (multi-line)]. Control 2 is the only thing that holds the multi-line branch to the rule.Tests 2 failed | 6 passed (8). Control 3 is red withregistered [] where [ActionConditionInputSchema] was expected, and the ratchet goes red withSTALE covers ... ui/action.zod.ts:actionObject.visible. Thealiasfloor (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_requestormerge_groupevents.ci.yml's THE TWO SPEEDS block putsdogfood(three shards),dogfood-verify,temporal-conformanceandconsole-pinon the full battery only, throughif: ... github.event_name != 'pull_request' && github.event_name != 'merge_group'. The aggregateDogfood Regression Gatekeepsif: always(), so with zero expected shard attestations it concludes success.On the introducing PR's head
e058ce508e, the check-runs readDogfood Regression Gate (${{ matrix.shard }}/3) | skippedandDogfood 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.tshere or in the queue. The local run above is the only pre-merge reading, and the first CI reading ismain's push and hourly battery after merge. Nothing is built for this.Local verification, at head
2c5a411baexpression-conformance.test.ts:Tests 8 passed (8).pnpm --filter @objectstack/dogfood typecheck(tsc --noEmit, whoseincludecoverstest/**/*): exit 0. Both ran afterturbo 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.check:dual-build-cjs-loadslacked 8 packages'dist/.check:type-check-debttimed out in the queue for the shared lock.--ranreconciliation reads:55 derived, 55 run, 0 NOT-MEASURED, 0 UNRUN.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) andpnpm lint: all exit 0.Acceptance notes
.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 inpackages/spec/srctoday. Zod'sdecode/encodemethod family is likewise not in the screen. No roster or alias name is a receiver of either in the spec today.main's full battery.Generated by Claude Code