Skip to content

fix(service-analytics)!: the analytics where door runs the shared comparand-TYPE face on the object spelling (#20035) - #20058

Merged
objectstack-fleet[bot] merged 9 commits into
mainfrom
claude/issue-20035-analytics-where-type-face
Sep 25, 2026
Merged

objectstack-fleet[bot] merged 9 commits into
mainfrom
claude/issue-20035-analytics-where-type-face

Conversation

@objectstack-fleet

@objectstack-fleet objectstack-fleet Bot commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #20035

Clause-②: no (narrowing)

What this changes

The analytics where door (lowerAnalyticsWhere in packages/services/service-analytics/src/strategies/filter-normalizer.ts) now runs the shared comparand-TYPE face, normalizeFilterComparandTypes (@objectstack/spec/data), on the object spelling. It runs after the comparand-shape face and before any node is built, the order parseFilterAST and the engine seam use. The condition the door lowers is the face's RETURN value, so a bigint within 2^53 is narrowed to its number, copy-on-write, as the engine does it.

The governing record is the #7872 ruling (2026-08-12): the accepted comparand types are string | number | bigint | boolean | null | Date, and the face 「refuses everything else loudly at the compile face」. The FilterArray spelling of this door (inside parseFilterAST) and the ObjectQL engine seam already ran it. The object spelling did not.

How:

  • normalizeWhereComparands (new, exported) is the door's comparand gate: the shape passes (unchanged, now module-private as assertWhereComparandShapes), then the type pass over the whole condition.
  • The type pass hands each field entry to the face as a one-entry node with the path of the node that holds it, the same hand-over the shape pass makes. The face therefore reports exactly the path it reports on the whole condition, so the object spelling gets the FilterArray spelling's bytes.
  • Nested-relation entries are handed over too. The face leaves a nested-relation object alone as filter structure; this compiler flattens it to a dotted member, so its entries are comparands. { acct: { amt: … } } is judged at where.acct.amt, the path the dotted spelling gets.
  • The shared traversal (forEachWhereFieldEntry, now a copy-on-write mapWhereFieldEntries) descends a nested relation only when it is a PLAIN object (prototype Object.prototype or null). That is the type face's own structure test. Before, a Uint8Array, Map or class instance in the implicit slot was descended as a relation: { stage: Uint8Array } compiled to stage.0 = 1 AND stage.1 = 2. Now it is visited as the comparand it is.
  • The draft preview (preview-evaluator.ts) calls the same gate and evaluates the narrowed condition it returns. This closes the card's class-a leg: an undefined comparand is refused instead of answering no row.

Measured first (recorded on this branch as 4e1cd13aac, before any source change)

Base origin/main 246314dffe. The harness was a real sql.js engine (driver-sqlite-wasm) over rows d1 (amt 1, 'won'), d2 (5, 'lost'), d3 (10, 'open'), d4 (NULL, NULL) and d5 (3, ''). The faces were: the native SQL execute; the /analytics/sql echo; the ObjectQL engine path over a real ObjectQL engine.aggregate; engine.find on the same object where (the engine seam); and the draft preview.

cell object spelling at base FilterArray spelling engine seam (object)
plain object $ne {stage:{$ne:{a:1}}} native bound the JSON text '{"a":1}': EVERY row; echo DATABASE_ERROR / 500; engine path 400; preview EVERY row 400, type face at where.stage.$ne 400
plain object $gt native: no row; echo 500; engine path 400; preview: no row 400 400
plain object $eq native: no row; echo 500; the engine path received {stage:{a:1}} and driver-sql refused it in its own words no $eq spelling (= lowers to a nested relation) 400
plain-object $between endpoint the lower bound compared amt against the JSON text '{"a":1}': no row; echo 500; the engine path refused it as a $gte the author never wrote 400 at where.amt.$between[0] 400
plain object as an $in member / $contains refused 400 in this door's #5234 wording 400, type face 400
{ $field: 5 } under $gt (not a reference) native: no row; echo 500; engine path 400 400 (a plain object) 400
binary $eq / $in member native bound JSON TEXT '{"0":1,"1":2}', not a blob: no row; echo bound the raw buffer: no row; engine path 400 400 400
binary $ne native, echo and preview: EVERY row; engine path 400 400 400
binary implicit {stage: Uint8Array} flattened as a nested relation: native DATABASE_ERROR / 500; echo and engine path INVALID_FIELD / 400; preview refused operator "0" 400 at where.stage 400
Map $eq native bound '{}': no row; echo 500; engine path 400 no $eq spelling 400
Map implicit refused as #5240's zero-operator wrapper; preview EVERY row 400 (a Map instance) 400
bigint within 2^53 {amt:{$gt:2n}} native, echo and engine path d2, d3, d5 (bound as a bigint); preview d2, d5 (ordered as text: '10' before '2') narrowed to 2: d2, d3, d5 d2, d3, d5
bigint within 2^53, implicit 5n / $in [5n,10n] the same rows as the narrowed number on every face narrowed the same rows
bigint beyond 2^53 $gt / $in member native and echo bound it: no row; engine path 400; preview d2, d5 / no row 400 400
undefined, implicit / $gt / $in member refused 400 in #6386's wording ([analytics] comparand at "amt".$gt is undefined); the preview answered NO row 400, the type face's undefined sentence 400
{$null: undefined} / {$null: {a:1}} lowered to set (IS NOT NULL): d1, d2, d3, d5 on native, echo and engine path; the preview refused $null as unevaluable no spelling 400
nested relation {acct:{amt:{$gt:{a:1}}}} native: no row; echo 500; engine path 400 dotted ['acct.amt','>',{a:1}] 400 400
under $or {$or:[{id:'d3'},{stage:{$ne:{a:1}}}]} native and preview: EVERY row; echo 500 400 at where.$or[1].stage.$ne 400
controls {amt:{$gt:2}}, {stage:null} d2, d3, d5 / d4 on every face and both spellings the same the same

After the change, every refusal row is refused on all four faces, before any statement or engine.aggregate call. The object message equals the FilterArray message and the face's own message, byte for byte. The bigint rows serve the same rows on all four faces, the preview included, and no face binds or receives a bigint.

Binary: reconciled, not kept as a declared local extra

The type face's docblock lets a door keep "its recorded driver-local extras — binary bindables … declared at the use site", and #8186 asked for an explicit keep-or-reconcile call. The call is reconcile. The evidence:

Pins re-judged: predicted 39 red in 9 files, measured 39 red in the same 9 files

Before wiring the gate I read every candidate pin and predicted the red set. I then wired the gate and ran the package suite (86907678c0): Tests 39 failed | 2617 passed (2656), the identical set. Each pin keeps its verdict, or flips with a note quoting the #7872 ruling. ⛔ None is deleted.

Census of consumer pins outside the package. I searched all 3904 tracked test files under packages/** and examples/** (outside this package) for #6386's sentence, #5234's two sentences, #6444's wording and the face's sentence. The hits were driver-sql's and driver-turso's own refusals and spec's own tests, none through this door. A second pass covered the 127 test files that reach analytics (@objectstack/rest 15, runtime 17, qa/dogfood 11 and the rest). It looked for an undefined, plain-object, binary, Map or bigint comparand in a where, and found none. So there are zero hits and no edit outside the package. @objectstack/rest ran in full anyway (below).

Compile surfaces

surface verdict
lowerAnalyticsWhere / normalizeAnalyticsFilterTree: caller where, dataset scope filter, measure filter; native execute, /analytics/sql echo, ObjectQL engine path changed. Refused before any statement or engine.aggregate; a bigint within 2^53 is narrowed. Pinned per face over a real engine.
evaluateAnalyticsQueryOverRows (draft preview) changed. The same gate; it evaluates the narrowed condition.
normalizeFilterComparandTypes (the shared face) already compliant. This PR calls it and does not change it.
parseFilterAST (this door's FilterArray spelling) already compliant. Measured at base: every cell refused or narrowed.
ObjectQL engine seam (lowerWhereFilterArray) already compliant. Measured at base with engine.find.
compileScopedFilterToSql (service-analytics read scope) out of scope. A separate door and envelope (500); #20018 is in flight in read-scope-sql.ts. Not touched.
driver-sql, driver-turso RemoteTransport, formula, driver-memory, driver-mongodb already compliant at the platform doors. They sit behind parseFilterAST or the engine seam, which run the face. Not re-measured here.
objectql HAVING (applyHaving / matchesHaving) out of scope. A different door, over aggregated rows.

Tests and evidence (head f4ba18ba9a)

  • New src/__tests__/where-type-face-refusal.test.ts, 106 tests. Every refusal asserts code + status + the face's opening sentence.
  • Package run at f4ba18ba9a (pnpm --filter @objectstack/service-analytics test && … typecheck, under the verify lock): Test Files 120 passed (120), Tests 2762 passed (2762), tsc --noEmit exit 0. tsc --listFiles includes all 12 touched TS files. Base: 119 files / 2656 tests.
  • @objectstack/rest full suite (its closure built, service-analytics dist/ carrying normalizeWhereComparands): Test Files 194 passed (194), Tests 3265 passed | 1 skipped (3266).
  • Ablations. Each ran from the committed fix through scripts/ablation-replace.mjs: anchor x1 to x0, marker grep -c 1, blob changed. The restore was proven as blob == HEAD with git diff HEAD empty, and each wrapper carried its own trap restore. The tests import the source relatively, so no build is on the path. Every count was predicted before the run:
    • A, the type walk removed (return normalizeWhereComparandTypes(node, path); became return node;). Predicted 138; measured Tests 138 failed | 2624 passed (2762): 99 in the new file and the 39 re-judged pins. Samples: native execute: expected a refusal, got rows: expected [ 'd1', 'd2', 'd3', 'd4', 'd5' ] to be undefined, expected 'accept' to be 'INVALID_FILTER/400', native execute: a bigint was bound: expected true to be false.
    • B, the preview evaluates query.where instead of the gate's return. Predicted 1; measured 1 failed | 2761 passed: draft preview: $gt 2n: expected [ 'd2', 'd5' ] to deeply equal [ 'd2', 'd3', 'd5' ].
    • C, the door discards the gate's return. Predicted 3; measured 3 failed | 2759 passed: the tree-narrowing, copy-on-write and bound-bigint cases.
    • D, the plain-object check removed from the nested-relation test. Predicted 12; measured 12 failed | 2750 passed: the binary / Map / class-instance implicit cells. Samples: expected 'DATABASE_ERROR' to be 'INVALID_FILTER', expected '[analytics] "stage" carries a field c…' to be 'Filter comparand at where.stage is a …'.
  • Gates. dispatch-gates --repo objectstack-ai/objectstack --commands at f4ba18ba9a derived 60 families. The union run was 61: those 60 plus check:dispatcher-error-vocabulary from the dispatch list, which exited 0. 59 exited 0. NOT MEASURED (2): check:dual-build-cjs-loads and check:type-check-debt exited 3 (PREREQUISITE NOT MET: they need the whole workspace built, which CI does). --ran: 60 derived famil(ies) accounted for — 58 run, 2 NOT-MEASURED (2 DERIVED from a recorded exit 3), 0 UNRUN. The passes include check-adr-0087-registration --base origin/main (it accepted the changeset's disposition), check-changeset-no-major, check:changeset-gate-self-tests, check:nul-bytes, check:where-matcher, check:test-source-alias, check:cross-package-test-inputs and check:issue-citations. check-issue-citations also ran in its board-probing mode with GITHUB_TOKEN: exit 0, 43 citations resolve.
  • Lint, narrowed to the change. eslint --no-inline-config --format json over the 12 touched TS files: 12 files, 0 errors, 0 warnings. eslint --print-config resolves a config for each, which is the population read from eslint's own config. eslint.config.mjs never enables type-aware linting (its note at line 327), so this diff cannot move the verdict on an untouched file.

Changeset and the ADR-0087 disposition

.changeset/20035-analytics-where-type-face.md: @objectstack/service-analytics minor, a ! headline, Clause-②: no (narrowing), and a BREAKING paragraph with a before / now table for every accept-cell that now refuses.

The marker is not-required (no-migration-prescription), and it is the honest one. #7872's transition is not on the ledger. No entry in packages/spec/src/migrations/registry.ts names the type face or its accepted set, and #7872's own changeset declared no breaking change. So already-registered would name an id that does not cover this change, which ADR-0087's own addendum calls out. registered needs a ledger entry in packages/spec, which this card excludes. And no ledger entry could carry this change: a stored plain-object comparand has no accepted comparand to be rewritten to, and the other refused values cannot be stored as JSON at all. The table records each cell's verdict before and now and prescribes no rewrite.

Deviations from the dispatch, stated

  1. "FROM → TO table" is spelled "before / now". The gate reads a literal FROM / TO label as a migration prescription, which would refuse the only honest disposition above. The table is the transition in substance (the verdict each accept-cell had and has), and its columns say so. The seat may prefer registering a ledger entry in a packages/spec follow-up instead; that is out of this card's surface.
  2. The traversal's nested-relation test changed (plain objects only). It was needed for byte identity: without it, { stage: Uint8Array } and { stage: new Map() } never reach the type face. Ablation D pins it.
  3. assertWhereComparandShapes is now module-private. Its only other caller, the preview, now calls normalizeWhereComparands. The package entry never re-exported it.
  4. origin/main merged in (round 2), as a merge commit (0164eba2b7), after fix(service-analytics)!: the NativeSQL execute face and the /analytics/sql echo refuse a read scope the shared comparand faces refuse #20046 (service-analytics: the NativeSQL read-scope compiler and the /analytics/sql echo compile two scope shapes the shared comparand faces refuse (plain-object comparand under $eq, null member in $in): one scope, two answers across faces #20018) landed as 980bc05e5b. Two test files conflicted; both sets of re-judgements hold on the merged tree (see "Round 2" below). origin/main has since moved one commit, to 3557f85fa5 (a driver-turso README). It touches none of these files, so it was not merged again.

Round 2: merged with origin/main after #20046 (head 0164eba2b7)

Round 3: stale docblocks carried (head 8c78550a75)

  • One commit, comparand-shape.ts docblocks only: 53 comment lines added, 6 removed, with no code or test line changed. So there is no ablation this round: it moves no behaviour and no assertion.
  • Evidence at 8c78550a75:
    • the service-analytics suite: Test Files 121 passed (121), Tests 2795 passed (2795); tsc --noEmit exit 0; eslint on the file: 0 errors, 0 warnings;
    • dispatch-gates --commands: the same 60 families; the union of 61 gave 59 exit 0 and 2 exit 3 (the same NOT MEASURED pair); --ran: 60 derived famil(ies) accounted for — 58 run, 2 NOT-MEASURED;
    • check-issue-citations board-probing: exit 0, 72 resolve.
  • origin/main is at b76aad5f6f at the time of writing. None of its commits since the merge base 980bc05e5b touches service-analytics or the shared filter faces, so it was not merged.

Round 4: the unbindable list-member refusal no longer offers "(or a binary value)" (head cf1563d6f8)

  • What changed. This is the seat's round-3 answer (option A, on this PR). It is one commit, cf1563d6f8: 3 files, +28 / −10.
  • Census of pins on the old wording. This is a git grep -F over all 9482 tracked files at cf1563d6f8, so every package is covered, packages/rest included. The tree has no snapshot files.
    • "or a binary value": 4 hits. One is sql-driver.ts:3007, driver-sql's own copy, which is expected and stays. The other 3 are prose that names the removed words: the changeset line, the re-judged pin's comment and the comparand-shape.ts docblock. None of them is an assertion.
    • The wider "binary value" gives 7 hits. The 3 extra hits are another driver-sql message, a driver-sql comment and the docblock's quote of the old hand copy. None of them is an assertion.
    • "in its own right — use": 2 hits, both runtime strings: this message (comparand-shape.ts:700) and driver-sql's copy (sql-driver.ts:3006).
    • "Every member of an $in/$nin/$between list is a comparand": 1 hit, this message itself.
    • "Refusing rather than binding it": 5 hits. They are this message, the new pin (comparand-door-single-source.test.ts:325), driver-sql's copy, and two other messages (comparand-shape.ts:595, driver-turso remote-transport.ts:4141).
    • unbindableListMemberMessage: 9 hits. They are the definition, its two door callers (filter-normalizer.ts:680, read-scope-sql.ts:1009), their imports and the matrix test's three calls. The other two calls assert only the accepted-set sentence and bigint, and neither moved.
    • Control: "cannot be bound as a SQL", a string known to be present, gives 10 hits. They include this message (comparand-shape.ts:698) and three door pins that still hold on it (where-type-face-refusal.test.ts:229-230, cross-field-reference-refusal.test.ts:288).
    • Result: the push missed no pin on the old wording.
  • Ablation of the new negative pin. It ran from the committed head through scripts/ablation-replace.mjs, which put "(or a binary value)" back into the runtime string.
    • On disk: anchor x1 → x0, injected text x0 → x1, blob 4cc83d7bf44c → 77c17ca505c8.
    • Predicted: 1 red in the whole package suite. Measured: Test Files 1 failed | 120 passed (121), Tests 1 failed | 2794 passed (2795).
    • The red was comparand-door-single-source.test.ts › "binary stays a package-local extra the door does not admit": AssertionError: expected '"$in" on "status" has a value at inde…' not to contain 'binary'.
    • Restore: blob == HEAD (4cc83d7bf44c), git diff HEAD empty, git status --porcelain empty. The wrapper carried its own trap restore.
    • Vitest stops a case at its first failed assertion, so the pin's second assertion (the accepted-set sentence followed directly by "Refusing rather than binding it") was not measured on its own here.
  • Evidence at cf1563d6f8:
    • the dependency closure: 14 of 14 turbo tasks cached;
    • the service-analytics suite: Test Files 121 passed (121), Tests 2795 passed (2795); tsc --noEmit exit 0;
    • eslint --no-inline-config --format json on the two touched TS files: 2 files, 0 errors, 0 warnings;
    • dispatch-gates --commands: the same 60 families as rounds 1 to 3. 58 exited 0. 2 exited 3 and are NOT MEASURED (check:dual-build-cjs-loads, check:type-check-debt), because they need the whole workspace built. --ran: 60 derived famil(ies) accounted for — 58 run, 2 NOT-MEASURED (2 DERIVED from a recorded exit 3);
    • check-issue-citations in board-probing mode: exit 0, 74 citations resolve.
  • origin/main is at 66960564d9, 5 commits past the merge base 980bc05e5b. None of those commits touches service-analytics or the shared comparand-type face. This round changes no code, so origin/main was not merged.

Acceptance notes


Generated by Claude Code

…ement before the fix

Measurement only; no source change. Taken on this branch at its base
(origin/main 246314d) before any fix, with a throwaway harness that is
not committed: a real sql.js engine (driver-sqlite-wasm) over rows
d1 amt 1 'won', d2 amt 5 'lost', d3 amt 10 'open', d4 NULL NULL,
d5 amt 3 ''. Faces: normalizeAnalyticsFilterTree (tree);
NativeSQLStrategy.execute (native, the statement that runs);
ObjectQLStrategy.generateSql then run (echo, /analytics/sql);
ObjectQLStrategy.execute over a REAL ObjectQL engine.aggregate (engine
path); engine.find on the same object where (engine seam);
evaluateAnalyticsQueryOverRows (preview). The FilterArray spelling was run
through tree, native and engine.find.

Shared comparand-TYPE face (normalizeFilterComparandTypes, #7872)
-----------------------------------------------------------------
cell | object spelling | FilterArray spelling | engine seam (object)
1. plain object $ne {stage:{$ne:{a:1}}} | native binds JSON text
   '{"a":1}': EVERY row d1..d5; echo DATABASE_ERROR/500 (raw object
   bound); engine path 400 (type face, on the laundered
   $and/$or/$ne); preview EVERY row | 400 type face at where.stage.$ne |
   400
2. plain object $gt {amt:{$gt:{a:1}}} | native JSON text: no row; echo
   500; engine path 400; preview no row | 400 | 400
3. plain object $eq {stage:{$eq:{a:1}}} | native JSON text: no row;
   echo 500; the engine path receives {stage:{a:1}} (implicit) and
   driver-sql refuses in its own words | no $eq spelling: '=' lowers to
   {stage:{a:1}}, a nested relation (stage.a = 1; native 500) | 400
4. plain-object $between endpoint [{a:1},5] | native `amt >= '{"a":1}'
   AND amt <= 5`: no row; echo 500; engine path 400 at $gte (an
   operator the author never wrote); preview no row | 400 at
   where.amt.$between[0] | 400
5. plain object $in member | refused 400, #5234 wording ("cannot be
   bound as a SQL parameter") | 400 type face | 400
6. plain object $contains | refused 400, #5234 wording ("TEXT of a
   pattern") | 400 type face | 400
7. non-string reference {$gt:{$field:5}} | native JSON text: no row;
   echo 500; engine path 400 | 400 type face (a plain object) | 400
8. binary (Uint8Array [1,2]) $eq | native binds JSON TEXT
   '{"0":1,"1":2}', not a blob: no row; echo binds the raw Uint8Array:
   no row; engine path 400; preview no row | (no $eq spelling) | 400
9. binary $ne | native, echo and preview: EVERY row; engine path 400 |
   400 | 400
10. binary $in member | native JSON text: no row; echo raw: no row;
    engine path 400; preview no row | 400 | 400
11. binary implicit {stage: Uint8Array} | compiled as a NESTED RELATION
    `stage.0 = 1 AND stage.1 = 2`: native DATABASE_ERROR/500; echo and
    engine path INVALID_FIELD/400 (cross-object "stage.0"); preview
    refuses operator "0" | 400 type face at where.stage | 400
12. Map $eq | native binds '{}': no row; echo 500; engine path 400;
    preview no row | (no $eq spelling) | 400
13. Map implicit {stage: Map} | refused 400 as #5240's zero-operator
    wrapper on native/echo/engine path; preview EVERY row | 400 type
    face (a Map instance) | 400
14. bigint within 2^53 $gt 2n | native/echo/engine path d2,d3,d5
    (bigint bound as-is); preview d2,d5 (d3 lost: the preview orders a
    bigint as TEXT, '10' < '2') | narrowed to 2: d2,d3,d5 | d2,d3,d5
15. bigint within 2^53 implicit 5n / $in [5n,10n] | same rows as
    narrowed on every face (d2 / d2,d3), kept as bigint | narrowed |
    same rows
16. bigint beyond 2^53 $gt 2n**60n | native/echo bind it: no row;
    engine path 400; preview d2,d5 | 400 | 400
17. bigint beyond 2^53 $in member | native/echo: no row; engine path
    400; preview no row | 400 | 400
18. undefined implicit / $gt / $in member | refused 400 on native, echo
    and engine path, #6386 wording ('[analytics] comparand at "amt".$gt
    is undefined'); preview NO ROW, no refusal | 400, the type face's
    undefined sentence | 400
19. {$null: undefined} / {$null: {a:1}} | lowered to `set` (IS NOT
    NULL): d1,d2,d3,d5 on native, echo and engine path (which receives
    $ne: null); preview refuses $null as an unevaluable operator | no
    FilterArray spelling | 400 type face
20. nested relation {acct:{amt:{$gt:{a:1}}}} | tree `acct.amt gt
    [{a:1}]`; native no row; echo 500; engine path 400 | dotted
    ['acct.amt','>',{a:1}] 400 at where.acct.amt.$gt | 400 (driver-sql)
21. under $or {$or:[{id:'d3'},{stage:{$ne:{a:1}}}]} | native EVERY row;
    echo 500; preview EVERY row | 400 at where.$or[1].stage.$ne | 400
CONTROLS | {amt:{$gt:2}} d2,d3,d5 and {stage:null} d4 on every face and
    both spellings.

Governing record: the #7872 ruling (2026-08-12), 「refuses everything
else loudly at the compile face」.

Claude-Session: https://claude.ai/code/session_01Evb5jFDZGKQE9KG4jbMfMF
Co-authored-by: Claude <noreply@anthropic.com>
…parand-TYPE face on the object spelling

lowerAnalyticsWhere now hands every field entry of an object-form where,
nested-relation entries included, to normalizeFilterComparandTypes after
the comparand-shape face and before any node is built, the order
parseFilterAST and the engine seam use, and lowers the face's RETURN
value (a bigint within 2^53 narrowed to its number, copy-on-write). The
draft preview calls the same gate and evaluates the narrowed condition.

WIP: pins not yet re-judged.

Claude-Session: https://claude.ai/code/session_01Evb5jFDZGKQE9KG4jbMfMF
Co-authored-by: Claude <noreply@anthropic.com>
…d-TYPE face moves

Each keeps its verdict or flips with a note quoting the #7872 ruling;
none is deleted. Predicted 39 red in 9 files before wiring the gate;
measured 39 red in the same 9 files, the identical set.

Claude-Session: https://claude.ai/code/session_01Evb5jFDZGKQE9KG4jbMfMF
Co-authored-by: Claude <noreply@anthropic.com>
… the where door

Byte identity (object = FilterArray = face) for every moved cell, the
order against the shape face and the door's own gates, the positions
the face leaves to the door, the copy-on-write bigint narrowing, the
four faces over a real ObjectQL engine with zero statements and zero
aggregates on refusal, and the stored-dataset service doors.

Claude-Session: https://claude.ai/code/session_01Evb5jFDZGKQE9KG4jbMfMF
Co-authored-by: Claude <noreply@anthropic.com>
…door's docblocks

The module header gains the #20035 section; the #6386, #5234 and
toSqlBindValue notes say which positions the type face now answers
first and which the door's own gates keep.

Claude-Session: https://claude.ai/code/session_01Evb5jFDZGKQE9KG4jbMfMF
Co-authored-by: Claude <noreply@anthropic.com>
…and-TYPE face

Clause-②: no (narrowing). ADR-0087: not-required
(no-migration-prescription); the #7872 transition is not on the ledger.

Claude-Session: https://claude.ai/code/session_01Evb5jFDZGKQE9KG4jbMfMF
Co-authored-by: Claude <noreply@anthropic.com>
@github-actions github-actions Bot added size/xl documentation Improvements or additions to documentation tests tooling labels Sep 25, 2026
@github-actions

github-actions Bot commented Sep 25, 2026 •

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

10 anchor(s) derived from 1 changed package(s); no hand-written page names any of them, so this run has nothing to list — not a clean bill of health. This check sees only pages that NAME a derived anchor: one that documents this change in prose, or enumerates it in an authoring dialect, names none and stays invisible to it on every run.

What this run could not see
  • the SDK route bridge reached 60 of 215 client-bound route-ledger rows — the other 155 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 155: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 100 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 9 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json b76aad5f6fac71cbbcd4ea6bfdf21a59e2fd4d56 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 8de57ae03502aba21a7257a595aa3cd0120b006d — the merge of head cf1563d6f80d26b97a5bb63040ceed9984877e2a into base b76aad5f6fac71cbbcd4ea6bfdf21a59e2fd4d56, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 8de57ae03502aba21a7257a595aa3cd0120b006d && git checkout 8de57ae03502aba21a7257a595aa3cd0120b006d
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin b76aad5f6fac71cbbcd4ea6bfdf21a59e2fd4d56 cf1563d6f80d26b97a5bb63040ceed9984877e2a && git checkout -B drift-repro b76aad5f6fac71cbbcd4ea6bfdf21a59e2fd4d56 && git merge --no-ff cf1563d6f80d26b97a5bb63040ceed9984877e2a

node scripts/docs-audit/affected-docs.mjs --json b76aad5f6fac71cbbcd4ea6bfdf21a59e2fd4d56

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Resolves the two test conflicts with #20046 (#20018) so both sets of
re-judgements hold: the comparand matrix keeps #20018's read-scope cells
and this branch's where-door cells, each named with the change that
moved it; the non-string $field case holds on both doors, each in its
own envelope. Sentences either PR made false on the merged tree are
scoped: the #5234 one-sentence note, the binary predicate note, the
comparand-shape.ts binary-extra docblock (#20018's "on the where door,
that is"), and this branch's changeset and normalizer lines on the
read-scope binary admission.

Claude-Session: https://claude.ai/code/session_01Evb5jFDZGKQE9KG4jbMfMF
Co-authored-by: Claude <noreply@anthropic.com>
@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

CI note · Test Core (3/6) red on head 0164eba2b7: not this PR's diff · domain:services seat · 2026-09-25T00:42Z

  • What failed: one test in @objectstack/client: src/auth-login-register-envelope.test.ts › "register() parses as the declared envelope…" (:273), with Error: Test timed out in 5000ms. The package ran 1 failed | 568 passed. The @objectstack/rest ELIFECYCLE line in the same log is turbo stopping sibling tasks after the first failure; the summary lists only @objectstack/client#test as failed.
  • Why it isn't this PR's: the diff touches only packages/services/service-analytics/** and a changeset. Neither @objectstack/client nor its auth path imports service-analytics. This test is the file's first case: it cold-starts a real ObjectQL + SqliteWasmDriver + better-auth pipeline under the default 5 s timeout, on a shard whose package import time was 184 s. That is a timing failure, not an assertion failure.
  • Re-run: this seat has no re-run channel (its writes go only through the fleet relay, which has no re-run action). The PR stays watched. The next CI run on this PR comes with the dev's next push. If the same case times out again on a later head, it is filed against packages/client with both runs.
  • No fix is ported here: the timeout is in another lane's test and no fix exists yet.

Generated by Claude Code

…ces to the type face

Docblocks only. The undefined table's where row now names the shared
comparand-TYPE face as the first refusal (same verdict and envelope),
with #6386's gate kept for the two positions the face steps around; the
header's envelope clause, the non-string $field note, the two shared
refusal sentences and the $between endpoint notes are scoped to the door
each is true of. The "(or a binary value)" parenthetical in
unbindableListMemberMessage's text is flagged in its docblock, not
changed (no code change in this round).

Claude-Session: https://claude.ai/code/session_01Evb5jFDZGKQE9KG4jbMfMF
Co-authored-by: Claude <noreply@anthropic.com>
…offers a binary value as a repair

Neither analytics door accepts a binary comparand any more (#20018 at
the read scope, #20035 at the where door), so unbindableListMemberMessage
names the accepted set and nothing else. isBindableComparand's binary
arm (the driver-sql mirror) and driver-sql's own copy of the sentence
are unchanged. The #8186 pin is re-judged; the changeset gains one line.

Claude-Session: https://claude.ai/code/session_01Evb5jFDZGKQE9KG4jbMfMF
Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/xl tests tooling

Projects

None yet

1 participant