Skip to content

feat(spec): comparand-type door — the accepted literal comparand set, enforced once at the shared compile face for all five drivers (#7872) - #8234

Merged
huangyiirene merged 7 commits into
mainfrom
claude/issue-7872-comparand-type-door
Aug 13, 2026
Merged

huangyiirene merged 7 commits into
mainfrom
claude/issue-7872-comparand-type-door

Conversation

@huangyiirene

Copy link
Copy Markdown
Collaborator

Fixes #7872

Implements the maintainer ruling on this card (comment 5265944890, 「同意你的决策」): the shared filter-compilation face in packages/spec defines the accepted literal comparand-type set as the measured superset — string | number | bigint | boolean | null | Date (#7956's divergence matrix; the "any JSON-representable literal" wording was rejected because bigint is not JSON-representable yet accepted by 4/5 drivers) and refuses everything else loudly at the compile face, with code: INVALID_FILTER + status: 400 + guidance naming the accepted set.

One PM mechanism assumption was falsified — the measured call graph, and the adjusted wiring

The dispatch assumed parseFilterAST is on every driver's path. Measured on this branch's base:

The object form is exactly how the #7956 matrix (and the mongo worst cell) arrives, so a door wired in parseFilterAST alone would have missed the dominant form — the NormalizedFilterSchema lesson again. The adjusted wiring keeps the ruling's substance intact:

Net: every platform path to every driver passes the door; parseFilterAST is now on the object form's path wherever it previously was at all. Flagged here rather than silently absorbed; if the PM reads assumption 1's STOP clause as outranking this, the wiring commit is cleanly separable.

The bigint cell — how the ruling's two sentences were reconciled

The ruling both (a) puts bigint in the accepted set and (b) says the memory BigInt crash cell "becomes a refusal". A bigint passed through verbatim still crashes mingo (Query.compile → JSON.stringify), and driver-local patches are frozen (#5499) — so (a) and (b) cannot both be literal. The approved recommendation's own sentence ("mongo's silent-edit cell and memory's crash cell both die at the door without touching frozen drivers") is implementable, and that is what this PR does:

  • an exact-range bigint (|v| ≤ 2^53) is narrowed copy-on-write to its JS number at the door — the same declared-conversion pattern as RemoteTransport's Date → ISO — so no driver path ever sees a bigint, and the memory cell becomes a working compile (the conformance table pins {qty: {$eq: BigInt(100)}} matching its row on all five drivers);
  • a bigint beyond ±2^53 is refused loudly (precision loss must not answer silently — the exact evil this thread outlaws);
  • direct transport callers keep libsql's native bigint binding (remote-transport-comparand-refusal.test.ts's pin is untouched).

The #6915 citation in the ruling is a mis-cite (that card is driver-memory tenant isolation); the BigInt crash cell is this card's own memory×BigInt cell. Both readings are surfaced in the report's open_questions for the maintainer to confirm.

Both directions pinned — FILTER_COMPARAND_TYPE_CASES, run by all five drivers

New conformance table in spec/data (registered in scripts/check-driver-conformance.mjs; the matrix is now 45 covered / 0 DEBT / 0 exempt):

  • accepted direction: each of the six types compiles on every driver path — executed on better-sqlite3 (driver-sql), sql.js (driver-sqlite-wasm), libsql local (driver-turso), mingo (driver-memory), and — server-free, as on measure: how each driver answers the six non-JSON comparand types (#7872 restart condition b) #7956 and the sibling conformance suites, since a live mongod is unreachable in CI — translateFilter + the mongodb package's own BSON round trip (driver-mongodb). Stated in that suite as the accepted substitute.
  • refused direction: undefined (operator form, implicit form — the mongo worst cell — and as an $in member), function, Symbol, Map, Set, class instance, plain object in a scalar slot, oversized bigint — all refused at the door with the envelope, and the suites assert no driver ever runs (the engine suite pins zero driver reads/writes on refusal).
  • reverse-direction pins, so the door's work cannot be mistaken for driver changes: driver-memory pins that the direct-path bigint crash still exists (mingo unfixed, frozen); driver-mongodb pins that {qty: undefined} still BSON-encodes to {} = match everything on the direct path. Both notes say when to retire the pins.

SQL-family allowlists reconciled to the door, not duplicated

  • driver-sql isBindableComparand / isRenderableTextComparand now delegate type membership to isAcceptedFilterComparand; refusal messages quote ACCEPTED_FILTER_COMPARAND_TYPES_SENTENCE. Two recorded driver-local extras stay, each with its reason in place: the undefined → true arm (unreachable behind the drivers: undefined 比较数被发射器读作 null、却被守卫/校验读作「值」—— turso local 抛裸 knex 错、remote 静默答 IS NULL(实测,origin/main) #6050 walk, which owns the purpose-written undefined refusal) and ArrayBuffer.isView (blob bindables — a direct-caller surface the engine door does not admit; nothing on engine paths uses them, measured by grep and by the suites).
  • driver-turso serializeComparand delegates the scalar membership to the same predicate; its pinned refusal wording ("must be a string, number, bigint, boolean, null or Date") is now the shared constant, byte-identical, so reconciling changed no message.
  • Their ADR-0112 envelopes are untouched — the ruling's "keep their local raise but source the SET from the door" fallback was not needed beyond the two recorded extras above.

Boundaries the door deliberately does not judge (each has a recorded owner)

Changeset: minor, argued from the diff

Precedent #3746 (acceptance narrowing at a validation face, minor) fits what this diff does: no authorable key is removed, no export is removed, and every newly-refused input was measured answering a crash, a silent wrong row count, or a silent match-all — not a documented capability. The one previously-"working" behavior that narrows is mongo/memory deep-equality via $eq: {plain object} through engine paths (direct driver calls unchanged) — measured divergent 3-vs-2 across drivers, which is the divergence the ruling closes. #4583-major (removal of a declared key with migration prescription) does not describe this diff. Not declared-breaking ⇒ no ADR-0087 marker required; the changeset carries the full caller-facing delta anyway.

Verification

All local runs serialized on the shared verify lock, NODE_OPTIONS=--max-old-space-size=4096, scoped filters.

Pre-merge batch (base 37b82ed):

  • @objectstack/spec test — full suite green (386 files; earlier baseline run: 10236 tests, the two reds it surfaced — a 521-char refusal message and the stale export snapshots — were fixed/regenerated and the suite re-ran green).
  • @objectstack/objectql test — 191/192 green; the one red was engine-filter-array-lowering.test.ts's pass-through pin for { $eq: { $in: 'not-an-operator' } }, a fixture that pinned exactly the limb the ruling re-judges (plain object in a scalar slot). Re-judged per the fixture-triage rule: the case now asserts the door's TYPE refusal at where.stage.$eq and that the message never misreads the inner $in data key as a malformed operator (not.toContain('requires an ARRAY')) — the old pin's surviving concern, kept.
  • @objectstack/metadata-protocol (Door 1), all five driver suites, @objectstack/service-analytics, @objectstack/runtime + @objectstack/rest — green (runtime: 145 files / 2195 tests).
  • typecheck ×7 (spec, objectql, five drivers) — green.
  • check:driver-conformance — 45 covered / 0 DEBT / 0 exempt (was 40/0/0; the five new cells are the door column, all covered in this PR). check:nul-bytes — green (7424 files).
  • check:generated — 2 of 13 stale (api-surface/, export-origins/ — the 13 new exports), regenerated with --fix, additions only.

Post-merge lap (merged origin/main @ fa48973; incoming touched objectql/src/engine.ts — the same file as the door wiring — and spec/src/contracts/*): rebuilt the closure, then re-ran the moved packages — spec check:generated green (artifacts still current), spec test 386/386 files green, objectql test 193/193 files green (the re-judged pin and the new door suite included).

Examples validate ×3: all green (app-crm, app-showcase, app-todo — "Validation passed"; todo's two draft-flow warnings pre-exist this branch). The first two attempts failed on this worktree's own build state (packages/cli and connector-mcp had no dist — command validate not found / missing module), not on the diff; building the examples' dependency closures by package name fixed both.

Out-of-scope findings


Generated by Claude Code

claude added 4 commits August 12, 2026 18:38
…rced at the shared compile face (#7872)

The measured superset string | number | bigint | boolean | null | Date is
defined in packages/spec (filter-comparand-type.ts), enforced by
parseFilterAST on everything it returns and by the engine's lowering seam on
the object form; everything else is refused with the INVALID_FILTER / 400
envelope naming the accepted set. bigint is narrowed copy-on-write to its
exact number (refused beyond 2^53), which kills the driver-memory mingo
crash cell without touching the frozen driver; the mongo silent-edit cell
({qty: undefined} wiring to {} = match everything) becomes a refusal at the
door. SQL-family allowlists source their set membership and wording from the
door instead of local copies. FILTER_COMPARAND_TYPE_CASES conformance table
added and consumed by all five driver suites.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0123k4cam2jEAkPmbJeoaY3r
…d-type door exports (#7872)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0123k4cam2jEAkPmbJeoaY3r
@vercel

vercel Bot commented Aug 12, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Aug 12, 2026 11:45pm

Request Review

@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 4 package(s): @objectstack/driver-sql, @objectstack/driver-turso, @objectstack/objectql, @objectstack/spec.

113 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/spec)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/spec)
  • content/docs/automation/approvals.mdx (via @objectstack/spec)
  • content/docs/automation/connectors.mdx (via @objectstack/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via @objectstack/objectql, packages/spec)
  • content/docs/concepts/north-star.mdx (via @objectstack/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/driver-sql, @objectstack/driver-turso, @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via packages/objectql, @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/cli.mdx (via @objectstack/driver-turso, @objectstack/spec)
  • content/docs/deployment/environment-variables.mdx (via @objectstack/driver-turso)
  • content/docs/deployment/migration-from-objectql.mdx (via @objectstack/objectql)
  • content/docs/deployment/self-hosting.mdx (via @objectstack/driver-turso)
  • content/docs/deployment/tenancy-modes.mdx (via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/deployment/vercel.mdx (via @objectstack/objectql)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/glossary.mdx (via @objectstack/driver-sql, @objectstack/driver-turso)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/objectql, @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via @objectstack/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/data-service.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/examples.mdx (via packages/objectql, @objectstack/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via @objectstack/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via @objectstack/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/driver-sql, @objectstack/objectql, @objectstack/spec)
  • content/docs/kernel/services.mdx (via @objectstack/objectql, @objectstack/spec)
  • content/docs/permissions/authentication.mdx (via @objectstack/objectql)
  • content/docs/permissions/authorization.mdx (via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/permissions/system-context.mdx (via packages/objectql, packages/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/anatomy.mdx (via @objectstack/driver-sql)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/objectql, @objectstack/spec)
  • content/docs/plugins/packages.mdx (via @objectstack/driver-sql, @objectstack/driver-turso, @objectstack/objectql, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/http-protocol.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/driver-sql, @objectstack/objectql, @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/driver-sql, @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/driver-sql, packages/objectql, @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/objectql, @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/apps.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/field-grouping-and-order.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

⛔ 7 release-owned page(s) also reference the affected code. These are read-only:

  • content/docs/releases/implementation-status.mdx (via @objectstack/driver-sql, @objectstack/objectql, @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v16.mdx (via @objectstack/spec)
  • content/docs/releases/v17.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/spec)

content/docs/releases/ is RELEASE-OWNED (AGENTS.md "Documentation Guardrails"): release
notes are written centrally at release time, and a code PR that edits them is the exact PR
that guardrail exists to stop. They are still audited — read-only. If one of them is actually
wrong, file an issue or open a dedicated docs-only PR; do not edit it here.

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

Copy link
Copy Markdown
Collaborator Author

os-regen relay lap over #8230 / #8232 — complete. Merge sha 2abb1e8ee (merges origin/main @ 29488ccae), regen commit 81f6752e4, both pushed; head is 81f6752e4. Ran via scripts/pm/os-regen-merge.sh (merge → take main's side of every os-regen path → commit → regenerate from the merged tree; the pre-commit deferral marker recorded and cleared itself).

Sibling-entry assertions on the regenerated artifacts:

Gates and suites on the new head: check:generated — all 13 artifacts up to date; spec scoped suites (comparand-type door, filter, operator vocabulary, text conformance, export-origins) — 5/5 files green; objectql scoped (door suite + lowering suite) — 2/2 files, 58 tests green. One false red on the way: the recreated worktree had no @objectstack/types dist, so the first objectql run failed at import resolution — fixed by building the dependency closure, not by touching the diff. engine.ts re-merged cleanly (main does not carry #8236 yet, as expected).

PR stays draft; over to the PM for gate verification on 81f6752e4 and the flip.


Generated by Claude Code


Generated by Claude Code

…erased engine option bags (#7872)

The door suite's registerObject(deal) call was missing the required
packageId (the +1 over objectql's frozen TEST_DEBT 355); and its option
bags used bare 'as any', which the query-options-erasure ratchet counts
(240 -> 245). The bags now type-check uncast — FilterCondition's index
signature admits the deliberately off-contract comparands, which is the
point of the door — except the one FilterArray case, which carries the
sanctioned 'as unknown as EngineQueryOptions' naming the contract it
bypasses (not counted).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0123k4cam2jEAkPmbJeoaY3r

Copy link
Copy Markdown
Collaborator Author

Patch round for the two red farm jobs — fixed, new head dee462fdb. Both reds were mine (verified — neither reproduces from origin/main content; both point at the door suite added in this PR):

  • TypeScript Type Check (check:type-check-debt --re-measure): objectql TEST_DEBT 355 → 356 (+1). The new error was engine-comparand-type-door.test.ts's registerObject(deal) missing the required packageId argument (TS2554 — the sibling lowering suite carries the same call inside the frozen 355; mine was the increment). Fixed by passing the id (registerObject(deal, 'test')), not by raising the ledger. Re-measured locally with the exclusion lifted, exactly as the gate counts: 355 — back at the frozen number, zero errors from this PR's files (exit 0).
  • ESLint (check:query-options-erasure): test surface 240 → 245. The door suite's option bags used bare as any. Now they type-check uncast — FilterCondition's index signature admits the deliberately off-contract comparands, which is precisely why the runtime door has to exist (noted in the test) — except the one FilterArray case, which carries the sanctioned as unknown as EngineQueryOptions form naming the contract it bypasses (not counted). Local re-run: ratchet holds, 240 test sites, "none new" (exit 0).

Behavior re-verified after the edits: door suite + lowering suite — 2 files / 58 tests green (exit 0). Diff is one test file; no source, no artifacts, no changeset delta. PR stays draft on dee462fdb for the farm re-read.


Generated by Claude Code


Generated by Claude Code

@huangyiirene
huangyiirene marked this pull request as ready for review August 13, 2026 00:08
@huangyiirene
huangyiirene added this pull request to the merge queue Aug 13, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Aug 13, 2026
@github-actions

Copy link
Copy Markdown
Contributor

⛔ merge queue 构建失败 — 先分诊,再决定要不要重排

队列构建 31653423757 红了。队列跑的是全量套件(PR 侧 CI 只跑 affected 子集),
所以失败的测试可能在本 PR 没碰过的包里 —— 那不是重排能修的。每次盲目重排都会让排在后面的所有 PR 重建一轮。

失败的 job(日志抽取,best effort):

  • Test Core (2/3) — 失败步骤: Run this shard's tests

    �[41m�[1m FAIL �[22m�[49m test/watch-dot-root.test.ts�[2m > �[22mFileSystemRepository watcher — dot-rooted watch root (#7150)�[2m > �[22msees an external edit when the root is under a dot-directory
    

历史信号:

  • 本 PR 过去 24h 无队列失败记录(首次)。
  • 过去 24h 队列共有 7 个失败构建(不含本次)。

分诊清单:

  1. 失败测试在本 PR 改动的包里 → 真回归,修 PR。
  2. 失败测试与本 PR 无关 → 在其他 PR 的同类评论里搜同名测试;出现过 ⇒ flaky 实锤,开 issue 修/隔离那条测试。修好前重排只会再烧一轮全队列。
  3. 两者都不是 → 可能与同组 PR 语义冲突;等前面的 PR 落地或失败出队后再重排一次即可,不要连续重排。

Generated by Claude Code · merge-queue-triage workflow (#4859)

Merged via the queue into main with commit 7e5af5c Aug 13, 2026
27 checks passed
@huangyiirene
huangyiirene deleted the claude/issue-7872-comparand-type-door branch August 13, 2026 00:53
huangyiirene pushed a commit that referenced this pull request Aug 13, 2026
os-regen merge lap for landing: merge origin/main (committed first, per
scripts/pm/os-regen-merge.sh), then wholesale regen of the os-regen'd
references/api pages from the merged tree. Sibling assertions on the
regenerated tree: #8230 retirement state (def names only in retirement
prose + migration entry), #8232 currency fraction-digit guard, #8234's
13 comparand-door exports in api-surface/data.json, #8236's groupBy
union row all survive; MONGODB_MULTI_TENANT_UNSUPPORTED stays absent
from the ledger and the references ErrorCode union reads main-minus-one
(+265 -> +264; the union grew by 2 on main since the previous lap).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0123k4cam2jEAkPmbJeoaY3r
huangyiirene pushed a commit that referenced this pull request Aug 13, 2026
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Aug 17, 2026
…ger — a boot refusal never reaches a wire envelope (objectstack-ai#8035) (objectstack-ai#8239)

* fix(spec): MONGODB_MULTI_TENANT_UNSUPPORTED leaves the error-code ledger (objectstack-ai#8035)

The code was registered under @objectstack/driver-mongodb (objectstack-ai#3724) but no
response envelope can carry it: the CLI boot handler rethrows the tenancy
refusal pre-HTTP and aborts, and the one request-reachable trigger
(assertObjectsNotTenantScoped via syncObjectSchema inside the metadata
protocol's ensureObjectStorage) is swallowed by a documented best-effort
catch. Registered-but-unemittable is the class the ledger header names;
precedent OVERLAY_PERSISTENCE_FAILED / objectstack-ai#5783 — this instance is the sister
shape (never had a wire path, rather than lost it), now documented in the
header.

The boot refusal stays untouched: MULTI_TENANT_UNSUPPORTED_CODE, the error
class, its message, and the CLI's duck-typed literal match. A new
driver-mongodb test pins the code literal (with the ledger row gone it is
the only cross-package guard on the literal serve.ts matches); the ledger
test now asserts the wire vocabulary refuses the retired string. Reference
docs regenerated (ErrorCode enum 264 -> 263).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0123k4cam2jEAkPmbJeoaY3r

* docs(spec): regenerate references from the merged tree (objectstack-ai#8035)

os-regen merge lap for landing: merge origin/main (committed first, per
scripts/pm/os-regen-merge.sh), then wholesale regen of the os-regen'd
references/api pages from the merged tree. Sibling assertions on the
regenerated tree: objectstack-ai#8230 retirement state (def names only in retirement
prose + migration entry), objectstack-ai#8232 currency fraction-digit guard, objectstack-ai#8234's
13 comparand-door exports in api-surface/data.json, objectstack-ai#8236's groupBy
union row all survive; MONGODB_MULTI_TENANT_UNSUPPORTED stays absent
from the ledger and the references ErrorCode union reads main-minus-one
(+265 -> +264; the union grew by 2 on main since the previous lap).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0123k4cam2jEAkPmbJeoaY3r

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Aug 17, 2026
…ered/virtualScroll (ADR-0049, objectstack-ai#7176) (objectstack-ai#8252)

* refactor(spec)!: retire pass-through-only list-view keys striped/bordered/virtualScroll (ADR-0049, objectstack-ai#7176)

Maintainer ruling 2026-08-10: every measured reader copies the key forward
and ObjectGrid, where the chains end, never applies any of the three —
copy-without-apply is dead in effect. retiredKey() tombstones on
ListViewSchema (radiating into ObjectListViewSchema), ADR-0087 D2 conversion
view-list-passthrough-keys-removed + D3 chain step + six
RETIRED_KEYS_BY_MAJOR[17] entries, ledger rows flipped to dead with the
retirement note, form inputs pruned, docs updated, changeset with FROM->TO.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0123k4cam2jEAkPmbJeoaY3r

* test(spec): match the striped prescription's actual casing in the pin test

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0123k4cam2jEAkPmbJeoaY3r

* merge origin/main (os-regen artifacts taken from main; regeneration follows)

* chore(spec): regenerate artifacts after merging origin/main (4dc1c7d relay: objectstack-ai#8230/objectstack-ai#8232/objectstack-ai#8234/objectstack-ai#8236/objectstack-ai#8239)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0123k4cam2jEAkPmbJeoaY3r

---------

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 1, 2026
…ving grounds (objectstack-ai#13196)

The maintainer lifted the objectstack-ai#5499 investment freeze for driver-mongodb and
driver-memory on 2026-08-11 (recorded in the head note of
packages/spec/src/data/aggregation-conformance.ts), but ~13 in-code sites
still asserted it in the present tense, three of them as the REASON for a
structural choice.

Group A (present-tense claims): re-dated so each records what was true when
it was written, keeping the objectstack-ai#5499 anchor.

Group B (three design rationales in packages/spec/src/data/): each rationale
survives losing the freeze premise and is restated on the surviving ground —
the objectstack-ai#7956 divergence matrix and the objectstack-ai#7872 / objectstack-ai#8234 shared-face rulings. The
freeze is kept on record as the premise the argument was FIRST made from,
never silently deleted.

Group C (dated/historical records) is untouched, and so is
packages/spec/src/migrations/registry.ts, which is generated.

packages/runtime/src/datasource-autoconnect.test.ts asserted RETIREMENT, not
freeze, on the same anchor: re-anchored to objectstack-ai#5704 / objectstack-ai#6664, the mechanism that
actually owns it and that check:driver-memory-census enforces.

filter-logic-conformance.ts:179 is deliberately left alone: it is the table
cell for the behavioural divergence objectstack-ai#13166 owns, and that card is undecided.

Co-authored-by: Claude <noreply@anthropic.com>
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Oct 7, 2026
…rds instead of a tracker number (stage 6) (objectstack-ai#21593)

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

Stage 6 of the `domain:spec` lane's share of the runtime-string
burn-down (ruling `5902360492`, form D): class (c), the case notes and
names of the eight shared conformance modules in `packages/spec/src`,
with the two test seams that bind their text. Every rewritten note or
name now states in words what the cited decision was, or drops a
citation its sentence already explained. Text only.

## What changed

- **58 messages / 68 tracker ids / 40 distinct cards** in eight modules:
`contracts/metadata-service-roundtrip-conformance.ts` and, in `data/`,
`aggregation-conformance.ts`, `filter-comparand-type-conformance.ts`,
`filter-logic-conformance.ts`, `filter-text-conformance.ts`,
`filter-text-operator-declared-type.ts`, `temporal-conformance.ts`,
`value-roundtrip-conformance.ts`. Fifty-two are `note` / `why` texts and
six are case names.
- **The selector seam**
(`packages/lint/src/validate-empty-combinators.test.ts:275`): selects
the same four empty-combinator cases by a phrase the rewritten notes
carry instead of `objectstack-ai#5322` (A3 below).
- **The name-pin seam**
(`packages/services/service-analytics/src/__tests__/icontains-dialect-sql.test.ts:369`):
pins the renamed infix `icontains` case verbatim, the same strength (A4
below).
- One `@objectstack/spec` **patch** changeset, `Clause-②: no`: the
conformance tables ship in the package's `dist` (measured: the new note
text is in `dist/data/index.mjs`; the two test files ship nowhere, since
`@objectstack/lint` and `@objectstack/service-analytics` publish `dist`
only and neither dist carries the seam text, with the package's own
symbols as the positive control).
- No generated artifact moves (`check:generated` green; the tables are
values behind annotated types, so the API surface is unchanged).

## Census at the base (A1)

Stage 3's instrument (`census.cjs`, byte-identical copy, md5
`e5fe562290fcd1ac392bd560cb86d861`) at base `cc645f2385`: class (c) is
**58 messages / 68 ids in 8 files, 40 distinct cards**, exactly stage
3's count (the `[c-SELECTOR]` 4 / 7 and `[c-NAMEPIN]` 1 / 1 rows
included). Under the ~60-card bar, so one stage. Lines are the base's.

| file (under `packages/spec/src`) | line:id | messages / ids | field |
consumer |
|:--|:--|--:|:--|:--|
| `contracts/metadata-service-roundtrip-conformance.ts` | 194:objectstack-ai#6725 ·
202:objectstack-ai#6725 · 280:objectstack-ai#7378 · 296:objectstack-ai#7378 · 304:objectstack-ai#7378 | 5 / 5 | `why` | none
prints it: both runners title a row by its `id`; the text is a reader's
note in the shared table |
| `data/aggregation-conformance.ts` | 342:objectstack-ai#6409 · 398:objectstack-ai#11065,objectstack-ai#11151 ·
407:objectstack-ai#11065 · 417:objectstack-ai#11152 419:objectstack-ai#11249 · 535/550/561:objectstack-ai#15546 · 577/596:objectstack-ai#6401
| 9 / 11 | `note` | the assertion message in the driver-memory,
driver-sql, driver-turso, driver-sqlite-wasm, objectql and
mongodb-translation suites |
| `data/filter-comparand-type-conformance.ts` | 153:objectstack-ai#7956 · 160:objectstack-ai#7872 ·
272/282/289:objectstack-ai#19757 (names) · 178:objectstack-ai#5234 · 191:objectstack-ai#6050 (notes) | 7 / 7 | 5
`name`, 2 `note` | names are test titles in five driver suites and in
`filter-save-door-face-parity.test.ts`; notes are assertion messages |
| `data/filter-logic-conformance.ts` | 339/362:objectstack-ai#3774 · 420:objectstack-ai#5322 ·
426:objectstack-ai#5322,objectstack-ai#5134 · 432/438:objectstack-ai#5322,objectstack-ai#5297 · 470:objectstack-ai#5298 · 476:objectstack-ai#5146 ·
503/509:objectstack-ai#5298,objectstack-ai#13356 · 539:objectstack-ai#5299,objectstack-ai#5962 · 561/567/573/579/595:objectstack-ai#20444 | 16
/ 22 | `note` | the assertion message in the eleven filter-logic
harnesses; **the one selector**:
`validate-empty-combinators.test.ts:275` picks the four `objectstack-ai#5322` notes |
| `data/filter-text-conformance.ts` | 300:objectstack-ai#8934 (name) · 309:objectstack-ai#8934 ·
349:objectstack-ai#6518,objectstack-ai#6682 · 428:objectstack-ai#4706 · 436:objectstack-ai#5710,objectstack-ai#6993 · 452:objectstack-ai#5240 | 6 / 8 | 1
`name`, 5 `note` | names are test titles in the text-conformance suites;
**the one name pin**: `icontains-dialect-sql.test.ts:369`; notes are
assertion messages |
| `data/filter-text-operator-declared-type.ts` | 392:objectstack-ai#8296 · 553:objectstack-ai#8371 ·
554:objectstack-ai#8296 | 3 / 3 | `note` | the assertion message in
`engine-text-operator-declared-type-door.test.ts` |
| `data/temporal-conformance.ts` | 211:objectstack-ai#3773 · 215/216:objectstack-ai#3777 ·
220:objectstack-ai#20600 (row `why`) · 270:objectstack-ai#3777 · 393:objectstack-ai#3773 · 448/457:objectstack-ai#1874 ·
549:objectstack-ai#3994 (case `note`) | 9 / 9 | 4 `why`, 5 `note` | a row `why` is
seeded as DATA into each harness's `why` string column (never filtered
on); a case `note` is an assertion message |
| `data/value-roundtrip-conformance.ts` | 213:objectstack-ai#11535 · 225/226:objectstack-ai#11782 |
3 / 3 | `note` | printed in the TEST TITLE `round-trips NAME (NOTE)` in
five driver suites |

No non-test code reads any of these texts (every `.note` / `.why` read
outside a test is another table's: `RETIRED_FILTER_OPERATORS`, the
driver-conformance ledger).

## Delivered (A2): each site, the decision as read, the new words

Each cited card was read with its body and every comment; the decision
column names the record (comment id, or the landing record where the
card carries none). Line = the base line of the edit anchor. "Old → new"
shows only the words that changed.

| file:line | ids | decision as read | old → new |
|:--|:--|:--|:--|
| `contracts/metadata-service-roundtrip-conformance.ts:194` | objectstack-ai#6725 |
objectstack-ai#6725: card answers 404 here; landing record: changesets 1507ba3 /
bbee302 (objectql and spec CHANGELOGs):
MetadataFacade.register('object') wrote into a map none of its own
object reads consulted | objectstack-ai#6725 would have failed → that catches an
object write landing in a store none of the object reads consult — the
hole a shipped implementation once fell through |
| `contracts/metadata-service-roundtrip-conformance.ts:202` | objectstack-ai#6725 |
objectstack-ai#6725: card answers 404 here; landing record: changesets 1507ba3 /
bbee302 (objectql and spec CHANGELOGs):
MetadataFacade.register('object') wrote into a map none of its own
object reads consulted | produced objectstack-ai#6725 → let an object write land in
the generic store while every object read looked elsewhere |
| `contracts/metadata-service-roundtrip-conformance.ts:280` | objectstack-ai#7378 |
objectstack-ai#7378: maintainer ruling 5261734580 (2026-08-12): row 1 refuse a
name/data.name disagreement, row 2 one answer converged with
check:meta-type-normalized, row 3 refuse non-object data | objectstack-ai#7378,
maintainer 2026-08-12, row 1 → maintainer 2026-08-12, row 1 of the
round-trip ruling |
| `contracts/metadata-service-roundtrip-conformance.ts:296` | objectstack-ai#7378 |
objectstack-ai#7378: maintainer ruling 5261734580 (2026-08-12): row 1 refuse a
name/data.name disagreement, row 2 one answer converged with
check:meta-type-normalized, row 3 refuse non-object data | objectstack-ai#7378,
maintainer 2026-08-12, row 2 → maintainer 2026-08-12, row 2 of the
round-trip ruling |
| `contracts/metadata-service-roundtrip-conformance.ts:304` | objectstack-ai#7378 |
objectstack-ai#7378: maintainer ruling 5261734580 (2026-08-12): row 1 refuse a
name/data.name disagreement, row 2 one answer converged with
check:meta-type-normalized, row 3 refuse non-object data | objectstack-ai#7378,
maintainer 2026-08-12, row 3 → maintainer 2026-08-12, row 3 of the
round-trip ruling |
| `data/aggregation-conformance.ts:342` | objectstack-ai#6409 | objectstack-ai#6409: card body,
executing the objectstack-ai#6188 split ruling: count_distinct stays declared and is
enforced as distinct non-null values on the SQL family | objectstack-ai#6409: →
count_distinct was kept and enforced on every face, as distinct non-null
values: |
| `data/aggregation-conformance.ts:398` | objectstack-ai#11065 objectstack-ai#11151 | objectstack-ai#11065: card
answers 404 here; landing record: driver-memory changeset 2095040:
avg/sum over a boolean answer the same numbers as every SQL face (a
boolean is worth 1 or 0) · objectstack-ai#11151: PR objectstack-ai#12819 (triage 5446817173):
driver-mongodb's boolean aggregand answers the ruled values too |
objectstack-ai#11065/objectstack-ai#11151: an arithmetic accumulator that drops booleans answers its
identity 0 here — a plausible number, which → A boolean aggregand is
worth 1 or 0 on every face — driver-memory and then driver-mongodb were
both moved onto that answer: an arithmetic accumulator that drops
booleans answers its identity 0 here — a plausible number, which |
| `data/aggregation-conformance.ts:407` | objectstack-ai#11065 | objectstack-ai#11065: card answers
404 here; landing record: driver-memory changeset 2095040: avg/sum over
a boolean answer the same numbers as every SQL face (a boolean is worth
1 or 0) | objectstack-ai#11065: the rate-over-a-flag-column shape (an SLA-violation
rate, a win rate). → The rate-over-a-flag-column shape (an SLA-violation
rate, a win rate) that driver-memory answered null for until it counted
a boolean as 1 or 0. |
| `data/aggregation-conformance.ts:417` | objectstack-ai#11152 objectstack-ai#11249 | objectstack-ai#11152:
maintainer ruling 2026-08-28, option A (changeset f6fa22c in the driver
CHANGELOGs; decision request 5446817173): booleans aggregate as numbers,
min/max answer 0/1 · objectstack-ai#11249: ruling 5386670755 (false/true for min/max
over a boolean), superseded by objectstack-ai#11152's 2026-08-28 ruling | objectstack-ai#11152
ruling (2026-08-28): booleans aggregate as numbers with no per-aggregate
exception, so the order statistics answer 0/1 in the same domain sum/avg
answer in — not false/true (objectstack-ai#11249, superseded) → Ruled 2026-08-28:
booleans aggregate as numbers with no per-aggregate exception, so the
order statistics answer 0/1 in the same domain sum/avg answer in — not
the false/true an earlier ruling had chosen, which this one superseded |
| `data/aggregation-conformance.ts:535` | objectstack-ai#15546 | objectstack-ai#15546: ruling
5572006116 (option A): a non-empty group whose aggregand is NULL in
every row sums to 0 on every face | objectstack-ai#15546: → Ruled: a non-empty group
whose aggregand is NULL in every row sums to 0. |
| `data/aggregation-conformance.ts:550` | objectstack-ai#15546 | objectstack-ai#15546: ruling
5572006116 (option A): a non-empty group whose aggregand is NULL in
every row sums to 0 on every face | objectstack-ai#15546: the reachability control for
the → The reachability control for the all-NULL-sums-to-0 |
| `data/aggregation-conformance.ts:561` | objectstack-ai#15546 | objectstack-ai#15546: ruling
5572006116 (option A): a non-empty group whose aggregand is NULL in
every row sums to 0 on every face | objectstack-ai#15546: the partial-null control →
The partial-null control for the same ruling |
| `data/aggregation-conformance.ts:577` | objectstack-ai#6401 | objectstack-ai#6401: dev report
5229051388 (PR objectstack-ai#6849): GroupByNode.alias is enforced, not removed; every
SQL face projects the group under alias ?? field | objectstack-ai#6401: t → A
structured group node's `alias` names the projected group column on
every face — the SQL faces that ignored it were made to honour it. T |
| `data/aggregation-conformance.ts:596` | objectstack-ai#6401 | objectstack-ai#6401: dev report
5229051388 (PR objectstack-ai#6849): GroupByNode.alias is enforced, not removed; every
SQL face projects the group under alias ?? field | objectstack-ai#6401: the degenerate
alias. Its twin above → The degenerate alias, under the same every-face
`alias` rule. Its twin above |
| `data/filter-comparand-type-conformance.ts:153` | objectstack-ai#7956 | objectstack-ai#7956:
ACCEPT 5264821447: a measurement only; its control cell ({qty: {$eq:
100}}) returned the row on every driver, so the zeros were real answers
| objectstack-ai#7956 control cell → control cell of the cross-driver comparand-type
measurement |
| `data/filter-comparand-type-conformance.ts:160` | objectstack-ai#7872 | objectstack-ai#7872:
maintainer ruling 5265944890 and ACCEPT 5273103599 (PR objectstack-ai#8234): a shared
comparand-type door; a safe-range bigint narrows to its exact number,
beyond 2^53 is refused | (objectstack-ai#7872) → at the shared comparand door |
| `data/filter-comparand-type-conformance.ts:178` | objectstack-ai#5234 | objectstack-ai#5234:
ACCEPT 5217619370 (PR objectstack-ai#6296): a non-$field object member of $in/$nin and
an object LIKE comparand are refused; each member is judged on its own |
right (objectstack-ai#5234) → right, each judged like a scalar comparand |
| `data/filter-comparand-type-conformance.ts:191` | objectstack-ai#6050 | objectstack-ai#6050:
ruling B 5211349926: an undefined comparand is refused loudly; null
keeps its meaning as the null predicate | (objectstack-ai#6050's untouched half) — → —
the half left untouched when an undefined comparand was ruled a loud
refusal — so |
| `data/filter-comparand-type-conformance.ts:272` | objectstack-ai#19757 | objectstack-ai#19757:
ruling 5793368540 (letter 乙): an array in the implicit-equality slot is
refused at the shared face, for every driver at once | (objectstack-ai#19757) → at the
shared face |
| `data/filter-comparand-type-conformance.ts:282` | objectstack-ai#19757 | objectstack-ai#19757:
ruling 5793368540 (letter 乙): an array in the implicit-equality slot is
refused at the shared face, for every driver at once | (objectstack-ai#19757) → at the
shared face |
| `data/filter-comparand-type-conformance.ts:289` | objectstack-ai#19757 | objectstack-ai#19757:
ruling 5793368540 (letter 乙): an array in the implicit-equality slot is
refused at the shared face, for every driver at once | too (objectstack-ai#19757) →
too, at the shared face |
| `data/filter-logic-conformance.ts:339` | objectstack-ai#3774 | objectstack-ai#3774: defect:
driver-sql OR-ed a $or branch's own keys and operators; rule: everything
inside one filter object ANDs at every depth (card body) | objectstack-ai#3774:
compiled → A $or combines its branches, never the keys inside one: a
driver that OR-ed a branch's own keys compiled this |
| `data/filter-logic-conformance.ts:362` | objectstack-ai#3774 | objectstack-ai#3774: defect:
driver-sql OR-ed a $or branch's own keys and operators; rule: everything
inside one filter object ANDs at every depth (card body) | objectstack-ai#3774: a
single-key branch is miscompilable too — the operator map is looped with
the same → A single-key branch is miscompilable too — that driver looped
the operator map with the same OR |
| `data/filter-logic-conformance.ts:420` | objectstack-ai#5322 | objectstack-ai#5322: maintainer
ruling 5185589944: every face reduces an empty combinator to its boolean
identity ($and:[] all rows, $or:[] none, {} TRUE, $not:{} none), whole
tree | objectstack-ai#5322: a → Ruled: every face reduces an empty combinator to its
boolean identity. A |
| `data/filter-logic-conformance.ts:426` | objectstack-ai#5322 objectstack-ai#5134 | objectstack-ai#5322:
maintainer ruling 5185589944: every face reduces an empty combinator to
its boolean identity ($and:[] all rows, $or:[] none, {} TRUE, $not:{}
none), whole tree · objectstack-ai#5134: defect repaired by PR objectstack-ai#5243 (ACCEPT
5178806144): driver-sql dropped an empty $and/$or group instead of
applying the boolean identity; empty $or and empty $not now compile to
FALSE | objectstack-ai#5322/objectstack-ai#5134: a disjunction of zero conditions matches nothing.
Fail-closed for an RLS scope — a disjunct list that loops to zero items
hides every row instead of exposing the table → Ruled: every face
reduces an empty combinator to its boolean identity. A disjunction of
zero conditions matches nothing. Fail-closed for an RLS scope — a
disjunct list that loops to zero items hides every row instead of
exposing the table, as a SQL lowering that dropped the empty group once
did |
| `data/filter-logic-conformance.ts:432` | objectstack-ai#5322 objectstack-ai#5297 | objectstack-ai#5322:
maintainer ruling 5185589944: every face reduces an empty combinator to
its boolean identity ($and:[] all rows, $or:[] none, {} TRUE, $not:{}
none), whole tree · objectstack-ai#5297: dispatch 5184116664: read-scope-sql's { $not:
{} } compiled to nothing (an RLS scope with no WHERE) and dropped a {}
disjunct; both aligned to the boolean identity | objectstack-ai#5322: collapsing to
the surviving branches instead compiles `a = x` — a silently NARROWED
scope (objectstack-ai#5297) → Ruled: every face reduces an empty combinator to its
boolean identity, so `{}` is a TRUE disjunct. Collapsing to the
surviving branches instead compiles `a = x` — a silently NARROWED scope,
the answer the RLS read-scope compiler gave until it was aligned |
| `data/filter-logic-conformance.ts:438` | objectstack-ai#5322 objectstack-ai#5297 | objectstack-ai#5322:
maintainer ruling 5185589944: every face reduces an empty combinator to
its boolean identity ($and:[] all rows, $or:[] none, {} TRUE, $not:{}
none), whole tree · objectstack-ai#5297: dispatch 5184116664: read-scope-sql's { $not:
{} } compiled to nothing (an RLS scope with no WHERE) and dropped a {}
disjunct; both aligned to the boolean identity | objectstack-ai#5322: emitting nothing
for it runs the query UNSCOPED — on an RLS lowering that is a permission
bypass (objectstack-ai#5297) → Ruled: every face reduces an empty combinator to its
boolean identity, so NOT of `{}` is FALSE. Emitting nothing for it runs
the query UNSCOPED — on an RLS lowering that is a permission bypass,
which the read-scope compiler was until it compiled this to an
always-false clause |
| `data/filter-logic-conformance.ts:470` | objectstack-ai#5298 | objectstack-ai#5298: ruling
5202271174 (option A, confirmed 5204511921 item 4): $ne / $nin /
$notContains are NULL-safe on the non-negated path, a no-value row is
included | objectstack-ai#5298 → Ruled NULL-safe on every face |
| `data/filter-logic-conformance.ts:476` | objectstack-ai#5146 | objectstack-ai#5146: maintainer
ruling 5181102507: $not is NULL-safe on every driver; a row with no
value does not satisfy the negated condition, so the negation returns it
| objectstack-ai#5146: the same ruling reached through the combinator → The same
NULL-safe ruling reached through the combinator, where it was first made
for `$not` itself |
| `data/filter-logic-conformance.ts:503` | objectstack-ai#5298 | objectstack-ai#5298: ruling
5202271174 (option A, confirmed 5204511921 item 4): $ne / $nin /
$notContains are NULL-safe on the non-negated path, a no-value row is
included | objectstack-ai#5298 option A → Ruled NULL-safe |
| `data/filter-logic-conformance.ts:503` | objectstack-ai#13356 | objectstack-ai#13356: PR objectstack-ai#13356:
driver-memory's reference matcher realigned so a no-value row satisfies
$nin / $notContains | PR objectstack-ai#13356 → it was realigned to the ruling |
| `data/filter-logic-conformance.ts:509` | objectstack-ai#5298 | objectstack-ai#5298: ruling
5202271174 (option A, confirmed 5204511921 item 4): $ne / $nin /
$notContains are NULL-safe on the non-negated path, a no-value row is
included | objectstack-ai#5298 option A → Ruled NULL-safe |
| `data/filter-logic-conformance.ts:509` | objectstack-ai#13356 | objectstack-ai#13356: PR objectstack-ai#13356:
driver-memory's reference matcher realigned so a no-value row satisfies
$nin / $notContains | PR objectstack-ai#13356 → it was realigned to the ruling |
| `data/filter-logic-conformance.ts:539` | objectstack-ai#5299 objectstack-ai#5962 | objectstack-ai#5299: ruling
5219858433 item 2, kept by 5238865560: $exists means has a value
(non-null), never key-presence, since SQL cannot tell a missing key from
null · objectstack-ai#5962: PR objectstack-ai#5962 (ACCEPT 5205011148 on objectstack-ai#5298): the NULL-safe
operators and the $exists has-a-value reading shipped | objectstack-ai#5299 cell 2 /
objectstack-ai#5962: `$exists` means HAS A VALUE, never key-presence → Ruled:
`$exists` means HAS A VALUE, never key-presence, because SQL cannot tell
a missing key from a stored null |
| `data/filter-logic-conformance.ts:561` | objectstack-ai#20444 | objectstack-ai#20444: card body
executing ruling A on objectstack-ai#20399 (5865693155): every compile surface answers
$empty by the field's declared type; $empty: false is the exact
complement | objectstack-ai#20444: null is empty on every row of the ruled table →
Every face answers `$empty` by the field's declared type, and null is
empty under every type's arm |
| `data/filter-logic-conformance.ts:567` | objectstack-ai#20444 | objectstack-ai#20444: card body
executing ruling A on objectstack-ai#20399 (5865693155): every compile surface answers
$empty by the field's declared type; $empty: false is the exact
complement | objectstack-ai#20444: the exact complement → `$empty: false` is the exact
complement by ruling |
| `data/filter-logic-conformance.ts:573` | objectstack-ai#20444 | objectstack-ai#20444: card body
executing ruling A on objectstack-ai#20399 (5865693155): every compile surface answers
$empty by the field's declared type; $empty: false is the exact
complement | objectstack-ai#20444: `$empty` spells its NULL case out, so it → The
ruled `$empty` arms spell their NULL case out, so `$empty` |
| `data/filter-logic-conformance.ts:579` | objectstack-ai#20444 | objectstack-ai#20444: card body
executing ruling A on objectstack-ai#20399 (5865693155): every compile surface answers
$empty by the field's declared type; $empty: false is the exact
complement | objectstack-ai#20444: → Under the same declared-type arms, |
| `data/filter-logic-conformance.ts:595` | objectstack-ai#20444 | objectstack-ai#20444: card body
executing ruling A on objectstack-ai#20399 (5865693155): every compile surface answers
$empty by the field's declared type; $empty: false is the exact
complement | objectstack-ai#20444: a face that lowers `$empty` → A face that lowers
`$empty` to the field's declared-type arm |
| `data/filter-text-conformance.ts:300` | objectstack-ai#8934 | objectstack-ai#8934: ruling A
5305708745: icontains joins the view and infix vocabularies;
ilike/$ilike and icontains/$icontains are never aliased onto each other
| objectstack-ai#8934 → ruled never an alias of ilike |
| `data/filter-text-conformance.ts:309` | objectstack-ai#8934 | objectstack-ai#8934: ruling A
5305708745: icontains joins the view and infix vocabularies;
ilike/$ilike and icontains/$icontains are never aliased onto each other
| capability (objectstack-ai#8934) → capability, ruled when `icontains` joined the
view and infix vocabularies |
| `data/filter-text-conformance.ts:349` | objectstack-ai#6518 objectstack-ai#6682 | objectstack-ai#6518: ACCEPT
5226386725 (PR objectstack-ai#6706): the SQL family's $contains family is case-exact
(GLOB on the SQLite dialects), per objectstack-ai#4706 Q2 = A · objectstack-ai#6682: ACCEPT
5251849449 and the memory half: the hardcoded case-folding flag came off
driver-mongodb and driver-memory, the last two folding faces | match.
SQLite's LIKE folds ASCII — the defect objectstack-ai#6518 replaced with GLOB on the
SQLite dialects; a JS backend's equivalent is a RegExp carrying the `i`
flag, which objectstack-ai#6682 took off → match: the `$contains` family is
case-sensitive on every backend, by ruling. SQLite's LIKE folds ASCII —
the defect the SQL family replaced with GLOB on the SQLite dialects; a
JS backend's equivalent is a RegExp carrying the `i` flag, since taken
off driver-memory and driver-mongodb, |
| `data/filter-text-conformance.ts:428` | objectstack-ai#4706 | objectstack-ai#4706: ruling B
5199214776: $regex retired under ADR-0049 with a loud refusal naming the
replacement, $icontains added | objectstack-ai#4706 retired the operator over →
`$regex` was retired over, by ruling, with a loud refusal naming
`$icontains` |
| `data/filter-text-conformance.ts:436` | objectstack-ai#5710 objectstack-ai#6993 | objectstack-ai#5710: ACCEPT
5201171068 (PR objectstack-ai#5812): plugin-auth's adapter stopped emitting bare
$regex for better-auth contains, unblocking the $regex retirement ·
objectstack-ai#6993: ACCEPT 5231388316 (PR objectstack-ai#7054): the expired status claims were
re-measured by executing each face (2026-08) and rewritten | objectstack-ai#5710
flipped that producer before any backend enrolled this case (re-verified
2026-08, objectstack-ai#6993 → That producer was moved off `$regex` before any backend
enrolled this case (re-verified 2026-08 by executing each face |
| `data/filter-text-conformance.ts:452` | objectstack-ai#5240 | objectstack-ai#5240: maintainer
ruling 5181107825: { field: {} } is refused loudly (INVALID_FILTER) on
every backend, not read as TRUE or FALSE | objectstack-ai#5240 refused `{ field: {} }`
over → for which a field with zero operators, `{ field: {} }`, is
refused by ruling |
| `data/filter-text-operator-declared-type.ts:392` | objectstack-ai#8296 | objectstack-ai#8296:
standing ruling recorded in 5277439872: a where on a formula field is
refused (INVALID_FIELD 400) because no driver materialises the column |
objectstack-ai#8296 door refuses EVERY formula filter with INVALID_FIELD 400 whatever
the `returnType` → unmaterializable-field door refuses EVERY formula
filter with INVALID_FIELD 400 whatever the `returnType` (no driver
stores a formula column) |
| `data/filter-text-operator-declared-type.ts:553` | objectstack-ai#8371 | objectstack-ai#8371:
ruling 5300373151 (option 2): a type-directed verdict on the dotted head
segment; the structured/JSON head stays deliberately unjudged | unjudged
there, objectstack-ai#8371 → left unjudged there, by ruling |
| `data/filter-text-operator-declared-type.ts:554` | objectstack-ai#8296 | objectstack-ai#8296:
standing ruling recorded in 5277439872: a where on a formula field is
refused (INVALID_FIELD 400) because no driver materialises the column |
objectstack-ai#8296 → the unmaterializable-field door |
| `data/temporal-conformance.ts:211` | objectstack-ai#3773 | objectstack-ai#3773: defect: SQLite
read an epoch-ms Field.datetime as a Julian day so date buckets were
NULL; repaired by storage-aware bucketing (card body; cross-update
5099832227 on objectstack-ai#3777) | (objectstack-ai#3773) → as SQLite bucketing once did |
| `data/temporal-conformance.ts:215` | objectstack-ai#3777 | objectstack-ai#3777: defect: a bare-day
$lte on a datetime column stopped at midnight and dropped the rest of
the final day; the bound keeps the whole day (card body; 5099832227) |
by the objectstack-ai#3777 bug → when a bare-day upper bound stopped at midnight |
| `data/temporal-conformance.ts:216` | objectstack-ai#3777 | objectstack-ai#3777: defect: a bare-day
$lte on a datetime column stopped at midnight and dropped the rest of
the final day; the bound keeps the whole day (card body; 5099832227) |
by the objectstack-ai#3777 bug → when a bare-day upper bound stopped at midnight |
| `data/temporal-conformance.ts:220` | objectstack-ai#20600 | objectstack-ai#20600: triage direction
5886142901, landed PR objectstack-ai#20643: the whole-day bound past 9999-12-31 is
unbounded above and the drivers compile none | (objectstack-ai#20600) → and none is
compiled |
| `data/temporal-conformance.ts:270` | objectstack-ai#3777 | objectstack-ai#3777: defect: a bare-day
$lte on a datetime column stopped at midnight and dropped the rest of
the final day; the bound keeps the whole day (card body; 5099832227) |
objectstack-ai#3777: the default dashboard window → The default dashboard window,
whose bare-day $lte keeps the whole final day |
| `data/temporal-conformance.ts:393` | objectstack-ai#3773 | objectstack-ai#3773: defect: SQLite
read an epoch-ms Field.datetime as a Julian day so date buckets were
NULL; repaired by storage-aware bucketing (card body; cross-update
5099832227 on objectstack-ai#3777) | objectstack-ai#3773 famil → family of the SQLite bucketing
defect that read an epoch-ms datetime as a Julian da |
| `data/temporal-conformance.ts:448` | objectstack-ai#1874 | objectstack-ai#1874: the templates'
date-equality family that surfaced ADR-0053, whose Phase 1 stores
Field.date as its calendar day (ADR-0053 Surfaced-by line; card body, no
comments) | objectstack-ai#1874: `date == today` silently matched nothing while dates
were stored as instants. E → `date == today` silently matched nothing
while dates were stored as instants; ADR-0053 stores a date as its
calendar day, so e |
| `data/temporal-conformance.ts:457` | objectstack-ai#1874 | objectstack-ai#1874: the templates'
date-equality family that surfaced ADR-0053, whose Phase 1 stores
Field.date as its calendar day (ADR-0053 Surfaced-by line; card body, no
comments) | from the objectstack-ai#1874 family → that surfaced ADR-0053 |
| `data/temporal-conformance.ts:549` | objectstack-ai#3994 | objectstack-ai#3994: Field.time takes
one canonical HH:MM:SS[.fff] text form on write, filter and read
(ADR-0053 addendum D-C1..D-C3; driver-sql changeset 9774b78) | objectstack-ai#3994,
measured → Measured before `Field.time` took one canonical
`HH:MM:SS[.fff]` text form |
| `data/value-roundtrip-conformance.ts:213` | objectstack-ai#11535 | objectstack-ai#11535: defect: a
single-to-multi-value change kept the old text column on Postgres, so
arrays came back as stringified literals; the drift detector now reports
the base-type mismatch (claim 5395496220) | objectstack-ai#11535's exact shape → the
exact shape a single-value column kept for a multi-value field corrupted
|
| `data/value-roundtrip-conformance.ts:225` | objectstack-ai#11782 | objectstack-ai#11782: landing
5406878917 (PR objectstack-ai#12019): a declared boolean answers JSON booleans on
every read door and dialect; MySQL's tinyint 1/0 leaked before | objectstack-ai#11782
read this back as 1 on MySQL → MySQL read this back as 1 until every
read door presented a declared boolean as true/false |
| `data/value-roundtrip-conformance.ts:226` | objectstack-ai#11782 | objectstack-ai#11782: landing
5406878917 (PR objectstack-ai#12019): a declared boolean answers JSON booleans on
every read door and dialect; MySQL's tinyint 1/0 leaked before | objectstack-ai#11782
→ that MySQL read-back |
| `lint: validate-empty-combinators.test.ts:275` | objectstack-ai#5322 | objectstack-ai#5322:
maintainer ruling 5185589944: every face reduces an empty combinator to
its boolean identity ($and:[] all rows, $or:[] none, {} TRUE, $not:{}
none), whole tree | objectstack-ai#5322 → every face reduces an empty combinator to
its boolean identity |
| `analytics: icontains-dialect-sql.test.ts:369` | objectstack-ai#8934 | objectstack-ai#8934: ruling
A 5305708745: icontains joins the view and infix vocabularies;
ilike/$ilike and icontains/$icontains are never aliased onto each other
| objectstack-ai#8934 → ruled never an alias of ilike |

## The selector seam (A3)

The filter over `FILTER_LOGIC_CASES` now tests `(c.note ??
'').includes('every face reduces an empty combinator to its boolean
identity')` where it tested `.includes('objectstack-ai#5322')`. The phrase is the
objectstack-ai#5322 ruling in words, and the four rewritten notes open with it.
Measured with an AST extractor over the table (`name` and `note` of all
36 cases, then the selector applied exactly as the test applies it):

- base `cc645f2385` with `objectstack-ai#5322` → **4 of 36**: `$not of {} is FALSE —
NOT TRUE`, `a {} branch is a TRUE disjunct and absorbs its $or`, `empty
$and is TRUE — the AND identity`, `empty $or is FALSE — the OR
identity`.
- head with the phrase → **the same 4 of 36**, the same names.
- cross: the old selector on the head selects 0 (so the move is
required), the new phrase on the base selects 0.
- control outside the set: the broader word `Ruled` selects 8, the four
plus `$exists true selects exactly the valued rows`, `$ne …`, `$nin …`
and `$notContains returns the rows with no value`; none of those four is
picked by the phrase.
- mutation control on a scratch copy (anchor hit 1, replacement present
1, anchor left 0): dropping the phrase from the empty-`$or` note leaves
3, which the test's own guard (`toEqual` on the four sorted names,
`toBe(4)`) turns red.

The test asserts exactly what it asserted before (the names pin, the
count, and the row-set wording per case); only the selector moved.
`validate-empty-combinators.test.ts` ran green: 21 of 21, the three
identity cases included.

## The name pin (A4)

`'icontains (the infix/view spelling, objectstack-ai#8934) lowers to $icontains — %
stays a LITERAL through that door too'` becomes `'icontains (the
infix/view spelling, ruled never an alias of ilike) lowers to $icontains
— % stays a LITERAL through that door too'` in `FILTER_TEXT_CASES`, and
the `toEqual` pin in `icontains-dialect-sql.test.ts:369` moves to the
new string verbatim. No other consumer keys on that name (searched the
whole tree for the old name and its fragments: the source and the pin
only); the other consumers use it as a test title. The file ran green:
16 of 16. The five renamed `FILTER_COMPARAND_TYPE_CASES` names have no
pin anywhere; they are test titles only.

## Text only (A5)

Stage 3's `skeleton.cjs` (one line changed: the TypeScript load path to
this worktree), TypeScript 6.0.3: leg 1 an AST skeleton with every
string's text masked (a `+` chain's adjacent string operands read as one
string), leg 2 the text of every string group, each changed group
required to carry a tracker id before and none after, every other group
byte-identical. Base copies vs the committed files, **10 of 10 SAME on
both legs, exit 0**:

| file | tokens | string groups | changed |
|:--|--:|--:|--:|
| `metadata-service-roundtrip-conformance.ts` | 1352 | 157 | 5 |
| `aggregation-conformance.ts` | 1268 | 133 | 9 |
| `filter-comparand-type-conformance.ts` | 1309 | 107 | 7 |
| `filter-logic-conformance.ts` | 1682 | 223 | 16 |
| `filter-text-conformance.ts` | 1093 | 137 | 6 |
| `filter-text-operator-declared-type.ts` | 1821 | 114 | 3 |
| `temporal-conformance.ts` | 2041 | 374 | 9 |
| `value-roundtrip-conformance.ts` | 1197 | 175 | 3 |
| `validate-empty-combinators.test.ts` | 2684 | 129 | 1 |
| `icontains-dialect-sql.test.ts` | 4345 | 217 | 1 |

Parse diagnostics 0 / 0 throughout. 58 changed groups in the eight
modules, one per seam. No case's filter, input, expected rows, verdict,
code, operator, dialect, order or count moves. Edits were applied by a
script whose 62 anchors each had to hit exactly once before any write,
and were read back from disk after it (each anchor gone, each
replacement present once). Census after the edit: class (c) **0 / 0**;
non-test 118 → 60 messages, 297 → 229 ids; test strings unchanged (1804
/ 1920).

## Pins, titles and quotes (A6)

- Pins moved: the two seams above, nothing else. Every old note and name
was searched across the tree in 32-character windows: no test asserts a
changed phrase; the remaining hits are code comments and other modules'
own prose that share a phrase with an unchanged part of a note.
- Titles that follow the text: the comparand-type and text-case names
(test titles in the driver suites), and the `VALUE_ROUNDTRIP_CASES`
notes (`round-trips NAME (NOTE)`). No skip list, ledger or snapshot
names any of those titles.
- Quotes: no `content/docs/**` page and no `skills/**` file quotes a
changed note or name.
- `TEMPORAL_ROWS[].why` is seeded into a `string` column (a `varchar` on
Postgres and MySQL), so the four rewritten `why` texts stay short (164
characters at most).

## Gates

Head `1427993cc3` (the merge of `origin/main` `901e7cf13a`; that merge
touched no file under `packages/spec`, `packages/lint` or
`packages/services/service-analytics`, 0 diff lines there).

- `node scripts/pm/dispatch-gates.mjs --repo objectstack-ai/objectstack
--commands`: 11 paths vs merge base `901e7cf13`, 153 changed lines; **88
derived commands, 88 exit 0** on `1427993cc3`, each exit code written
from `$?` before any pipe; `--ran`: "✓ dispatch-gates --ran: 88 derived
famil(ies) accounted for — 88 run, 0 NOT-MEASURED".
- `pnpm --filter @objectstack/spec build` exit 0 (the dependency closure
is empty: no workspace dependencies); `check:generated`: "✓ All 15
generated artifacts are up to date".
- `pnpm --filter @objectstack/spec test`: "Test Files 606 passed (606) /
Tests 17952 passed | 1 todo (17953)"; `typecheck` exit 0
("check:test-typecheck: OK — … 52 file(s) / 246 error(s) / 135 pinned
signature(s) held").
- `pnpm --filter @objectstack/lint test`: "Test Files 119 passed (119) /
Tests 5620 passed (5620)"; `typecheck` exit 0.
- `pnpm --filter @objectstack/service-analytics test`: "Test Files 175
passed (175) / Tests 4152 passed | 253 skipped (4405)"; `typecheck` exit
0; `icontains-dialect-sql.test.ts` alone 16 of 16.
- `pnpm check:doc-authoring` (self-test, then the run): "✓ doc authoring
guard: 17337 customer-facing string(s) across 1251 spec sources clean"
and "sibling-package prose ids hold the baseline — 0 pinned site(s) … no
growth, no burn-down unrecorded". `pnpm check:nul-bytes`:
"check-nul-bytes: OK (scanned 9947 text file(s) …; no raw ASCII control
bytes)".
- Changeset gates: `check-changeset-no-major.mjs`,
`check-empty-changeset.mjs`, `check-adr-0087-registration.mjs` (each
`--base origin/main`) exit 0; with this body as the `--event` payload,
see the report.
- ESLint, a proven narrowing: `eslint --no-inline-config --format json`
over the 10 changed TS files reads 10 files, 0 errors, 0 warnings; the
population is ESLint's own (`calculateConfigForFile` returns a config
for each, `isPathIgnored` false); invariance: `eslint.config.mjs`
enables no type-aware linting (`parserOptions.project` /
`projectService` null for all 10), so a string-text edit cannot move an
untouched file's verdict. Repo-wide `pnpm lint` is CI's.
- Spec `test:repo` was not run locally: none of its 51 files names a
changed module or table.

## Acceptance notes

- **Two cited cards answer 404** in this repository (objectstack-ai#6725 and objectstack-ai#11065;
the same numbers in objectui are unrelated PRs). Their decisions were
read from the shipped landing records instead: the objectql and spec
CHANGELOG entries `1507ba3` / `bbee302`
(`MetadataFacade.register('object')` wrote into a map none of its own
object reads consulted) and the driver-memory entry `2095040` (a boolean
aggregand counts as 1 or 0, as on every SQL face). Neither decision read
as unclear.
- Two decisions live in shipped records rather than card comments:
objectstack-ai#11152's 2026-08-28 ruling (option A, booleans aggregate as numbers,
superseding objectstack-ai#11249) is recorded in changeset `f6fa22c` and matches the
source comment above those cases; objectstack-ai#3994's in ADR-0053's D-C addendum.
- Left for later stages, untouched here: the seam files' own test titles
(`(objectstack-ai#5322/objectstack-ai#5659)`, `[objectstack-ai#16028]`) and every other test string; code comments
(including the section comments beside these cases); class (f) in
`api/error-code-ledger.zod.ts` and `kernel/public-auth-features.ts`;
`migrations/registry.ts`.
- Not governed: no `.claude/**`, `skills/**`, ADR, NORTH-STAR or
AGENTS.md path.

## Line budget

153 changed lines (+90 / -63) over 11 files vs merge base `901e7cf13`
(dispatch-gates), under the 5000 human-merge threshold: the eight
modules, the two test files (one line each) and the changeset. No
generated file, no governed surface.

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

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

2 participants