Repository navigation
fix(spec)!: the filter save and validate doors refuse a dotted addDays offset - #26655
Conversation
…idate doors
The comparand-type walk and the addDays offset schema now refuse an
addDays { $field } naming a dotted path, on the { $field, addDays } pair
and on the $now anchor, in driver-sql's own same-table words. A data
filter, a dataset filter and a dataset measure filter fail at save and
at os validate instead of on the first SQL read.
Also takes the two should-fixes from the contract review of the
now-anchor extra-key refusal: the subDays clause prints only when
subDays is among the undeclared keys, and the isNowAnchor docblock
no longer claims each executor refuses a malformed anchor.
Claude-Session: https://claude.ai/code/session_01WGufzeyqyEtWpnqFTkcC9r
Co-Authored-By: Claude <noreply@anthropic.com>
…escribe Claude-Session: https://claude.ai/code/session_01WGufzeyqyEtWpnqFTkcC9r Co-Authored-By: Claude <noreply@anthropic.com>
… characters or more Claude-Session: https://claude.ai/code/session_01WGufzeyqyEtWpnqFTkcC9r Co-Authored-By: Claude <noreply@anthropic.com>
📓 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:
What this run could not see
Coarse fallback — 147 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 e2e30243cc3e812cb65ca84e0363e4dc3358d79b && git checkout e2e30243cc3e812cb65ca84e0363e4dc3358d79b
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin dbf7e04adff66aba72e634c65aae754440a8d89d c7f0010efadca7f13953892ba37c139351f87209 && git checkout -B drift-repro dbf7e04adff66aba72e634c65aae754440a8d89d && git merge --no-ff c7f0010efadca7f13953892ba37c139351f87209
node scripts/docs-audit/affected-docs.mjs --json dbf7e04adff66aba72e634c65aae754440a8d89d
|
Contract reviewServed-tier: Inputs read: card #26601 (body and all three comments: the seat ruling 6439436943, the claim 6439488124 and the os-dev report 6440958464), the governing records it names (maintainer ruling 6415710193 and the workload reading 6414128401 on #25808, the #14104 ruling 5507665791 and its correction 5550346132, the #15103 ruling 5548479553, and the prior contract review 6438749334 on #26592), PR #26655 (body, file list, net diff against main at this head) and the check-runs on the head. The rest of the diff (the changeset, the three service-analytics test files and the two docs pages) is the lane seat's ACCEPT at TIER_DEFAULT. Check-runs at 2026-10-27T11:44Z. Complete and green: Lint & Repo Gates and every Lint lane, Governed Surface Queue Guard, Check Changeset, Spec property liveness, Dogfood Regression Gate, Type Check source gates, Build Docs, Build UI Deps, Test UI, Check PR Size, the PR-body guards and the docs link check. Still running: Build Core, all eight Test Core shards, Type Check workspace, Type Check consumer gates, Type Check debt ledger, Type Check UI and Build & E2E UI. Nothing had failed. The Temporal, opt-in and matrix-shard lanes are skipped, as this diff does not reach them. This record judges the design. The landing still waits for every required check to finish green, as the tier rule says. ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgmentpackages/spec/src/data/filter-comparand-type.ts The scenario: an author writes a dataset measure filter whose
No must-fix. packages/spec/src/data/filter.zod.ts
No must-fix. The tree does what the seat ruling dispatched: a dotted offset the runtime refuses is refused at save and validate time, with the reason named, and nothing is widened. Implemented-by: VERDICT: PASS |
席位核对 · ACCEPT ·
|
Requested by Yibo Wang · project thread
Fixes #26601
Clause-②: no (narrowing) — the save and validate doors refuse a dotted
addDays$fieldthat every SQL read already refused. Nothing is widened.Metadata-protocol: narrowed (a filter, a dataset filter or a dataset measure filter may no longer carry an
addDaysoffset whose$fieldis a dotted path)Skills-docs: none —
skills/objectstack-query/rules/filters.mdalready teaches the offset as a number field of the same record and says SQL push-down refuses a dotted offset; this moves that refusal to save time and adds no new authoring rule. Moving "dotted" into that page's refused-at-400 list is a one-word follow-up for a skills PR (governed surface, kept out of this one).Ruling-ref: 6415710193 (letter A on #25808), seat ruling 6439436943, claim 6439488124. Scope is the author-time half only; B′ (an offset through a to-one path) is not in scope.
维护者速读
addDays偏移量若写成带点的跨表路径(如contract_type.review_sla_days),现在在保存与os validate时就被拒(INVALID_FILTER/ 400),措辞与 SQL 读取时的拒绝一致;同时把数据集度量过滤器读取时被隐去的原因一并带出。os validate通过、看板每次读取都 400;内存求值器沿路径读到的是关联记录的 id,偏移按 0 天算,静默算错。裁决 A 已把偏移限定为同一记录的数值列。packages/spec/src/data/filter-comparand-type.ts(judgeAddDaysOffset、dottedAddDaysOffsetRefusal)、packages/spec/src/data/filter.zod.ts(AddDaysReferenceSchema的regex与addDaysOffsetMessage)、.changeset/26601-dotted-add-days-offset-refused.md。pnpm --filter @objectstack/spec exec vitest run src/data/filter.test.ts -t 26601,预期 7 个用例全绿,其中数据集度量过滤器与ObjectStackDefinitionSchema均在measures.1.filter.review_started_at.$lt处被拒。What changes
filter-comparand-type.ts,normalizeFilterComparandTypes) still steps around a{ $field }reference and a well-formed$nowanchor, except for one part: anaddDaysoffset whose$fieldis a dotted path is refused at every scalar comparison withINVALID_FILTER/ 400. The reason isdriver-sql's own sentence for the same refusal (sql-driver-cross-field-comparison.ts:352and:517):the addDays offset "contract_type.review_sla_days" is a dotted path, and SQL push-down compiles same-table column references only (no relation traversal, no alias-qualified columns).The walk runs at theFilterConditionSchemasave door (data filters, datasetfilter, dataset measurefilter), inparseFilterAST, and at the engine'swhere,aggregations[i].filterandhavingdoors.filter.zod.ts,AddDaysReferenceSchema) refuses a dotted$fieldwith the same reason. That coversFieldReferenceSchema.addDays,NowAnchorSchema.addDaysand the ordering slots ofFieldOperatorsSchema/ComparisonOperatorSchema. It is aregex, not arefine, so the published JSON Schema carries the rule as apatternand the dropped-refinement ledger does not grow.aggregations[i].filterdoor, whereassertAggregationFilterIsEvaluable(having-filter.ts) refuses cross-field rules inwhere's withheld words, with no author-provenance restore. The walk runs before it (engine.tsnear line 20463), so a dotted offset there is now refused by the walk with the path and reason named. No executor file changed. The other cross-field refusals at that door stay withheld (acceptance note below).subDaysis among the undeclared keys, so a one-key refusal of any other key keeps its last sentence inside the 500-character client bound; the staleisNowAnchordocblock sentence ("refused … by each executor at query time") is trimmed to name the walk.filter.test.ts:1972(the dotted nested reference accepted "exactly as$fielditself allows"). Reason: ruling A on Analytics datasets: no filter compares a datetime column against now minus an SLA held on a related record #25808 bounds the offset to a same-record column, and the only executor that walked the path, the formula evaluator, reads a lookup's id on a stored row and counts the offset as zero days. The pin now asserts the refusal.content/docs/protocol/objectql/query-syntax.mdxloses "A dotted offset … validates" in both places it said so;content/docs/references/data/filter.mdxregenerated for the new describe.$field(a dotted base reference keeps its asymmetry: memory walks it, SQL refuses it), the bare implicit form{ field: { $field, addDays } }(an operator spec named$field, whose fate is unchanged), and every executor.Executors measured before the change (origin/main c8ed4c9)
driver-sql(anddriver-sqlite-wasm, which inherits it): refuses a dotted offset on the pair and the anchor (conformance cells:858,:1118).driver-mongodb: refuses every$fieldand$nowanchor by shape, dotted or not (mongodb-filter.ts,carriesFieldReference/carriesNowAnchor).@objectstack/formulamatchesFilterCondition: walks the path withgetPath. On a flat row (the lookup holds an id) the offset reads undefined, counts as 0 days, and answers silently: an SLA of -10 days read as 0 counted a 4-day-old review as overdue. It answers correctly only on a record with the relation expanded.driver-memoryfind: answers[]for any cross-field or anchor comparand handed to it directly (the known [finding] driver-memory's own reference matcher has no$fieldarm — a cross-field comparand (bare or withaddDays) reaching it is presumably compared as a literal object rather than resolved or refused (grep reading, to be measured) #15104 gap).FieldReferenceSchema,NowAnchorSchema,FieldOperatorsSchema,FilterConditionSchema,parseFilterAST, the walk,DatasetSchemameasurefilterandfilter.Tests
packages/spec/src/data/filter.test.ts: a[#26601]block. The schemas refuse ataddDays.$field; the four ordering slots on both operator copies;FilterConditionSchemaat all six scalar comparisons and under$and/$or/$not;DatasetSchemameasurefilterandfilterat the slot;ObjectStackDefinitionSchema(the schemaos validateparses) atdatasets.0.measures.1.filter.review_started_at.$lt, with a same-record control through the same door; controls for a same-record offset and a dotted base reference. The:1972pin flipped; the ordering-slot pin at:1986now uses a same-record offset.packages/spec/src/data/filter-comparand-type.test.ts: the walk refuses on the pair, the anchor and the resolved anchor at all six comparisons, with code, status and thedriver-sqlsentence; under combinators and bothparseFilterASTforms; the measure-filter position stays under 500 characters with its last sentence intact; controls kept by reference. The subDays clause is pinned both ways, plus a one-key refusal under$andwith the engine prefix inside 500 characters.service-analytics(tests only):now-anchor-dataset.test.ts,cross-field-offset-dataset.test.tsandinclude-relation-cross-field-boundary.test.tsasserted that the dotted offset validates at the schema door. They now assert the refusal, and they keep their read-time pins by putting the dotted filter back past the save door. The bare dotted arm still validates and is still refused by the driver.Acceptance notes
aggregations[i].filter(out of scope, no card): an undeclared, non-numeric or tenant offset column in a dataset measure filter is still answered inwhere's withheld words byassertAggregationFilterIsEvaluable, with no author-provenance restore. A dataset measure filter is the author's own, so a provenance mark there would restore those reasons; that is a design question for the analytics lane, not a pass-through in a file this PR touches.addDays(the CEL compiler emits none), so no policy text reaches this sentence.$fieldasymmetry kept:{ $lte: { $field: 'duty.due_on' } }still validates; memory walks it and SQL refuses it. Same reasoning would apply there, but it is outside this card's order.matches-filter-now-anchor.test.ts:69). Every in-repo caller sits behind a door that runs the walk.Verification
Package suites ran on
b9c396e87; the merge oforigin/mainthat madec7f0010efbrought no file in spec, service-analytics, objectql, formula, plugin-security, lint or driver-memory. Dogfood and the derived gates ran onc7f0010ef. Every command ran underscripts/pm/os-verify-lock.sh, with the exit code captured before any pipe.Package suites and typecheck
@objectstack/spectest: Test Files 730 passed, Tests 22073 passed, 1 todo. Typecheck exit 0.@objectstack/service-analyticstest: 191 files, 4647 passed, 282 skipped. Typecheck exit 0.@objectstack/objectqltest: 472 files, 8795 passed. Typecheck exit 0.@objectstack/formulatest: 54 files, 1452 passed.@objectstack/plugin-securitytest: 229 files, 4962 passed, 45 skipped.@objectstack/linttest: 166 files, 7389 passed.@objectstack/driver-memorytest: 72 files, 1758 passed.driver-sql/driver-sqlite-wasm/driver-mongodbnot re-run: none of them imports the walk or the offset schema, and no file of theirs changed.pnpm --filter @objectstack/spec check:generated --fix, then plain: "All 14 generated artifacts are up to date" (onlyreferences/data/filter.mdxmoved).Dogfood (owed for the narrowing arm) at c7f0010
pnpm --filter @objectstack/dogfood testafter the@objectstack/dogfood^...closure build: VERDICT command-exit 0, Test Files 301 passed and 3 skipped (304), Tests 2501 passed and 18 skipped (2519).Derived gates at c7f0010
node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackgave 106 commands, run serially with exit codes recorded. 105 exit 0.check:skill-examplesfirst exited 3 (client SDK not built); after building@objectstack/client-react...it exited 0 with "266 prose examples type-check".check:dual-build-cjs-loadsexits 3 (PREREQUISITE NOT MET, it needs the whole workspace built) and is left to CI.--ranreconciliation: "106 derived famil(ies) accounted for — 105 run, 1 NOT-MEASURED".Reverse verification, both legs restored by blob hash
dottedAddDaysOffsetmade to answerundefined(ablation-replace.mjs, anchor 1 to 0, blob48b975f2toedc0bfb4). 6 failed / 231 passed across the two spec files. Every red is a walk or save-door pin: the scalar comparisons, the combinators, the length bound, the FilterCondition door, the dataset doors and the validate door. The schema-level pins stayed green. Restored: blob equals HEAD,git diff HEADempty.AddDaysReferenceSchemapattern made vacuous (blob2403f93ato34ee3e03). 3 failed / 234 passed: the flipped:1972pin, the two schemas, and the ordering slots. The walk pins stayed green. Restored the same way.Engine probe at the measure-filter door (objectql dist, after the change)
engine.aggregate('clm_contract', { aggregations: [{ function: 'count', filter: { review_started_at: { $lt: { $now: true, addDays: { $field: 'contract_type.review_sla_days' } } } } }] })answersINVALID_FILTER/ 400:aggregate('clm_contract'): Filter comparand at aggregations[0].filter.review_started_at.$lt is refused: the addDays offset "contract_type.review_sla_days" is a dotted path, and SQL push-down compiles same-table column references only …. The same measure withaddDays: { $field: 'review_sla_days' }answers[{"over":1}].Generated by Claude Code