Repository navigation
fix(spec)!: every filter door refuses a $now anchor that carries a key beyond $now / addDays - #26592
Conversation
…y beyond $now / addDays
isWellFormedNowAnchor read addDays only, so { $now: true, addDays: -30,
subDays: 7 } passed the comparand-type walk that every filter door runs
(find object and array form, aggregate where, aggregations[i].filter,
having, and FilterConditionSchema on save) and ran on the addDays reading
alone, while NowAnchorSchema declares the anchor closed. The walk now
refuses any other key with INVALID_FILTER / 400, and the refusal names
the key and says the anchor takes $now and addDays only.
Claude-Session: https://claude.ai/code/session_01WGufzf89Y8Q23nPngB94Bo
Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WGufzf89Y8Q23nPngB94Bo Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): 4 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 2 release-owned page(s) also name something this change touched. These are read-only:
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 4245e590d1b9132fbbf868f5cc8798b51c08f93c && git checkout 4245e590d1b9132fbbf868f5cc8798b51c08f93c
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 68edbcf10fdc8351d6238c6a9a34e0cc66c71bb0 98b536ffd50ac587f83c82e55b5aadaa5db73118 && git checkout -B drift-repro 68edbcf10fdc8351d6238c6a9a34e0cc66c71bb0 && git merge --no-ff 98b536ffd50ac587f83c82e55b5aadaa5db73118
node scripts/docs-audit/affected-docs.mjs --json 68edbcf10fdc8351d6238c6a9a34e0cc66c71bb0
|
Contract reviewServed-tier: Inputs read: card #26413 (body and all six comments, the two os-dev reports and the withdrawal note included), PR #26592 (body, file list, net diff against main at this head), and the check-runs on the head. The rest of the diff (the changeset and the three test files outside spec) is the lane seat's ACCEPT at TIER_DEFAULT. Check-runs at the time of this record: every required job is completed and green, including Lint and Repo Gates, TypeScript Type Check, Check Changeset, Governed Surface Queue Guard, Spec property liveness, Dogfood Regression Gate, Build Core and all eight Test Core shards. Test Core (5/8) was still in progress on my first read and finished green before this record was closed. The skipped jobs are the UI, docs, temporal and opt-in lanes that this diff does not reach. ① Derived judgments
② Semver level
③ Boundary flags
④ Design judgmentpackages/spec/src/data/filter-comparand-type.ts The change holds against the code it cites and the scenario it is accepted against: an author writes
No must-fix found. The tree does what the card's option (a) asked for and what the skill already taught. Implemented-by: VERDICT: PASS |
席位核对 · ACCEPT ·
|
Requested by Yibo Wang · project thread
Fixes #26413
Clause-②: no (narrowing) — every filter door refuses a
$nowanchor carrying a key beyond$now/addDays, which it ran before. Nothing is widened.Metadata-protocol: narrowed (a filter saved through
FilterConditionSchemamay no longer carry a key beside$nowandaddDayson a now-anchored comparand)Skills-docs: none —
skills/objectstack-query/rules/filters.md(the refusal list under "Now-Anchored Comparands") already teaches that "any extra key (there is nosubDays)" is refused withINVALID_FILTER/ 400, and evalfilters-pagination-search.jsonalready forbidssubDays; this diff makes that sentence true at every door, so it needs no edit.维护者速读
$now锚点检查(isWellFormedNowAnchor)现在拒收$now/addDays之外的任何键,报INVALID_FILTER/ 400 并点名多余的键。NowAnchorSchema声明为封闭,但入口只看addDays,subDays: 7这类键被静默忽略,查询按addDays跑完、不报错(ADR-0049:声明即须执行)。filter-comparand-type.ts一处。packages/spec/src/data/filter-comparand-type.ts(isWellFormedNowAnchor与malformedNowAnchorRefusal新分支)、packages/objectql/src/engine-now-anchor.test.ts(五个入口的钉)、.changeset/26413-now-anchor-extra-key-refused.md。pnpm --filter @objectstack/spec exec vitest run src/data/filter-comparand-type.test.ts,预期新的[#26413]组全绿。What changes
packages/spec/src/data/filter-comparand-type.ts:isWellFormedNowAnchornow answers false for an anchor with any key other than$now/addDays, so the comparand-type walk that every filter door runs refuses it.malformedNowAnchorRefusalgains an undeclared-key arm, checked first:it carries an undeclared key "subDays" — the anchor takes $now and addDays only (there is no subDays: a negative addDays subtracts). No new error code: the doors keepINVALID_FILTER/ 400. The key set is restated locally (filter.zod.tsimports this module) and the test reconciles it againstNowAnchorSchema.shape.find()/count()/update()in object and array form,aggregate()'swhere,aggregations[i].filter,having, andFilterConditionSchemaon save. A well-formed anchor and its resolved form ($nowholding the pinned ISO instant) run as before.@objectstack/specchangeset,minor(see Acceptance notes on the level).Tests
packages/spec/src/data/filter-comparand-type.test.ts: new[#26413]group: the door's key set equalsNowAnchorSchema.shape; refusal with code, status and the named key at all six scalar comparisons for an integer offset, a{ $field }offset and the resolved form; under$and/$or/$notand through bothparseFilterASTforms; every undeclared key named, before a malformed offset; well-formed anchors kept by reference.packages/spec/src/data/filter.test.ts: theFilterConditionSchemasave door refusessubDaysandnote, naming the key, beside the existing closed-schema pin; the clean anchor still parses.packages/objectql/src/engine-now-anchor.test.ts: new[#26413]group, one answer at every door:find()object form (driver never called),find()array form,aggregate()where,aggregations[i].filter,having, each with code, status and the named key, and each with the clean anchor still running.packages/formula/src/matches-filter-now-anchor.test.ts: narrowed, not retired. Measured: it does not contradict the refusal. The engine reaches this evaluator only throughhaving-filter.ts(havingand theaggregations[i].filterrows fallback), both behind the door, and the CEL lowering for the RLScheckemits no anchor. The assertion stays; the title and a comment now say the evaluator sits behind the door and does not re-judge the keys, instead of presenting the tolerance as the anchor's contract.Acceptance notes
FilterConditionSchema.safeParse({ opened_at: { $lt: { $now: true, addDays: -30, subDays: 7 } } })wassuccess: trueon base and is refused now, so a value a metadata author could save is refused:narrowed. AGENTS.md's level table makes a declared kind at leastminoron a moved package, so the changeset is@objectstack/spec: minor.objectqlandformulachange tests only and publish nothing.not-required (no-migration-prescription): no key is added, removed or renamed (NowAnchorSchemadeclared exactly two keys since 18.2.0), and an extra key never had a meaning at any executor, so no rewrite keeps what the author meant.{ opened_at: { $now: true, addDays: -30, note: 'x' } }is read by the walk as an operator bag, not a comparand, so it is outside this refusal.havingalready refuses the bare spelling; its answer atwhereagainst a real driver was not measured.Verification
Readings at head
98b536ff. Builds and tests ran underos-verify-lock.Package tests and typecheck
pnpm --filter @objectstack/spec test: exit 0, 729 files, 22055 passed, 1 todo.pnpm --filter @objectstack/formula test: exit 0, 54 files, 1452 passed.pnpm --filter @objectstack/objectql test: exit 0, 472 files, 8794 passed (afterpnpm --filter '@objectstack/objectql^...' build).pnpm --filter @objectstack/{spec,formula,objectql} typecheck: exit 0 each.pnpm exec eslint --no-inline-configon the five changed source and test files: exit 0 (the repo-widepnpm lintis CI's run).Ablation (one, on the guard line)
Committed fix first, then
node scripts/ablation-replace.mjsreplacedif (undeclaredNowAnchorKeys(value).length > 0) return false;with a branch that never fires (markerABLATED_26413). Anchor 1 to 0, bloba7e88059to64df538e; spec rebuilt;ablation-dist-preflightfound the marker in 2 built files. Expected direction red; observed red: spec 3 failed / 221 passed (the three new refusal pins), objectql 5 failed / 15 passed (all five new door pins). Restore: blob equals HEAD,git diff HEADempty, spec rebuilt, preflight--absentclean over 580 built files, whole-treegit status --porcelainempty.Derived gates
node scripts/pm/dispatch-gates.mjs --commandsderived 82 families; 80 exit 0.check:dual-build-cjs-loadsandcheck:lean-entry-closureexit 3 (prerequisite: they read whole-repo built output; this worktree built the objectql closure only), NOT MEASURED locally, left to CI.--ranreconciliation: 82 derived, 80 run, 2 NOT-MEASURED, 0 UNRUN.check-adr-0087-registration --base origin/main: one declared-breaking changeset, disposition carried. Local-only:check-skill-frame-freshnessexit 0;check-dev-prereqsexit 1 because the workspace is not fully built (37 packages lackdist/), an environment state, not a finding. The full dogfood battery reading goes in the os-dev report on the card.Human Minimum
packages/specholds no runtime logic.pnpm lintpasses.main, one pull request.packages/spectouched: its generated artifacts are regenerated and committed.content/docs/references/,packages/*/CHANGELOG.mdorcontent/docs/releases/.Generated by Claude Code