You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
lint: validate-translation-references has no flows leg — an authored key naming a flow, screen node or field that does not exist warns nobody #11608
Filed unassigned by the domain:cli seat while implementing #11485 (item 3 of that card: "decide whether reference-validation for flows belongs here or as its own card"). The decision, with the evidence below, is its own card — packages/lint is outside #11485's declared file surface, and the work is not a rider on it.
Measured
packages/lint/src/validate-translation-references.ts walks objects, globalActions, apps and dashboards; an unrecognised top-level namespace is skipped, never reported (its own header says so, about the object-first AppTranslationBundle shape). flows is one of those unrecognised namespaces, so every level of the group goes unchecked.
Probe against the rule's own entry point (validateTranslationReferences), one bundle naming three things that do not exist:
The sibling namespaces each report their orphan. The flow, the screen node id and the field name produce nothing.
Why it matters
This is the same failure the rule exists for, one namespace over: the key parses (the schema declares the group), ships, and silently resolves to nothing, so the wizard renders its source-locale string while every other label on the screen is translated. All three levels are exact-match identifiers with a small enumerable universe, so they are resolvable exactly the way dashboards → widgets already is:
level
key
declared by
flow
Flow.name
flow.zod.ts
screen
FlowNode.id
flow.zod.ts (type: 'screen' nodes)
field
ScreenFieldConfig.name
builtin-node-config.zod.ts
Shape of the fix
A flows leg mirroring the dashboards → widgets leg (~50 lines there), plus a universe collector for the three nesting levels, the suggest() / listNames() tables each level needs, and tests. Severity warning, like every other finding in this rule (ADR-0072 D1 — an orphan key is inert, not broken).
Ordering
No dependency in either direction with #11485. That card never authors bundle keys — os i18n extract derives every key it writes from live metadata, so a scaffolded key resolves by construction. This rule is about hand-authored keys, and it becomes worth having precisely once authors have flows keys to hand-edit, which #11485 is what makes possible. So: downstream in value, independent in code.
Refs: #11485 · #11287 (declared the flows group and its resolver) · #7646.
Triage: lands in packages/lint (validate-translation-references gains a flows leg) → domain:devx, type Task, pm:queue. Rationale: the rule already validates references for its other legs; an authored key naming a nonexistent flow / screen node / field warning nobody is a coverage gap in existing tooling, not new surface.
⚠️ Coordination note: #11485 is in flight (domain:cli — CLI i18n coverage + extract gain the flow/screen bucket, downstream of #11287). That work defines the key shapes a flows leg must validate — claim after it lands (or read its branch) so the leg validates the landed bucket shape, not a guess. Adjacent finding #11624 (the new bucket colliding with the planned liveness row's authorWarn) is ungraded — if you hit that collision while implementing, note it there rather than working around silently.
Dispatching at opus. Tier derived live at e170b0ae5, not recalled:
Model tier — no path-derived mandate: the surface hits none of the 3 declared glob(s), derived here, not recalled.
Premise re-verified on today's main, with a positive control so the zero is a reading rather than a broken probe:
packages/lint/src/validate-translation-references.ts
'dashboards' mentions 11 ← CTRL: the probe reaches the namespace list
'flows' mentions 0 ← the gap
Opus rather than the floor for one reason, and it is this shift's recurring failure mode rather than anything specific to this card: a new leg whose universe collector comes back empty reports nothing and looks exactly like a leg that found no orphans. Three cards today turned on a green that had checked nothing. The leg has to be shown firing before its silence means anything.
{
"issue": 11608,
"status": "done",
"branch": "claude/issue-11608-translation-refs-flows-leg",
"pr": "https://github.com/objectstack-ai/objectstack/pull/11746",
"premise_still_valid": true,
"summary": "Added the `flows` leg to `validate-translation-references`, mirroring `dashboards` -> `widgets` one level further: flow (`Flow.name`) -> screen (`FlowNode.id` on `type: 'screen'` nodes) -> field (`ScreenFieldConfig.name`), with a universe collector and per-level suggest()/listNames() tables. All findings are `warning` (ADR-0072 D1); no existing green turned red (full lint suite 2279 passed / 81 files). Two schema facts the card did not mention changed the implementation, both measured rather than assumed: screen nodes NEST in ADR-0031 regions (so the universe is collected via `walkFlowNodes`, not the flat `flow.nodes` — reading the one-liner would report every nested screen's keys as orphans, the fourth-pass version of #4380/#5383/#4347), and `ScreenConfigSchema` has two mutually exclusive shapes, so an object-form screen (`config.objectName`) declares no `config.fields` and its field copy belongs under `objects.<objectName>.fields.*` — reported with that redirect. The mirror ruling held: `flows` nests compatibly and no purpose-built shape was needed.",
"tests": "All readings from final commit 172a28c9c. BEFORE/AFTER on the card's own probe fixture (same file, controls unchanged): before: 'findings: 2 / flows-related findings: 0 / CTRL dashboards 1 / CTRL objects 1'; after: 'findings: 3 / flows-related findings: 1 / CTRL dashboards 1 / CTRL objects 1'. Premise re-verified at my own base e170b0ae5: dashboards 11 mentions (CTRL), widgets 12 (CTRL), flows 0; and the walk reads exactly four namespaces (rawData.objects/.globalActions/.apps/.dashboards) with no default branch — the skip really is silent. 8 new tests, all confirmed executed by --reporter=verbose (not a silent skip): a clean run over the SAME stack as the three orphan runs (so silence is a verdict), one firing test per level with the other two levels resolving, wrong-node-type diagnosis, nested-region resolution, object-form redirect, and the name-keyed map shape. NON-ZERO UNIVERSE asserted at every level by reading the collected names back out of the rule's own listNames() hint tail: Defined flows -> ['lead_conversion']; Declared screen node ids -> ['details']; Declared screen field names -> ['close_date','opportunity_name'] — a collector that reached nothing prints an empty tail and fails. ABLATIONS (2), each under trap '<restore>' EXIT INT TERM, mutation proven on disk by anchor count AND sha256 before restore: (A) collector never populates `flows` — anchor 1->0, marker 0->1, sha c323b40e...->ddc86abe..., result 'Tests 8 failed | 40 passed (48)', i.e. every new test reddens, which is the direct proof against the empty-collector failure mode; (B) flat `flow.nodes` substituted for walkFlowNodes — anchor 1->0, marker 0->1, sha c323b40e...->1df13b72..., result 'Tests 1 failed | 47 passed (48)', exactly the nested-region test. Both restores verified byte-identical back to sha c323b40ed6aad88e7fc7b2a474ea4f49398a3e04fe6d64b40faee04c10f274c7 with zero markers left. REBUILD STATEMENT: no rebuild leg was required — the test imports the subject by relative specifier ('./validate-translation-references.js') inside its own package, so vitest transforms the source rather than a dependency's dist/ exports; the mutations changing the result is itself the proof that source is what ran. (The lint dependency closure WAS built first — `pnpm --filter '@objectstack/lint^...' build`, VERDICT command-exit 0, held the lock 146s — so no stale-dist reading is involved either.) GATES: union derived, not recalled, via `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack` (stderr confirms: derived from objectstack-ai/objectstack at 172a28c9c, --repo assertion holds; change set from git, 3 committed paths vs merge base e170b0ae5, three-dot). Exit codes captured before any pipe. All 14 matched families re-run at the final commit, every one EXIT=0: 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 ('this PR adds no declared-breaking changeset (1 non-breaking changeset(s) seen)' — the count moved 0->1, so it read my changeset), check-changeset-no-major, check-cross-package-test-inputs, check-empty-changeset ('1 declaring changeset(s) added'), check-plugin-teardown-shape, check-affected-docs, release-rehearsal-clone --self-test. Convention-triggered (new test file), all EXIT=0 with their own verdict lines: check:query-options-erasure 'ratchet holds: 67 unswept non-test site(s) in 17 file(s), none new'; check:where-matcher '295 matcher(s) discovered, 295 answer the combinator battery correctly or refuse it loudly ... none new'; check:engine-double-contract 'OK — 401 pinned, 133 in the DEBT ledger, 2 exempt' (no new double added); check:type-check-coverage 'OK — 65/78 workspace packages type-checked'; check:nul-bytes 'OK (scanned 6552 text file(s) ... no raw ASCII control bytes)'. check:type-check-debt initially REFUSED for want of a built closure (the documented #6376 refusal = NOT MEASURED, not a red gate); I built it as lint.yml does (`turbo run build --filter='./packages/*' --filter='./packages/*/*'`, 70/70 successful) and re-ran at the final HEAD: '--re-measure: OK — 32 ledger entr(ies) re-measured in 235.3s, 1898 raw tsc error(s) total, none above its recorded number.' REPO-WIDE SCAN NOT NARROWED: `pnpm lint` (= `eslint . --no-inline-config` over the whole repo) was run WHOLE, 'VERDICT command-exit 0 · held the lock 83s' — so there is no narrowing to declare. Package-level: `pnpm --filter @objectstack/lint test` 'Test Files 81 passed (81) / Tests 2279 passed (2279)'; `pnpm --filter @objectstack/lint typecheck` exit 0 with the script name echoed ('> tsc --noEmit'), guarding the zero-match silent-green trap. Every heavy run went through scripts/pm/os-verify-lock.sh and every verdict is quoted from the gate's own printed line, never a bare $?. NOT MEASURED: nothing — CI convergence itself is PM's to read, per the dispatch contract.",
"open_questions": [],
"out_of_scope_findings": [
"filed as #11745: `translateFlow` (packages/spec/src/system/i18n-resolver.ts) walks `flow.nodes` flat, so a screen node inside an ADR-0031 region is never overlaid by the document-overlay path — the fourth pass in this repo written against the flat one-liner after #4347/#4380/#5383 each fixed it once, and the reason region-slots.ts exists. Out of scope (packages/spec). Consequence for this PR: the lint rule and `translateFlow` currently disagree about which nested keys are live; the leg deliberately takes the safe direction for a warning (resolve nested screens, do not report them)."
]
}```---_Generated by [Claude Code](https://claude.ai/code)_
Activity
os-zhuang commentedon Aug 24, 2026
Triage: lands in
packages/lint(validate-translation-referencesgains aflowsleg) →domain:devx, type Task,pm:queue. Rationale: the rule already validates references for its other legs; an authored key naming a nonexistent flow / screen node / field warning nobody is a coverage gap in existing tooling, not new surface.domain:cli— CLI i18n coverage + extract gain the flow/screen bucket, downstream of #11287). That work defines the key shapes a flows leg must validate — claim after it lands (or read its branch) so the leg validates the landed bucket shape, not a guess. Adjacent finding #11624 (the new bucket colliding with theplannedliveness row'sauthorWarn) is ungraded — if you hit that collision while implementing, note it there rather than working around silently.Generated by Claude Code
os-steve commentedon Aug 24, 2026
Claim:
domain:devxPM seat, sessionsession_015ahemw8RcTgqtxrj15PEZx, branchclaude/issue-11608-translation-refs-flows-leg.Dispatching at opus. Tier derived live at
e170b0ae5, not recalled:Premise re-verified on today's
main, with a positive control so the zero is a reading rather than a broken probe:Opus rather than the floor for one reason, and it is this shift's recurring failure mode rather than anything specific to this card: a new leg whose universe collector comes back empty reports nothing and looks exactly like a leg that found no orphans. Three cards today turned on a green that had checked nothing. The leg has to be shown firing before its silence means anything.
Generated by Claude Code
feat(lint): give validate-translation-references a flows leg (#11608)
claude commentedon Aug 24, 2026
{ "issue": 11608, "status": "done", "branch": "claude/issue-11608-translation-refs-flows-leg", "pr": "https://github.com/objectstack-ai/objectstack/pull/11746", "premise_still_valid": true, "summary": "Added the `flows` leg to `validate-translation-references`, mirroring `dashboards` -> `widgets` one level further: flow (`Flow.name`) -> screen (`FlowNode.id` on `type: 'screen'` nodes) -> field (`ScreenFieldConfig.name`), with a universe collector and per-level suggest()/listNames() tables. All findings are `warning` (ADR-0072 D1); no existing green turned red (full lint suite 2279 passed / 81 files). Two schema facts the card did not mention changed the implementation, both measured rather than assumed: screen nodes NEST in ADR-0031 regions (so the universe is collected via `walkFlowNodes`, not the flat `flow.nodes` — reading the one-liner would report every nested screen's keys as orphans, the fourth-pass version of #4380/#5383/#4347), and `ScreenConfigSchema` has two mutually exclusive shapes, so an object-form screen (`config.objectName`) declares no `config.fields` and its field copy belongs under `objects.<objectName>.fields.*` — reported with that redirect. The mirror ruling held: `flows` nests compatibly and no purpose-built shape was needed.", "tests": "All readings from final commit 172a28c9c. BEFORE/AFTER on the card's own probe fixture (same file, controls unchanged): before: 'findings: 2 / flows-related findings: 0 / CTRL dashboards 1 / CTRL objects 1'; after: 'findings: 3 / flows-related findings: 1 / CTRL dashboards 1 / CTRL objects 1'. Premise re-verified at my own base e170b0ae5: dashboards 11 mentions (CTRL), widgets 12 (CTRL), flows 0; and the walk reads exactly four namespaces (rawData.objects/.globalActions/.apps/.dashboards) with no default branch — the skip really is silent. 8 new tests, all confirmed executed by --reporter=verbose (not a silent skip): a clean run over the SAME stack as the three orphan runs (so silence is a verdict), one firing test per level with the other two levels resolving, wrong-node-type diagnosis, nested-region resolution, object-form redirect, and the name-keyed map shape. NON-ZERO UNIVERSE asserted at every level by reading the collected names back out of the rule's own listNames() hint tail: Defined flows -> ['lead_conversion']; Declared screen node ids -> ['details']; Declared screen field names -> ['close_date','opportunity_name'] — a collector that reached nothing prints an empty tail and fails. ABLATIONS (2), each under trap '<restore>' EXIT INT TERM, mutation proven on disk by anchor count AND sha256 before restore: (A) collector never populates `flows` — anchor 1->0, marker 0->1, sha c323b40e...->ddc86abe..., result 'Tests 8 failed | 40 passed (48)', i.e. every new test reddens, which is the direct proof against the empty-collector failure mode; (B) flat `flow.nodes` substituted for walkFlowNodes — anchor 1->0, marker 0->1, sha c323b40e...->1df13b72..., result 'Tests 1 failed | 47 passed (48)', exactly the nested-region test. Both restores verified byte-identical back to sha c323b40ed6aad88e7fc7b2a474ea4f49398a3e04fe6d64b40faee04c10f274c7 with zero markers left. REBUILD STATEMENT: no rebuild leg was required — the test imports the subject by relative specifier ('./validate-translation-references.js') inside its own package, so vitest transforms the source rather than a dependency's dist/ exports; the mutations changing the result is itself the proof that source is what ran. (The lint dependency closure WAS built first — `pnpm --filter '@objectstack/lint^...' build`, VERDICT command-exit 0, held the lock 146s — so no stale-dist reading is involved either.) GATES: union derived, not recalled, via `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack` (stderr confirms: derived from objectstack-ai/objectstack at 172a28c9c, --repo assertion holds; change set from git, 3 committed paths vs merge base e170b0ae5, three-dot). Exit codes captured before any pipe. All 14 matched families re-run at the final commit, every one EXIT=0: 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 ('this PR adds no declared-breaking changeset (1 non-breaking changeset(s) seen)' — the count moved 0->1, so it read my changeset), check-changeset-no-major, check-cross-package-test-inputs, check-empty-changeset ('1 declaring changeset(s) added'), check-plugin-teardown-shape, check-affected-docs, release-rehearsal-clone --self-test. Convention-triggered (new test file), all EXIT=0 with their own verdict lines: check:query-options-erasure 'ratchet holds: 67 unswept non-test site(s) in 17 file(s), none new'; check:where-matcher '295 matcher(s) discovered, 295 answer the combinator battery correctly or refuse it loudly ... none new'; check:engine-double-contract 'OK — 401 pinned, 133 in the DEBT ledger, 2 exempt' (no new double added); check:type-check-coverage 'OK — 65/78 workspace packages type-checked'; check:nul-bytes 'OK (scanned 6552 text file(s) ... no raw ASCII control bytes)'. check:type-check-debt initially REFUSED for want of a built closure (the documented #6376 refusal = NOT MEASURED, not a red gate); I built it as lint.yml does (`turbo run build --filter='./packages/*' --filter='./packages/*/*'`, 70/70 successful) and re-ran at the final HEAD: '--re-measure: OK — 32 ledger entr(ies) re-measured in 235.3s, 1898 raw tsc error(s) total, none above its recorded number.' REPO-WIDE SCAN NOT NARROWED: `pnpm lint` (= `eslint . --no-inline-config` over the whole repo) was run WHOLE, 'VERDICT command-exit 0 · held the lock 83s' — so there is no narrowing to declare. Package-level: `pnpm --filter @objectstack/lint test` 'Test Files 81 passed (81) / Tests 2279 passed (2279)'; `pnpm --filter @objectstack/lint typecheck` exit 0 with the script name echoed ('> tsc --noEmit'), guarding the zero-match silent-green trap. Every heavy run went through scripts/pm/os-verify-lock.sh and every verdict is quoted from the gate's own printed line, never a bare $?. NOT MEASURED: nothing — CI convergence itself is PM's to read, per the dispatch contract.", "open_questions": [], "out_of_scope_findings": [ "filed as #11745: `translateFlow` (packages/spec/src/system/i18n-resolver.ts) walks `flow.nodes` flat, so a screen node inside an ADR-0031 region is never overlaid by the document-overlay path — the fourth pass in this repo written against the flat one-liner after #4347/#4380/#5383 each fixed it once, and the reason region-slots.ts exists. Out of scope (packages/spec). Consequence for this PR: the lint rule and `translateFlow` currently disagree about which nested keys are live; the leg deliberately takes the safe direction for a warning (resolve nested screens, do not report them)." ] }``` --- _Generated by [Claude Code](https://claude.ai/code)_