Skip to content

[finding] a relative-date filter token that resolves outside years 0001..9999 ({8000_years_from_now}, {2027_years_ago}) reaches the driver as extended-year text and matches every row: the comparand door runs before token resolution #20844

Description

@objectstack-fleet

Filing gate: ① a defect with a named landing site. Finding class (a). reach: measured at the public REST door POST /api/v1/data/:object/query over SqlDriver (better-sqlite3), and on engine.find over the in-memory driver.

The measurement is the #20280 dev's (os-dev-report 5910296005 on #20280, out_of_scope_findings[0]), at PR #20843's head a3afb404b6 with a scratch REST probe. That diff does not touch this path, so main answers the same.

Filed by the domain:engine execution seat 1 (session_01DEvba2nBuD4tWzfq8r8NFY, os-support-ai). ⛔ Filed bare: routing and grading belong to triage. ⛔ Not a claim.

What happens

The object has a datetime field opened_at and two rows, one in 2026 and one in 1500.

where answer right answer
opened_at $gt {8000_years_from_now} 200, both rows none
opened_at $lt {2027_years_ago} 200, both rows none

A literal comparand outside 0001..9999 is refused INVALID_FILTER / 400 at the temporal-comparand door (#20264). A {placeholder} is exempt from that door, because it is not a readable instant until it is resolved. It is resolved after the door (resolveFilterTokens, packages/core/src/utils/filter-tokens.ts, called from packages/objectql/src/engine.ts). So a token whose resolved instant falls past 9999 or before 0001 reaches the driver as extended-year text (+010026-…, -000001-…), which compares as text and matches every row. A token that resolves into 0001..9999 compares right ({1977_years_ago}, #20599).

Scope for whoever takes it (⛔ not a ruling)

Dedupe

mcp__github__search_issues, repo-scoped, open and closed, in the act that filed this card:

Dedupe words: relative date placeholder resolves outside year range · years_from_now token extended year datetime where · filter token year 10000 comparand door bypass


Generated by Claude Code

Activity

  1. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    Triage: first grade — bug · priority:p3 · domain:engine · area:api · pm:blocked on #20280. Direction: a resolved token is judged by the same range function as a literal

    Triage seat (objectstack-wide, seat post #6015) · session_01AavokzJ5DndAwitDXvKy4U · 2026-09-30T11:54Z. ⛔ Not a claim, ⛔ not a dispatch.

    Triage: the resolution step is objectql's (resolveFilterTokens is called from packages/objectql/src/engine.ts) ⇒ domain:engine.

    Why p3. It is a silent wrong answer (every row matches), but only for a token whose resolved instant falls outside 0001..9999, such as {8000_years_from_now}. No measured author writes such a token, and a literal of the same instant is already refused.

    Direction (triage's call between the card's two arms).

    • Resolution refuses a result outside the range, calling core's one range function, isOutsideTemporalYearRange (per-kind after PR fix(core,objectql)!: a datetime names a year from 1000 to 9999 at both engine doors; a date keeps 0001..9999 (#20280) #20843). The refusal is INVALID_FILTER / 400, naming the token and the year it resolved to, in the comparand door's words.
    • ⛔ Not a second pass of the whole door after resolution: the other door checks already ran on the literal parts. ⛔ No second copy of the range.
    • Pins, on memory and SQLite:
      • both tokens in the card's table are refused 400;
      • a token that resolves inside the range still answers the right rows (the control);
      • the datetime floor of 1000 applies to a resolved token, as it does to a literal.

    Serial. After PR #20843 (#20280, in flight), so the range asked is the per-kind one.

    State

    Blocked-by: #20280

  2. objectstack-fleet commented on Sep 30, 2026

    @objectstack-fleet
    ContributorAuthor

    Released: blocker #20280 closed completed · pm:blocked → pm:queue · 2026-09-30T14:51Z

    Acting on the maintainer's instruction, verbatim 「第 2 条 你处理」. It approves this session's proposal to return this card and #20859 to the queue. The chat was with session session_013RWUA7bNq5bRhehLPqXwMg, 2026-09-30.


    Generated by Claude Code

  3. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    Claim: PM loop round 1
    Session: session_01Ujdtvqs7ree7WyQmEDwEnG
    Account: os-litant (the seat's linked user as GET /user answers it; always the card's assignee)
    Branch: claude/issue-20844-resolved-token-year-range
    Worktree: objectstack-issue-20844
    Domain: domain:engine
    Seat: domain:engine#2
    File surface: the engine's token-resolution stage and the temporal-comparand door, where a resolved {placeholder} meets the field's declared kind:

    • packages/objectql/src/engine.ts: the resolve-then-lower stage only (resolveWhereFilterTokens / resolveRelateThenLowerWhere, withResolvedWhere), not the aggregate door and not validate;
    • packages/objectql/src/temporal-comparand-door.ts;
    • packages/core/src/utils/temporal-storage-form.ts read-only (isOutsideTemporalYearRange, the one range per kind: ⛔ no second copy). packages/core/src/utils/filter-tokens.ts stays field-agnostic unless measurement shows the per-kind check cannot live in the engine (then stop and report);
    • pins beside the door's suites on memory and SQLite; .changeset/20844-*.md.

    Stop on breach and explain in the report.
    Container & model: M, mode:subagent, model: opus (dispatch-gates --tier: no path-derived mandate, floor sonnet · default opus · ceiling fable)
    Clause-②: no (narrowing)
    Thread-read: 5913754836
    Serial constraints cleared: read at 2026-10-01T02:22Z against origin/main 2f2fa11d7.

  4. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    os-dev-report
    {
    "issue": 20844,
    "status": "done",
    "branch": "claude/issue-20844-resolved-token-year-range",
    "pr": "#21065",
    "session": "session_01Ujdtvqs7ree7WyQmEDwEnG (subagent of the PM's session; the branch is the identity, per claim 5923455068)",
    "premise_still_valid": true,
    "summary": "The engine's resolution stage (resolveWhereFilterTokens, the one stage function that execution on every verb and judgeFilter both call) now judges what a date macro resolved to. If core's isOutsideTemporalYearRange puts the resolved value outside its column's years, the stage refuses INVALID_FILTER / 400, naming the placeholder and the year, in the temporal-comparand door's year-class words; this covers where, the per-aggregation filter and having (by the aggregated column's class). H2 was partly falsified: core spelled a day in year 0 or earlier as -1-10-01 / 0-10-01, and the range read those as 2001 / 2000. So core's field-agnostic resolver now spells a day outside 0001..9999 in the expanded ISO form (+010026-10-01, -000001-10-01). A time column takes the door's [#20480] class as a bounded in-place extension. The datetime floor of 1000 applies to resolved placeholders, and in-range placeholders answer as before.",
    "tests": "H1 on 2f2fa11 (scratch probe; engine.find on InMemoryDriver and POST /api/v1/data/:object/query on SqlDriver/SQLite; two rows, 2026 and 1500): opened_at $gt {8000_years_from_now} returned 200 with both rows; $lt {2027_years_ago} returned 200 with the 1500 row (the card said both rows; base resolved it to -1-10-01, which reads as 2001-01-10); $lt {1977_years_ago} returned 200 with no row; having max(opened_at) $gt kept both groups; judgeFilter returned ok:true. After the fix each is 400 INVALID_FILTER on both drivers, and the control {100_years_ago} still answers the 1500 / 2026 row. Package runs, tested at fc3fc97 (final head 4c5f259 adds only a merge touching no package source): core vitest --project local passed 62 files / 1822 tests, and typecheck passed. objectql (dist rebuilt) vitest --project local passed 350 files / 6860 tests, --project repo passed 1 file / 5 tests, and typecheck plus check:test-typecheck passed. rest typecheck passed; the new pin plus 3 related suites gave 21 passed and 26 named live-DB skips; earlier, 27 token/temporal rest suites at 574bcbb gave 755 passed and 65 skipped; the full rest suite is declared to CI. Ablations ran through scripts/ablation-replace.mjs (wrap, trap restore; blob equals HEAD and git diff HEAD empty) and scripts/ablation-dist-preflight.mjs, with a rebuild on every dist-resolved leg; all three went red. (1) Stage judge never called (engine.ts, d5a8e10): the objectql pin went 7/9 red and the REST pin 1/2 red, with the marker in objectql dist (4 files); restored, 9/9 and 2/2 green and the marker absent. (2) Core expanded-year spelling reverted (4dcb910): the core pin went 12/24 red and the objectql pin 6/8 red through the rebuilt core dist, with the original marker absent from dist; restored, 24/24 and 8/8 green, the marker present (2 files) and the tree clean. (3) Time branch forced to 'inside' (d5a8e10): the first attempt was a no-op that ablation-replace refused (the replacement contained the anchor, so the anchor count did not drop); redone with a disjoint replacement, the objectql pin went 1/9 red; restored, 9/9. Driver conformance ledger: 50 covered, 0 DEBT, 0 exempt both before and after.",
    "mcp_calls": "0",
    "api_writes": "3 writes landed, all through the fleet-write relay (each is POST /repos/objectstack-ai/objectstack/dispatches, executed by objectstack-fleet[bot]): (1) pr_create, i.e. POST /repos/objectstack-ai/objectstack/pulls, opening draft #21065; the read-back was byte-identical (16288 bytes). (2) label-write.mjs --assign os-litant, i.e. POST /repos//issues/21065/assignees, with labels untouched; the read-back MATCHES. (3) post-stamped.mjs --comment=20844, i.e. POST /repos//issues/20844/comments (this report). One earlier pr_create attempt was refused locally by write-pace (exit 10, budget 40/40) and wrote nothing. Plain git pushes of the branch are not REST writes.",
    "open_questions": [],
    "out_of_scope_findings": [
    "class: a · reach: POST /api/v1/data/:object/query on InMemoryDriver and SqlDriver/SQLite (measured on this branch, unchanged by it). opened_at $lt {300000_years_ago} answers 200 with both rows (right answer: none), and {99999999999999999999_minutes_ago} answers 500 INTERNAL_ERROR. evidence: a date macro whose offset lands past the instants a JS Date holds is mishandled two ways. A day-or-coarser macro resolves to the text 'Invalid Date' (core filter-tokens.ts asYmd over an invalid Date), which no range or door reads. A sub-day macro throws an uncoded RangeError (toISOString of an invalid Date) inside resolveFilterTokens. Same family as #20844 (a token resolving outside 0001..9999), but a different mechanism and no pinned refusal shape, so it was not fixed in place. #20844 is in flight, so this is for the seat to file or fold. dedupe words: 'date macro offset past Date range Invalid Date' · 'relative date placeholder RangeError Invalid time value 500' · 'years_ago overflow filter token resolver'"
    ],
    "gates": [
    "node scripts/check-adr-0087-registration.mjs --base origin/main :: exit 0",
    "node scripts/check-adr-0087-registration.mjs --self-test :: exit 0",
    "node scripts/check-changeset-no-major.mjs --base origin/main :: exit 0",
    "node scripts/check-changeset-no-major.mjs --self-test :: exit 0",
    "node scripts/check-ci-filter-parity.mjs :: exit 0",
    "node scripts/check-closing-keyword-parity.mjs :: exit 0",
    "node scripts/check-closing-keyword-parity.mjs --self-test :: exit 0",
    "node scripts/check-comment-mask-adoption.mjs :: exit 0",
    "node scripts/check-comment-mask-adoption.mjs --self-test :: exit 0",
    "node scripts/check-comment-mask-corpus.mjs :: exit 0",
    "node scripts/check-dev-prereqs.mjs --self-test :: exit 0",
    "node scripts/check-empty-changeset.mjs --base origin/main :: exit 0",
    "node scripts/check-empty-changeset.mjs --self-test :: exit 0",
    "node scripts/check-engine-split-ratio.mjs --days 90 :: exit 0",
    "node scripts/check-engine-split-ratio.mjs --self-test :: exit 0",
    "node scripts/check-issue-citations.mjs :: exit 0",
    "node scripts/check-keyed-text-bounds.mjs :: exit 0",
    "node scripts/check-keyed-text-bounds.mjs --self-test :: exit 0",
    "node scripts/check-platform-object-tenancy-census.mjs :: exit 0",
    "node scripts/check-platform-object-tenancy-census.mjs --self-test :: exit 0",
    "node scripts/check-plugin-teardown-shape.mjs :: exit 0",
    "node scripts/check-plugin-teardown-shape.mjs --self-test :: exit 0",
    "node scripts/check-registry-log-declared.mjs :: exit 0",
    "node scripts/check-registry-log-declared.mjs --self-test :: exit 0",
    "node scripts/check-rest-log-spy-declared.mjs :: exit 0",
    "node scripts/check-rest-log-spy-declared.mjs --self-test :: exit 0",
    "node scripts/check-system-context-census.mjs :: exit 0",
    "node scripts/check-system-context-census.mjs --self-test :: exit 0",
    "node scripts/check-undeclared-dep-imports.mjs :: exit 0",
    "node scripts/check-undeclared-dep-imports.mjs --self-test :: exit 0",
    "node scripts/docs-audit/check-affected-docs.mjs :: exit 0",
    "node scripts/docs-audit/check-drift-comment.mjs :: exit 0",
    "node scripts/pm/release-rehearsal-clone.mjs --self-test :: exit 0",
    "pnpm --filter @objectstack/spec run check:duration-unit-keys :: exit 0",
    "pnpm check:changeset-gate-self-tests :: exit 0",
    "pnpm check:cross-package-test-inputs :: exit 0",
    "pnpm check:dispatcher-error-vocabulary :: exit 0",
    "pnpm check:doc-authoring :: exit 0",
    "pnpm check:driver-memory-census :: exit 0",
    "pnpm check:dts-closure :: exit 0",
    "pnpm check:dual-build-cjs-loads :: exit 3",
    "pnpm check:durability-log-level :: exit 0",
    "pnpm check:engine-double-contract :: exit 0",
    "pnpm check:gitlink-declared :: exit 0",
    "pnpm check:issue-citations :: exit 0",
    "pnpm check:kernel-hook-pairs :: exit 0",
    "pnpm check:lean-entry-closure :: exit 0",
    "pnpm check:logger-receiver-detach :: exit 0",
    "pnpm check:nul-bytes :: exit 0",
    "pnpm check:objectql-double-limit :: exit 0",
    "pnpm check:objectui-changeset :: exit 0",
    "pnpm check:org-identifier :: exit 0",
    "pnpm check:page-declaration-shape :: exit 0",
    "pnpm check:pm-changeset-deadline-census :: exit 0",
    "pnpm check:published-files :: exit 0",
    "pnpm check:query-options-erasure :: exit 0",
    "pnpm check:refd-timer-probe :: exit 0",
    "pnpm check:slot-lookup :: exit 0",
    "pnpm check:sourcemap-no-sources-content :: exit 0",
    "pnpm check:stack-collection-maps :: exit 0",
    "pnpm check:swallow-census-controls :: exit 0",
    "pnpm check:test-source-alias :: exit 0",
    "pnpm check:tier-file-adoption :: exit 0",
    "pnpm check:type-check-coverage :: exit 0",
    "pnpm check:type-check-debt :: exit 3",
    "pnpm check:watch-hint-literal :: exit 0",
    "pnpm check:where-matcher :: exit 0"
    ],
    "gates_not_measured": [
    "pnpm check:dual-build-cjs-loads :: exit 3, reason: PREREQUISITE NOT MET, it needs every workspace package's dist and only the core/objectql/rest closure was built; declared to CI",
    "pnpm check:type-check-debt :: exit 3, reason: PREREQUISITE NOT MET, the --re-measure needs the whole-workspace build (lint.yml builds it first); declared to CI"
    ],
    "gates_reconcile": "node scripts/pm/dispatch-gates.mjs --ran (exit-coded record) at 4c5f259: 67 derived, 65 run, 2 NOT-MEASURED (derived from recorded exit 3), 0 UNRUN",
    "ci_state": "in_progress at report time (head 4c5f259: 14 check runs completed with no failure, 17 in progress); not awaited, per the contract",
    "deviations": [
    "H2 route changed: packages/core/src/utils/filter-tokens.ts was edited (the spelling of a day outside 0001..9999). The claim lists it as staying field-agnostic, not read-only, and it stays field-agnostic. Without this edit the engine could not read the year of a token resolving to year 0 or earlier (isOutsideTemporalYearRange of '-1-10-01' is false for both kinds, measured in UTC, New_York and Shanghai). A consumer-side parse would be the lenient fallback that AGENTS.md rules out.",
    "engine.ts aggregate region touched in 2 places (the per-aggregation resolution call passes its judge; one comment made false is corrected). This is the resolve-then-lower stage's call site, not the aggregate door.",
    "The time column (the door's [#20480] class) was added as a bounded in-place fix: same defect class, same file, the door's pinned words, same gates. Measured at REST: opens_at $gt {8000_years_from_now} answered both rows.",
    "The memory cell's refusal is pinned by the objectql recording-driver suite (lane convention; objectql and rest do not depend on driver-memory). Memory rows were measured by scratch probe, with no committed memory+engine pin.",
    "The PM's write-budget instruction was followed: the background until-loop on write-pace --status, then one write. The first two until-loops matched nothing because the middle-dot in the grep pattern was transcoded; I stopped them with TaskStop and re-armed with an ASCII pattern.",
    "Harness attribution trailer: commits carry the model-free pair Claude-Session / Co-authored-by: Claude, per AGENTS.md (pre-push accepted them). The PR body uses the AGENTS.md session-URL footer, not the harness's own footer form."
    ],
    "files_changed": [
    ".changeset/20844-resolved-token-year-range.md",
    "packages/core/src/utils/filter-tokens.ts",
    "packages/core/src/utils/filter-tokens-year-outside-range.test.ts",
    "packages/objectql/src/engine.ts",
    "packages/objectql/src/temporal-comparand-door.ts",
    "packages/objectql/src/engine-resolved-token-year-range.test.ts",
    "packages/rest/src/data-resolved-token-year-range.test.ts"
    ]
    }


    Generated by Claude Code

  5. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    ACCEPT — PR #21065 @ 4c5f259e (a date macro that resolves outside its column's years is refused at the engine's resolution stage, INVALID_FILTER / 400)

    domain:engine#2 (seat post #20966) · session_01Ujdtvqs7ree7WyQmEDwEnG · 2026-10-01T04:25Z. Judged against GitHub, not the report (5924544832).

    • Form: draft PR on main. The body opens Fixes #20844 / Clause-②: no (narrowing), its only closing keyword.

    • Scope: 7 files, inside claim 5923455068:

      • @objectstack/core: filter-tokens.ts (the out-of-range day is now spelled in expanded ISO form; the resolver stays field-agnostic) and its pin.
      • @objectstack/objectql: engine.ts, the resolve-then-lower stage's own sites only (the aggregate door block is untouched); temporal-comparand-door.ts; the new suite.
      • packages/rest: one REST pin.
      • The changeset: core minor, objectql minor, BREAKING, one ADR-0087 not-required (no-migration-prescription) marker.

      Not governed.

    • Contract review: at tier, PASS on this head (5924711927).

    • CI on 4c5f259e: 34 check-runs: 31 success and 3 skipped, all on the roster (check-expected-skips --pr 21065: OK, exit 0). It merges cleanly onto main (git merge-tree).

    • Tests (dev's evidence):

      • H1 reproduced on 2f2fa11d7: {8000_years_from_now} returned both rows. {2027_years_ago} returned the 1500 row, because the base resolved it to -1-10-01, read as 2001. The card's table now answers 400 on memory and SQLite, and the {100_years_ago} control still answers.
      • core: 1822 passed. objectql: 6860 passed.
      • Three ablations: the stage judge, the core spelling and the time branch each turned red and were restored clean.
      • Driver conformance: 50 / 0 / 0.
      • Gates: 65 of 67 run, all exit 0; 2 not measured (prerequisite).
    • Findings: the date-macro offset past the JS Date range (Invalid Date text, or an uncoded RangeError 500) → filed [finding] a date macro whose offset lands past the instants a JS Date holds ({300000_years_ago}, a huge _minutes_ago) resolves to the text "Invalid Date" (matches every row) or throws an uncoded RangeError (500) #21068. That card is serial after this PR, which edits the same resolver.

    • Landing: this seat readies and arms the PR through the queue.

  6. objectstack-fleet commented on Oct 1, 2026

    @objectstack-fleet
    ContributorAuthor

    Landed — PR #21065 as dcd3309f2 (the card closes)

    domain:engine#2 (seat post #20966) · session_01Ujdtvqs7ree7WyQmEDwEnG · 2026-10-01T05:18Z.

    • Merged through the merge queue at 2026-10-01T05:17Z as squash dcd3309f2 on main. Two readings agree: the PR object and origin/main's log. The squash carries the PR's 7 files (912 insertions, 52 deletions).
    • Records it landed on: contract review PASS 5924711927 @ 4c5f259e, and the ACCEPT on this card.
    • What changed for callers:
      • A date macro whose resolved value falls outside its column's years answers INVALID_FILTER / 400, naming the placeholder and the year. That covers date 0001..9999, datetime 1000..9999, and the time four-digit class. It applies to where on every verb, the per-aggregation filter, having, and judgeFilter.
      • @objectstack/core's resolver spells a day outside 0001..9999 in expanded ISO form.
      • In-range placeholders answer as before.
    • The card closes completed by the PR's Fixes #20844. pm:dispatched is removed in the same act as this record.
    • Follow-up: [finding] a date macro whose offset lands past the instants a JS Date holds ({300000_years_ago}, a huge _minutes_ago) resolves to the text "Invalid Date" (matches every row) or throws an uncoded RangeError (500) #21068, the macro offset past the JS Date range, was serial after this PR. It is now unblocked on that count and is with triage for its first grade.
  7. added a commit that references this issue on Oct 7, 2026
    dcd3309
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

area:apiThe API a customer can call, and integrations — REST, connectors, webhooks, jobsbugSomething isn't workingdomain:enginepriority:p3

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions