Skip to content

feat(drivers,formula,objectql): the engine's filter faces answer the staged $empty operator (#20444) - #20523

Merged
objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-20444-empty-operator-engine-arms
Sep 28, 2026
Merged

objectstack-fleet[bot] merged 7 commits into
mainfrom
claude/issue-20444-empty-operator-engine-arms

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Fixes #20444
Clause-②: yes (widening)

The domain:engine lane's arms for the staged $empty operator, under ruling A on #20399 (5865693155): 「One sibling card per compile-surface lane, each Blocked-by: #20311's spec PR: domain:engine — driver-sql and its heirs, turso RemoteTransport, driver-memory, driver-mongodb, formula, objectql having; domain:services — service-analytics' two faces. The two faces with no field declarations (the formula matcher, objectql having) judge by value, diverging only on a non-text column holding '' (the write-door class #20308 closed).」

Every arm calls the spec's one expansion from PR #20442 (expandEmptyOperator / isEmptyFilterValue in @objectstack/spec/data); no face keeps a copy of the table. The staging does not move (the maintainer's 「照 $like 先例分阶段」, 5868169573): $empty is not added to FILTER_OPERATORS, the is_empty / is_not_empty lowering still emits $null, and the engine's front door still refuses the operator. A driver or evaluator called directly now answers it.

What each face does now

face reads $empty: true undeclared field
driver-sql applyFilterCondition (and driver-sqlite-wasm, driver-turso local, which inherit it) declared row null-only: col IS NULL; text: (col IS NULL OR col = ''); multi-value: (col IS NULL OR L) refused
driver-turso RemoteTransport.buildWhereSQL declared row, via a resolver TursoDriver wires from the same registry same SQL, SQLite dialect refused (also when used standalone with no resolver)
driver-memory live path (find / count / update / delete through mingo) declared row null-only { f: { $eq: null } }; text { f: { $in: [null, ''] } }; multi-value { $or: [{ f: { $eq: null } }, { f: { $size: 0 } }] } refused
driver-mongodb translateFilter (and the aggregate $match) declared row, via a new optional valueShape resolver the same three documents refused (also standalone with no resolver)
driver-memory reference matcher (match) by value isEmptyFilterValue(value) answered by value (it holds no declarations)
formula matchesFilterCondition by value isEmptyFilterValue(actual) answered by value
objectql having and per-aggregation filter by value isEmptyFilterValue(value) answered by value
driver-memory analytics (cube) face — refused INVALID_FILTER / 400 as a declared operator it cannot compile, as it refuses $null —

$empty: false is the exact complement on every face: (col IS NOT NULL AND NOT L) / a non-null value other than '' (the not-equal operator against a bound '') / IS NOT NULL on SQL, $nin / $nor / $ne on the document faces, !isEmptyFilterValue on the value faces. A non-boolean flag is refused on every query face (INVALID_FILTER / 400, on each driver's validating walk, so an identity that settles the node first cannot skip it); formula answers it false, its standing posture for an unevaluable check.

L, the empty-list test on a multi-value column (a JSON column: TEXT on SQLite, json on PostgreSQL and MySQL):

  • SQLite (and libSQL): (CASE WHEN json_valid(col) THEN json_type(col) = 'array' AND json_array_length(col) = 0 ELSE 0 END) — a malformed legacy cell answers FALSE instead of failing the statement; a non-array JSON value is not an empty list;
  • PostgreSQL: (CAST(col AS jsonb) = CAST('[]' AS jsonb));
  • MySQL: (JSON_TYPE(col) = 'ARRAY' AND JSON_LENGTH(col) = 0);
  • any other knex dialect: the multi-value row is refused (the text and null-only rows need no dialect).

An empty list is always tested as a stored value, never bound as a $eq: [] comparand (ruling 乙 on #19757 stands). Every SQL predicate is TOTAL (never UNKNOWN), so $not over $empty needs no NULL guard: both SQL compilers' polarity tables gain the row (operatorIsNullTotal → true, nullValueSatisfiesOperator → value === true).

PM hypotheses, measured

  • H1 — held, with the sources named. Measured on base 4a1df1965 by driving each face directly (a scratch probe, not committed) with { f: { $empty: true } }, $empty: false and { $and: [{ g: 'x' }, { f: { $empty: true } }] }, beside a $null control (answered on every face) and a $bogus control. Refusal sources: driver-sql the emitter's default: arm (unsupportedFilterOperatorError); turso remote its own vocabulary refusal (unsupportedOperator); driver-memory live path and matcher both at the shared shape gate (assertFilterConditionShape, filter-refusal.ts); driver-mongodb translateFieldOperators' default:; objectql having unknownOperator. All INVALID_FILTER / 400. formula answered [] for all three shapes (the silent false), exactly as $bogus. After this PR, the same probe answers ['2','3'] / ['1'] / ['2','3'] on every face that holds the declaration or judges by value, and refuses on the two standalone entry points given no declaration.
  • H2 — each declared-type face's declaration. driver-sql: a new per-table registry valueShapeFields ({ type, multiple } per field), filled beside jsonFields at registerManagedObjectMetadata (so initObjects and registerObjectMetadata), registerExternalObject, and the shard alias. turso remote: registerRemoteFieldMetadata → registerExternalObject fills the same registry, and TursoDriver hands the transport setDeclaredValueShapeResolver. driver-memory and driver-mongodb: a map filled by syncSchema beside the temporal-kind map. The engine's registry injects the audit / tenant / owner fields into the object's field map before it is synced (per registry.ts' own docblock; not re-measured end to end here), so those are declared too. A field with no declaration (a knex-built table, the builtin id, a field with no type) is a refusal, never a row guessed from a value: the spec's by-value reading has no SQL form without the type (amount = '' is a type error on PostgreSQL). A declared non-member type (string, object, array from an introspected or test object) takes the row the spec's expansion gives it, null-only.
  • H3 — SQL arms, above. Pinned on SQLite locally; sql-driver-20444-empty-operator.test.ts runs on every cell of the live dialect matrix, so PostgreSQL and MySQL are measured by the Temporal Conformance (live PG + MySQL) job. Locally NOT MEASURED on PG / MySQL: no server is reachable in this container. The MySQL ' ' row relies on the NO PAD default collation of the job's mysql:8.0.
  • H4 — the conformance table. FILTER_LOGIC_CASES gains seven $empty cases on the fixture's nullable column d (true, false, both under $not, inside $or, inside $and, beside $ne on the same field). The fixture stores neither '' nor [], so on it every row of the table agrees; the rows pin that every face HAS an arm, that $not over it is total and that it composes. The per-type discrimination is each face's own suite (below). Census of every consumer that iterates the table:
    • driver-sql sql-driver-or-filter.test.ts — built its table through knex, so the harness now registers the fixture's declaration (registerObjectMetadata);
    • driver-sqlite-wasm, driver-turso local and remote, driver-memory live path and matcher, driver-mongodb live suite — already declared the fixture (initObjects / syncSchema), pass unchanged;
    • driver-memory analytics face — the harness's rule is "agree or refuse loudly", and it refuses;
    • driver-mongodb mongodb-filter-logic-translation.test.ts — calls translateFilter standalone, so it now passes a declaration resolver;
    • formula matches-filter-or-semantics.test.ts — by value, passes unchanged;
    • spec filter-verdict.test.ts — the rows reduce to clause, passes unchanged; lint validate-empty-combinators.test.ts reads only the #5322 rows;
    • service-analytics read-scope-sql-conformance.test.ts and native-sql-filter-logic-conformance.test.ts — outside this lane. Since PR fix(service-analytics): both filter faces answer $empty by the field's declared type (#20445) #20498 (merged) both faces answer $empty, but only when handed the field's declaration; each harness now passes a text declaration for the fixture (test-only, no service-analytics source touched), so they pass the rows rather than partition them. Declared as a deviation below.
  • H5 — having's conclusion. By value over the aggregated row: null, a column the row lacks, '' and [] are empty. A numeric aggregate holding 0 (a count over nothing, a sum netting to zero) is not empty. A groupBy text column holding '' is empty — the row a declared text field takes too. The per-aggregation filter shares the walker and the reading. Pinned in having-empty-operator.test.ts, including the row-independent refusal of a non-boolean flag.
  • H6 — formula's docblock. Its header claimed a DECLARED operator never gets the silent false; that was false from [Decision] what 「is empty」 means on a text column and on a multi-value column: null only (the spec's lowering today), or null OR '' / [] — three objectui builders disagree, and a stored sharing rule's rows depend on the answer #20311's declaration until this arm. The header now records that, names the declared-but-staged set ($like, $ilike, $empty) as answered, and says the next declared name is owed an arm by the PR that lets an author write it or by its staging's lane card.

Tests (head measured: 436a10a3e)

  • New per-face pins, each over a text, a multi-value and a scalar field with null, '', [] and value rows, $empty: false, nesting under $and / $or / $not, a sibling operator on the same field, and refusals asserted by code + status: sql-driver-20444-empty-operator.test.ts (dialect matrix), turso-20444-empty-operator.test.ts (local and remote held to one row set, plus count()), memory-20444-empty-operator.test.ts (live, matcher, analytics face, and the one pinned cell where the declared row and the by-value reading part), mongodb-20444-empty-operator.test.ts (emitted documents and their rows; a live-mongod half runs when the opt-in server is available), matches-filter-empty-operator.test.ts, having-empty-operator.test.ts.
  • Extended: the withheld-refusal seam tests of driver-sql (three new builders, one needing the 'unknown' dialect) and of the turso remote transport (two methods, and the local / remote one-sentence table), and driver-memory's operator-key clobber sweep (now declares its column and covers $empty).
  • Full package suites on the pre-merge head ea3d95994, each run through the verify lock: driver-sql 197 files passed, 1 failed, 11 skipped — the failure was the withheld-refusal seam enumeration, which the new refusal builders owed rows; they are added in this PR and that file re-ran green (107 tests); driver-turso 77 files, 2080 passed; driver-sqlite-wasm 36 files, 665 passed; driver-memory 59 files, 1419 passed; driver-mongodb 29 passed / 5 skipped, 656 passed; formula 42 files, 1227 passed; objectql --project local 332 files, 6636 passed; service-analytics 134 files, 3165 passed.
  • On the merged head 436a10a3e: typecheck exit 0 for all seven packages above (spec's own typecheck ran green on the pre-merge head); the $empty suites and every FILTER_LOGIC_CASES harness re-run green (driver-sql 154 passed / 4 skipped, turso 240, sqlite-wasm 37, memory 194, mongodb 65 / 50 skipped, formula 43, objectql 36, service-analytics 72, spec 73).
  • Ablations, each through scripts/ablation-replace.mjs on the committed tree with a restore trap; every leg restored to blob == HEAD with git diff HEAD empty:
    • A1 — driver-sql's text arm drops its '' limb: 4 red in sql-driver-20444-empty-operator.test.ts; the FILTER_LOGIC_CASES sweep stayed green, which is the measured proof the shared rows do not discriminate the text row.
    • A2 — driver-memory's multi-value lowering written as $in: [null, []]: 8 red (mingo does not match a stored [] that way).
    • A3 — formula's arm removed (the silent false back): 13 red, 6 in the new pins and all 7 $empty rows of the shared table. The first A3 attempt did not run: its replacement text already occurred in the anchor, the tool refused the non-rising count, and the file was restored; it was re-run with a distinct replacement.
  • check:driver-conformance read before and after: 50 covered cells, 0 DEBT, 0 exempt on both sides.

Gates

node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstack at 436a10a3e (after merging origin/main with a merge commit) derived 91 commands; all 91 ran, each exit code recorded before any pipe. --ran reconciliation: "91 derived famil(ies) accounted for — 89 run, 2 NOT-MEASURED". NOT MEASURED: check:dual-build-cjs-loads and check:type-check-debt, both exit 3 (PREREQUISITE NOT MET: they need the whole workspace built). Narrowed probe instead: the built CJS entry of each changed package loads under require (driver-sql 51 exports, driver-turso 14, driver-memory 23, driver-mongodb 11, formula 47, objectql 178).

Lint, narrowed: eslint.config.mjs lints packages/**/*.{ts,tsx,mts,cts} with no type-aware parsing (no parserOptions.project), so no verdict on an untouched file can move with this diff. pnpm exec eslint --no-inline-config --format json over the 26 changed .ts files: 26 file entries, 0 errors, 0 warnings.

Deviations

  • service-analytics test files (read-scope-sql-conformance.test.ts, native-sql-filter-logic-conformance.test.ts) are edited, although the order bars service-analytics. The edit is test-only: it hands each harness the fixture's declaration so the new shared rows pass (H4). No service-analytics source moves.
  • packages/spec/src/data/filter-logic-conformance.ts gains the seven rows and a header paragraph, a declared cross-lane test-data edit (the claim names it).

Acceptance notes (observations, not filed)


Generated by Claude Code

…he field's declared row

driver-sql's filter compiler and driver-turso's remote transport compile
{ f: { $empty: true | false } } through the spec's expandEmptyOperator,
reading a new per-table registry of declared value shapes. A field with no
declaration is refused rather than guessed.

Claude-Session: https://claude.ai/code/session_01N8TPEsoJxPsdSdNKGnNGEN
Co-authored-by: Claude <noreply@anthropic.com>
…d $empty operator

driver-memory's live path and driver-mongodb translate $empty by the
field's declared row (expandEmptyOperator over the declaration syncSchema
recorded); driver-memory's reference matcher, formula's
matchesFilterCondition and objectql's having judge by value
(isEmptyFilterValue). formula's header docblock is corrected.

Claude-Session: https://claude.ai/code/session_01N8TPEsoJxPsdSdNKGnNGEN
Co-authored-by: Claude <noreply@anthropic.com>
…IC_CASES

Seven $empty cases over the fixture's nullable column; the harnesses that
built their fixture without a field declaration now declare it, since a
declared-type face refuses $empty on an undeclared field.

Claude-Session: https://claude.ai/code/session_01N8TPEsoJxPsdSdNKGnNGEN
Co-authored-by: Claude <noreply@anthropic.com>
…ed row

Text, multi-value and scalar fields over null, '', [] and value rows;
$empty: false as the exact complement; nesting under $and / $or / $not;
refusals for an undeclared field and a non-boolean flag (code + status).
driver-sql's suite runs on every cell of the live dialect matrix.

Claude-Session: https://claude.ai/code/session_01N8TPEsoJxPsdSdNKGnNGEN
Co-authored-by: Claude <noreply@anthropic.com>
…am and clobber sweeps

The remote transport's two $empty refusals get rows in the withheld-refusal
seam test (and the local/remote one-sentence table); driver-memory's
operator-key clobber sweep declares its column and covers $empty.

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

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 7 package(s): @objectstack/driver-memory, @objectstack/driver-mongodb, @objectstack/driver-sql, @objectstack/driver-turso, @objectstack/formula, @objectstack/objectql, @objectstack/spec, touching 67 documentable anchor(s). ⚠️ 1 changed file(s) yielded no anchor (packages/drivers/driver-mongodb/src/index.ts), so the pages documenting them are NOT COVERED by this run — this is not a clean bill of health for those files.

28 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: node scripts/docs-audit/affected-docs.mjs --json 0bbe4005e82fce2058238720cac7ba5cd2f182d5.

⛔ 9 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails.

What this run could not see
  • 1 changed file(s) yielded no anchor (packages/drivers/driver-mongodb/src/index.ts) — pages documenting those are invisible to this run
  • 7 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 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; 97 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 — 142 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 0bbe4005e82fce2058238720cac7ba5cd2f182d5 → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 5b14b72f2a8d2af92b517f7567b8911939a096e0 — the merge of head 436a10a3e9c7ac73ddc9983ad770e4b7469a1b72 into base 0bbe4005e82fce2058238720cac7ba5cd2f182d5, 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 5b14b72f2a8d2af92b517f7567b8911939a096e0 && git checkout 5b14b72f2a8d2af92b517f7567b8911939a096e0
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 0bbe4005e82fce2058238720cac7ba5cd2f182d5 436a10a3e9c7ac73ddc9983ad770e4b7469a1b72 && git checkout -B drift-repro 0bbe4005e82fce2058238720cac7ba5cd2f182d5 && git merge --no-ff 436a10a3e9c7ac73ddc9983ad770e4b7469a1b72

node scripts/docs-audit/affected-docs.mjs --json 0bbe4005e82fce2058238720cac7ba5cd2f182d5

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

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs 0bbe4005e82fce2058238720cac7ba5cd2f182d5 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: 436a10a3e9c7ac73ddc9983ad770e4b7469a1b72
Local-runs: none

① Derived judgments

Inputs: card #20444 body and all six comments (triage 5871590917, unlock 5875809404, cross-lane 5876791510, claim 5878203784, os-dev-report 5880043060, seat answer 5880101190); #20399 body, ruling A 5865693155 and the pointer 5868183894; #20311's comments including the amendment 5868169573 and the flip-card naming (5870558181, 5874872012); PR #20442 and PR #20498 bodies; PR #20523 body, its 27-file list and the net diff against merge base 0bbe4005e (+1965 / −25, read in full); read-only git show of the touched modules and of every FILTER_LOGIC_CASES consumer at the head; the check-runs on the head, twice (22:49Z with nine in progress, 22:58Z final). Nothing built, run or re-run.

  1. One expansion, no private reading — RIGHT. Every declared-type arm calls expandEmptyOperator (driver-sql applyEmptyOperator, turso RemoteTransport.pushEmptyOperator, driver-memory emptyOperatorCondition, driver-mongodb translateEmptyOperator) and every by-value arm calls isEmptyFilterValue (formula evalOp, driver-memory's matcher checkCondition, objectql checkCondition). Each driver switches on expansion.arm and spells that row in its own dialect; the closed union is guarded by a never default in all four, so a fourth row fails loudly instead of being guessed. No face keeps a table of its own.

  2. driver-sql and its heirs — RIGHT. The registry valueShapeFields is filled beside jsonFields at registerManagedObjectMetadata (so initObjects / registerObjectMetadata), registerExternalObject and aliasShardBookkeeping, from the RAW type with no 'string' default, so a typeless field, the builtin id and a knex-built table refuse (undeclaredEmptyOperatorFieldError, withheld). Predicates: null-only IS NULL / IS NOT NULL; text (col IS NULL OR col = '') / (col IS NOT NULL AND col not-equal ''); multi-value (col IS NULL OR L) / (col IS NOT NULL AND NOT L), each one knex group so an inner OR cannot re-associate with a sibling conjunct. L: SQLite CASE WHEN json_valid(col) THEN json_type(col) = 'array' AND json_array_length(col) = 0 ELSE 0 END (a malformed TEXT cell answers FALSE, a non-array JSON value answers FALSE); PostgreSQL CAST(col AS jsonb) = CAST('[]' AS jsonb); MySQL JSON_TYPE(col) = 'ARRAY' AND JSON_LENGTH(col) = 0; null on 'unknown', which raises emptyListUnsupportedDialectError while the text and null-only rows still compile there. TOTAL on every dialect: both polarities spell the NULL case out and L is never NULL for a stored value, so $not needs no guard, and the polarity tables carry the row (operatorIsNullTotal true; nullValueSatisfiesOperator value === true). $empty: false is the exact complement on each dialect and is pinned as a partition per field on every cell. [] is never bound as a comparand (ruling 乙 on [finding] the comparand-SHAPE face declares it closes the door "for every driver at once", but an array in the IMPLICIT-EQUALITY slot passes it — and driver-mongodb alone answers it, as an exact-array match #19757 stands). The non-boolean flag is refused on the validating walk (classifyFilterKey), so an identity cannot skip it; pinned under $or: [{}, …]. The arm sits after assertCompilableComparand, assertOperatorAppliesToColumn and the TEXT_OPERATORS gate (which does not name $empty) and before the calendar-day rewrites and coercions — the right slot. PostgreSQL and MySQL: ci.yml's Temporal Conformance (live PG + MySQL) job runs the WHOLE @objectstack/driver-sql suite with OS_TEST_POSTGRES_URL, OS_TEST_MYSQL_URL and OS_EXPECT_LIVE_DIALECT_MATRIX=1, and sql-driver-20444-empty-operator.test.ts declares its cells through DIALECT_CELLS / declareDialectCell, so the new pins DID run on both live dialects; that job is success on this head (22:50:00Z). driver-sqlite-wasm overrides neither dialectName nor applyFilterCondition (grep: no hit) and reports sqlite; turso local inherits the same compiler. The MySQL ' ' row rests on the NO PAD default collation of the job's mysql:8.0, as the PR says.

  3. turso RemoteTransport — RIGHT, with one surface note. setDeclaredValueShapeResolver(resolver) is a NEW PUBLIC METHOD on the exported RemoteTransport class — a surface change, and the changeset names it. TursoDriver wires it to SqlDriver.declaredValueShape, which reads the same valueShapeFields registry that registerRemoteFieldMetadata → registerExternalObject fills, so local and remote read one declaration. The remote SQL is the local twin's SQLite construct verbatim, every clause parenthesised as one conjunct (the transport joins a node's clauses with a bare AND); both polarity tables gain the row. A bare transport refuses $empty before any statement runs (pinned, executed === false). Local equals remote: pinned on one row set for text, multi-value and scalar under $and / $or / $not and a sibling operator, plus count(). $empty joins the remote SUPPORTED_FILTER_OPERATORS list and the temporal coercion's hands-off set, which is required — a transport refusing what its local twin answers is the fork the parity suites exist to catch. Note: DeclaredValueShapeResolver is exported from remote-transport.ts but not re-exported by the package index.ts (only FilterColumnSqlResolver is), so the method's parameter type is structurally usable but not nameable from the package entry.

  4. driver-memory and driver-mongodb — RIGHT. Both keep a valueShapes map filled by syncSchema beside the temporal-kind map, from the RAW type. Memory: $empty is split out of the field's operator map and pushed as its own extraAndConditions conjunct (so it can contest no mingo key with a sibling — pinned in the clobber sweep), lowered { f: { $eq: null } } / { f: { $in: [null, ''] } } / { $or: [{ f: { $eq: null } }, { f: { $size: 0 } }] } with $ne / $nin / $nor as the complements; find, count, updateMany, deleteMany, distinct and aggregate all route through convertToMongoQuery → normalizeFilterCondition, so the arm reaches every query method. The reference matcher holds no declarations and judges by value; undefined is added to its no-value pass-through so a missing key reaches the arm. The shared shape gate refuses a non-boolean flag for both faces; SUPPORTED_FIELD_OPERATORS admits $empty by hand exactly as it admits $like. The analytics (cube) face refuses INVALID_FILTER / 400 — pinned, and the shared-table harness's rule ("agree with the live path or refuse, never a third answer") tolerates that refusal, verified at head. Mongo: translateFilter(where, temporalKind?, valueShape?), the resolver threaded through translateCondition into $and / $or / $not branches and into the aggregate $match through buildAggregationPipeline.valueShape; MongoDBDriver passes valueShapeFor(object) from find, findOne, count, updateMany, deleteMany, aggregate and explain. Standalone call, unknown field and typeless field refuse; the non-boolean flag is refused on the walk. The emitted documents are pinned by identity and by a server-free reader of the Mongo vocabulary; the live-mongod half is opt-in. Observation, not a breach: on both document faces { f: { $eq: null } } also matches a LIST HOLDING A NULL ELEMENT (MongoDB and mingo null-equality; the test's own mongoEquals models it), so a multi-value cell like ['a', null] reads as empty there and not on SQL. That is a stored state the declaration does not predict (the class ruling A already accepts for '' in a non-text column); the fixtures store none, and it is unpinned — carried in ③.

  5. formula and objectql having — RIGHT. formula's evalOp case $empty reads raw, not the resolved v (a { $field } in the slot is not resolved into a flag): true → isEmptyFilterValue(actual), false → its negation, anything else → false, the write denied — this face's standing answer to an unevaluable condition, stated in the PR body. The header now records the window in which a declared $empty got the silent false, names the declared-but-staged set ($like, $ilike, $empty — equal to STAGED_AHEAD_OF_BACKENDS at head) as answered, and says who owes the next arm; the default-arm comment is corrected in the same sense. having: $empty joins CONDITION_OPERATORS (a ruled addition — ruling A names having), NO_VALUE_ANSWERED_BY_OPERATOR (a column the row lacks is empty), and the arm judges by value; a non-boolean flag is refused before the vocabulary check in both assertConditionIsEvaluable and checkCondition. The conclusion is explicit and pinned: 0 from a count or sum is NOT empty, a groupBy '' IS empty, a max over nothing is empty, [] and a missing column are empty; the per-aggregation filter shares the walker.

  6. FILTER_LOGIC_CASES — RIGHT. The seven rows check out against the fixture (d null on rows 3-4; b: 'y' on 1 and 3; b: 'zz' on 2 and 4; d: 'v1' on 1): ['3','4'], ['1','2'], ['1','2'], ['3','4'], ['1','2','3'], ['4'], ['2']. The header paragraph states truthfully what the rows pin and do not (the fixture stores neither '' nor [], so the per-type rows are each face's own suite; ablation A1 measured exactly that). Census by grep at head: 13 harnesses iterate the table — driver-sql sql-driver-or-filter (knex-built; now registerObjectMetadata, right), driver-sqlite-wasm (initObjects with string, the null-only row, agrees on this fixture), turso local (initObjects) and remote (syncSchema), driver-memory live plus matcher plus analytics (syncSchema, text) and memory-matcher-or-semantics (by value), driver-mongodb live (syncSchema) and mongodb-filter-logic-translation (standalone; now FIXTURE_SHAPES, right), formula matches-filter-or-semantics (by value), spec filter-verdict (asserts clause, which the rows are), lint validate-empty-combinators (the #5322 subset only), service-analytics read-scope-sql-conformance and native-sql-filter-logic-conformance (now hand { type: 'text' }, matching ReadScopeCompileOptions.declaredValueShape and DatasetScopedStrategyContext.declaredValueShape from PR fix(service-analytics): both filter faces answer $empty by the field's declared type (#20445) #20498). None silently skips a row; the dev's census equals the grep. The two service-analytics edits are test-only (+13 / −1; no packages/services/service-analytics/src/*.ts source in the file list) and right: the fixture's columns are text.

  7. Enrolment edits — RIGHT, no assertion weakened. driver-sql seam: three doors added (its enumeration half requires one row per withheldFilterError builder, which is the pre-merge failure the PR names); the optional per-row setup hook is additive, the 'unknown' override is applied on the last door only and the next describe builds its own driver; JSON_COL is declared lookup, multiple: true in that fixture, so the door reaches emptyListUnsupportedDialectError. turso seam: two doors plus two local/remote one-sentence rows. Clobber sweep: syncSchema('t', { v: text }) is what the declared-row refusal now requires, and $empty: false enters the comparand table so every pair is swept. mongodb translation harness: passes the declaration it now needs.

PR-body and changeset sentences: every claim I could read against the diff, the head or the check-runs is TRUE — the A1 measurement, H1 to H6, the dialect constructs, the totality argument, the refusal census, the FILTER_LOGIC_CASES census, the live-dialect claim, and the staging sentences (filter.zod.ts untouched, FILTER_OPERATORS unchanged, the is_empty / is_not_empty lowering unchanged, the $empty arms only). Not verifiable read-only and not re-run per the brief: the local suite counts, ablations A1 to A3, the 91-command gate sweep and check:driver-conformance — the head's check-runs stand in for them (all green, ③). Two changeset sentences carry a scope note, in ②.

② Semver level

RIGHT. Clause-②: yes (widening) on the PR body (line 2), the changeset and the claim — one arm from the closed pair. Seven packages minor (driver-sql, driver-turso, driver-memory, driver-mongodb, formula, objectql, spec), which is what yes requires (at least minor), and each line matches what the diff publishes: the four drivers' filter accept sets widen to the staged operator (declared INVALID_FILTER / 400 before, answered now when the declaration is held); formula's evaluator stops answering false; objectql having widens CONDITION_OPERATORS; spec's shipped FILTER_LOGIC_CASES gains seven rows, so a backend that runs the table must answer $empty or go red — a published-data change, minor is right. The widening is stated on both prose faces exactly as the brief asks: "A driver or evaluator called directly now answers it" and "the engine's front door still refuses it until the flip card adds it". driver-sqlite-wasm with no line: right — no source moved, and every one of these packages sits in the fixed group of .changeset/config.json, so it takes the same version regardless; only its CHANGELOG entry is absent. Check Changeset success.

New exports and members, read against the changeset's "New optional API" sentence. Named: translateFilter's third parameter and the ValueShapeResolver type on the driver-mongodb index; buildAggregationPipeline.valueShape; RemoteTransport.setDeclaredValueShapeResolver. Not named — each inside minor, none widening beyond the stated widening: DeclaredValueShapeResolver (a module export, not on the driver-turso index); SqlDriver.valueShapeFields and SqlDriver.declaredValueShape, both protected on an exported class and therefore in the .d.ts the heirs subclass against; driver-memory's nonBooleanEmptyComparandError and undeclaredEmptyOperatorFieldError stay internal (filter-refusal.ts is not re-exported). The sentence is TRUE as a list and incomplete as an inventory; no level moves. The sentence "Refused, never guessed (INVALID_FILTER / 400): … and a flag that is not a boolean" is TRUE for every query face and for the drivers it is scoped to; formula, in the by-value bullet above it, answers a non-boolean flag false and refuses nothing — the PR body says so, the changeset does not. A one-clause prose gap, not a level error; the seat may fold it into the changeset at landing or leave it.

③ Boundary flags

open_questions: [] — nothing to answer. The four deviations and three out-of-scope notes, each answered:

Dev flags in the PR body:

Check-runs on the head, final read 2026-09-28T22:58:28Z: 35 runs — 32 success, 3 skipped (Build Docs, Console Pin Gate, Packed-tarball smoke: path-filtered or opt-in), 0 failure, 0 in progress. Named: Lint & Repo Gates success (22:51:22Z); Temporal Conformance (live PG + MySQL) success (22:50:00Z); Test Core and its six shards success (22:57:40Z); Type Check · workspace, source gates, consumer gates and debt ledger success; TypeScript Type Check success; Check Changeset success; Governed Surface Queue Guard success; the three claim and single-writer guards success. At the first read (22:49Z) nine of these were in progress; every one completed green.

Implemented-by: claude/issue-20444-empty-operator-engine-arms
Reviewed-by: session_01N8TPEsoJxPsdSdNKGnNGEN

VERDICT: PASS

@objectstack-fleet
objectstack-fleet Bot marked this pull request as ready for review September 28, 2026 23:06
@objectstack-fleet
objectstack-fleet Bot added this pull request to the merge queue Sep 28, 2026
Merged via the queue into main with commit fb38607 Sep 28, 2026
37 checks passed
@objectstack-fleet
objectstack-fleet Bot deleted the claude/issue-20444-empty-operator-engine-arms branch September 28, 2026 23:25
veigajoao pushed a commit to veigajoao/objectstack that referenced this pull request Sep 29, 2026
…its that decided them (stage 3) (objectstack-ai#20533)

Part of objectstack-ai#20234
Clause-②: no

## What changed

This is stage 3 of the staged sweep. It covers
`packages/spec/src/data/**` and nothing else. It leaves out the files an
open PR or an in-flight claim holds: `data-engine.zod.ts`,
`data-engine.test.ts`, `hook.form.ts`, `analytics*.ts`,
`cube-member-inner-name-retirement.test.ts`, `driver/turso.zod.ts` and
`filter-subtree-provenance.ts`, as the claim names them. It also leaves
out four files that open PRs started editing after the claim:
`driver/turso.test.ts` (PR objectstack-ai#20504, objectstack-ai#20437's, opened 2026-09-28T20:08Z),
`object.form.ts` (PR objectstack-ai#20519, objectstack-ai#20432's, 21:55Z), `object.zod.ts` (PR
objectstack-ai#20521, objectstack-ai#20494's, 22:10Z) and `filter-logic-conformance.ts` (PR objectstack-ai#20523,
objectstack-ai#20444's, 22:39Z). See Acceptance notes. Later stages cover the other
areas, so this PR says `Part of`.

Every comment or docblock site in scope that cited a tracker number
answering 404 has been rewritten in ruling C+D's form C (comment
5749154545 on objectstack-ai#19123). That is **163 sites on 161 lines in 44 files,
covering 40 numbers**. Each rewritten line now cites the commit in
`origin/main` history that decided what the line describes, and it says
in its own words what that commit decided.

No ADR or ruling-record file in `docs/adr/` or `scripts/adr-anchors/`
records the decision behind any of the 43 dead numbers in scope.
ADR-0104 names objectstack-ai#12380 only as a reference, and ADR-0055 states the rule
that objectstack-ai#8772's ruling enforced, not the ruling itself. So every anchor is
a commit: **38 distinct shas**. One number was dropped rather than
anchored: objectstack-ai#17286, a tracking card that recorded an axis as undecided,
under which no commit landed. The sentence keeps its reason in words.

Three comment sites in scope are left on purpose (see Acceptance notes).
Two are the `[objectstack-ai#6259]` marker in `api-derivation.ts:163`, which a test
string reads, and the test comment that names that marker. The third is
`field.zod.ts:370`, whose `objectstack-ai#6111` is objectui's number.

Only comments changed. Every source file keeps its line count (174 lines
out, 174 in, over 45 files), so no line citation into these files moves.
Thirteen of those 174 lines held no dead citation. Eleven are the other
half of a sentence that had to be reflowed or rewritten. One is a table
header (`value-roundtrip-conformance.ts:20`, 「card」 to 「card or commit」,
because its row now holds a commit). One is `api-derivation.ts:164`,
which now carries the `[objectstack-ai#6259]` sentence's commit. No code token moves
(see the guard below). The 41 string-literal sites that carry a dead
number are tokens, so they are left as they were and listed below.

**No citation number is added.** Every tracker number on an added line
was already on the line it replaces. No PR number stands on an added
line.

Two more kinds of file change, both mechanical:
- **One regenerated reference page.** Two of the rewritten docblock
lines (`feed.zod.ts:15`, `:18`) project into
`content/docs/references/data/feed.mdx`. `check:docs` proved that page
stale, and `pnpm --filter @objectstack/spec check:generated --fix`
regenerated only it. The diff is two lines, each the same substitution
as its source line. No page a held file projects into (`analytics.mdx`,
`data-engine.mdx`, `hook.mdx`, `driver-turso.mdx`) moved.
- **A `patch` changeset** for `@objectstack/spec` (see Changeset below).

## Census: `data/`, before and after

**Instrument.** This is the instrument of stages 1 and 2. It sends REST
`GET /repos/objectstack-ai/objectstack/issues/N` without following
redirects, for every distinct number cited in `packages/spec/src/data`.
The population is:
- the citation gate's own exported `CITATION_RE` and
`NON_CITATION_HEADS`, kept when the qualifier is none, `objectstack`,
`objectstack-ai/objectstack`, `framework`, `pre-` or `post-`;
- widened here to the capitalised spellings of those qualifiers (`Pre-`,
`POST-`, `Framework`: 7 sites, one of them dead), which stage 2's
case-sensitive set did not read;
- N of 100 or more, excluding `summon` heads.

Each site is classified by the TypeScript parser as a line comment, a
docblock, a block comment or a string.

**Controls.** The lit controls were `objectstack-ai#16862`, `objectstack-ai#16847` and `objectstack-ai#17698`. The
dead controls were `objectstack-ai#16714`, `objectstack-ai#16715` and `objectstack-ai#16697`. They were probed at
the start, after every 100 numbers and at the end. They read 24 of 24
lit (200) and 24 of 24 dead (404) over 8 checkpoints in both runs.

| reading | tree | numbers probed | 200 | 404 | 301 or other | dead
sites, all of `data/` | in scope | excluded (held files) | in-scope
lines | in-scope files | dead numbers in scope |
|---|---|---|---|---|---|---|---|---|---|---|---|
| before | base `9bf5e67af`, probed 2026-09-28T19:32Z to 19:36Z | 618 |
571 | 47 | 0 | **240** | 207 | 33 | 204 | 47 | 43 |
| after | head `96fd49caa2`, probed 2026-09-28T23:19Z to 23:23Z | 600 |
571 | 29 | 0 | **77** | 44 | 33 | 43 | 16 | 21 |

**Before, in scope, by class.** 92 non-test docblock sites and 13
non-test line comments. 16 test docblock sites and 45 test line
comments. 39 test string sites. 2 non-test string sites.

**After, in scope.** 41 string sites and 3 comment sites remain, all
three deliberate. The head probe found no number newly dead since the
base probe: the same 571 numbers answer 200.

PR objectstack-ai#20226's area table read `data` 239 at an earlier base; this census
reads 240 at `9bf5e67af`. The 33 excluded sites sit in `object.zod.ts`
(15), `analytics.zod.ts` (3), `analytics-strictness-batchd.test.ts` (2),
`analytics-date-range-two-bound-window.test.ts` (1),
`driver/turso.zod.ts` (2), `driver/turso.test.ts` (3),
`filter-subtree-provenance.ts` (3), `filter-logic-conformance.ts` (3)
and `object.form.ts` (1). `data-engine.*` and `hook.form.ts` carry none.

## Per-number table

The counts are in-scope sites and files at the base. `rewritten / left`
gives comment sites rewritten and sites left. Every anchor was read in
its diff or message, not only in its subject: it is the commit that made
the change the line now describes, and its own diff or message names the
number it replaces.

| number | sites / files | rewritten / left | anchor: what it decided |
|---|---|---|---|
| `objectstack-ai#6111` (objectui) | 1/1 | 0/1 | objectui's number, left: see
Acceptance notes |
| `objectstack-ai#6259` | 5/2 | 1/4 | `6968885ef`: retires the producer-less `batch:
'bulk'` row of `DATA_ACTION_TO_API_OPERATION` and the prose calling
`batch` a runtime action. The marker and 2 test strings stay (see
Acceptance notes) |
| `objectstack-ai#6345` | 18/5 | 17/1 | `e2798fab7`: one driver vocabulary; both boot
hosts read the shared table; `mongo` to `mongodb`; turso a builtin; the
fork-1 and fork-2 refusals |
| `objectstack-ai#6571` | 10/2 | 8/2 | `2f3e79351`: `$between` endpoints accept the
ISO/clock strings the platform produces, as a bare string (rider ①) |
| `objectstack-ai#8495` | 9/2 | 6/3 | `4bfe1a539`: refuses `${…}` placeholders in
memory `persistence.path` / `persistence.key` at publish |
| `objectstack-ai#8656` | 1/1 | 0/1 | a test title only |
| `objectstack-ai#8696` | 20/8 | 17/3 | `90a12fb18`, the card's mongodb arm: a bound
secret rides beside an unmodified url as MongoClient `auth`. Its own
pins carry the multi-host form `new URL()` cannot parse and the bound
secret outranking `options.auth` |
| `objectstack-ai#8772` | 3/2 | 3/0 | `75b7c240a`: Direction 2 of the 2026-08-16
maintainer ruling. The builder forces `required: true` on a
`master_detail` under `controlled_by_parent`, and raw parse stays
tolerant. ADR-0055 stays cited beside it |
| `objectstack-ai#8778` | 1/1 | 1/0 | `7901b2dd2`: stamp-only
`tenancy.organizationField`, declared by `sys_api_key` |
| `objectstack-ai#8794` | 2/1 | 2/0 | `1850ebbb0`: corrects the reuse-safety claim on
the filter-subtree mark from the survey's measurement, and routes a
mechanism change to a spec-seat ruling (stage 1's anchor too) |
| `objectstack-ai#8836` | 2/1 | 2/0 | `1850ebbb0`: the same commit, which pins the
invariant (one line carries both numbers) |
| `objectstack-ai#8873` | 6/3 | 6/0 | `096106522`: a bound `credentialsRef` reaches
the postgres server on the DSN branch. Its diff records that `pg` sends
a password only when the server asks |
| `objectstack-ai#8874` | 1/1 | 1/0 | `d70428ae7`: a declared mysql `ssl` reaches
`mysql2` as its own TLS options object, because `mysql2` rejects a bare
boolean |
| `objectstack-ai#8876` | 9/5 | 6/3 | `d634e665b`: exports `urlUserinfoUsername`, and
its diff states the asymmetry that a username is not credential material
|
| `objectstack-ai#9040` | 20/6 | 14/6 | `24206416a`: refuses a credential in the mongo
options passthrough at publish, and redacts the passthrough secret paths
on read |
| `objectstack-ai#9041` | 22/2 | 17/5 | `d491625c1`: refuses a bound `credentialsRef`
with a user-less mongo `config.url`, with the triage's fences |
| `objectstack-ai#10165` | 5/1 | 1/4 | `801296050`: `ttl.onlyWhen` with the canonical
null predicate (maintainer ruling 2026-08-20, option A) |
| `objectstack-ai#10274` | 1/1 | 1/0 | `d1ba685ec`: re-measures the objectui pin
citations and gates the class |
| `objectstack-ai#10329` | 6/2 | 6/0 | `15d58dbf1`: retires the import lookup
transform's steering params (ADR-0049) |
| `objectstack-ai#10347` | 2/1 | 2/0 | `530c1df65`: the Archiver honours a declared
`ttl` (maintainer ruling 2026-08-20) |
| `objectstack-ai#10527` | 2/1 | 1/1 | `5649efbf9`: refuses a diverging retention +
ttl + archive triple at parse time |
| `objectstack-ai#11065` | 7/3 | 5/2 | `20950404c`: a boolean aggregand counts as 1 or
0 in `avg` and `sum`, the first face aligned. No commit message names
the card; this is where the number first entered the tree |
| `objectstack-ai#11195` | 3/1 | 2/1 | `b37231883`: `UserActionsConfigSchema` adopts
`group` / `hideFields` / `rowColor` |
| `objectstack-ai#11215` | 1/1 | 1/0 | `42a117b88`: documents
`NoSQLIndexSchema.unique`'s deliberate scope-vocabulary omission |
| `objectstack-ai#11350` | 1/1 | 1/0 | `ece4dad31`: records the 2026-08-23 maintainer
ruling on entry nameability (stage 1's anchor too) |
| `objectstack-ai#11408` | 2/1 | 1/1 | `f11fc61c5`: declares `editMode` (maintainer
ruling 2026-08-24) |
| `objectstack-ai#11507` | 5/2 | 5/0 | `88b9d749a`: declares `sys_activity.type` an
open, author-extensible vocabulary (maintainer ruling 2026-08-24,
direction 4) |
| `objectstack-ai#11658` | 1/1 | 1/0 | `1a6a19c31`: opens `RecordActivityProps.types`
to author-contributed kinds |
| `objectstack-ai#12380` | 4/2 | 4/0 | `4045b954d`: makes the SQLite `Field.json`
codec injective; its message carries the measured boundary |
| `objectstack-ai#12868` | 1/1 | 0/1 | a test title only. Its comment site sits in
`object.form.ts`, now held by PR objectstack-ai#20519; its deciding commit is
`c459da6bc` (see Acceptance notes) |
| `objectstack-ai#13156` | 1/1 | 1/0 | `fd289be45`: strips tracker ids from
function-declaration-built refusal prose (the card's A half) |
| `objectstack-ai#13644` | 3/2 | 2/1 | `34ce8e7db`: declares
`ctx.referentialFieldClear` on `HookContextSchema` |
| `objectstack-ai#14426` | 2/2 | 1/1 | `40a44b91b`: the undefined-comparand refusal
prescribes the null predicate by its ruled spellings, position-safe |
| `objectstack-ai#14676` | 1/1 | 1/0 | `13c48c2a5`: retires `connector.errorMapping`;
its test states the same assertion-set reasoning |
| `objectstack-ai#16126` | 2/2 | 2/0 | `859ded3ec`: refuses a whitespace-only
`reference` on lookup / master_detail |
| `objectstack-ai#16685` | 4/2 | 4/0 | `ed7243d52`: accepts boolean / toggle for sum /
avg / min / max (decision batch objectstack-ai#80) |
| `objectstack-ai#16867` | 3/2 | 2/1 | `0ee32edef`: `notNull` / `not_null` prescribe
`storage.notNull`, not `required` |
| `objectstack-ai#17014` | 3/2 | 2/1 | `80aef8032`: the one-day date-range presets
prescribe a one-day window, and the table states its end-token
convention |
| `objectstack-ai#17286` | 1/1 | 1/0 | dropped: a tracking card with no landing. The
sentence now says the card is gone and to measure `driver-memory` for
the open set |
| `objectstack-ai#17348` | 1/1 | 1/0 | `51efbf116`: pins the `driver-memory` temporal
text-operator divergence by name in that driver's conformance suite |
| `objectstack-ai#17590` | 1/1 | 1/0 | `e04a0aff2`: `$contains` on a JSON column is a
per-dialect membership test (director-seat ruling 2026-09-12) |
| `objectstack-ai#18012` | 8/3 | 7/1 | `176b03582`: `$between` requires two non-blank
endpoints (decision batch objectstack-ai#146 item 5, letter A) |
| `objectstack-ai#19377` | 6/2 | 6/0 | `a60c913de`: refuses a `{ $field }` reference
as a `$between` endpoint at the runtime filter door |

Every cited sha matches exactly one commit (`git rev-parse
--disambiguate`, count 1), and every one is an ancestor of the base
(`merge-base --is-ancestor`, exit 0). That is 38 distinct shas.

Wordings to check, each true of its commit:
- `datasource.zod.ts:352` names only the card's mongo arm (`90a12fb18`)
for "the defect class … closed", because the paragraph is about mongo.
The card's mysql arm (`72050cc47`) is not cited anywhere in this stage.
- `datasource.zod.ts:354`: 「the triage's, as commit d491625 landed
them」. `d491625c1`'s message lists the fences as "per triage".
- `filter.zod.ts:1021-1025`: the `objectstack-ai#17286` pointer becomes 「was measured
on a tracking card … That card is gone: measure `driver-memory` for the
open set, ⛔ not this text.」 The warning that this paragraph is not the
authority is kept.

## The 41 string sites left as tokens

- **Test titles and test-code strings (39 sites).**
`driver/driver-credential-refusal.test.ts` 14, `object.test.ts` 6,
`datasource-credential-redaction.test.ts` 3,
`driver/driver-placeholder-refusal.test.ts` 3, `filter.test.ts` 3,
`api-derivation.test.ts` 2 (the `split('[objectstack-ai#6259]')` literal and its
message), `field.test.ts` 2, and 1 each in `date-range-presets.test.ts`,
`driver/postgres.test.ts`, `field-rows-option-description.test.ts`,
`filter-comparand-type.test.ts`, `hook.test.ts` and
`object-strictness-batch20.test.ts`.
- **Non-test strings (2 sites).** `aggregation-conformance.ts:398` and
`:407`, the `note` of two exported `AGGREGATION_CASES` rows (`objectstack-ai#11065`,
`objectstack-ai#11151`). They ship as data. Their only readers are driver conformance
suites, which print a `note` as the assertion message when a case fails,
to a driver developer and never to a metadata author. So they are
neither comments nor form D author-shown text. This is the same
disposition stage 1 gave the two `why` strings and stage 2 the
`PROVENANCE_WAIVERS` reason.

No author-shown text in `data/` carries a dead number, so nothing here
is objectstack-ai#20233's form D.

## Mechanical guard: no code token moves

The check compares leaf tokens with comments stripped, base `9bf5e67af`
against head `96fd49caa2`. It uses the TypeScript parser's leaf tokens,
so template literals are scanned in context, and it excludes JSDoc
nodes. It ran over all 45 touched `.ts` files.

- Real run: 140,379 base tokens, **0 files with a token change** (exit
0).
- Comment-insertion control: 0 files changed, as expected (exit 0).
- Positive control (a declaration inserted into `feed.zod.ts`): 1 file
reads DIFFER (exit 1).
- Positive control (one digit changed inside the `split('[objectstack-ai#6259]')`
string in `api-derivation.test.ts`): 1 file reads DIFFER (exit 1).

## Changeset

This change ships bytes, so a `patch` changeset for `@objectstack/spec`
is included. It says only that the provenance comments were re-anchored.

Measured on the built package: 14 of the touched sources are
`src/**/*.zod.ts`, which `files[]` ships verbatim. The rewritten
docblocks also reach `dist`. `88b9d749a`, `e2798fab7` and `24206416a`
each appear in 1 declaration file. `24206416a` appears in 20 bundled
`.js` files and `2f3e79351` in 28. The positive control, a pre-existing
`feed.zod.ts` docblock sentence, appears in `dist/data/index.d.ts`.

## Gates (head `96fd49caa2`)

- **Citation judging pass, run as CI runs it:** `pnpm
check:issue-citations && node scripts/check-issue-citations.mjs` exits
0. The self-test passes 73 cases in 7 batteries. The live run judged 11
citations across 25 files, and all 11 resolve.
- **Doc authoring:** `pnpm check:doc-authoring` exits 0.
- **Derived gates:** `node scripts/pm/dispatch-gates.mjs --commands
--repo objectstack-ai/objectstack` at the final head derived 108
families, and all 108 exit 0. `--ran` reports 108 run, 0 NOT MEASURED, 0
unrun, and exits 0. (`check:i18n` was derived at the earlier heads from
`object.form.ts`, and left the set when that file went back to base.)
- At an earlier head, four gates first exited 3 (PREREQUISITE NOT MET)
because the workspace was unbuilt: `check:doc-formula-expressions`,
`check:doc-security-posture`, `check:skill-examples` and
`check:docs-transcript-drift`. At the final head a full `turbo run
build` of `./packages/*` ran first (71 tasks, exit 0, under the shared
verify lock), and every gate exited 0 on its first run.
- `check:generated` was run under the lock against that build: all 15
artifacts are up to date.
- **Build, tests, typecheck and lint:**
  - `pnpm --filter @objectstack/spec build` exits 0.
- `vitest run --maxWorkers=2 src/data` in `packages/spec` at the final
head: 107 files and 3,517 tests pass (1 todo), covering every touched
test file.
- The 12 spec suites outside `src/data` that read `data/` source text
pass at the final head: 12 files, 503 tests. These are
`scripts/{file-description,root-index,skill-map-guards,strictness-ledger}.test.ts`,
`src/api/api-entry-graph.pin.test.ts`,
`src/contracts/scoped-context.test.ts`,
`src/shared/{alias-integrity,evaluated-slot-population,retired-key-migrate-sentence}.test.ts`,
`src/system/constants/platform-object-names.test.ts`,
`src/type-alias-convention.pin.test.ts` and `src/ui/dashboard.test.ts`.
- `pnpm --filter @objectstack/spec typecheck` at the final head exits 0,
including `check:test-typecheck` (53 files, 251 errors, 138 pinned
signatures held).
- Lint, as a proven narrowing at the final head: `eslint
--no-inline-config --format json` over the 45 touched `.ts` files gives
45 files, 0 errors and 0 warnings. All 45 are in eslint's own population
(`isPathIgnored` is false for each). `eslint.config.mjs` never enables
type-aware linting (no `parserOptions.project`, which its own line 328
states), so a comment edit here cannot move the verdict on any untouched
file. The repo-wide `pnpm lint` is CI's run.

## Acceptance notes

- **The `[objectstack-ai#6259]` marker.** `api-derivation.test.ts:236` splits
`DATA_ACTION_TO_API_OPERATION`'s TSDoc on the literal `[objectstack-ai#6259]`, and a
test string may not change here. So the marker line
`api-derivation.ts:163` is byte-identical to the base, and the test
comment at `:232` that names the marker stays too. The sentence's
deciding commit sits on the next line instead: 「(both by commit
6968885)」. A first attempt wrote the commit onto the marker line
itself. The diff-scoped `check-issue-citations` then read the kept
`objectstack-ai#6259` as an added citation and exited 1, so it was moved one line down
(commit `b93f08f8d0`).
- **objectui's `objectstack-ai#6111`.** `field.zod.ts:370` reads 「objectui#6110 +
objectstack-ai#6111 (section)」. The qualifier covers only the first number, so the
citation grammar reads `objectstack-ai#6111` as this repository's (404 here). It is
objectui's number: its introducing commit `f887e5249` writes
`(objectui#6111)` in the same diff, and `objectstack-ai/objectui`
answers REST 200 for objectstack-ai#6111 to this session (and for objectstack-ai#6110 and objectstack-ai#10264).
objectui has no `refs/pull/6111/head`, so it is an issue there, not a
PR. The line is left unchanged. This is objectstack-ai#20330's grammar family, the
same as stage 2's `objectui PR objectstack-ai#10264`, and it is noted there, not
filed.
- **Capitalised qualifiers.** `CITATION_RE` classes `Pre-#N`, `POST-#N`
and `Framework#N` (7 sites in `data/`) as cross-repo and never judges
them. This census read them as this repository's. One was dead and is
rewritten here (`object.test.ts:223`, `POST-objectstack-ai#10347`). This is the same
objectstack-ai#20330 family as stage 1's `pre-` / `post-` finding.
- **Four files held after the claim.** Each joined the exclusions and
went back to the base bytes (hypothesis 2 of the dispatch). Each PR's
hunks were disjoint from this PR's lines, but the dispatch's rule is
file-level.
- `driver/turso.test.ts`: PR objectstack-ai#20504 (objectstack-ai#20437's) opened at
2026-09-28T20:08Z and edits it. Its two comment sites (`:4`, `:58`, both
`objectstack-ai#6345`) went back to blob `7fe99ebf9` in commit `86463ed0a1`. A
no-driver `merge-tree` of that head with PR objectstack-ai#20504's head `5dfa45e9f`
exits 0.
- `object.form.ts`: PR objectstack-ai#20519 (objectstack-ai#20432's) opened at 21:55Z and edits it.
Its one comment site (`:256`, `objectstack-ai#12868`, whose deciding commit is
`c459da6bc`) went back to blob `60713e06f` in commit `3479600dda`.
- `object.zod.ts`: PR objectstack-ai#20521 (objectstack-ai#20494's) opened at 22:10Z and edits one
line at `:2123`. Its 15 comment sites (`objectstack-ai#8772`, `objectstack-ai#10165`, `objectstack-ai#10347`,
`objectstack-ai#10527`, `objectstack-ai#11195`, `objectstack-ai#11408`, `objectstack-ai#13608`) went back to blob `befde04ca` in
commit `96fd49caa2`. Their deciding commits are `75b7c240a`,
`801296050`, `530c1df65`, `5649efbf9`, `b37231883`, `f11fc61c5` and
`fc9ba76a5`, all read for this stage.
- `filter-logic-conformance.ts`: PR objectstack-ai#20523 (objectstack-ai#20444's) opened at 22:39Z.
Its 3 comment sites (`objectstack-ai#13195`) went back to blob `c9b32acba` in the same
commit. Their deciding commit is `9dac1ae01`, with `PR objectstack-ai#13529` as the
link.
- **What stays for later stages.**
- The 33 dead sites in the held files listed above. The later stage can
reuse the deciding commits named for them here.
  - The 41 string sites and the 3 deliberate comment sites above.
- The `data/` numbers that also appear in
`packages/spec/src/migrations/**`. Those are objectstack-ai#20233's form D, or the
migrations stage.
- **The rung.** Several anchored changes also have ADR-0087 entries in
`packages/spec/src/migrations`. Examples are
`cbp-master-detail-required-forced` for objectstack-ai#8772,
`filter-between-blank-endpoint-refused` for objectstack-ai#18012, the `datasource-*`
entries for objectstack-ai#9040, objectstack-ai#9041 and objectstack-ai#8873, and the
`mapping-lookup-params-removed` conversion for objectstack-ai#10329. This PR takes the
commit rung, as stages 1 and 2 did, so it is precedent-consistent. The
D3 id is the more durable in-repo record, if the ruling's first rung is
later read to include those entries.
- **The citation gate's reach.** It defers `packages/**/*.test.ts`, so
20 of the 45 touched `.ts` files never enter its judging population. The
added-minus-removed count over the whole diff covers them: 0 numbers
added.
- **Base.** The branch is 22 commits behind `origin/main` (`1378ec7c0c`,
read at 2026-09-29T00:18Z). Four of those commits touch `data/`, all in
excluded files: objectstack-ai#20475's `hook.form.ts`, objectstack-ai#20487's `data-engine.*`, and,
since this stage excluded them, PR objectstack-ai#20521's `object.zod.ts`
(`9e1689f8e2`) and objectstack-ai#20444's `filter-logic-conformance.ts`
(`fb386074f5`). None touches a file in this diff, and a no-driver
`merge-tree` of the head onto `1378ec7c0c` exits 0. So there was no
merge. The open-PR file lists were re-read at 00:18Z: 11 open PRs, none
touching a file in this diff.

---
_Generated by [Claude
Code](https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx)_

---------

Co-authored-by: Claude <noreply@anthropic.com>
veigajoao pushed a commit to veigajoao/objectstack that referenced this pull request Sep 29, 2026
… commits that decided them (stage 4) (objectstack-ai#20548)

Part of objectstack-ai#20234
Clause-②: no

## What changed

This is stage 4 of the staged sweep: the `data/` remainder. It covers
the six `packages/spec/src/data/` files stage 3 (PR objectstack-ai#20533, landed
`03b19d9cfd`) left out because an open PR held them, and nothing else.
They are `object.zod.ts`, `filter-logic-conformance.ts`,
`object.form.ts`, `data-engine.zod.ts`, `data-engine.test.ts` and
`hook.form.ts`. Later stages cover the other areas, so this PR says
`Part of`.

The census below measured all six. Three of them carry comment or
docblock sites that cite a tracker number answering 404.
`data-engine.zod.ts`, `data-engine.test.ts` and `hook.form.ts` carry
none, so they are not in the diff.

Every such site has been rewritten in ruling C+D's form C (comment
5749154545 on objectstack-ai#19123). That is **19 sites on 19 lines in 3 files,
covering 9 numbers**. Each rewritten line now cites the commit in
`origin/main` history that decided what the line describes, and it says
in its own words what that commit decided. Where a PR number was already
on the line (`PR objectstack-ai#13529`), it stays beside the commit as the link.

No ADR or ruling-record file in `docs/adr/` or `scripts/adr-anchors/`
records the decision behind any of the 9 numbers: a search for each
number, with and without `#`, finds nothing there. So every anchor is a
commit: **9 distinct shas**. Stage 3 had already read these commits and
recorded them in PR objectstack-ai#20533's body. They were not copied from there. Each
one was re-read against the current line it anchors: its own message or
diff names the number it replaces, and it made the change the line
describes. `object.zod.ts` and `filter-logic-conformance.ts` moved on
`main` after stage 3 read them (PRs objectstack-ai#20521 and objectstack-ai#20523). Each site was
therefore re-read at this base, `03b19d9cfd`.

Only comments changed. Every source file keeps its line count (20 lines
out, 20 in, over 3 files), so no line citation into these files moves.
One of the 20 lines held no dead citation:
`filter-logic-conformance.ts:249`, the first half of a sentence reflowed
onto `:250`. No code token moves (see the guard below).

**No tracker number is added.** Every tracker number on an added line
was already in the hunk it replaces. `PR objectstack-ai#13529` stands on three added
lines, and on the three removed lines of the same hunks. It is the link
beside commit `9dac1ae01`, which stage 3 recorded the same way.

No reference page under `content/docs/references/` moved: none of the
rewritten docblocks projects into one (`check:docs` at the head: `226
generated files in sync`). The PR adds one `patch` changeset for
`@objectstack/spec` (see Changeset below).

## Census: the six files, before and after

**Instrument.** This is the instrument of stages 1 to 3. It sends REST
`GET /repos/objectstack-ai/objectstack/issues/N` without following
redirects, for every distinct number cited in `packages/spec/src/data`.
The population is:
- the citation gate's own exported `CITATION_RE` and
`NON_CITATION_HEADS`, kept when the qualifier is none, `objectstack`,
`objectstack-ai/objectstack`, `framework`, `pre-` or `post-`;
- widened case-insensitively to `Pre-`, `POST-` and `Framework`, as in
stage 3;
- N of 100 or more, excluding `summon` heads.

Each site is classified by the TypeScript parser as a line comment, a
docblock, a block comment or a string.

Two cross-checks close the population. First, a raw `#N` count in each
of the six files equals the census rows plus the cross-repo rows in five
files. In the other two it is one higher, and the extra is a second
number after a slash inside a string (`objectstack-ai#5322/objectstack-ai#5134` in a `note`,
`objectstack-ai#6262/objectstack-ai#6433` in a test title). Both answer 200. Second, no spelled
citation (`issue N`, `PR N`, `card N`) occurs in any of the six.

**Controls.** The lit controls were `objectstack-ai#16862`, `objectstack-ai#16847` and `objectstack-ai#17698`. The
dead controls were `objectstack-ai#16714`, `objectstack-ai#16715` and `objectstack-ai#16697`. They were probed at
the start, after every 100 numbers and at the end: 24 of 24 lit (200)
and 24 of 24 dead (404) over 8 checkpoints in the base run, and 21 of 21
lit and 21 of 21 dead over 7 checkpoints in the head run.

| reading | tree | numbers probed | 200 | 404 | 301 or other | dead
sites, all of `data/` | dead sites, the six files | lines | files |
numbers |
|---|---|---|---|---|---|---|---|---|---|---|
| before | base `03b19d9cfd`, probed 2026-09-29T01:11:59Z to 01:15:49Z |
601 | 572 | 29 | 0 | **77** | 19 | 19 | 3 | 9 |
| after | head `53c9070dfd`, probed 2026-09-29T01:25:55Z to 01:29:35Z |
597 | 572 | 25 | 0 | **58** | 0 | 0 | 0 | 0 |

The head probe found no number newly dead since the base probe: the same
572 numbers answer 200. The base reading of 77 equals stage 3's after
reading at `96fd49caa2`.

**Per file.** Cited sites here are every in-repo citation the population
reads, live or dead.

| file | cited sites (base) | dead sites before | by class | dead sites
after |
|---|---|---|---|---|
| `object.zod.ts` | 120 | 15 | 8 docblock, 7 line comment | 0 |
| `filter-logic-conformance.ts` | 97 | 3 | 2 docblock, 1 line comment |
0 |
| `object.form.ts` | 31 | 1 | 1 line comment | 0 |
| `data-engine.zod.ts` | 48 | 0 | | 0 |
| `data-engine.test.ts` | 29 | 0 | | 0 |
| `hook.form.ts` | 0 | 0 | | 0 |

None of the 19 sites is a string, so this stage leaves no string token
behind.

## Per-number table

| number | sites / lines | anchor: what it decided |
|---|---|---|
| `objectstack-ai#8772` | 4 / 4, `object.zod.ts:2718`, `:2731`, `:2744`, `:2910` |
`75b7c240a`: Direction 2 of the 2026-08-16 maintainer ruling.
`ObjectSchema.create()` forces `required: true` on a `master_detail`
reference under `controlled_by_parent` and refuses an explicit
`required: false`. Raw parse stays tolerant, and runtime tolerance is
the ruling's other half. Its changeset records the measurement that only
the security gate closed that shape while the declaration surface
accepted it (`:2731`). ADR-0055 stays cited beside it. It is the same
anchor stage 3 gave `object.test.ts` |
| `objectstack-ai#10165` | 2 / 2, `object.zod.ts:818`, `:1036` | `801296050`:
`ttl.onlyWhen` with the canonical null predicate (maintainer ruling
2026-08-20, option A). One shared `onlyWhen` union, and both of
`retention.onlyWhen`'s conflicts mirrored. Its diff wrote both
`[objectstack-ai#10165]` blocks |
| `objectstack-ai#10347` | 3 / 3, `object.zod.ts:1006`, `:1042`, `:1049` |
`530c1df65`: the Archiver honours a declared `ttl`. It selects by the
ttl cutoff on `ttl.field` when `ttl` is declared, and by `created_at` /
`archive.after` otherwise (maintainer ruling 2026-08-20) |
| `objectstack-ai#10527` | 1 / 1, `object.zod.ts:1005` | `5649efbf9`: refuses a
diverging retention + ttl + archive triple at parse time. Its diff wrote
this very paragraph |
| `objectstack-ai#11195` | 1 / 1, `object.zod.ts:1791` | `b37231883`:
`UserActionsConfigSchema` adopts `group` / `hideFields` / `rowColor`
(the "last three" the line names) |
| `objectstack-ai#11408` | 1 / 1, `object.zod.ts:2189` | `f11fc61c5`: declares
`editMode` on the object document (maintainer ruling 2026-08-24, the
`objectstack-ai#10144` declare-or-rule-out family, which stays cited) |
| `objectstack-ai#13608` | 3 / 3, `object.zod.ts:2317`, `:2354`, `:2366` |
`fc9ba76a5`: `publicSharing.eligibility` is held at redemption, not only
at mint, fail-closed, with the undifferentiated `null` refusal. Its
changeset heads with objectstack-ai#13608. It is the same anchor stage 1 gave
`contracts/share-link-service.ts` |
| `objectstack-ai#13195` | 3 / 3, `filter-logic-conformance.ts:190`, `:250`, `:525` |
`9dac1ae01`, PR objectstack-ai#13529's squash commit, which stays as the link:
`$exists` means has-a-value on driver-memory's live mingo path, its
analytics face and driver-mongodb's `translateFilter` (the "last three
key-presence exits") |
| `objectstack-ai#12868` | 1 / 1, `object.form.ts:256` | `c459da6bc`: narrows the
per-option `default` key out of the form-view options vocabulary, which
offered a key nothing on that surface read. Commit `e808890958`, which
wrote this line, names objectstack-ai#12868 as the same offer-vs-door class |

The shas were checked at the base and again at `origin/main`
`288611e3e5`. Every one matches exactly one commit (`git rev-parse
--disambiguate`, count 1). Every one is an ancestor (`git merge-base
--is-ancestor`, exit 0 for 9 of 9). The control leg `e9584681a4` also
exits 0, and the repository is not shallow. For each commit, a grep of
its own message or diff finds the number it replaces. Seven of the nine
name it in the message. `fc9ba76a5` names it in its diff (20 lines,
including its changeset heading), and so does `c459da6bc` (8 lines,
including its changeset heading).

Wordings to check, each true of its commit:
- `object.zod.ts:2731` now reads 「closes that shape, and commit
75b7c24 records that the declaration and the enforcement disagree」.
The measurement was the card's. The commit's changeset records it: "only
the security gate closed that shape while the declaration surface
accepted it".
- `object.zod.ts:2189` reads 「Declared here by commit f11fc61's
maintainer ruling」, and `:2744` reads 「the other half of commit
75b7c24's ruling」. This is stage 3's wording for the same relation
(`object.test.ts`, 「the other half of commit 75b7c24's ruling」): the
commit that landed the ruling and quotes it.
- `object.zod.ts:1049` reads 「That is the whole of what [commit
530c1df] changed here」. Commit `52db1d1f2a` wrote the paragraph.
`530c1df65` is the change it describes.

## Mechanical guard: no code token moves

The check compares leaf tokens with comments stripped, base `03b19d9cfd`
against head `53c9070dfd`. It uses the TypeScript parser's leaf tokens
(TypeScript from the head's lockfile), so template literals are scanned
in context, and it excludes JSDoc nodes. It ran over all 3 touched `.ts`
files. It is the stage-3 instrument, unchanged.

- Real run: 13,624 base tokens (object.zod.ts 8,774, object.form.ts
3,226, filter-logic-conformance.ts 1,624), **0 files with a token
change** (exit 0).
- Comment-insertion control (`object.form.ts`): 0 files changed, as
expected (exit 0).
- Positive control (a declaration inserted into `object.zod.ts`): 1 file
reads DIFFER at token 1629 (exit 1).
- Positive control (one digit changed inside the `objectstack-ai#5322/objectstack-ai#5134` `note`
string in `filter-logic-conformance.ts`): 1 file reads DIFFER at token
889 (exit 1).

Line balance: `object.zod.ts` +15 / -15, `filter-logic-conformance.ts`
+4 / -4, `object.form.ts` +1 / -1. Line counts are equal at base and
head: 3,240, 621 and 751.

## Changeset

This change ships bytes, so a `patch` changeset for `@objectstack/spec`
is included. It says only that the provenance comments were re-anchored.
`Clause-②: no`: no export, key, value or type moves (the guard above).

Measured on the head's built package: `object.zod.ts` is
`src/**/*.zod.ts`, which `files[]` ships verbatim. The rewritten
comments also reach `dist`:
- `9dac1ae01` appears in `dist/data/index.d.ts` (the
`filter-logic-conformance.ts` docblock) and in 4 bundled `.js` files;
- `fc9ba76a5`, `f11fc61c5` and `b37231883` each appear in 22 bundled
`.js` files, and `c459da6bc` in 12;
- the positive control, the pre-existing `object.zod.ts` sentence
「Fail-CLOSED at both points」, appears in 11 bundled `.js` files.

## Gates (head `53c9070dfd`)

- **Citation judging pass, run as CI runs it:** `pnpm
check:issue-citations && node scripts/check-issue-citations.mjs` exits
0. The self-test passes 73 cases in 7 batteries. The live run judged 6
citations across 3 files: 3 resolve (`objectstack-ai#9138` twice, `objectstack-ai#11410`) and 3
resolve as a pull request (`objectstack-ai#13529`, the link).
- **Doc authoring:** `pnpm check:doc-authoring` exits 0.
- **Derived gates:** `node scripts/pm/dispatch-gates.mjs --commands
--repo objectstack-ai/objectstack` at the head derived 79 families, and
all 79 exit 0. `--ran` reports 79 run, 0 NOT MEASURED, 0 unrun, and
exits 0. A full `turbo run build` of `./packages/*` ran first, under the
shared verify lock: 71 of 71 tasks, VERDICT command-exit 0. So no gate
met an unbuilt prerequisite.
- `pnpm --filter @objectstack/spec run check:generated`: under the lock
against that build, `All 15 generated artifacts are up to date`, VERDICT
command-exit 0.
- **Tests and typecheck:**
- `pnpm --filter @objectstack/spec exec vitest run --maxWorkers=2
src/data` under the lock: Test Files 107 passed (107), Tests 3527
passed, 1 todo (3528), VERDICT command-exit 0. It covers every test in
`data/`, among them `object.test.ts`, which reads these schemas.
- The 13 spec suites outside `src/data` that read the touched files'
source text or pin their line numbers, under the lock: Test Files 13
passed (13), Tests 544 passed (544). They are stage 3's 12
(`scripts/{file-description,root-index,skill-map-guards,strictness-ledger}.test.ts`,
`src/api/api-entry-graph.pin.test.ts`,
`src/contracts/scoped-context.test.ts`,
`src/shared/{alias-integrity,evaluated-slot-population,retired-key-migrate-sentence}.test.ts`,
`src/system/constants/platform-object-names.test.ts`,
`src/type-alias-convention.pin.test.ts`, `src/ui/dashboard.test.ts`)
plus `src/shared/union-author-message-pins.test.ts`, which pins
`data/object.zod.ts:855`.
- `pnpm --filter @objectstack/spec typecheck` under the lock exits 0,
including `check:test-typecheck` (53 files, 251 errors, 138 pinned
signatures held).
- **Lint, as a proven narrowing at the head:** `eslint
--no-inline-config --format json` over the 3 touched `.ts` files gives 3
files, 0 errors and 0 warnings. All 3 are in eslint's own population
(`isPathIgnored` is false for each). `eslint.config.mjs` never enables
type-aware linting (no `parserOptions.project`, which its own line 328
states), so a comment edit here cannot move the verdict on any untouched
file. The repo-wide `pnpm lint` is CI's run.

## Acceptance notes

- **Base.** The branch forked from `03b19d9cfd`, stage 3's landing.
`origin/main` then moved two commits (`05077d4c26`, PR objectstack-ai#20532, and
`288611e3e5`, PR objectstack-ai#20536), and neither touches `data/`. `dispatch-gates`
flagged its derivation as stale because `scripts/regen-artifacts.mjs`
had moved, so `origin/main` was merged in (`53c9070dfd`, a clean merge
with no driver-deferred path) before the gates ran. The PR's delta
against `origin/main` is exactly its 4 files. `origin/main` has since
moved two more commits: `7e36a3cd7c` (PR objectstack-ai#20531) and `ba5927f714` (PR
objectstack-ai#20460). Neither touches `data/` or anything the gate derivation reads,
and a re-derivation prints the same 79 commands. A no-driver
`merge-tree` of the head onto `ba5927f714`, from a bare shared clone,
exits 0. So there is no second merge.
- **Open PRs, re-read at 2026-09-29T02:01Z:** 9 open PRs, and none
touches any of the six files. The `data/` files open PRs touch are
objectstack-ai#20458's `analytics*` files, objectstack-ai#20504's `driver/turso.*`, and objectstack-ai#20545's
`filter-number-comparand-declared-type.*`, which is disjoint. Since the
claim, PR objectstack-ai#20460 has landed (`ba5927f714`) without touching
`filter-subtree-provenance.ts`. That file's 3 dead sites are outside
this claim's fence, so they are left for a later stage.
- **The rung.** Two anchored changes also have ADR-0087 entries in
`packages/spec/src/migrations`: `cbp-master-detail-required-forced` for
objectstack-ai#8772, and `form-view-option-default-retired` for objectstack-ai#12868. The second
entry's own header names commit `c459da6bc`. This PR takes the commit
rung, as stages 1 to 3 did. The D3 id is the more durable in-repo
record, if the ruling's first rung is later read to include those
entries.
- **What stays in `data/` after this stage: 58 dead sites.**
- **12 comment sites in files other open work still holds.**
`analytics.zod.ts`, `analytics-strictness-batchd.test.ts` and
`analytics-date-range-two-bound-window.test.ts` hold 5 (objectstack-ai#20300, PR
objectstack-ai#20458). `driver/turso.zod.ts` and `driver/turso.test.ts` hold 4
(objectstack-ai#20437, PR objectstack-ai#20504). `filter-subtree-provenance.ts` holds 3. It was held
by objectstack-ai#20367 and is now free (see above).
- **3 comment sites stage 3 left on purpose.** They are the test-read
`[objectstack-ai#6259]` marker at `api-derivation.ts:163`, the test comment at
`api-derivation.test.ts:232` that names it, and `field.zod.ts:370`,
whose `objectstack-ai#6111` is objectui's number.
- **43 string sites**, left as tokens: 41 test strings (2 of them in the
held analytics and turso test files) and the 2 exported
`AGGREGATION_CASES` note strings in `aggregation-conformance.ts`
(`:398`, `:407`, objectstack-ai#11065), which objectstack-ai#20489's claim holds.
- **Outside `data/`,** the card's other remaining items are unchanged:
the migrations and ui areas, the `liveness/**` notes, the `why` strings,
the `PROVENANCE_WAIVERS` reason, and `rest-server.zod.ts`.
- **The citation gate's reach.** It defers `packages/**/*.test.ts`. No
test file is touched here, so all 3 touched files are in its judging
population.

---
_Generated by [Claude
Code](https://claude.ai/code/session_014EJ1ED8X4MMrT18BhVx4tx)_

---------

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 protocol:data size/xl tests tooling

Projects

None yet

2 participants