Skip to content

Commit f284ab2

Browse files
docs(adr): amend ADR-0053 D-D1 in place — the bare-day upper bound moves to one shared lowering at the seams (#20754)
Part of #5930 Clause-②: no **Step 1 of the maintainer's ruling on #5930** (comment 5902355785, 2026-09-30, 「5930 v18 启动」): the ADR-0053 D-D1 amendment, drafted by the `domain:engine` seat for the maintainer's hand. **Tier H — this PR stays a draft.** It changes no behaviour. The code slice (step 2) waits on it (Prime Directive #13) and cites it. #5930 stays open for steps 2 to 4. ## What changes One file, `docs/adr/0053-date-and-datetime-semantics.md` (+122 / -11): - **D-D1 is amended in place** (the ruling's D5). What a bare-day upper bound *means* is unchanged. *Where* it is applied moves from each comparison emitter to one shared `FilterCondition → FilterCondition` lowering. The original text stays. It is struck only where the amendment makes it false: the heading's placement, the lead-in sentence, `ObjectQLStrategy`'s "single execution-path authority", the primitive's home, and "emitter-side". An **Amended (2026-09-30)** block at the end of D-D1 states the placement step 2 will follow, in ten numbered items: 1. one lowering, applied once: a bare-day `$lte` becomes `$lt` the next day; `$between` becomes two conjuncts; the last supported day drops the bound; 2. where it runs: the seven seam positions of the design's §3.6, named by symbol; 3. at a seam, **after the doors and after filter-token resolution** (Acceptance note 1); 4. where it lives: `@objectstack/spec/data`, never the package root entry (D3); 5. drivers receive the lowered filter (D4 (b)); 6. D-A1 and D-E3 are kept, and D-E3 becomes structural; 7. the column-type scope is unchanged; 8. `dateRange` windows (Acceptance note 2); 9. the interim state: idempotent copies until each face's deletion card; 10. the open confidence gap the ruling recorded. - **The status line** gains an `Amended (2026-09-30, D-D1, #5930)` entry in the file's existing style, with the ruling id and date. ## Dispatch hypotheses, measured at `97005aed04` - **H1 held.** D-D1 is the section "Operator-sensitive translation lives at the comparison EMITTERS". Its line "One primitive backs all of them: `nextUtcCalendarDay` (`@objectstack/core`)" was already out of date. The later D-D2 ("The primitive lives in `spec`") moved the primitive to `@objectstack/spec/data`, and `packages/core/src/utils/datetime.ts` re-exports it. It is the same decision (where the D-D rule's code lives), so the amended section reconciles it with a strike and a pointer. - **H2 held, and nothing outside D-D1 needed an edit.** Each section that restates the placement was checked: - the first D-D2 (the conformance axis) stays true: the suites it names exist and still pin row results; - D-E3 is kept as ruled and stays true; it is the property the amendment makes structural; - D-E4's struck paragraph is past history and stays true; - the second D-D2's decision (the primitive lives in spec) is what the ruling's D3 extends; its "six backends now share one definition" is a dated record of the emitter era, and amendment item 9 covers it; - D-A3.1 item 1 is a first-run record that names today's strategy arms; it stays true through the interim state; - D-A3.2's "the whole-day rule must NOT reach" a time column stays true, because the lowering uses the same primitive; - D-A1 is untouched. - **H3 held: no other ADR cites D-D1's placement.** - `git grep -n -E "D-D[0-9]*" -- docs/adr` outside ADR-0053: 0 hits (exit 1). Control leg `D-E3|D-F1` outside ADR-0053: 1 hit (exit 0), ADR-0074's status line citing D-F1. - No other ADR mentions `nextUtcCalendarDay` or a whole-day or bare-day bound (0 hits, exit 1). - The ADRs that cite ADR-0053 (0054, 0074, 0104) cite its storage and read semantics, not this placement. - No ADR index lists ADR-0053's status. The one `scripts/adr-anchors/` entry that names ADR-0053 (on `sql-driver.ts`) pins D-F1 to D-F3, not D-D1. - So no second file had to change. - **H4 delivered**, with two precision points that the design's §3.6 table does not carry (Acceptance notes 1 and 2). ## Gates at `f04f3711b7` `node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack` (no paths) derived 19 commands. All 19 were run and all exited 0. `--ran` reconciliation: 19 derived, 19 run, 0 NOT-MEASURED, 0 UNRUN. - `node scripts/check-adr-links.mjs` (+ `--self-test`): 691 relative link destinations under `docs/adr/` resolve - `node scripts/check-adr-symbol-anchors.mjs` (+ `--self-test`): 2138 anchors across 140 records resolve - `pnpm check:adr-anchors`: OK, 39920 citations across 4917 files resolve - `pnpm check:doc-authoring`, `pnpm check:nul-bytes` (9378 text files, no control bytes), `pnpm check:pm-governed-merges`, `pnpm check:pm-prior-rulings` - `node scripts/check-ci-filter-parity.mjs`, `node scripts/check-closing-keyword-parity.mjs` (+ `--self-test`), `node scripts/check-comment-mask-corpus.mjs` - `pnpm --filter @objectstack/lint run check:doc-formula-expressions` (after building `@objectstack/lint` and its closure with `OS_SKIP_DTS=1` under the verify lock; the tree stayed clean) - `pnpm check:cross-package-test-inputs`, `pnpm check:driver-memory-census`, `pnpm check:gitlink-declared`, `pnpm check:refd-timer-probe`, `pnpm check:watch-hint-literal` Changeset: none. `docs/adr/**` ships in no package, so the PR carries `skip-changeset`. No package is touched, so no package build or test is owed. ## Acceptance notes (not filed) 1. **The design's §3.6 engine positions sit before token resolution.** On the engine, the comparand doors run in the first stage of `where` admission (`lowerWhereFilterArray`), and filter tokens resolve in the second (`resolveWhereTokens` on the read verbs, `withResolvedWhere` on `update` / `delete`). `aggregations[i].filter` and `having` follow the same order. Read at `97005aed04`: `engine.ts` lowers at 11245 and resolves at 11323 (`find`), and lowers at 13004 and resolves at 13017 (`update`). A lowering placed "after `normalizeFilterComparandTypes`", as the design's table reads, would meet `{today}` unresolved and leave it bare. Once the faces' copies are deleted, the #3777 loss would return on every token bound. Amendment item 3 therefore states "after the doors and after filter-token resolution". The analytics seams already resolve tokens first (`analytics-service.ts` `resolveQueryTokens`), and the read scope resolves at its own entry. Carrier: the step-2 card, which cites item 3. 2. **`dateRange` windows are not `FilterCondition`s.** The design's §3.2 names only `$lte` and `$between`. But an explicit `dateRange` window end takes the whole-day rule too, in `NativeSQLStrategy`, the preview evaluator and the memory cube face. Item 8 records the consequence: a window reaches the lowering only once its face expresses it as the `{ $gte, $lte }` pair that the ObjectQL strategy already hands the engine. Until then its window arm is an interim copy. Carrier: the step-3 card. 3. **The RLS compile seam's column-type reading is left to measurement.** `judgeCompiledComparands` reads nothing but the filter. Its `using` output reaches typed drivers, which rewrite `datetime` only, and its `check` output reaches `matchesFilterCondition`, which rewrites type-blind. Item 7 keeps the scope unchanged and leaves the choice to the card that wires that seam. Carrier: the step-2 card. 4. **ADR-0053 has two sections headed `D-D2`** ("Consequences for D-A3" and "The primitive lives in `spec`"). They are left as they are. The ruling and `calendar-day.ts` cite "D-D2" for the primitive, so renaming either would move a citation. Carrier: none; noted only. ## 维护者速读(草稿) **改了什么**:只改一份架构决策记录(ADR-0053)里的一节(D-D1),不改代码,也不改任何产品行为。这一节规定"筛选到某天为止(含当天全天)"这条规则在哪里执行。今天它在 10 个文件里各写一份;修订后,改为在各入口处由一个共享模块统一换算一次,各数据源只接收换算好的条件。 **为什么改**:同一条"含当天"规则现在手抄了 10 个文件、41 处。调查实测到两个问题:报表的读取权限范围里完全没有这条规则(最后一天的数据会被漏掉,#20733);另一个面曾先转存储格式再换算,顺序写反(#20661)。您 9 月 30 日裁定「5930 v18 启动」,第一步就是先改这份决策记录。后续代码卡必须以它为施工依据;若不先改,代码就等于悄悄推翻一条已接受的决定。 **风险与代价(含回滚)**:本 PR 只是文档,不影响任何客户数据或查询结果;回滚就是还原这一个提交。真正的风险在后续代码卡,修订里已写明两条:(1) 换算必须放在解析 {today} 这类日期占位符之后,否则带 {today} 的筛选会重新漏掉当天(第 3 条);(2) 直接调用数据驱动、绕过入口的调用方,在各处副本删除后不再自动"含当天"。这是裁定 D4(b) 接受的取舍,影响范围尚未实测,已列为"待测"(第 10 条)。 **席位意见**: **你要做的(一个动作)**:读 ADR-0053 D-D1 末尾的修订块(10 条)。同意就在本 PR 上批准(Approve);不同意就在 PR 上留言,指出要改的条目。 --- _Generated by [Claude Code](https://claude.ai/code/session_01DEvba2nBuD4tWzfq8r8NFY)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 7053333 commit f284ab2

1 file changed

Lines changed: 122 additions & 11 deletions

File tree

‎docs/adr/0053-date-and-datetime-semantics.md‎

Lines changed: 122 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# ADR-0053: `date` is a timezone-naive calendar day; `datetime` is an instant rendered in a reference timezone
22

3-
**Status**: Accepted (2026-06-16) — Phase 1 + addendum D-A1 implemented (`sql-driver.ts` `toDateOnly` write/read/filter normalization; analytics `coerceTemporalFilterValue`), Phase 2 landing incrementally; D-A2 resolved 2026-07-30: `temporalFilterValue` + `temporalFilterColumnSql` are optional `IDataDriver` contract members with identity semantics, and analytics types its driver seam from the contract. **Partly superseded (2026-07-29, addendum D-B1..D-B4):** Phase 1's "`Field.datetime` stays stored as UTC epoch ms" is replaced by one canonical UTC instant per dialect — `YYYY-MM-DDTHH:MM:SS.sssZ` text on SQLite, `timestamptz` on Postgres, `DATETIME(3)` on MySQL — applied on write and to filter comparands alike (#3912, #3942). **Extended (2026-07-30, addendum D-C1..D-C3):** `Field.time` takes the same construction — canonical `HH:MM:SS[.fff]` wall-clock text, one function on write/filter/read, `TIME(3)` on MySQL, UTC `NOW()` defaults on every dialect (#3994). **Extended (2026-09-07, addendum D-F1..D-F3):** the READ side takes the same canon — every `@objectstack/driver-sql` record read door ~~but `findWithWindowFunctions` (#16609)~~ presents `Field.datetime` values and the builtin `created_at` / `updated_at` audit stamps as the canonical `YYYY-MM-DDTHH:MM:SS.sssZ` text on every dialect, folded at the driver's read boundary with the client parsers untouched; those doors never hand out a JS `Date` for those columns, save an Invalid `Date`, which has no canonical text and passes through unchanged (#13973, maintainer ruling B1 narrow, 2026-09-02). **Corrected 2026-09-08 (#16609 / PR #16716):** that exception is gone — `findWithWindowFunctions` now routes each row through the same `formatOutput` pass, so those two column classes present as the same canonical text there, with the window ALIAS columns carved out (a computed alias wins the key and its value stays raw). D-F1 rules those two classes and no more: the other presentations that pass applies at that door are #16609's contract, not this ADR's.
3+
**Status**: Accepted (2026-06-16) — Phase 1 + addendum D-A1 implemented (`sql-driver.ts` `toDateOnly` write/read/filter normalization; analytics `coerceTemporalFilterValue`), Phase 2 landing incrementally; D-A2 resolved 2026-07-30: `temporalFilterValue` + `temporalFilterColumnSql` are optional `IDataDriver` contract members with identity semantics, and analytics types its driver seam from the contract. **Partly superseded (2026-07-29, addendum D-B1..D-B4):** Phase 1's "`Field.datetime` stays stored as UTC epoch ms" is replaced by one canonical UTC instant per dialect — `YYYY-MM-DDTHH:MM:SS.sssZ` text on SQLite, `timestamptz` on Postgres, `DATETIME(3)` on MySQL — applied on write and to filter comparands alike (#3912, #3942). **Extended (2026-07-30, addendum D-C1..D-C3):** `Field.time` takes the same construction — canonical `HH:MM:SS[.fff]` wall-clock text, one function on write/filter/read, `TIME(3)` on MySQL, UTC `NOW()` defaults on every dialect (#3994). **Extended (2026-09-07, addendum D-F1..D-F3):** the READ side takes the same canon — every `@objectstack/driver-sql` record read door ~~but `findWithWindowFunctions` (#16609)~~ presents `Field.datetime` values and the builtin `created_at` / `updated_at` audit stamps as the canonical `YYYY-MM-DDTHH:MM:SS.sssZ` text on every dialect, folded at the driver's read boundary with the client parsers untouched; those doors never hand out a JS `Date` for those columns, save an Invalid `Date`, which has no canonical text and passes through unchanged (#13973, maintainer ruling B1 narrow, 2026-09-02). **Corrected 2026-09-08 (#16609 / PR #16716):** that exception is gone — `findWithWindowFunctions` now routes each row through the same `formatOutput` pass, so those two column classes present as the same canonical text there, with the window ALIAS columns carved out (a computed alias wins the key and its value stays raw). D-F1 rules those two classes and no more: the other presentations that pass applies at that door are #16609's contract, not this ADR's. **Amended (2026-09-30, D-D1, #5930):** the bare-day upper bound is applied once, by one shared `FilterCondition → FilterCondition` lowering in `@objectstack/spec/data` (never the package root entry), at the seams that already run the shared comparand doors — after the doors and after filter-token resolution — instead of at each comparison emitter; drivers receive the lowered filter, and each face keeps its own copy only until its deletion card lands. D-A1 and D-E3 are kept, D-E3 now structural (maintainer ruling 「5930 v18 启动」, 2026-09-30, recorded on #5930 as comment 5902355785).
44
**Deciders**: ObjectStack Protocol Architects
55
**Builds on**: [ADR-0032](./0032-unified-expression-layer.md) (unified expression layer — CEL dialect, `today()`/`daysFromNow()`), [ADR-0014](./0014-record-form-field-type.md) (field types)
66
**Consumers**: `@objectstack/spec` (`Field.date`/`Field.datetime`), `@objectstack/driver-sql` (`coerceFilterValue`, `formatInput`/`formatOutput`, `dateFields`/`datetimeFields`), `@objectstack/formula` (`stdlib` time functions, `cel-engine` hydration), `@object-ui/core` (`FormulaFunctions`, the client-evaluated `TODAY()`; addendum D-G1), `@objectstack/objectql` (`applyFormulaPlan`), schedule/cron executors, report/analytics date bucketing, `sys-user-preference.timezone`.
@@ -702,14 +702,18 @@ temporal-conformance job's non-UTC matrix).
702702
> "today", so `{ $lte: <today> }` anchored to midnight silently dropped every
703703
> row created after 00:00 of the final day.
704704
705-
### D-D1 — Operator-sensitive translation lives at the comparison EMITTERS
705+
### D-D1 — Operator-sensitive translation lives ~~at the comparison EMITTERS~~ in one shared lowering, run at the seams (amended 2026-09-30)
706706

707707
`YYYY-MM-DD` anchors to midnight UTC (D-B1). That instant is the correct
708708
comparand for `$gte`/`$gt`/`$lt` — and the wrong one for `$lte`, whose author
709709
means "through the whole of that day". The translation is therefore a property
710710
of the *comparison*, not of the value: `temporalFilterValue` stays
711-
operator-blind (form only), and each emitter that owns an operator compiles a
712-
bare-day upper bound half-open:
711+
operator-blind (form only), and ~~each emitter that owns an operator compiles a
712+
bare-day upper bound half-open:~~ — **amended 2026-09-30** — one shared lowering
713+
rewrites a bare-day upper bound half-open, at the seams, before any emitter sees
714+
the filter (*Amended*, at the end of this section). The emitters below compiled
715+
it themselves when this addendum landed; each keeps its copy only until its
716+
deletion card lands:
713717

714718
- **`SqlDriver` filter compiler** (`calendarDayUpperBoundRewrite` /
715719
`calendarDayBetweenRewrite`): `$lte`/`<=` → `< next-day-midnight` in storage
@@ -719,13 +723,15 @@ bare-day upper bound half-open:
719723
`where` spellings. `driver-sqlite-wasm` inherits.
720724
- **`NativeSQLStrategy`** windows and `lte` filters bind `< next-day` instead
721725
of `BETWEEN`/`<=` when the bound is a bare day.
722-
- **`ObjectQLStrategy`** leaves its lowered `{$gte, $lte}` bounds bare — the
723-
driver rewrite is the single execution-path authority — and renders
724-
`/analytics/sql` half-open so the echoed SQL reproduces execution.
726+
- **`ObjectQLStrategy`** leaves its lowered `{$gte, $lte}` bounds bare — ~~the
727+
driver rewrite is the single execution-path authority~~ the shared lowering
728+
is, since 2026-09-30 — and renders `/analytics/sql` half-open so the echoed
729+
SQL reproduces execution.
725730
- **The dataset preview evaluator** applies the same rule in memory, replacing
726731
its `'~'`-suffix string hack, so draft numbers match published numbers.
727732

728-
One primitive backs all of them: `nextUtcCalendarDay` (`@objectstack/core`),
733+
One primitive backs all of them: `nextUtcCalendarDay` (~~`@objectstack/core`~~
734+
`@objectstack/spec/data` since D-D2 below; `@objectstack/core` re-exports it),
729735
which rejects instants, `Date`s and impossible days (`2026-02-30`) rather than
730736
inventing a bound. Half-open — never an inclusive `23:59:59.999`, which
731737
re-opens the gap at whatever precision the dialect stores beyond milliseconds
@@ -737,9 +743,114 @@ scopes the rewrite to `datetime` so `date`/`time` columns compile byte-identical
737743
to before.
738744

739745
The filter-token resolver (`filter-tokens.ts`) keeps its documented refusal to
740-
widen: a resolver-side fix would change what a token *is*; the emitter-side fix
741-
changes what a comparison *does* with it, per column type — which is the layer
742-
that owns that knowledge.
746+
widen: a resolver-side fix would change what a token *is*; the ~~emitter-side~~
747+
comparison-side fix (since 2026-09-30, the shared lowering, which runs after the
748+
token has resolved) changes what a comparison *does* with it, per column type —
749+
which is the layer that owns that knowledge.
750+
751+
> **Amended (2026-09-30) — the rule stands; where it is applied moves to one
752+
> shared lowering, run at the seams.** Provenance: maintainer ruling on #5930 —
753+
> 「5930 v18 启动」, 2026-09-30, given to the director seat on batch #248, which
754+
> carried the `domain:engine` seat's decision request (comment 5900897393) on
755+
> the convergence investigation
756+
> ([`docs/design/predicate-compilation-convergence.md`](../design/predicate-compilation-convergence.md));
757+
> recorded on the card as comment 5902355785: the investigation's shape A as the
758+
> end state, built in the order of its first slice (B), starting with this
759+
> amendment. The ruling's D5 amends this section in place and keeps D-A1 and
760+
> D-E3 as they are. Why: the investigation found this rule applied by hand in
761+
> ten face files (41 call sites), missing on one face that binds raw SQL
762+
> (#20733) and applied after the storage-form conversion on another (#20661),
763+
> while a rule enforced once at a shared seam cost one dev round and no face
764+
> file (its §2).
765+
>
766+
> 1. **One lowering, applied once.** A `$lte` whose comparand is a bare
767+
> `YYYY-MM-DD` becomes `$lt` `nextUtcCalendarDay(day)`; a `$between` becomes
768+
> two `$and` conjuncts, `$gte` its minimum and `$lte` its maximum, which the
769+
> same rule then reads; on the last supported day (`UNBOUNDED_ABOVE`) the
770+
> upper bound is dropped, so a lone `$lte` keeps only `{ $null: false }`. One
771+
> shared `FilterCondition → FilterCondition` lowering does this (the ruling's
772+
> D2, T1), in the calendar-string domain. What the bound *means* is
773+
> unchanged: half-open, never `23:59:59.999`; `$gte`/`$gt`/`$lt` keep their
774+
> midnight anchor; an instant or a `Date` comparand is never widened. The
775+
> same lowering carries the NULL-polarity guards, which are rulings recorded
776+
> in code, not in this ADR.
777+
> 2. **Where it runs: the seams that already run the shared comparand doors**
778+
> (`assertListComparandShapes`, `normalizeFilterComparandTypes`), the seven
779+
> positions of the investigation's §3.6 — the engine's `where` admission, its
780+
> `aggregations[i].filter` and its `having` (`@objectstack/objectql`
781+
> `engine.ts`); the RLS compile seam, `judgeCompiledComparands`
782+
> (`plugin-security` `rls-compiler.ts`), which serves `using` and `check`
783+
> alike; the analytics `where` and draft-preview door,
784+
> `normalizeWhereComparands` (`service-analytics` `filter-normalizer.ts`); the
785+
> analytics read scope, at the entry of `compileScopedFilterToSql`
786+
> (`read-scope-sql.ts`, which applies no whole-day bound today, #20733); and
787+
> `driver-memory`'s cube face, `normalizeFilters` (`memory-analytics.ts`), the
788+
> one new door, which also installs the two comparand doors that face runs
789+
> without. A seam added later runs the lowering too, in the order item 3
790+
> states.
791+
> 3. **At a seam, after the doors and after filter-token resolution.** The
792+
> lowering reads the comparand the comparison will run with: a date macro
793+
> (`{today}`, `{current_month_end}`) resolves to the bare day this rule
794+
> widens. On the engine, the doors run in the first stage of `where`
795+
> admission (`lowerWhereFilterArray`) and tokens resolve in the second
796+
> (`resolveWhereTokens` on the read verbs, `withResolvedWhere` on `update` /
797+
> `delete`; the same order on `aggregations[i].filter` and `having`), so the
798+
> lowering runs after the second stage, not beside the doors. Run beside
799+
> them, it would meet `{today}` unresolved and leave it bare — the #3777 loss,
800+
> back on every token bound once the faces' copies are deleted.
801+
> 4. **It lives in `@objectstack/spec/data`**, beside `calendar-day.ts` and the
802+
> shared comparand doors — **never the package root entry**
803+
> (`@objectstack/spec`). The ruling's reason: D-D2 below already put
804+
> `nextUtcCalendarDay` there, because what a day denotes as a bound is
805+
> protocol, and `@objectstack/formula` depends on `spec` alone. Every seam's
806+
> package already depends on `spec`, so no dependency edge is added. The
807+
> tension with Prime Directive #2 is the one the investigation named (§3.6):
808+
> the lowering is a pure function of the filter, holding no I/O, no state and
809+
> no dialect.
810+
> 5. **Drivers receive the lowered filter (the ruling's D4 (b)).** A driver is
811+
> not a seam: it inherits the rule by receiving already-lowered input, the
812+
> pattern the comparand-type door set (`filter-comparand-type.ts`, ruling
813+
> #7872). No face keeps a permanent copy for a caller that reaches it without
814+
> passing a seam — a direct driver call, or a filter composed onto the query
815+
> after its seam; once that face's copy is deleted, such a caller gets the
816+
> comparison it wrote.
817+
> 6. **D-A1 and D-E3 are kept; D-E3 becomes structural.** The lowering emits a
818+
> calendar string, never a storage form: `temporalFilterValue` stays
819+
> operator-blind, and each driver converts the lowered bound as it converts
820+
> any comparand. And because the lowering runs at the seam, before any face
821+
> converts a comparand to its storage form, D-E3's order — widen the bare day
822+
> first, convert the resulting bound second — holds by construction on every
823+
> seam-fed face; the #20661 class (a face that converted first) cannot recur
824+
> there.
825+
> 7. **The column-type scope is unchanged.** The rewrite is meant for a
826+
> `datetime` column. A seam that can read the declared field type rewrites
827+
> that column only, so a `date`, `time` or non-temporal column lowers
828+
> byte-identical — the scope `SqlDriver` holds today. A seam that cannot
829+
> applies the rewrite type-blind, as the type-blind emitters above do today
830+
> (sound on `Field.date` text, where `< next-day` orders exactly as
831+
> `<= day`). The RLS compile seam reads nothing but the filter today, and its
832+
> `using` output reaches typed drivers while its `check` output reaches
833+
> `matchesFilterCondition`; which reading it applies is measured on the card
834+
> that wires it.
835+
> 8. **Windows.** An explicit `dateRange` window end takes this rule too; a
836+
> resolved preset window states its own upper reading and never does
837+
> (#16322). A window is not a `FilterCondition`, so it reaches the lowering
838+
> only once its face expresses it as the `{ $gte, $lte }` pair the ObjectQL
839+
> strategy already hands the engine; until then that face's window arm is one
840+
> of the interim copies below.
841+
> 9. **Interim state.** Until a face's deletion card lands, the face keeps its
842+
> own copy of the rule: the emitters listed above and the others the
843+
> investigation counted, 41 call sites in 10 face files at `origin/main`
844+
> `97005aed04`. A copy is idempotent on lowered input — the lowering turns
845+
> each bare-day `$lte` it applies to into a `$lt`, leaves no `$between`, and
846+
> no copy rewrites a `$lt`, so a bound is widened at most once. Each copy is
847+
> removed by its
848+
> lane's deletion card, queued as the seam that feeds that face lands (the
849+
> ruling's order of work, step 4); none is left without one.
850+
> 10. **Open.** Unmeasured, and recorded by the ruling as a confidence gap:
851+
> whether an in-repo direct driver caller, or a filter composed after a
852+
> seam, carries a bare-day upper bound that a deleted copy would have
853+
> widened.
743854
744855
### D-D2 — Consequences for D-A3
745856

0 commit comments

Comments
 (0)