Uh oh!
There was an error while loading. Please reload this page.
Warn when a bare flow-condition identifier is shadowed by a declared flow variable - #14263
Conversation
…clared variable WIP — implementation + tests + changeset, before the first verified build. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q5WBDtaUnoz5XuJ6jk8pQ5
…urce scan Replaces the private comment-stripper the new test carried (check:comment-mask-adoption reds on a new one) with assertions over the module's exported constants, which pin the values the collection walk indexes with rather than the spelling someone typed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q5WBDtaUnoz5XuJ6jk8pQ5
rule-id-barrel-exports reads every slug-shaped `export const` in src/ as a rule id that a published barrel must carry, and 'assignment' is slug-shaped. It is a node type, not a rule id; its gate is pinned through behaviour instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Q5WBDtaUnoz5XuJ6jk8pQ5
📓 Docs Drift CheckThis PR changes 1 package(s): 2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 5 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 5c539e964928c53fc9b717425818364053cba918 && git checkout 5c539e964928c53fc9b717425818364053cba918
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin e7645078d8680027e1d2b9760dfc686c2cefb8e8 d142c49b9df746f9f39cf500fc6b707cb4e23208 && git checkout -B drift-repro e7645078d8680027e1d2b9760dfc686c2cefb8e8 && git merge --no-ff d142c49b9df746f9f39cf500fc6b707cb4e23208
node scripts/docs-audit/affected-docs.mjs --json e7645078d8680027e1d2b9760dfc686c2cefb8e8
|
Uh oh!
There was an error while loading. Please reload this page.
Closes#14089
Implements the maintainer's ruling of 2026-09-01 (director batch #23): option C — warn only on the shadowing sub-case. Options A, B and D were excluded, and the PM's earlier three-way ruling was struck.
What changes
A flow node/edge
conditionis evaluated in a flattened scope, so a barestatusnormally resolves to the trigger record's field. That form is correct:@objectstack/formula's published contract says so (ExprSchemaHint.scope,cel-engine.ts),AutomationEngine.seedRunVariablesflattens the record's fields for exactly that purpose, and two shipped example apps read fields bare. This PR does not judge a bare identifier for being bare, and adds no error.It names one sub-case that is both genuinely ambiguous and genuinely broken: a bare name that is BOTH a declared flow variable AND a field on the bound object.
The variable wins, the field is unreachable under its own name, and nothing anywhere reports the collision — on the surface where a wrong predicate is least visible, because a flow condition that never fires produces no record, no error and no log line. Severity is
warning; the diagnostic names the mechanism and both repairs.New module
packages/lint/src/flow-variable-scope.tsholds the collection surface and the oracle;validate-expressions.tsgains the two emission call sites (flow-node condition, flow-edge condition) and one collection pass.The collection surface, measured against the executors
Variables are flow-scoped, not graph-scoped —
seedRunVariablesbuilds one map per run — so the set is one flat union across every ADR-0031 regioncollectFlowGraphsyields.flow.variables[].nameseedDeclaredVariablesconfig.iteratorVariable/indexVariable/errorVariable/outputVariablelogic-nodes.tsvariable/name/key)elsebranchevaluateConditionexpands anodeId.outputKeyvariable into a nested object AT the node id, overwriting whatever scalar was flattened thereRow 8 is gated on the node type, deliberately: it reads every top-level config key, which is correct for an assignment node and catastrophic over-collection for any other. There is a test for that gate.
The rejected
itemVariablealias is not read — tolerating a spellingcontrol-flow.zod.tsrefuses by name would be consumer-side alias tolerance (Prime Directive 12), and it cannot arrive on the parsed path this rule runs on.The oracle
firstUndeclaredReferencefrom@objectstack/formula, notcollectCelRootIdentifiers, as the maintainer pinned. The former acts only on cel-js's own unknown-variable fault, so comprehension-macro variables and function names cannot false-positive; the latter reports macro variables as roots, and a macro variable sharing a field's name would then be flagged for a collision that cannot exist. Both are covered by tests.Its known, deliberate blind spot is pinned rather than left to be discovered:
SCOPE_ROOTSmembers are declared in the strict environment, so a flow variable namedresult/data/itemgoes unwarned. That is an under-report, the safe direction for a new warning, and closing it means consulting the AST — which re-opens the macro-variable false positive.packages/formulais untouched. No new dependency edge:packages/lintalready declares@objectstack/formula, and a sibling file in the same package already imports this helper.Verification — all at
d142c49bunless statedpnpm --filter @objectstack/lint test— 89 files, 2505 tests passed, exit 0 (captured by redirect before any pipe).pnpm --filter @objectstack/lint typecheck— exit 0.validate-expressions.test.ts"does NOT flag bare references in a flow condition (flattened scope)" (bareamount/ barestageat zero issues) andformula/src/validate.test.ts's flattened-scope pin are untouched by this diff — neither file's pinned assertions were edited, deleted or re-baselined.git diff HEAD. No rebuild leg was needed and none is claimed: the test imports./validate-expressions.jsrelatively, so vitest reads this package's source, and nothing resolves it throughpackages/lint/dist.objectstack validateexits 0 onapp-todo,app-showcaseandapp-crmwith no shadowing warning. Positive control through the same channel: injecting astatusflow variable intoapp-todo's realtask_completionflow (the one whose start condition reads barestatus) makes the diagnostic appear, atwarningseverity withvalidatestill exiting 0. Both legs restored, verified by an emptygit diff HEAD.scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack --commands— not a hand-written list. 30 green. 3 NOT MEASURED, none of them a red:check-test-completenessandcheck:dual-build-cjs-loadsfirst returned exit 3 (PREREQUISITE NOT MET); the latter was then run on the built closure and passed, while the former has no local turbo test log to grade.scripts/pm/check-half-states.mjsis a network-bound GitHub patrol that does not terminate in this container.check:type-check-coverage --re-measure— 27 ledger entries, 1217 raw tsc errors, none above its recorded number, surplus none. Also green at that head:comment-mask-adoption,engine-double-contract,where-matcher,query-options-erasure,type-check-coverage,dual-build-cjs-loads.Two findings from those gates were repaired rather than routed around, and both are visible in the commit history: the new test file originally carried a private comment-stripper (
check:comment-mask-adoptionreds on a new one) and now asserts on the module's exported constants instead; andASSIGNMENT_NODE_TYPEwas made module-private afterrule-id-barrel-exportscorrectly read a slug-shapedexport constas a rule id owing a barrel line.Clause 2: no. The accept set does not move in either direction. Nothing that linted clean before this diff lints red after it, and nothing previously rejected is now accepted — the only behavioural change is one additional
warning, and a warning never fails the build. The four surfaces the ruling protects are untouched: no contract change, no pinned test deleted or re-baselined, no example app edited, andpackages/formulaunmodified.Meta-guard note
validate-expressions.test.ts's#5017receiver scan gained one PLUMBING entry,scope. It is not a receiver: it is the tail of the'./flow-variable-scope.js'import specifier, which the scan cannot tell from a property read — the same artefact as thefieldsandguardsentries already there. No metadata receiver'sexpectedlist changed, so the declared-key guard loses no coverage.Generated by Claude Code
Generated by Claude Code