fix(service-analytics)!: the read scope, the where tree and the draft preview take the shared lowering's bound and NULL guards; their own copies are deleted - #21553
Conversation
…heir whole-day and NULL-polarity copies are deleted #5930 step 4 (domain:services), faces F9 and F10. The shared lowering (lowerFilterCondition) is now the one source of the whole-day bound, the $between split and the NULL-polarity guards on the analytics read scope and the where tree: - native-sql-strategy: buildFilterClause's bare-day lte arm is deleted; the dateRange window is the { $gte, $lte } pair, lowered by the same reader as the where (ADR-0053 D-D1 item 8). The reader reads a column the host cannot name type-blind (item 7). - objectql-strategy: the /analytics/sql echo renders the window through the same lowering; the reader leaves an undeclared column as written, for the engine seam to read. - filter-normalizer: the $not-operand rewrite and the #5298 leaf wrap, with their polarity tables, are deleted. - read-scope-sql: the $not-operand rewrite, its three tables and the IS NULL OR wrap are deleted. Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
…d lowering; the echo window renders the declared column's bound #5930 step 4. Every pin that recorded a face's own copy of the NULL guard stacked inside the shared lowering's (the step-3 rows marked "until the copy's deletion card") now reads the single guard. No row answer moved: every id-set assertion in these files is unchanged. The /analytics/sql window pins wire the declared type the plugin relays (sourceFieldMeta, close_date a datetime), and two controls pin the render on a declared date and where the host names no type (the bound execute() hands the engine, as written), and a preset that stops before its end. Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
…LL rule's source #5930 step 4: the $empty and non-text-column notes named the deleted $not rewrite and its operatorIsNullTotal table. Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
… F9 and F10, and the bare day on every face #5930 step 4. analytics-faces-one-lowering.test.ts holds: - the enumeration: no analytics source file holds a whole-day helper but the draft preview (F11, pending its reader), and none holds a NULL-polarity copy; a positive control proves the scan reads the faces; - one source: the native compiler and the echo emit the bound the lowering hands them (a function of the declared type alone), and every null predicate F9 and F10 emit is one the lowering wrote; - the typed drivers' answer on every face over a real engine (SQLite, and PostgreSQL where OS_TEST_POSTGRES_URL is set): datetime, date and text columns, $lte, $between, $not and dateRange windows, the carrier-note text cell included; - a host with no typed reader: the native face reads type-blind, and the ObjectQL face hands the engine the bound as written; - TEMPORAL_CASES on the native and ObjectQL faces of the plugin's composition and through the read scope. native-sql-temporal-conformance.test.ts runs its matrix with and without the declared-type hook. Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
…e native face's bare-day bound on a non-temporal column #5930 step 4. The measured answer move, the reader-less hosts, the /analytics/sql echo changes and the ADR-0087 disposition. Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
…alytics-faces-delete-copies
…in as a query The dateRange pair is a tuple in AnalyticsQuery; tsc refused the widened string[]. Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
…with no private comment stripper
check:comment-mask-adoption refused the pin's regex comment stripper.
A file holds a helper when it imports, declares or calls it; a prose
mention (a backticked name, a {@link}) is none of those, so no comment
stripping is needed.
Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ
Co-authored-by: Claude <noreply@anthropic.com>
…with a declared-type reader (F11)
The preview's whole-day copy (`lteBound` and the window's own bare-day,
last-day and full-timestamp readings) is deleted. `queryDataset`'s preview
branch hands `evaluateAnalyticsQueryOverRows` the drafted object's declared
types (`sourceFieldMeta`), which `declaredPreviewLowering` turns into the
lowering's reader: `datetime` rewritten, any other declared type compared as
written, undeclared read type-blind (ADR-0053 D-D1 item 7). A window is the
`{ $gte, $lte }` pair through the same lowering and `matchesWhere` (item 8).
Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ
Co-authored-by: Claude <noreply@anthropic.com>
…alytics-faces-delete-copies
…bare-day cells onto the engine, the window as the where pair Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
…ured Claude-Session: https://claude.ai/code/session_01DiCSbmJrkzNhuEAier4VoJ Co-authored-by: Claude <noreply@anthropic.com>
📓 Docs Drift CheckThis PR changes 1 package(s): ⛔ 3 release-owned page(s) name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 10 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 2a8f8d27e32fe966bdd07f7d96baa983a9848cd9 && git checkout 2a8f8d27e32fe966bdd07f7d96baa983a9848cd9
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin fd5a1cd5973983bf8b1ad69a10148a85c74c9137 e08db242409248faf1eb6a1a7c644c08b43424cc && git checkout -B drift-repro fd5a1cd5973983bf8b1ad69a10148a85c74c9137 && git merge --no-ff e08db242409248faf1eb6a1a7c644c08b43424cc
node scripts/docs-audit/affected-docs.mjs --json fd5a1cd5973983bf8b1ad69a10148a85c74c9137
|
Fixes #21417
Clause-②: no (narrowing)
#5930 step 4 (
domain:services) covers three faces:wheretree, its native compiler and its/analytics/sqlecho;On all three, the shared lowering (
lowerFilterCondition,@objectstack/spec/data) is now the one source of the whole-day bound, the$betweensplit and the NULL-polarity guards, and each face's own copy is deleted. With F11 in, nothing remains open on the card.Claim: the PM's
Claim:comment 5962872882 and its revision 1, 5963878819 (Q1 = A: one PR; Q2 = A: F11 in, with oneanalytics-service.tscall site). Branchclaude/issue-21417-analytics-faces-delete-copies. Base0b8239111, withorigin/mainmerged twice (no rebase).Each face and its typed column reader
buildFilterClause's bare-dayltearm; thedateRangewindow arm's own widening; the$not-operand rewrite and the #5298 leaf wrap with their polarity tables (filter-normalizer.ts)declaredDatetimeLowering(ctx, target, 'type-blind'), the context'sdeclaredFieldTypehook, which the plugin answers from the engine registry (sourceFieldMeta). A column the hook names no type for is read type-blind (ADR-0053 D-D1 item 7), because this face is the last seam before its statement runs. The window is the{ $gte, $lte }pair, lowered by the same reader (item 8).'as-written'for a column it names no type for: the engine'swhereseam reads the object's own field map, and applies item 7 itself to an object with none.$not-operand rewrite and its three polarity tables; theIS NULL ORwrap on$ne/$nin/$notContains. F9 never had a whole-day copy (measured).ReadScopeCompileOptions.declaredValueShape, filled by both consumers from the context'sdeclaredValueShapehook.lteBound(the$lteand$betweenpredicates); the window's own bare-day, last-day and'~'-suffix readingsdeclaredPreviewLowering, over the drafted object's declared types.queryDataset's preview branch passessourceFieldMeta(dataset.object, field)?.type; that one call site is the onlyanalytics-service.tsedit. A declareddatetimeis rewritten, any other declared type is compared as written, and a column with no declared type is read type-blind (item 7). The window is the{ $gte, $lte }pair, matched by the samematchesWhereas thewhere(item 8).The
$betweensplit infieldLeavesstays. After the lowering it only meets a column declared something other thandatetime, where it is structural: inclusive at both ends, which is the typed drivers' comparison.Measured, before and after
F9 and F10 were measured through the plugin's own composition over a real
ObjectQLengine withSqlDriver. The databases were SQLite (better-sqlite3) and PostgreSQL 16.14 (a private server, session timezone America/New_York, stopped and deleted afterwards).Rows:
signed_at(datetime):2026-07-27T10Z,2026-07-28T00Z,2026-07-28T10Z,2026-07-29T10Z, null;due_on(date): the same days;note(text):'2026-07-27','2026-07-28','2026-07-28 late','n', null.F11 was measured through the production path:
AnalyticsServicePlugin's own composition (sourceFieldMetafrom the engine registry,draftRowsResolverfrom aprotocolservice returning the same rows as a pending seed draft), thenqueryDatasetwithpreviewDrafts, over SQLite. "Before" is the same harness on the pre-F11 sources (d2f452b88); it reproduces the round-1 preview column cell for cell.The
notecells. Every move lands on the engine's answer:notecell$lte '2026-07-28'$lte '9999-12-31'(the carrier note's cell)$between ['2026-07-28', '2026-07-28']$between ['2026-07-28', '9999-12-31']$not: { $lte '2026-07-28' }['2026-07-28', '2026-07-28']['2026-07-28', '9999-12-31']Native results are identical on SQLite and PostgreSQL.
These are unchanged:
datetimeanddatecell, on all three faces;A host with no typed reader reads every column type-blind, as ADR-0053 D-D1 item 7 prescribes for a seam that cannot read declarations. The engine lane's seams already do this (
engine.tsdeclaredDatetimeLowering, the RLS seam). It does not read every column as "not datetime": read that way, the native face would have lost the last day of everydatetimebound (measured:datetime $lte a daywent from r1, r2, r3 to r1, r2).sourceFieldMeta, or an object the registry does not hold yet, now answersnote $lte '9999-12-31'with r1, r2, r3, r4 (was r1, r2, r3) and the$betweento that day with r2, r3, r4 (was r2, r3).lteBoundsniffed instants on the last day. Every other type-blind seam does not, and this host now answers as they do.The preview window's full-timestamp end (the deleted
'~'reading, "that instant and its own sub-values"). On adatetimecolumn over SQLite:…T10:00:00.000Z) answers as the engine, before and after.wherealready compared the same{ $gte, $lte }pair (that column did not move).The cells that moved:
…T10:00, main rows…T10:00:00, main rows…T10:00, rows at 09:59:59.999 / 10:00:00.000 / 10:00:00.500 / 10:00:30 / 10:01…T10:00:00, same rows…T09:59, same rowsAcross the 10 full-timestamp
datetimecells, 4 differed from the engine before this change and 5 differ after. Every difference, before and after, comes from comparing an instant written in a different spelling as text. The seat ruled that kind of defect out of this card ("a separate coercion defect, same class as #21505 … ⛔ do not build a coercion fix in this card"). See Acceptance notes.The preview window over
Daterows (the BSON storage form): it is now compared as instants, like thewhere. Run against the pre-F11 evaluator, the new matrix'sdateRangeassertions on the BSON row population fail 10 times, and the evaluator answers them by theDate's display text. They are green after.SQL text that moved, rows unchanged:
dateprints the inclusive bound./analytics/sqlecho prints a window on a declareddateinclusive, and a window on an undeclared column as the bound handed to the engine.this_year).The temporal conformance matrix, on every face
TEMPORAL_CASES, with its token anddateRangeaxes, answers unchanged on every face:The diff against the base sources is empty: the face files at
0b8239111(F9/F10) and the preview atd2f452b88(F11) were checked out in this tree and restored afterwards, with the blob equal to HEAD. The one exception is the preview'sdateRangeon BSONDaterows, which is new coverage. It was red on the pre-F11 evaluator (above).Pins
analytics-faces-one-lowering.test.ts(new):nextUtcCalendarDay,isUnboundedAbove,UNBOUNDED_ABOVE,lteBound), and none uses a NULL-polarity copy's name; both holder maps are empty. A file "uses" a name when it imports, declares or calls it. A positive control proves the scan reaches the seams that calllowerFilterCondition.engine.find:OS_TEST_POSTGRES_URL; a named skip without it);queryDatasetwithpreviewDraftsand the live path wired to throw. This coversnote lte 2026-07-28,between,$notand the window cells, plus the undeclared-host row.preview-temporal-conformance.test.tsnow runs the matrix throughevaluateAnalyticsQueryOverRowsunder both readers, on both row populations.preview-evaluator.test.tspins thatmatchesWherecompares the bound it is handed, and that the evaluator gives the whole day on a declareddatetime, compares as written on a declareddateor text column, and reads type-blind with no reader.Against the base sources, the new file is red on 21 tests for F9/F10 (enumeration 2, one-source 5, the text cells 7 on each database). Against the pre-F11 preview sources it is red on 7 more: the whole-day enumeration, the preview text cells
$lte,$betweenand window one day,$not, window to the last day, and the undeclared-host row. Both runs are green on the matrix, the read scope and the reader-less native rows.Fifteen existing pin files moved from the stacked guards to the single guard: these are the step-3 rows marked "until the copy's deletion card". No id-set assertion changed.
Ablations
Each direction was predicted before the run. Each mutation went through
scripts/ablation-replace.mjs, and every restore was proven by the blob equalling HEAD andgit diff HEADbeing empty. The pins importsrc/by relative path.ltecopy (import plus arm)IS NULL ORwraplteBound(import, function,$lte,$betweenmax)$ltea day,$betweenone day,$not, window one day red; the last-day text cells green (instant sniff); the matcher pin redanalytics-service.tscall site$ltea day,$betweenone day,$not, window one day) red, last-day ones green; the undeclared-host row red; both matcher pins red; the matrix's datetime bare-day cases red under both readersdate-range-array-arm-arity(#17124 control) and the step-3where-doorF11 pinGates
The pushed head
e08db24240has treea8afd8be53. The runs below were made on that same tree as local commitcf643cce9e. Before the first push, the three unpublished commits' co-author trailer was reworded to the model-free pair. Every rewritten commit's tree equals its original's, so no measurement below moved.cf643cce9e):dispatch-gates --commandsderives 64 families. All 64 were run with exit codes, all 0, and--ranreconciles 64 derived / 64 run / 0 NOT MEASURED.@objectstack/service-analyticstest(atc466dc77c8, rewritten asbbabcc6978; the later commit only edits the changeset): 174 files, 4142 passed, 247 skipped.typecheck: green. It covers the test files (171 listed).dist, all green:analytics-*: 22 files, 291 tests;analytics-*: 6 files, 41 tests;analytics-*: 8 files, 70 tests.**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}minus its ignores, with noparserOptions.project, so type-aware linting is off and this diff cannot move any untouched file's result. ESLint's JSON reports 27 files, 0 errors and 0 warnings.Size: 28 files, +1418 / -1107.
Acceptance notes
assertDefinedComparandsin F9 and F10 is a refusal door, not the lowering's meaning, and is kept.sourceFieldMetaparagraph names$emptyas the reason to wire it. It is now also the reader that tells adatetimecolumn from a text one, on the native face and the preview. No sentence is false, so it is not edited here.$ne#21505.{ signed_at: { $ne: '2026-07-28' } }answers r1 to r5 on the preview and r1, r3, r4, r5 on the engine, before and after (the reader does not move it);'~'deletion with A: keep it. The window is the{ $gte, $lte }pair (item 8), and the remaining off-engine cells belong to the draft-preview part of service-analytics read scope: compileScopedFilterToSql binds a temporal comparand on a declared datetime column as written (no ADR-0053 D-A1 storage coercion), so PostgreSQL reads a bare day in the session zone and SQLite misses$ne#21505.docs/design/predicate-compilation-convergence.md§4.1's face table still lists F9, F10 and F11 under "Deleted later". The F7 row was marked retired by its own deletion card (formula: retire F7's whole-day copy (lteBound in matches-filter.ts) now that the RLS write check judges the stored form (#21109, PR #21235); its direct-call cases move to the storage-form lowering #21242). That file is outside this claim's surface, so it is named here for the seat rather than edited.mainmerged since touches no analytics, ObjectQL or SQL-driver source (three test files indriver-sqlandobjectql, nothing else there).Generated by Claude Code