fix(spec,core): recognise a filter placeholder by INTENT — {TODAY()} refuses loudly instead of comparing as a literal (#5586) - #6680
Merged
Conversation
…wrapped value refuses loudly (#5586) Recognition used the token-NAME grammar, so a placeholder carrying a non-word character ({TODAY()}, {current-user-id}, {30 days ago}, {user.id}) classified as 'not a placeholder' and reached the driver to be compared as a literal string — the silent-wrong-rows mode the diagnostic exists to abolish. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MwoubC3jL271FYt9rGXwxb
…_RE (#5586) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01MwoubC3jL271FYt9rGXwxb
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 2 package(s): 119 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
This was referenced Aug 8, 2026
os-zhuang
marked this pull request as ready for review
August 8, 2026 12:14
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #5586
What was wrong
UnknownFilterTokenError's coverage stopped at the token-NAME grammar. Recognition ran through/^\$?\{([a-zA-Z0-9_]+)\}$/, so any placeholder carrying a non-word character classified asnull— "not a placeholder at all" — and was handed to the driver verbatim to be compared as a literal string. That is exactly the silent-wrong-result mode the diagnostic exists to abolish.The failure was inverted against the author: misspelling
{today}as{TODAY}was refused by name, while misspelling it as{TODAY()}returned rows — the wrong ones.Premise verification against
origin/main(0e043d8)The issue is a lead, so both halves of its evidence were re-measured before any edit. Both hold.
classifyFilterToken(measured,packages/spec):Read path, measured through a real
ObjectQL.find()with a recording driver:The issue's row-count evidence follows from the last four lines: on a string comparison
'2026-…' < '{'in lexicographic order, so a<window silently gained every future-dated row.packages/spec, notpackages/coreThe dispatch scoped this to
packages/core/src/utils/filter-tokens.ts. That file does not contain the grammar.classifyFilterTokenand the regex live inpackages/spec/src/data/context-tokens.zod.ts(line 104, aliasingDATE_MACRO_WRAPPED_REatpackages/spec/src/data/date-macros.zod.ts:203);packages/coreonly consumes the classifier. Fixing it in core would have meant a lenient re-implementation in a consumer — the contract-first anti-pattern — and would have left the author-time lint pass blind. So the fix landed at the producer, which is also what the dispatch's own mechanism assumption described ("widening the recognition regex inclassifyFilterToken").The two ⛔ files reserved for #5282 (
packages/core/src/index.ts,packages/core/src/kernel*.ts) are untouched — verified in the diff.The change
Recognition and vocabulary are now two named grammars rather than one, which is what route 1's 宽进严出 actually is:
FILTER_TOKEN_WRAPPED_RE = /^\$?\{([^{}]+)\}$/— did the author mean a placeholder? Anchored over the whole value, matching the existing anchoring style,$prefix included.CONTEXT_TOKEN_WRAPPED_RE/DATE_MACRO_WRAPPED_RE— unchanged. They stay the well-formed token NAME grammar, which is whatContextTokenPlaceholderSchema/DateMacroPlaceholderSchemaandscripts/analytics-reconcile/macros.tsactually want. Both schemas re-check membership after matching, so their verdicts are identical either way; widening them would have blurred a distinction the code depends on.classifyFilterTokennow recognises through the wide grammar and reports the raw text between the braces as the token, so the error names exactly what the author wrote.No escape hatch, and why
Per the ruling, none ships. The premise measurement found zero legitimate consumers comparing a literal
{…}-wrapped string through a filter:.json/.yml/.yaml)filter/filters/runtimeFiltersubtrees the lint rule walkspackages/**,apps/,examples/,scripts/TypeScript{record.id}x3,{currentTask.id})objectstack build, which runsvalidateFilterTokensas a gating rulefilter-tokenfindingsAn escape syntax is a public micro-contract with no measured pull (创业阶段聚焦原则); it can be added the day a real consumer appears.
Consumers of
classifyFilterToken— declared per consumerEnumerated by grep over
origin/main. Nothing is silent here.packages/core/src/utils/filter-tokens.ts—hasFilterToken+resolveFilterTokenskind === 'unknown'branch and throw. This is the fix; ObjectQL read + write paths and the analytics dataset executor all inherit it through this one function.packages/lint/src/validate-filter-tokens.ts—filter-token-unknownpackages/services/service-automation/src/builtin/template.ts—interpolateFilterclassifyFilterToken. It uses its own wide regex/^\{([^{}]+)\}$/and then consultsisKnownFilterToken(name)— the token-name vocabulary predicate, which this PR does not touch. So{record.id}/{TODAY() + 30}still resolve from flow variables before the filter reaches ObjectQL, and an unresolvable one still fails the step through the node's collapse guard. Notably this surface had already adopted the wide recognition shape for the one position where the two brace dialects meet (#3810); this PR brings the platform diagnostic into line with it.packages/spec/src/data/context-tokens.zod.ts—ContextTokenPlaceholderSchemascripts/analytics-reconcile/macros.tsDATE_MACRO_WRAPPED_REdirectly, which is unchanged.Two independent confirmations that the lint rule cannot reach flow filters, so no flow template becomes a build error: structurally,
flowsis a top-level stack key andvalidateFilterTokensonly scansdashboards/objects/views/reports/datasets/pages/apps; and empirically,examples/app-showcase/src/automation/flows/index.ts:111spellsfilter: { id: '{recordId}' }, which the narrow grammar already classified asunknown— CI is green today, so that walk demonstrably never reaches it.Edge shapes — decided, not emergent
Recognition is one brace pair around the whole value. Each of these is pinned by a test rather than left to fall out of the regex:
{TODAY()}{current-user-id}{30 days ago}{user.id}UnknownFilterTokenError${TODAY()}TODAY()$prefix variant is recognised identically{today}{current_user_id}{TODAY}TODAYa{b}ctitleFormat-style strings out of the rule{a}{b}{{x}}{a}bx{a}{}{ current_user_id }" current_user_id "Reverse verification
Direction predicted before running: red, and red only on the wide-shape assertions — the controls (
{today},{TODAY}, and every literal-passthrough shape) must not move, because the regex swap cannot reach them.Reverting
context-tokens.zod.tstoorigin/mainand rebuilding gave exactly that:All 17 failures are the wide-shape pins; every control assertion stayed green in all three suites.
Tests
Rejection-class cases assert the error identity — class,
name,code,status,.token, and the offending token echoed in the message — never a baretoThrow(). The resolver already throws for other reasons, so a throw-only assertion could not separate "refused with the right envelope" from "blew up elsewhere".packages/spec/src/data/context-tokens.test.ts— classification table for the four wide shapes, the$variant, padding, and six literal-passthrough shapes.packages/core/src/utils/filter-tokens.test.ts— fullUnknownFilterTokenErrorenvelope per shape;{today}still resolves;{TODAY}still refuses; literals still return by reference.packages/objectql/src/engine-filter-tokens.test.ts— the read path where the bypass was measured:find()refuses anddriver.findis never called;delete(multi:true)refuses on the write path too (verb parity, Flow node filters silently blank date macros: the template engine consumes{…}before the query engine sees it #3810 — a wideneddeleteis the most expensive place for a silent literal).No new fake engine was introduced; the existing recording driver in that file is reused.
Verification
pnpm lintclean;tsc --noEmitclean for spec, andturbo typecheckclean for spec/core/objectql/lint/service-automation, the example apps and the downstream consumer contract. All 50check:*steps enumerated from.github/workflows/lint.ymlpass, pluscheck:type-check-debt("none above its recorded number").api-surface/data.jsonis regenerated for the one added export (FILTER_TOKEN_WRAPPED_RE, 0 breaking).Note for client-side consumers
@object-ui/core'sresolveDateMacros/resolveContextTokenslive in theobjectuirepo and are unchanged. A client that forwards{TODAY()}now gets a 400 with a fixable message instead of a quietly wrong grid — the intended direction, and the reason this is stated in both directions in the changeset.Generated by Claude Code