feat(lint): report a flow-filter token neither the template evaluator nor ObjectQL resolves - #16732
Conversation
… nor ObjectQL resolves
`filter-token-unknown` walks seven presentation collections and not `flows`,
so `{TOMORROW()}` in a list view's filter fails the build while the identical
string in a flow node's `config.filter` is silent — although this package's
sibling filter rules have reached `flows` all along.
Adding `flows` to that rule's root list is the wrong close. A flow node's
filter is interpolated by the automation template evaluator BEFORE ObjectQL
sees it, and only what the evaluator cannot resolve is handed on. Judging a
flow filter against the ObjectQL vocabulary reports every legitimate
`{record.id}` / `{recordId}`: 7 findings on this repo's own example apps, all
7 false positives.
So this is a second rule id with the flow dialect as its reference set, and
`filter-token-unknown`'s declared surface list is untouched. Only the class
NEITHER dialect resolves is reported — a call to a name outside the flow
template dialect's closed function table, where the evaluator already raises a
guard refusal, so the node cannot run at all. The open arm (bare and dotted
identifiers addressing the run's variable map) stays silent and says why.
`flow-template-grammar.ts` mirrors the evaluator's whole-token dispatch because
this package may not depend on a runtime; its drift is pinned by a test that
reads the original from disk.
Finding delta on this repo's example apps: 0.
Claude-Session: https://claude.ai/code/session_01Vbw3RPgdtqesx4azk9SbW8
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…lter-token-flow-reach
`flow-filter-token-unknown` is the 44th registered author-time rule, and `check:docs-transcript-drift` derives that count from the registry for the four `os build` / `os validate` transcripts under content/docs that quote it as a literal. The inherited commit registered the rule without moving the literals, so the gate went red on this branch; this is the other half. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012GKcPZbMoGq7WPzKLfRBTU
📓 Docs Drift CheckThis PR changes 1 package(s): 1 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 5e54bf875145fd523f66d4a5f89530251064152f && git checkout 5e54bf875145fd523f66d4a5f89530251064152f
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 5e53d73d83553ba4d87cba49797e79b6c8754e5a de9b551431d80e4e396664dc23167b1eeb4d3617 && git checkout -B drift-repro 5e53d73d83553ba4d87cba49797e79b6c8754e5a && git merge --no-ff de9b551431d80e4e396664dc23167b1eeb4d3617
node scripts/docs-audit/affected-docs.mjs --json 5e53d73d83553ba4d87cba49797e79b6c8754e5a
|
… position The passage sat directly after "A flow node's `config.filter` takes these tokens too", so its rule id read as the one that fails an unknown token in a flow node. Since #16732 that position is owned by a second rule. The two ids stay distinct, because the two positions do not share a token vocabulary or a consequence: - flows -> `flow-filter-token-unknown` (validate-flow-filter-tokens.ts), which fires only when NEITHER dialect resolves the token and whose consequence is a guard refusal: "this node cannot run at all"; - dashboards/objects/views/reports/datasets/pages/apps -> `filter-token-unknown` (validate-filter-tokens.ts), which deliberately does not walk `flows` and whose consequence is the silent zero. Collapsing them into one statement would re-teach the confusion the split exists to prevent, so the flow sentence names its own rule and the general paragraph is scoped with "Outside a flow". Wording (not the id distinction) was tightened to stay under the published token ratchet: 2136 -> 2146 tokens against a 2149 ceiling. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01P58euzUXCVJNwmhuPC9DXY
Fixes #16096
What
A second gating rule,
flow-filter-token-unknown, that walksflowsand reports a{…}filter token in a flow node'sconfig.filterthat neither{…}dialect can resolve: a call to a name outside the flow template evaluator's closed function table ({TOMORROW()},{ROUND(x)},{Math.round(x)},{DATEADD(day, -45)}).filter-token-unknown's root list is untouched.The naive fix the card pre-refutes —
{ key: 'flows', kind: 'flow' }on the existing rule — is measured, not argued, in this PR: on this tree's own example apps it reports 4 findings on app-todo + app-crm (loaded through their configs) and 3 more on app-showcase (grep census), every one a legitimate run-time variable ({recordId}×3,{record.id}×3,{currentTask.id}×1). The new rule reports 0 on the same corpus.Reference set: the flow token grammar, mirrored and pinned
packages/lint/src/flow-template-grammar.tsmirrorsresolveToken's whole-token dispatch frompackages/services/service-automation/src/builtin/template.ts— the five regexes, the reserved-literal step, theNOW()/TODAY()whole-token form tried before the call-position scan (the only reason{TODAY() - 45}stays silent), and the six-name value-function table.@objectstack/lintmay not depend on a runtime, so the mirror is pinned byflow-template-grammar.test.ts, which readstemplate.tsfrom disk (declared cross-package input; spelled percheck:cross-package-test-inputs) and fails when any mirrored piece stops matching the original.Verified against the evaluator, not the filename: every mirrored regex source is present verbatim in
template.ts;EXPRESSION_FUNCTION_ARITY's key set equals the mirrored table;unknownFunctionErroris still thrown on an unknown call (A4 holds —TOMORROW()is a guard refusal at run time, so the node cannot run; it is not resolved to anything).Layering
Layer one asks
classifyFilterToken(the same spec classifierfilter-token-unknownuses); only itsunknownverdict goes to layer two,classifyFlowTemplateToken; onlyunknown-functionis a finding. So every finding is a strict subset of what the naive root addition would report (asserted in the test), and the open arm — bare and dotted identifiers addressing the run'sVariableMap— is deliberately silent and says why.Successor note — the inherited commit, diffed and verified
4c7d5c522(7 files) was pushed by a dispatch that died before opening a PR. Each file was read against the evaluator and the registry pins rather than trusted; nothing in it was thrown away or rebuilt. One thing it was missing: registering the 44th author-time rule stales the fouros build/os validatetranscripts undercontent/docsthat quote the count as a literal, andcheck:docs-transcript-driftwent red on this branch.de9b5514moves those four literals (43 → 44). The branch was also merged withorigin/main(bd62a569, clean;packages/lint/srcwas uncontested at the time of the check — open PR #16727 now touches two other files in that directory, none of this PR's).Evidence (tree
de9b5514)Reproduction of the card's control design on this tree, through
runAuthoringRules('lint', …)— the registry lineos lintruns (packages/cli/src/commands/lint.ts:514; exit 1 at:881when any error-severity finding exists):{TOMORROW()}in a viewfilter[].value{TOMORROW()}in a flow nodeconfig.filterauthoring-rules.ts+index.ts, dist rebuilt, marker absent from all 14 built files)filter-token-unknownerror → exit 1filter-token-unknownerror → exit 1flow-filter-token-unknownerror → exit 1{TODAY() - 45}and{record.id}in the flow position: 0 findings in both legs (the negative control triage asked for).pnpm --filter @objectstack/lint test— 102 files / 3549 tests passed (includes the two new suites plus the wiring, barrel-export and runtime-gate pins).pnpm --filter @objectstack/lint typecheck— OK, test layer undertsconfig.test.jsonOK (2 files / 6 pre-existing errors held in the ledger, unchanged).dispatch-gates --commands, 83 families on the final file set): see the report comment on the card for the per-family table. Two families readPREREQUISITE NOT METhere (whole-repo dists) and are declared to CI:check:dual-build-cjs-loads,check:type-check-debt.验收备注
skills/objectstack-query/rules/filters.md(governed surface, not touched) describesfilter-token-unknownas the rule that refuses a whole-string{…}token; the flow position now has its own id. Not wrong, only incomplete — noted, not filed; 承接者:无.scripts/check-plugin-teardown-shape.mjs --self-testcannot run on a shallow clone (its positive control is pinned to commit621a4876); fetching that one commit makes it green (47 cases). Environment, not a defect — noted, not filed.os validatelowers inline handlers before its parse, so the hook write-set family judges handler-authored hooks there too #16727 is the other open PR inpackages/lint/src; it does not touchauthoring-rules.ts, so the rule count stays 44 whichever lands first.Generated by Claude Code