Repository navigation
fix(core,objectql)!: a relative-date placeholder resolved outside its field's years is refused INVALID_FILTER / 400, naming the placeholder and the year - #21065
Conversation
… column's years Claude-Session: https://claude.ai/code/session_01Ujdtvqs7ree7WyQmEDwEnG Co-authored-by: Claude <noreply@anthropic.com>
…year range on every position Claude-Session: https://claude.ai/code/session_01Ujdtvqs7ree7WyQmEDwEnG Co-authored-by: Claude <noreply@anthropic.com>
…eld's years is refused Claude-Session: https://claude.ai/code/session_01Ujdtvqs7ree7WyQmEDwEnG Co-authored-by: Claude <noreply@anthropic.com>
…solved-token-year-range
…time column takes the door's time class Claude-Session: https://claude.ai/code/session_01Ujdtvqs7ree7WyQmEDwEnG Co-authored-by: Claude <noreply@anthropic.com>
…solved-token-year-range
…solved-token-year-range
📓 Docs Drift CheckThis PR changes 2 package(s): 6 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 — 34 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 35366c09110603c52f2c764a7e59f7828f05d78b && git checkout 35366c09110603c52f2c764a7e59f7828f05d78b
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 8f784959cf7e597e4582a70b93633b10eddb2dcf 4c5f259e5cb5455f712f792b06edb95058174cf9 && git checkout -B drift-repro 8f784959cf7e597e4582a70b93633b10eddb2dcf && git merge --no-ff 4c5f259e5cb5455f712f792b06edb95058174cf9
node scripts/docs-audit/affected-docs.mjs --json 8f784959cf7e597e4582a70b93633b10eddb2dcf
|
Contract reviewServed-tier: ① Derived judgmentsInputs: card #20844 (body; triage 5910693971, release 5913754836, claim 5923455068, os-dev-report 5924544832), PR #21065's body and file list, the net diff of Accept-set changes — all narrowings, all before any driver read, all
Public-surface changes:
Wording note, not a defect: the changeset's sentence "Measured before this on InMemoryDriver and SqlDriver on SQLite" also covers the ② Semver levelThe changeset is
③ Boundary flags
(a) (b) (c) The (d) The memory cell pinned by objectql's recording driver, memory rows measured by probe only. ANSWERED, accepted: the lane convention the dev cites is real ( (e) Write-budget handling (the until-loops, then one write). Process, not contract; noted. (f) Attribution trailers: every non-merge commit on the branch carries the model-free trailer pair AGENTS.md prescribes, the PR body ends in the session-URL footer, and the merge commits carry none. Noted; nothing owed. (g) (h) Check-runs on the head: 26 Implemented-by: VERDICT: PASS |
…Script Date holds is refused INVALID_FILTER / 400, naming the placeholder (objectstack-ai#21123) Fixes objectstack-ai#21068 Clause-②: no (narrowing) A date macro whose offset lands past every instant a JavaScript `Date` can hold (`{300000_years_ago}`, `{99999999999999999999_minutes_ago}`) names no day and no time. `@objectstack/core`'s resolver now refuses it with `INVALID_FILTER` / 400, naming the placeholder. It used to resolve a day macro to the text `Invalid Date`, which compares as text, and to throw an uncoded `RangeError` from `toISOString` for a sub-day macro, which the REST door answered with a 500. Dispatched by the PM loop (round 1, seat `domain:engine#2`), claim comment 5925905189, session `session_01Ujdtvqs7ree7WyQmEDwEnG`. ## What changes - **`packages/core/src/utils/filter-tokens.ts`.** `resolveFilterToken` now computes a date macro's instant separately from spelling it (`dateMacroInstant`: the `Date`, and whether it is spelled as an instant or as a calendar day). It asks one question of that instant before either spelling: is it a valid `Date`? If not, it throws `DateMacroPastDateRangeError`: `code: 'INVALID_FILTER'`, `status: 400`, and `token` set to the placeholder's name. `resolvePeriodToken` returns the period's bounding `Date` rather than its spelling. That is internal, and the spelling of every day and instant a `Date` holds is unchanged. - The check is the `Date`'s own validity, not a copy of its range. One step inside the edge still resolves: `{273847_years_ago}` resolves to `-271821-09-30`, and the engine's per-column year judge (PR objectstack-ai#21065) refuses it on a `date` or `datetime` column, as before. - The error class is not exported, so `@objectstack/core` publishes no new symbol. Callers read `code` and `status`, as for every door's refusal. Its message names the years each kind takes from `SUPPORTED_TEMPORAL_YEARS`, never a copy of the numbers. - Nothing else in core, objectql or rest changes. There is no second validity or range check outside the resolver, and the engine's year-range judge is untouched. ## The PM's hypotheses (zone 2) - **H1, reproduced** on `origin/main` `fed0db8f6` with a scratch probe (not committed), off 2026-09-30T12:00Z, on the card's object (`opened_at` `datetime`, rows at 2026-03-01T10:00Z and 1500-03-01T10:00Z). The memory column is `engine.find` on InMemoryDriver. The SQLite column is `POST /api/v1/data/:object/query` on SqlDriver over SQLite (better-sqlite3). | `where` | resolved to (base) | base, memory | base, SQLite | base, `judgeFilter` | now, all three | |:--|:--|:--|:--|:--|:--| | `opened_at $lt {300000_years_ago}` | the text `Invalid Date` | both rows | 200, both rows | `{ ok: true }` | 400 `INVALID_FILTER`, the placeholder named | | `opened_at $lt {99999999999999999999_minutes_ago}` | throws `RangeError: Invalid time value` | uncoded `RangeError` | 500 `INTERNAL_ERROR` | throws `RangeError` | 400 `INVALID_FILTER` | | `placed_on` (`date`) `$lt {300000_years_ago}` | `Invalid Date` | both rows | 200, both rows | `{ ok: true }` | 400 | | `opened_at $gt {300000_years_from_now}` | `Invalid Date` | no row (compared as text) | 200, no row | `{ ok: true }` | 400 | | `opened_at $lt {99999999999999999999_hours_ago}` | throws `RangeError` | uncoded `RangeError` | 500 | throws | 400 | | control: `opened_at $lt {100_years_ago}` | `1926-09-30` | the 1500 row | 200, the 1500 row | `{ ok: true }` | unchanged | The "now" column was measured with the same probe on this branch at `290ed978e`, with core's `dist/` rebuilt from the fix. - **H2, the code: `INVALID_FILTER`.** I read the declared meanings of the three candidates: - `FILTER_TOKEN_UNRESOLVED` means a context the request does not carry. Every place that states it says so: the class docblock ("a token IS in the vocabulary but the request carries no value for it … the spelling is correct, the context is not"), the spec contract `IObjectQLEngine.judgeFilter` ("A placeholder whose value this context does not carry is REFUSED (`FILTER_TOKEN_UNRESOLVED`)"), `engine.ts`'s resolution-stage comment, and the published CHANGELOG entries (`{current_user_id}` with no user, `{record_id}`). Here the context is complete, and the author's fix is a smaller offset, not a context. So that code reads false. - `FILTER_TOKEN_UNKNOWN` means outside the vocabulary. The token is in the vocabulary (`classifyFilterToken` says `date-macro`), so that code reads false too. - `INVALID_FILTER` is the standard catalog's "Invalid filter expression". It is also the code this family already answers: the engine refuses a macro that resolved outside its column's years with it (PR objectstack-ai#21065), and the temporal-comparand door refuses a literal of the same value with it. It reads true, and an author gets one code across the whole offset axis: `{273847_years_ago}` is refused by the engine's year judge, and `{273848_years_ago}` by the resolver. - No code is minted: `INVALID_FILTER` is a standard member and needs no ledger row. - **H3, one place, confirmed.** Both failures came from an invalid `Date` reaching a spelling: `asYmd` gave `String(date)`, and `toISOString` threw. The check sits right after the instant is computed and before either spelling, so it covers every date macro (fixed, period and parameterised) on both forms. `resolveFilterTokens` and every caller of `resolveFilterToken` inherit it. ## Pins - `packages/core/src/utils/filter-tokens-past-date-range.test.ts` covers every unit of the parameterised grammar in both directions. Each case asserts `code` `INVALID_FILTER`, `status` 400, `token`, the placeholder in the message, and that neither `Invalid Date` nor `Invalid time value` appears. It also covers `resolveFilterTokens` on a tree (UTC and `Asia/Shanghai`, input not mutated); the `Date`'s own edges on both sides (`{273847_years_ago}` resolves, `{273848_years_ago}` refuses; the same for `_from_now` and for minutes at exactly -8.64e15 ms); and in-range controls for a day, an instant and `{now}`. - `packages/objectql/src/engine-resolved-token-past-date-range.test.ts` is the memory cell, held by a recording driver. Both rows of the card are refused on `find`, `findOne`, `count`, multi-row `update` and `delete`, `aggregate` `where`, a per-aggregation `filter` and `having`. The column kinds are `datetime`, `date` and `text`; a text column's `having` is left out because `max` of text is the aggregate field-type door's to answer. The test asserts `code`, `status`, the placeholder named, and no driver read. `judgeFilter` returns execution's exact `code`, `status` and `message`. The control (`{100_years_ago}`, `{1_hour_ago}`) reaches the driver as the value it names. - `packages/rest/src/data-resolved-token-past-date-range.test.ts` is the SQLite cell, at `POST /api/v1/data/:object/query` on the card's two rows. Both placeholders get 400 `INVALID_FILTER` naming the placeholder, at `where`, the per-aggregation `filter` and `having`, with no read of the object, and the body never contains `Invalid Date`. The control answers the right rows, filter counts and `having` groups (`$lt` / `$gt {100_years_ago}`, `$lt {1_hour_ago}`). ## Verification (head `381d735b8`, unless a line says otherwise) - `@objectstack/core`: `pnpm --filter @objectstack/core test` passed 72 files / 2084 tests. `typecheck` passed, including `check:test-typecheck`; `tsc -p tsconfig.test.json --listFiles` lists the new test (1 hit). - `@objectstack/objectql`, against core's rebuilt `dist/`: - the full `vitest run --project local` passed 356 files / 6919 tests at `1dbee5b9b`. The later merge of `origin/main` touched no objectql file, and in core only the import-runner modules and `index.ts`. - At `381d735b8`, the six token, judge and temporal-door suites passed (131 tests), and `typecheck` passed. - `@objectstack/rest`: `typecheck` passed. The new pin plus five placeholder and temporal suites (`data-resolved-token-year-range`, `data-temporal-year-range`, `rest-aggregate-positions`, `data-number-comparand-door`, `analytics-dataset-selection-door`) passed 51, with 16 named skips for unprovisioned live PG / MySQL cells. The full rest suite is declared to CI. - `@objectstack/service-analytics` (consumer, at `1dbee5b9b`): `query-filter-tokens`, `dataset-filter-tokens`, `read-scope-placeholder-three-faces` and `objectql-read-scope-placeholder-refusal` passed 64. - **Gates.** `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` at `381d735b8` derived 65 commands. All 65 were run at that head: 63 exited 0, and `--ran` with the exit codes reconciles 65 / 65. - **NOT MEASURED: `pnpm check:dual-build-cjs-loads` and `pnpm check:type-check-debt`, reason: exit 3 PREREQUISITE NOT MET.** Both read every workspace package's built `dist/`, and only the rest closure was built here. Declared to CI. - In their place, a narrowed probe: core's `dist/index.cjs` loads under `require` and answers the refusal with its code, and neither the CJS nor the ESM entry exports `DateMacroPastDateRangeError`. - `check:adr-0087-registration` reads the changeset as `not-required (no-migration-prescription)`. `check:changeset-no-major` and `check:empty-changeset` are green. `scripts/pm/check-widening-tells.mjs --declaration no` over the PR diff exits 0. - `node scripts/check-driver-conformance.mjs`: before (`fed0db8f6`) "50 covered cell(s), 0 in the DEBT ledger, 0 exempt"; after (`381d735b8`) the same. ## Reverse verification (ablation, one-off, not committed) At `1dbee5b9b`, with the fix committed, I deleted the guard line in `filter-tokens.ts` through `scripts/ablation-replace.mjs` (wrap mode, anchor 1 to 0, blob `9c727e33e` to `becaf90bf`). Its restore was armed, and an outer `trap … EXIT INT TERM` restored from `HEAD`. - **Mutate leg.** Core was rebuilt, and `scripts/ablation-dist-preflight.mjs @objectstack/core 'Number.isNaN(macro.at.getTime())' --absent` passed (marker absent from all 14 built files; before the mutation it was present in `dist/index.js` and `dist/index.cjs`). The three pins went red, the usual direction: - core 10 / 11 (only the controls held); - objectql 2 / 3 (`find where opened_at $lt {300000_years_ago}`: resolved instead of refusing); - rest 1 / 2 (200 with both rows instead of 400). - **Restore leg.** The blob equals `HEAD` (`9c727e33e`), `git diff HEAD` is empty and `git status --porcelain` is empty. Core was rebuilt, and the preflight in default mode found the marker present in 2 built files. The pins went back to 11 / 11, 3 / 3 and 2 / 2. ## Acceptance notes - **Memory cell.** As with PR objectstack-ai#21065, the memory row is pinned by objectql's recording driver: the refusal answers before any driver is asked, and neither `objectql` nor `rest` depends on `driver-memory`. The InMemoryDriver readings in the table come from the scratch probe through `engine.find`. - **PostgreSQL / MySQL** were not run (no live servers here). The refusal precedes the driver. - **Every resolver caller gets the same coded error.** `service-analytics` (`analytics-service.ts`, `dataset-executor.ts`, `read-scope-sql.ts`) receives `INVALID_FILTER` / 400 where it used to receive the `Invalid Date` text or a `RangeError`. How each analytics door wraps a thrown coded error is unchanged and was not measured end to end here. - **Observed, not filed (no named producer): authoring does not refuse it.** `os validate` and the lint `validate-filter-tokens` accept any `{N_unit_ago}`, because the grammar admits every N. A stored filter with such an offset passes authoring and is now refused at run time with this message. The same holds for this family's year range (`{8000_years_from_now}`). No real producer writes such offsets, so this stays a note. - The doc comments that list the resolver's refusals (`engine.ts` `resolveWhereFilterTokens`, spec `IObjectQLEngine.judgeFilter`) name `FILTER_TOKEN_UNKNOWN` / `FILTER_TOKEN_UNRESOLVED` and do not mention this third refusal. They remain true but are incomplete. Left alone: they are outside this card's file surface. - `content/docs/releases/` and every `CHANGELOG.md` are untouched. The changeset is `.changeset/21068-macro-past-date-range.md`: `@objectstack/core` minor, BREAKING, one ADR-0087 marker `not-required (no-migration-prescription)`. That is the precedent of `05a7547c9` and PR objectstack-ai#21065: nothing an author can write is removed or renamed, and no registered id covers a value range. --- _Generated by [Claude Code](https://claude.ai/code/session_01Ujdtvqs7ree7WyQmEDwEnG)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
Fixes #20844
Clause-②: no (narrowing)
A relative-date placeholder is now judged by the year of the value it resolved to. If that value falls outside its column's years (a
date0001..9999, adatetime1000..9999, core'sisOutsideTemporalYearRange), the engine refuses it withINVALID_FILTER/ 400. The refusal names the placeholder as written and the year it resolved to, in the temporal-comparand door's words. It runs onwhere(find,findOne,count,aggregate, multi-rowupdateanddelete), a per-aggregationfilter,having, andjudgeFilter, before any driver read.Dispatched by the PM loop (round 1, seat
domain:engine#2), claim comment 5923455068, sessionsession_01Ujdtvqs7ree7WyQmEDwEnG.What changes
packages/objectql/src/engine.ts, the resolution stage.resolveWhereFilterTokenstakes a required judge and hands it the condition before and after resolution whenever something resolved. Execution and the judge call the same stage function, so they cannot drift:resolveRelateThenLowerWhereserveswhereon every verb,resolveThenLowerWhereserveshavingand the per-aggregationfilter, andjudgeWhereAdmissionservesjudgeFilter. The aggregate region changes in two places: the per-aggregation resolution call passes its judge, rooted ataggregations[i].filter, and one comment that this change made false is corrected. Neither is the aggregate door.packages/objectql/src/temporal-comparand-door.ts, beside the door. The door's one walk now carries a twin tree step for step, and the door itself passes the tree as its own twin, so its verdicts are unchanged (the door suites are green). The newassertResolvedTemporalTokensInRange/assertHavingResolvedTemporalTokensInRangewalk the caller's condition beside its resolution. They judge only a comparand written as a date macro (classifyFilterTokenkinddate-macro), by the year class the door asks of a literal of the same value:dateor adatetimeasks core'sisOutsideTemporalYearRange;timecolumn asks the door's[#20480]class, an instant whose UTC year has no four-digit spelling. Year 0 (0000-…) still reads, as it does for a literal.This is ⛔ not a second pass of the door: every other comparand was judged before resolution. There is ⛔ no second copy of the range, and ⛔ no new error code.
packages/core/src/utils/filter-tokens.ts, the producer. A date macro that lands on a day outside 0001..9999 now resolves to the expanded-year form of ECMAScript's date time string format:+010026-10-01,-000001-10-01, or0000-10-01for year 0. It used to take the storage rule's unpadded spelling (10026-10-01,-1-10-01,0-10-01).Date.parsereads that spelling through the host's legacy parser, in the host's zone, so-1-10-01read as 2001-01-10. Every reader read it that way,isOutsideTemporalYearRangeincluded, for both kinds (measured below). The resolver stays field-agnostic; the range is asked of the day itself. A day inside 0001..9999 and a sub-day placeholder's instant are spelled as before.Measured, before and after (scratch probes, not committed)
The card's object has two rows:
opened_at(datetime) at 2026-03-01T10:00Z and 1500-03-01T10:00Z,placed_on(date) on the same days, andopens_at(time) at 09:00 and 12:00. The engine column isengine.findon InMemoryDriver. The REST column isPOST /api/v1/data/:object/queryon SqlDriver over SQLite (better-sqlite3).whereopened_at $gt {8000_years_from_now}10026-10-01INVALID_FILTER, year 10026opened_at $lt {2027_years_ago}-1-10-01(read as 2001-01-10)opened_at $lt {1977_years_ago}0049-10-01datetimefloor)placed_on $gt {8000_years_from_now}10026-10-01placed_on $lt {1977_years_ago}0049-10-01datekeeps 0001..0999)opens_at $gt {8000_years_from_now}(see note)+010026-10-01havingmax(opened_at) $gt {8000_years_from_now}judgeFilterof the first two rows{ ok: true }{ ok: false, code: INVALID_FILTER, status: 400 }, execution's messageopened_at $lt {100_years_ago}/$gt1926-10-01{2027_years_ago}answered one row on the base, not two as the card records: the base spelling is read by the legacy parser as a day in 2001.Note on the
timerow: it was measured on this branch before thetimecommit (d5a8e100b), with core's new spelling already in, throughPOST /api/v1/data/:object/queryon InMemoryDriver and on SQLite. On the base the value is10026-10-01, which also compares as text above everyHH:MM:SS, so that row is read from the code, not measured on2f2fa11d7.The PM's hypotheses (zone 2)
2f2fa11d7(table above). One cell has moved since the card was measured:$lt {2027_years_ago}answers the 1500 row, where the card records both rows.isOutsideTemporalYearRangecould not see years at or below 0. Core spelled them-1-10-01/0-10-01, and the range read those as 2001 / 2000 for both kinds. Measured in UTC, America/New_York and Asia/Shanghai,isOutsideTemporalYearRangeansweredfalse. The defect is upstream, so it is fixed at the producer: one field-agnostic spelling change infilter-tokens.ts, which the claim lists as staying field-agnostic, not as read-only. A consumer-side parse of the unpadded spelling would have been the lenient fallback AGENTS.md rules out. Once the spelling is readable, triage's two halves hold together: resolution refuses per kind, and thedatetimefloor of 1000 applies to a resolved placeholder as it does to a literal.judgeWhereAdmissioncalls the same stage function with the same judge. The pin asserts thatjudgeFilterreturns execution's exactcode,statusandmessage.havingis in reach. On the base,max(opened_at) $gt {8000_years_from_now}kept both groups. It is now refused by the aggregated column's class (aggregatedRowColumnClasses:min/maxkeep the field's kind, adaybucket is adate).count/sum/avgcolumns are not temporal and are not judged. Text operators are stepped over onhaving, as thehavingdoor does.{N_years_*}and every other day-or-coarser macro resolve to a calendar day:YYYY-MM-DD, or now±YYYYYY-MM-DDoutside 0001..9999.{now}/{N_hours_*}/{N_minutes_*}resolve to an instant (toISOString). The message's year is pinned for both forms: days+010026-09-30(10026),-000001-09-30(-1) and0049-09-30(49), and the instant{80000000_hours_from_now}(11153).Faces that resolve
{tokens}outside the enginepackages/services/service-analytics/src/analytics-service.tsanddataset-executor.tsare already refused for a time-dimension member (code evidence plus a predicate measurement, not measured end to end); out of scope otherwise (domain:services). Both resolve the query's positions first and then pick a strategy. The ObjectQL strategy hands the resolved literal toengine.aggregate, where the temporal-comparand door refuses it as a literal. The native-SQL strategy declines a time-dimension member whose comparand core'sisUninterpretableTemporalComparandrefuses (comparand-shape.tsfindUninterpretableTemporalMember). That predicate answerstruefor both the base spelling and the new one (measured:-1-10-01,10026-10-01,-000001-10-01,+010026-10-01, both kinds), so the declined query reaches the engine door. The residual is the hole that package already records for a temporal column not declared as a time dimension.packages/services/service-analytics/src/read-scope-sql.tsis changed on the ObjectQL face, out of scope on the native-SQL face. On the ObjectQL face the engine resolves the original scope insidewhere, so this PR's judge applies. On the native-SQL face (compileScopedFilterToSql) the resolved tree is lowered straight to SQL. A read scope is platform-authored (CEL / stored policy), not caller input, and this isdomain:services. Not measured.packages/services/service-analytics/src/strategies/filter-normalizer.tsis already compliant: it has no resolver call, only a doc comment namingresolveFilterTokens'FILTER_TOKEN_UNRESOLVED.packages/plugins/plugin-security/src/position-catalog-refusal.tsis already compliant: it never resolves. A placeholder-shaped position name is compared in JavaScript with===against catalog names (catalogCarries), so no resolved instant reaches a comparison.packages/services/service-automation/src/builtin/template.tsis changed, through the engine.interpolateFilterpasses a recognised filter placeholder through verbatim to the engine, whose resolution stage now judges it.packages/core/src/utils/analytics-date-range.tsis unaffected (a resolver caller the dispatch list did not name). It asks only fixed tokens (today,week_start,7_days_ago, …), which never land outside 0001..9999.Pins
packages/objectql/src/engine-resolved-token-year-range.test.ts(recording driver,Datepinned to 2026-09-30T12:00Z). It pins every position for both kinds;code+status+ the placeholder +resolved to "…" (the year N)+ the kind's years in the message; and the door's words per position (at aggregations[1].filter…, thehavingcolumn phrase). It also covers a list member, a$betweenbound, implicit equality, a$notbranch and the array sugar. Further cases: thedatetimefloor ({1977_years_ago},{1027_years_ago}) with thedatecontrol reaching the driver as its day, and the in-range control on every position, reaching the driver as the day it names. The rest arejudgeFilterequal to execution, thetimeclass (year 0 still read), and a text column / context placeholder not judged.packages/rest/src/data-resolved-token-year-range.test.ts(SqlDriver on SQLite, the card's two rows plus atimecolumn) checks 400INVALID_FILTERnaming the placeholder and the year atwhere, the per-aggregationfilterandhaving, with no read of the object. The controls answer the right rows, filter counts andhavinggroups.packages/core/src/utils/filter-tokens-year-outside-range.test.tschecks the expanded-year spelling on both sides of 0001..9999 and year 0. It runs in UTC and Asia/Shanghai, with the range reading the year it names, and covers the edges inside, thedatetimefloor's edge and the sub-day instant.Verification
Tests ran on
fc3fc97f3. The final head4c5f259e5adds only a merge oforigin/mainthat touches no package source (pr-automation.yml,packages/spec/CHANGELOG.md,scripts/check-empty-changeset.mjs).@objectstack/core:vitest run --project local: 62 files / 1822 tests passed;typecheckpassed.@objectstack/objectql(dist rebuilt):vitest run --project local: 350 files / 6860 tests passed;--project repo1 / 5 passed;typecheck(incl.check:test-typecheck) passed.@objectstack/rest:typecheckpassed. The new pin plusaggregation-filter-array-membership,data-temporal-year-rangeandrest-aggregate-positionspassed (21 passed, 26 named skips for unprovisioned live PG / MySQL cells). Earlier, at574bcbb51, the 27 rest suites that mention tokens, temporal values or years: 755 passed, 65 skipped. The full rest suite is declared to CI.node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackat4c5f259e5(67 commands, all run at that head, exits recorded). 65 exited 0 and--ranreconciles 67 / 67. NOT MEASURED:pnpm check:dual-build-cjs-loadsandpnpm check:type-check-debt, reason: exit 3 PREREQUISITE NOT MET. Both read every workspace package's builtdist/, and only the core / objectql / rest closure was built here. This diff changes no build config, noexportsand no published type. Declared to CI.node scripts/check-driver-conformance.mjs: before "50 covered cell(s), 0 in the DEBT ledger, 0 exempt"; after the same.Reverse verification (ablations, one-off, not committed)
Each leg went through
scripts/ablation-replace.mjsin wrap mode, with a trap restore. A suite resolving throughdist/got a rebuild andscripts/ablation-dist-preflight.mjson both legs. Each restore was proven by blob equality and an emptygit diff HEAD, and the tree was clean after. All three went red, the usual direction.engine.ts, atd5a8e100b). The objectql pin went 7 / 9 red and the REST pin 1 / 2 red, with the marker in objectql'sdist/(4 files). Restored: 9 / 9 and 2 / 2 green, marker absent.filter-tokens.ts, at4dcb910f1). The core pin went 12 / 24 red (every outside-range row, both hosts). The objectql pin went 6 / 8 red through core's rebuiltdist/, including$inwith{2027_years_ago}answered rather than refused. Restored: 24 / 24 and 8 / 8 green, with the original marker back indist/(2 files).timebranch answers "inside" (temporal-comparand-door.ts, atd5a8e100b). The first attempt was a no-op thatablation-replacerefused: its replacement contained the anchor, so the anchor count did not drop and no mutation ran. Redone with a disjoint replacement, the objectql pin went 1 / 9 red (exactly thetimetest). Restored: 9 / 9.Acceptance notes
data-no-operator-object-door.test.ts's header: neitherobjectqlnorrestdepends ondriver-memory. The memory measurements in the table are from the scratch probe throughengine.findon InMemoryDriver.{ owner: { opened_at: … } }) is resolved by the parent's stage before the related read. The related object's own door then refuses the resolved literal, naming the value rather than the placeholder. This PR does not change that.timeclass joins on the bounded in-place rule: the same defect class (a resolved placeholder bypasses the door's year class), the door's own pinned words, the same file and the same gates.Dateholds. A day macro resolves to the textInvalid Date, soopened_at $lt {300000_years_ago}answers 200 with both rows on memory and SQLite throughPOST /api/v1/data/:object/query. A sub-day macro throws an uncodedRangeErrorinside the resolver, and the same door answers 500INTERNAL_ERRORfor{99999999999999999999_minutes_ago}. That mechanism is not the card's extended-year text, and no shape for its refusal is pinned, so this PR leaves it alone.content/docs/releases/and everyCHANGELOG.mdare untouched. The changeset is.changeset/20844-resolved-token-year-range.md(@objectstack/coreminor,@objectstack/objectqlminor, BREAKING).check:adr-0087-registrationreads its disposition asnot-required (no-migration-prescription). The05a7547c9precedent registered no ADR-0087 id, soalready-registeredhas nothing to name.Generated by Claude Code