Uh oh!
There was an error while loading. Please reload this page.
feat(metadata-core,objectql): publish assertEngineFindOnePredicate — the #4419 read-side guard engine doubles could not skip - #12072
Conversation
…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`.
📓 Docs Drift CheckThis PR changes 2 package(s): 13 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:
⛔ 1 release-owned page(s) also name something this change touched. These are read-only:
What this run could not see
Coarse fallback — 15 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 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
|
…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.
Uh oh!
There was an error while loading. Please reload this page.
Fixes#11957
ObjectQL.findOneapplieslimit: 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 calledfindOne({ where: [] })inside atry/catch, thecatchread 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 theassertEngineDeleteDispatch/assertEngineUpdateDispatchfamily, in the same package and for the same measured reason (@objectstack/objectqldepends on@objectstack/metadata-protocol, so the reverse import would close a cycle turbo refuses;metadata-coreis the package both sides already depend on, and this module adds no edge):assertEngineFindOnePredicate(object, query)— the line a fake'sfindOneopens 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 fromengine.tsunnoticed. It counts a throw as the verdict only when byte-identical toengineFindOnePredicateRefusalMessage(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
findOnedoubles inpackages/objectql(23 files). Verified by the gate: with afindOneslice 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:
where: []where: {}!= null)filter: { … }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
searchover an object with no searchable field (the engine refuses, this accepts — refusing everysearchwould be stricter than the producer on the mainline shape, and stricter invents failures), and a non-emptywherearray 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 afindOneslice. The slice is not in this PR. Measured by temporarily adding it toSLICESand running the gate:PINNED [findOne];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.jsonentry 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: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.Tests —
pnpm --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-lessfindOne, so the looseness there was dormant rather than load-bearing — a finding, not a clean bill for the other 209.Typecheck —
pnpm --filter @objectstack/metadata-core --filter @objectstack/objectql typecheck: bothDone, exit 0.Ablation — the conformance test can actually fail. Mutation: read
where: []the naive truthy way (return where.length > 0→return ['OSABLATE'].length > 0), i.e. exactly the copyist's mistake. Both legs rebuilt@objectstack/metadata-coreand 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 indist:The three that failed are the
where: []case, the refusal-message assertion and thewhere: []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.shcould not take the shared verify lock on this host: no usableflock. The shared verify lock is declared Linux-only (flockis 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 lintscans 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.mdcame back "File ignored because no matching configuration was supplied", which is the config deciding, not me. ②--format jsonreports 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 ineslint.config.mjs(noparserOptions.project, no typed@typescript-eslintrules — 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, notas any, and the widened double signatures takeEngineFindOneQueryInput—check:query-options-erasureis back at its ceiling of 240 rather than 242.Generated by Claude Code