Skip to content

feat(metadata-core,objectql): publish assertEngineFindOnePredicate — the #4419 read-side guard engine doubles could not skip - #12072

Merged
os-zhuang merged 3 commits into
mainfrom
claude/issue-11957-engine-double-findone-predicate
Aug 25, 2026
Merged

feat(metadata-core,objectql): publish assertEngineFindOnePredicate — the #4419 read-side guard engine doubles could not skip#12072
os-zhuang merged 3 commits into
mainfrom
claude/issue-11957-engine-double-findone-predicate

Conversation

@os-zhuang

Copy link
Copy Markdown
Contributor

Fixes#11957

ObjectQL.findOne applies limit: 1, so a query naming no particular record would return an ARBITRARY row — requireFindOnePredicate (#4419) REFUSES it. Every in-memory engine double in the repo instead read an absent filter as "match everything" and answered happily, so a production call site that violates #4419 read as working under every unit suite and only failed against a real engine. #11767 is what that already cost: a bootstrap probe called findOne({ where: [] }) inside a try/catch, the catch read the engine's refusal as the data answer, the declared first-run bypass became permanently inert on real deployments, and a 641-line all-green unit matrix over the double said nothing.

This publishes the producer's own decision so a double can import it instead of re-deriving it.

What landed

packages/metadata-core/src/engine-findone-predicate.ts — the read-side member of the assertEngineDeleteDispatch / assertEngineUpdateDispatch family, in the same package and for the same measured reason (@objectstack/objectql depends on @objectstack/metadata-protocol, so the reverse import would close a cycle turbo refuses; metadata-core is the package both sides already depend on, and this module adds no edge):

  • assertEngineFindOnePredicate(object, query) — the line a fake's findOne opens with. It takes the object name, unlike the two write-side asserts, because the engine's refusal message quotes the object twice and a fake reproducing only the prefix would let a test assert on wording the producer never emits.
  • resolveEngineFindOnePredicate / engineFindOnePredicateRefusalMessage / ENGINE_FINDONE_PREDICATE_CASES.

packages/objectql/src/engine-findone-predicate.ts — the re-export shim keeping objectql's public spelling, byte-for-byte the twins' pattern.

packages/objectql/src/engine-findone-predicate.test.ts — the case-set driven against the REAL engine with a recording driver, so the predicate cannot drift from engine.ts unnoticed. It counts a throw as the verdict only when byte-identical to engineFindOnePredicateRefusalMessage(object); a malformed-filter-array or unmaterializable-column refusal is a different door and rethrows rather than being laundered into a passing case.

Adoption: all 25 findOne doubles in packages/objectql (23 files). Verified by the gate: with a findOne slice added, that package reports 0 unpinned.

The predicate mirrors the engine, and says where it cannot

Read on the caller's own spelling, since a double is handed the query before the engine folds, lowers or expands anything. The three shapes a hand-written copy gets wrong are the whole argument for importing it:

shapeenginea truthiness copy
where: []REFUSES (lowered to "no filter") — the #11767 shapeaccepts (an empty array is truthy)
where: {}REFUSES (match-every-row)accepts (!= null)
filter: { … }accepts (alias folded before the guard, #4346)refuses

Two residuals are stated in the module header rather than hidden, both one-sided by construction because a double is not handed the object's schema: a search over an object with no searchable field (the engine refuses, this accepts — refusing every search would be stricter than the producer on the mainline shape, and stricter invents failures), and a non-empty where array that is not a well-formed filter AST (both refuse; only the wording differs).

⛔ What did NOT land, and the measurement behind that

The issue asks for the guard and extending check:engine-double-contract's ratchet to a findOne slice. The slice is not in this PR. Measured by temporarily adding it to SLICES and running the gate:

  • before adoption: 232 files, 275 doubles would newly fail PINNED [findOne];
  • after this PR's adoption: 209 files, 250 doubles remain.

169 of the original 232 are already in the pinned ledger for a write verb (a one-line pin each); 63 are not, and those include cases a dev seat cannot close on its own — the gate's own failure text says a scripts/engine-double-contract.baseline.json entry is "MAINTAINER-ONLY, NOT a co-equal option … do not take this path to get CI green." A 209-file diff whose acceptance criterion is 209 real suite runs is also past any reviewable size.

So this is the bounded half: the guard, proved against the producer, plus the adoption for the producer's own package. The exact remainder — all 209 files enumerated, broken down per package by ledger status — is filed as #12068, which also carries the maintainer decision the 63 need. That card remains open; it is not addressed here.

Verification

Gate union re-run on the final commit, cc3697f8e, working tree clean:

check:engine-double-contract exit 0 OK — 408 pinned, 133 in the DEBT ledger, 2 exempt
check:query-options-erasure exit 0 test surface: 240 site(s) in 47 file(s) — at the ceiling
check:slot-lookup exit 0
check:nul-bytes exit 0 scanned 6696 text file(s); no raw ASCII control bytes
check:type-check-coverage exit 0 65/78 workspace packages type-checked
check:durability-log-level exit 0
check:where-matcher exit 0

Also green earlier on the same tree: check:cross-package-test-inputs, check:test-source-alias, check:published-files, check:adr-0087-registration, check:type-source-resolution.

Testspnpm --filter @objectstack/objectql exec vitest run --maxWorkers=2 <the 23 adopted files> src/engine-findone-predicate.test.ts: Test Files 24 passed (24) · Tests 306 passed (306). No test went red under the assertion, in either direction: none of objectql's own doubles was being driven with a predicate-less findOne, so the looseness there was dormant rather than load-bearing — a finding, not a clean bill for the other 209.

Typecheckpnpm --filter @objectstack/metadata-core --filter @objectstack/objectql typecheck: both Done, exit 0.

Ablation — the conformance test can actually fail. Mutation: read where: [] the naive truthy way (return where.length > 0return ['OSABLATE'].length > 0), i.e. exactly the copyist's mistake. Both legs rebuilt @objectstack/metadata-core and both were proved on disk, anchored at the text being replaced (original-text hits 1→0, marker hits 0→1 on the mutate leg; the reverse on the restore leg) and in dist:

mutate leg ablation-dist-preflight ✓ marker present in 2 built files → 3 failed | 21 passed
restore leg ablation-dist-preflight --absent ✓ marker absent from all 18 files → 24 passed

The three that failed are the where: [] case, the refusal-message assertion and the where: [] unit — the direction predicted before the run. (A first attempt used a /* comment */ marker; the preflight correctly reported it landed only in sourcemaps and voided that run, so it was redone with a string literal. Recorded because a voided ablation reported as evidence is the defect one layer up.)

Declared narrowing — verification ran UNLOCKED.scripts/pm/os-verify-lock.sh could not take the shared verify lock on this host: no usable flock. The shared verify lock is declared Linux-only (flock is util-linux, and a stock macOS does not ship it), so every build/test command above was run directly, without the lock — a declared narrowing, not a silent one. No serialization guarantee held for these runs, nor for any sibling agent in this container while they ran.

Declared narrowing — lint was measured, not skipped.pnpm lint scans the whole repo and is CI's run; this one is targeted at the diff and the three pieces of evidence are here rather than implied. ① The population was handed to ESLint as the diff's own paths and selected by ESLint's own config resolution — the changeset .md came back "File ignored because no matching configuration was supplied", which is the config deciding, not me. ② --format json reports 25 entries, 24 of them TypeScript files actually linted: 0 errors, 0 warnings (the single warning is the ignored .md). ③ Type-aware linting is not enabled anywhere in eslint.config.mjs (no parserOptions.project, no typed @typescript-eslint rules — the config says so at its own line 328), so this diff cannot move the verdict on any file it does not touch.

One remedy taken rather than a ceiling raised: the conformance test's deliberately off-contract query bags are spelled as unknown as EngineQueryOptions, not as any, and the widened double signatures take EngineFindOneQueryInputcheck:query-options-erasure is back at its ceiling of 240 rather than 242.

Generated by Claude Code

…read-side guard
Mirrors ObjectQL.requireFindOnePredicate byte-for-byte and is proved against
the REAL engine over a shared conformance case-set.
#4419 predicate
Adds the changeset for the new public API and types the conformance test's
deliberately off-contract query bags as `as unknown as EngineQueryOptions`.
@github-actionsgithub-actionsBot added size/l documentation Improvements or additions to documentation tests tooling labels Aug 25, 2026
@github-actions

github-actionsBot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): @objectstack/metadata-core, @objectstack/objectql, touching 13 documentable anchor(s). ⚠️3 changed file(s) yielded no anchor (packages/metadata-core/src/index.ts, packages/objectql/src/engine-findone-predicate.ts, packages/objectql/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.

13 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/client-sdk.mdx(via orderBy (symbol), orderBy (literal))
  • content/docs/api/data-api.mdx(via orderBy (symbol), orderBy (literal))
  • content/docs/api/error-catalog.mdx(via orderBy (symbol), orderBy (literal))
  • content/docs/api/wire-format.mdx(via orderBy (symbol), orderBy (literal))
  • content/docs/automation/flows.mdx(via orderBy (symbol), orderBy (literal))
  • content/docs/data-modeling/queries.mdx(via orderBy (symbol), orderBy (literal))
  • content/docs/deployment/troubleshooting.mdx(via orderBy (symbol), orderBy (literal))
  • content/docs/getting-started/common-patterns.mdx(via orderBy (symbol), orderBy (literal))
  • content/docs/kernel/contracts/data-engine.mdx(via orderBy (symbol), orderBy (literal))
  • content/docs/kernel/runtime-services/data-service.mdx(via orderBy (symbol), orderBy (literal))
  • content/docs/protocol/kernel/http-protocol.mdx(via orderBy (symbol), orderBy (literal))
  • content/docs/protocol/objectql/index.mdx(via orderBy (symbol), orderBy (literal))
  • content/docs/protocol/objectql/query-syntax.mdx(via orderBy (symbol), orderBy (literal))

1 release-owned page(s) also name something this change touched. These are read-only:

  • content/docs/releases/v17.mdx(via orderBy (symbol), orderBy (literal))

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.

What this run could not see
  • 3 changed file(s) yielded no anchor (packages/metadata-core/src/index.ts, packages/objectql/src/engine-findone-predicate.ts, packages/objectql/src/index.ts) — pages documenting those are invisible to this run
  • 6 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 45 of 222 client-bound route-ledger rows — the other 177 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run: 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.

Coarse fallback — 15 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 2eed6dc6eaf5afa32554f2eafa5f5302a4282f43packageMentionDocs.

Which tree this was computed on

This run read content/docs from 286507275c638ce606a5672401e4c744fd1fd4bc — the merge of head 885a32bc3c1823124810332a141dabfa8d1126e5 into base 2eed6dc6eaf5afa32554f2eafa5f5302a4282f43, 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 286507275c638ce606a5672401e4c744fd1fd4bc && git checkout 286507275c638ce606a5672401e4c744fd1fd4bc
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 2eed6dc6eaf5afa32554f2eafa5f5302a4282f43 885a32bc3c1823124810332a141dabfa8d1126e5 && git checkout -B drift-repro 2eed6dc6eaf5afa32554f2eafa5f5302a4282f43 && git merge --no-ff 885a32bc3c1823124810332a141dabfa8d1126e5
node scripts/docs-audit/affected-docs.mjs --json 2eed6dc6eaf5afa32554f2eafa5f5302a4282f43

⚠️ 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 2eed6dc6eaf5afa32554f2eafa5f5302a4282f43 → pass the list as
args.docs, on the commit named under Which tree this was computed on.

…indOne conformance test
The one-argument spelling is a TS2554 that objectql's own tsconfig hides (it
excludes **/*.test.ts), visible only to the shrink-only TEST_DEBT re-measure,
where it read as 354 -> 355.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentationImprovements or additions to documentationsize/lteststooling

Projects

None yet

1 participant

@os-zhuang