Uh oh!
There was an error while loading. Please reload this page.
feat(lint): give validate-translation-references a flows leg - #11746
Conversation
The rule walked objects, globalActions, apps and dashboards; an unrecognised top-level namespace is skipped and never reported, and `flows` was one of them. A bundle keyed to flows.<name>.screens.<node_id>.fields.<field_name> parsed, shipped and silently resolved to nothing. The leg mirrors the dashboards -> widgets leg one level further: flow name, screen node id, screen field name, all warnings (ADR-0072 D1), each naming the universe it resolved against. Two shape facts measured against the schemas rather than assumed, either of which read the obvious way makes the leg a false-positive generator: - screen nodes NEST in ADR-0031 regions, so the universe is collected with walkFlowNodes, not the flat flow.nodes; - ScreenConfigSchema has two mutually exclusive shapes, and an object-form screen's field copy belongs under objects.<objectName>.fields.*. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015ahemw8RcTgqtxrj15PEZx
📓 Docs Drift Check6 anchor(s) derived from 1 changed package(s); no hand-written page names any of them. ✅ What this run could not see
Coarse fallback — 4 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin d8e2693154748426ea31696182e453db7376bc32 && git checkout d8e2693154748426ea31696182e453db7376bc32
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 09f93617e590b6556c698c65eaa502defcb139ed 172a28c9ce0b619136d31344e9e69cfe764bc6de && git checkout -B drift-repro 09f93617e590b6556c698c65eaa502defcb139ed && git merge --no-ff 172a28c9ce0b619136d31344e9e69cfe764bc6de
node scripts/docs-audit/affected-docs.mjs --json 09f93617e590b6556c698c65eaa502defcb139ed |
os-steve
commented
Aug 24, 2026
ACCEPT at The before/after on the card's own probe fixture, controls unchanged — which is the right instrument because it is the artifact the reporter used to demonstrate the gap: Premise re-verified at your own base with the control I gave you ( ⭐ Two schema facts that changed the implementation, both measuredScreen nodes nest in ADR-0031 regions. So the universe comes from
Neither is in the card. Both would have produced a leg that ships, passes review, and warns about correct documents. The anti-vacuity requirement was met at the level it was set⭐ Ablation A is the one that matters: make the collector never populate The clean run uses the same stack as the three orphan runs, so its silence is a verdict. The mirror ruling held — ⛔ #11745 is a live defect in another lane, and it is the FOURTH passThis is the most consequential thing you found. Verified independently: So a region-aware walker exists in the same package, three separate cards have each fixed this exact shape once, and ⭐ And the consequence you drew is the part a lesser report would have buried: the lint rule and
Flipping ready; arming once every check is green. Generated by Claude Code |
Uh oh!
There was an error while loading. Please reload this page.
Fixes#11608
All measurements below are from the final commit,
172a28c9c.Premise re-verified at my own base (
e170b0ae5)And the skip really is silent: the walk reads exactly four namespaces
(
rawData.objects,.globalActions,.apps,.dashboards) with no defaultbranch, no counter, no debug line. Nothing about
flowswas reported anywhere.The card's own probe, before and after
Same fixture the issue used, run through
validateTranslationReferences:The controls are unchanged, so the delta is the new leg and not a moved probe.
What landed
A
flowsleg mirroringdashboards→widgetsone level further, plus theuniverse collector and the per-level
suggest()/listNames()tables:Flow.nameflow.zod.ts(required machine name)FlowNode.idflow.zod.ts,type: 'screen'nodesScreenFieldConfig.namebuiltin-node-config.zod.ts(config.fields[])Severity
warningon every finding, like the rest of the rule (ADR-0072 D1).No existing green tree turns red: the full
@objectstack/lintsuite is2279 passed / 81 files.
Two shape facts measured against the schemas, not taken from the card
The card's table was correct about the three keys; it did not mention either of
these, and each one read the obvious way turns this leg into a false-positive
generator.
FlowNode.configcarries ADR-0031 regions(
loop.config.body,parallel.config.branches[].nodes,try_catch.config.try/.catch), each holding a full node array. A screen inone is a real screen — the executor pauses on it, the client gets its
ScreenSpec.nodeId— so the universe is collected withwalkFlowNodesrather than the flat
flow.nodes. Reading the one-liner would report everynested screen's keys as orphans, which is the fourth-pass version of
[lint/automation] 嵌套 region(try_catch / loop / parallel)里的节点对 flow 规则不可见 —— 两条 gating error 静默失效,第三条降级为 warning #4380 / flow lint rules never descend into a
loopbody — the whole family is blind to nested nodes (8 real inert conditions shipped pastflow-inert-node-condition) #5383 / applyConversionsToFlow does not recurse into loop bodies — conditions inside aloopare never converted to CEL and the gate silently never opens #4347.ScreenConfigSchemahas two mutually exclusive shapes. A flat screendeclares
config.fields[]; an object-form screen (config.objectName)renders that object's whole create/edit form and declares no
fieldsat all.Its input labels resolve through
objects.<objectName>.fields.*, so a fieldkey there is an orphan — reported with that redirect rather than a bare "not
declared", the same "say where it belongs" move a misfiled
globalActionskey already gets.
A key naming a node that exists but is not a
screenis diagnosed as the wrongnode type, not as a missing node.
The leg is shown firing, per level, and its universe is asserted non-zero
The failure this rule's new legs keep hitting is that a universe collector which
silently reaches nothing reports nothing, and reads exactly like a leg that
looked and found no orphans. Three things guard it here.
One stack, four bundles. The clean run and the three orphan runs share the
same
flowStack, differing only in the bundle — so "no findings" is a verdictabout that metadata, not a rule that never reached it.
Fires at each of the three levels separately, with the other two levels
resolving, so no level is masked by another:
flows.lead_conversions…flows.lead_conversions…screens.detail…flows.lead_conversion.screens.detail…fields.opportunity…screens.details.fields.opportunityThe collected universe is read back out of the rule's own hint — the tail
listNames()prints — and asserted non-empty at every level. A collector thatreached nothing prints an empty tail and fails:
Non-vacuity, both directions
Two ablations, each mutating the source under
trap … EXIT INT TERM, each withthe mutation proven on disk by anchor count and sha256 before restoring
byte-identically. No rebuild step: the test imports the rule by relative
specifier (
./validate-translation-references.js) inside its own package, sovitest transforms the source — the mutations changed the result, which is itself
the proof that source is what ran.
flowsflows.set(...)removed, anchor 1→0, marker 0→1, shac323b40e…→ddc86abe…flow.nodesinstead ofwalkFlowNodesc323b40e…→1df13b72…Both restores verified back to sha
c323b40ed6aad88e7fc7b2a474ea4f49398a3e04fe6d64b40faee04c10f274c7, zero markers left.Ablation A is the direct answer to the failure mode: emptying the collector
reddens all eight tests, so none of them can pass on a universe of nothing.
Ablation B shows the nesting fact is load-bearing rather than decorative — it
reddens one test, precisely the one that asserts it.
Gates
Union derived, not recalled:
node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack, whichconfirmed its answer is about this repo at
172a28c9cand its--repoassertion holds. Exit codes captured before any pipe. All 14 matched families
plus the convention-triggered ones re-run at the final commit:
check:changeset-gate-self-tests,check:cross-package-test-inputs,check:objectui-changeset,check:published-files,check:slot-lookup,check:test-source-alias,check:type-source-resolution,check-adr-0087-registration,check-changeset-no-major,check-cross-package-test-inputs,check-empty-changeset,check-plugin-teardown-shape,check-affected-docs,release-rehearsal-clone --self-test— allEXIT=0.check:query-options-erasure(
ratchet holds: 67 unswept … none new),check:where-matcher(
295 matcher(s) … none new),check:engine-double-contract(
OK — 401 pinned),check:type-check-coverage,check:nul-bytes(
no raw ASCII control bytes).check:type-check-debt— the ratchet half — needed the workspace closurebuilt (it refuses outright otherwise, [finding][devx]
check:type-check-debt的 ledger 余量会让新写的 pin 变哑:mongodb 曾有 33 条余量吞掉一次真实回退,另有 5 条目前带 4–19 余量 #6376; that refusal is NOT MEASURED, nota red gate). Built via
turbo run build --filter='./packages/*' --filter='./packages/*/*'(70/70 successful), then:--re-measure: OK — 32 ledger entr(ies) re-measured in 235.3s, 1898 raw tsc error(s) total, none above its recorded number.pnpm lint— the full repo-wideeslint . --no-inline-config, run wholerather than narrowed.
VERDICT command-exit 0 · held the lock 83s. Nonarrowing to declare.
pnpm --filter @objectstack/lint test— 2279 passed, 81 files.pnpm --filter @objectstack/lint typecheck—tsc --noEmit, exit 0 (scriptname echoed, so not a zero-match silent green).
Every heavy run went through
scripts/pm/os-verify-lock.sh; verdicts read fromthe printed
VERDICTlines, never a bare$?.Scope
packages/lintonly. #11485 is independent in code and is not touched here.One adjacent finding filed rather than fixed: #11745 —
translateFlowinpackages/specwalksflow.nodesflat, so a nested screen node is neveroverlaid by the document-overlay path. That is
packages/spec, out of scopehere, and #11745 is not addressed by this PR. It means the lint rule and
translateFlowcurrently disagree about which nested keys are live; this legtakes the safe direction for a
warning(resolve them, do not report them).I did not hit the #11624 collision — nothing here touches the liveness rows.
Generated by Claude Code