chore: T01.o est close, cinq rounds adverses et la garde sait echouer

This commit is contained in:
Sylvain Duchesne
2026-09-24 20:47:46 +02:00
parent a88b4497d9
commit 9e00a89fe8
@@ -1,276 +0,0 @@
---
id: T01.o
title: The read filter is bypassed by plain properties, and no fake shows it
parent: T01
depends_on: []
created: 2026-09-24
claimed_by: worker-T01.o
claimed_at: 2026-09-24
blocked: false
---
## Description
**A rights leak on the published surface.** The capability filter around the published `useShape` is bypassed by the SDK version this repo actually builds against, and every gate stays green because no fake anywhere returns the shape the SDK ships. Found by T01.g, the closing review.
### The leak - VERIFIED from primary sources on both sides, do not re-derive
`packages/polyfill/src/emulated-verifier/read-filter.ts` does `const v = Reflect.get(target, prop, target); if (typeof v !== "function") return v;` - a non-function member is returned RAW, on the recorded ground that "a plain property carries no items".
That ground is true of the CITED tree (alpha.18, where `useShape` returns a bare `DeepSignalSet`) and FALSE of the INSTALLED `@ng-org/orm@0.1.2-alpha.21`. Its shipped `dist/frontendAdapters/react/useShape.d.ts` returns `UseShapeResult`, whose members include three PLAIN PROPERTIES that each carry the items: `data: SUBSCRIPTION_DATA | undefined`, `promise: Promise<SUBSCRIPTION_DATA>`, and `subscription: RdfOrmSubscriptionFor<...>`. Its own doc comment states that `data` IS the value returned by `RdfOrmSubscription.signalObject`.
Measured by T01.g with bob as holder and `isEnforcing() === true`: the alpha.18 shape iterates to `["bob-own"]`, while alpha.21's `.data`, `.subscription.signalObject` and `await .promise` each yield `["bob-own", "alice-private"]`, and nothing throws. That falsifies `contract_polyfill-surface.md:256` ("anything else throws").
### THE ASYMMETRY IS THE FAULT, and the code already argues against itself
For an unknown FUNCTION member the same file refuses rather than forwards, with the reason written beside it: forwarding is a silent leak, and this view's one job is that it cannot show more than the holder may read. That reasoning was never applied to non-function members. Naming the asymmetry is not the same as prescribing the repair - weigh what the right policy is, including what refusing `data` would cost a legitimate caller, and say what you rejected.
**Do NOT fix this by filtering three member names.** `data`, `promise` and `subscription` are alpha.21's shape; a name list is what breaks silently at alpha.22, and breaking silently on an SDK bump is exactly how this defect arrived. Whatever you build must be safe for a shape nobody has seen yet.
**The line that must hold:** a legitimate caller still gets its own data, through whichever member the SDK offers. A repair that makes the published `useShape` unusable has traded a leak for a break.
### Why it was invisible, which must be fixed in the same task
`packages/polyfill/e2e/orm-live-entry.ts:101-109` - the only live gate for the published `useShape` - injects a HAND-WRITTEN `sub.signalObject` substitute. So no fake in this repository returns the shape the SDK actually ships, and the leak cannot fail a test. Fixing the filter without fixing this leaves the next SDK shape change just as invisible.
Build a fake that returns the SHIPPED shape - read it from `node_modules/@ng-org/orm/dist/frontendAdapters/react/useShape.d.ts`, which is the artifact that settles it - and make it the one the gates use. A test that would not have caught this defect is not coverage.
### Two documentation faults that ride with it
- `contract_polyfill-surface.md` instructs injecting "the REAL @ng-org/orm useShape" (see `docs/api-contract.md:50`), but `@ng-org/orm`'s `.` export maps to `dist/core.d.ts`, which exports NO `useShape` - it lives at `/react`, or as `reactUseShape` from `/all`. The instruction does not resolve. CONFIRMED by T01.g.
- Four present-tense claims that the SDK's `useShape` returns a bare `DeepSignalSet` are false of alpha.21, and TWO are load-bearing provenance grounds: `use-shape-query.ts:284`'s `announced-not-written` premise, and `:193`'s `ShapeQuery kind=invention level=none ref=none`, justified as "no upstream type carries load state" when `UseShapeResult.isLoading` does. This is the `readUnion` failure mode again - a citation that resolves, attached to a claim that is false - and `provenance.test.ts` cannot see it by construction.
### Verification
**The leak's own test comes first and must fail on baseline.** Bob holding no capability for alice's document must not reach her items through ANY member of the returned object. Name the test.
**Hard gate:** `bun test` -> 0 fail, baseline `465 pass / 0 fail` which the orchestrator measured; `pnpm exec tsc --noEmit -p packages/polyfill/tsconfig.json` -> exit 0; the e2e tsconfig -> exit 0; `python3 ~/.claude/skills/concept/lint.py` -> 0 ERROR; and the polyfill e2e run and reported verbatim, since the injected fake changes. Broker login flakes roughly 3 runs in 10 before library code runs - re-run a flaked login.
Anti-cheat: no `any`, no `@ts-ignore`, no `as unknown as`, no deleted or skipped test, no weakened assertion. And specific to this one: no widening `UseShapeLike` to make a shape typecheck - `src/model/types.ts:83` is already `(...args:any[]) => any`, which is why both typechecks stayed at 0 while the leak was open, and that is a finding in itself worth addressing.
**A guarantee and the test that pins it land in the same commit.** The contract's claim at `:256` is currently false; it becomes true or it changes, in the same commit as its test.
**An independent adversarial review runs on this afterwards.** Rights defects get one whatever the gate says.
## Progress
### The enumeration, done before the code
Read from the artifact that settles it: the INSTALLED `@ng-org/orm@0.1.2-alpha.21`, at
`node_modules/.pnpm/@ng-org+orm@0.1.2-alpha.21/node_modules/@ng-org/orm/dist/frontendAdapters/react/useShape.{d.ts,js}`.
The `.d.ts` declares `UseShapeResult_` with SIX members, wrapped by `WithMaybePagination`, which makes `nextPage` and
`previousPage` optional when the config asks for no `pageSize`. The shipped `.js` returns exactly those six keys. Both
were read; they agree.
| member | declared type | does it carry items? | what the filtered view must answer |
|---|---|---|---|
| `data` | `SUBSCRIPTION_DATA \| undefined` - a `DeepSignalSet<T>` with no `orderBy`, a `DeepSignal<ReadOnlyArray<T>>` with one | **YES, directly.** Its own doc comment: "This object is the value returned by `RdfOrmSubscription.signalObject`" | a read-filtered VIEW of that collection. `undefined` is a primitive and passes untouched |
| `promise` | `CONF extends undefined ? undefined : Promise<SUBSCRIPTION_DATA>` | **YES, on resolve** - the same collection | a promise that resolves to the read-filtered view of what it resolved to |
| `subscription` | `CONF extends undefined ? undefined : RdfOrmSubscriptionFor<ST, CONF, T>` | **YES, three ways**: `signalObject`, `readyPromise`, and `addChangeListener`'s `{adds, updates, removes}` of raw items | **REFUSE, loudly.** This surface already says `RdfOrmSubscription` is not published and that reaching it "defeats everything above"; handing it back through `useShape` publishes it by another door |
| `isLoading` | `boolean` | no | pass through - a primitive cannot reach an item |
| `nextPage` | `(() => void) \| undefined` (optional without `pageSize`) | no - returns `void`, it loads more rows into the RAW collection, which is only ever seen filtered | forward, bound to the target, like `add`/`delete`/`clear` |
| `previousPage` | `(() => void) \| undefined` (optional without `maxActivePages`) | no - same | forward, bound to the target |
| **anything a later version adds** | unseen | unknowable | decided by TYPE, not by name: a primitive passes, a function comes back as a function that throws when called, an object throws on read |
Two members of the COLLECTION had to be enumerated with it, because `data` is not always a Set:
| member | carries items? | answer |
|---|---|---|
| `length` (array-like `data`, i.e. an `orderBy` config) | a COUNT of them - showing the unfiltered count shows MORE than the holder may read | filtered, beside `size`, and each only when the target itself has it |
| `"0"`, `"1"`, ... (array-like `data`) | **YES** - a raw item | refused by the type rule (an item is an object). Left refused rather than projected: safe, loud, and making the ordered form ergonomic is a wider job than this task |
### The two-layer shape that falls out
The published `useShape` was wrapping an ENVELOPE in a filter written for a COLLECTION. So the envelope's own members
were read through `Reflect.get` and, being non-functions, returned raw. Under alpha.21 that view is ALSO unusable for
its owner - `[...view]` and `.size` throw, because an envelope is not iterable. There are two objects to filter, so
there are two views, and `surface/use-shape.ts` applies the envelope one.
## Result
Status: REFUTED once, repaired, and back for a fresh adversary. First pass was four commits on `caps-p1a-and-virtual-user-boundary`, `3a89411` `e16fd6a` `e86e249` `b1b099b`. Whole change: **20 files, 1117 insertions, 247 deletions** (`git diff 5993a8c --shortstat`). The published surface is `1.0.0-dev.11`, a major, with the change-policy entry written beside the guarantee it changes.
### The enumeration's conclusion
Written into `## Progress` BEFORE the code, off the installed package's `.d.ts` and the `.js` beside it (they agree). Six members, and the answer differs by what each carries, not by what it is called. `data`, `promise`, `subscription` carry the items; `isLoading`, `nextPage`, `previousPage` do not. Two members of the COLLECTION had to be enumerated with them, because `data` is a `DeepSignalSet` only when no `orderBy` is asked for.
### The policy, and what was rejected
**Decide by the TYPE of the value, not by the name of the member.** A primitive holds no reference, so it cannot reach an item and passes; an object or a function can, so it refuses unless this file has a filtered form of it. Nothing in the code names `isLoading` - it passes because it is a boolean, which is the proof the rule is name-independent.
Rejected, with the deciding reason each time:
- **Filtering the three names.** Ruled out by the task and correct to rule out: it is a list that goes stale on the next bump, which is exactly how this defect arrived.
- **Keeping ONE view and teaching it the envelope.** A `UseShapeResult` and a `DeepSignalSet` have nothing in common - the collection view threw on `[...result]` and `.size` because an envelope is not iterable. One object cannot be both, so there are two views and `surface/use-shape.ts` applies the envelope one.
- **Wrapping `subscription` rather than refusing it.** It reaches the items three ways (`signalObject`, `readyPromise`, `addChangeListener`'s raw arrays) and this surface already says `RdfOrmSubscription` is not published, because one wallet hosts every identity. Wrapping it would publish it by another door and make a second surface to keep honest.
- **Returning `undefined` for a refused member instead of throwing.** Silent is what this file exists not to be.
### Safe for a shape nobody has seen
A member a later `@ng-org/orm` adds is already decided: if it carries items it is an object or a function, and both refuse. `test/use-shape.test.ts` pins it with a hook returning `alpha.21`'s six members PLUS three nobody has declared - `rows` (the collection), `sample()` (a function yielding an item) and `totalCount` (a number). The first throws at the read, the second at the call, the third passes.
### Both directions proved
**The stranger blocked** - `no member of the result reaches a document this identity holds no cap for`: bob reads `data`, `data.size`, `await promise` and gets only his own, `subscription` throws, and the raw collection is asserted to hold BOTH so it is the filter that removed one. Plus `the item-yielding members of data are filtered too`. **5 of the 7 first-written tests FAIL on baseline** (measured by stashing `src/` alone), including both rights tests.
**The owner served** - `the holder reads their own items through every member the SDK offers`: alice reads `data`, `data.size`, `await promise`, `isLoading`, and both page commands forward. And against the DEPLOYED broker: `e2e/orm-live-read.ts`, `5 passed, 0 failed`, the live read ready in 11ms through `data` and `.delete()` on it removing the triples from the document.
### The new fake
`packages/polyfill/test/use-shape-fake.ts`, shared by `bun test` and BOTH e2e entries, returns the shipped envelope: `data` a `Set` or an ordered ARRAY per `SubscriptionData<T, CONF>`, `promise` resolving to it, `subscription` carrying the items three ways, `isLoading`, and the page commands present only under a paginated config. `e2e/orm-live-entry.ts` builds the same object out of the real `getOrCreate`, expression for expression with the shipped adapter minus React's two pieces, and calls the SDK's own `normalizeConf` rather than restating it.
### `UseShapeLike`
Narrowed to `<T extends BaseType>(shapeType: ShapeType<T>, conf: RdfOrmConfig<T> | string | undefined) => ShapeResult`, with `ShapeResult` restated in `model/types.ts` because `UseShapeResult` is exported from NO subpath of the package (`.` is `core`, `/react` the hook alone, `/all` the config types alone - VERIFIED). A compile-time assertion in `test/use-shape.test.ts` pins that the SDK's own `reactUseShape` is assignable with no cast. Stated plainly rather than overclaimed: **this does not by itself prevent a leak** - the defect was runtime proxy behaviour - what it removes is the `any` that let every piece of prose in the repo say the hook returns a bare `DeepSignalSet` with nothing able to contradict it.
### The contract at `:256`
**True now**, and rewritten to say what is filtered, what is refused, and how an unseen member is decided. `1.0.0-dev.11` in `packages/polyfill/package.json`, and the guarantee and its test are in the same commit (`3a89411`).
### The adversary re-run on the fix, which found three more holes
Per `adversarial-findings-need-verification`, measured rather than reasoned. `Object.getOwnPropertyDescriptor` reads a property WITHOUT the `get` trap: `(result, "data")` returned the RAW collection and `(result, "subscription")` the handle `get` refuses - a second door into the same room, and the same hole existed on the collection view for the dunder hatch. And the array form says how many items it holds in four places (`length`, `Object.keys`, `in`, each index's descriptor) of which only `length` was filtered, so the count came back out through the other three and the descriptors handed back the items. All four traps now call ONE `read` function. Fixed in `e86e249`, with the regression pinned in both suites.
### The gate, verbatim
```
475 pass / 0 fail / 1367 expect() calls (bun test; baseline 465 pass / 0 fail, verified)
src exit=0 (pnpm exec tsc --noEmit -p packages/polyfill/tsconfig.json)
e2e exit=0 (pnpm exec tsc --noEmit -p packages/polyfill/e2e/tsconfig.json)
0 ERROR (python3 ~/.claude/skills/concept/lint.py; the same 11 pre-existing WARNINGs)
== Polyfill e2e summary: 47 passed, 0 failed, 47 total - batch 3.4 min, slowest cold sync 31s ==
(46 declared before this task; the extra row is "the read-filtered view refuses the
ORM's own subscription handle". Run three times, 47/47 each time, no login flake.)
== ORM live-read summary: 5 passed, 0 failed, 5 declared - run 0.6 min ==
(run twice; `useShape` ready in 11ms both times, the delete reaching the document)
```
Anti-cheat: no `any`, no `@ts-ignore`, no `as unknown as`, no widened type, no test deleted, skipped or weakened. Checked on the added lines with `git diff 5993a8c | grep "^+"`: the only `any` occurrences are two comments QUOTING the old `UseShapeLike`.
### Surprises
- **The shipped hook's `isLoading` contradicts its own documentation.** `useShape.d.ts` says "`true` when no data is available yet"; `useShape.js` computes `isLoading: !ormSubscription ? false : ormSubscription.isReady`, which is `true` once it IS ready. VERIFIED on both artifacts. Upstream's, not ours; reproduced as written in `e2e/orm-live-entry.ts` and flagged there, because a fake that quietly corrects the target measures something nobody ships.
- **`UseShapeResult` is exported from NO subpath of `@ng-org/orm`.** `.` is `core.d.ts`, `/react` re-exports the hook alone, `/all` re-exports `OrderByConfig`/`RdfOrmConfig`/`SubscriptionData`/`WhereConfig` only. VERIFIED on the manifest and the three `.d.ts`. The type is restated in `model/types.ts` for that reason.
- **The provenance gate forced a citation choice.** `ShapeResult` cannot cite `UseShapeResult`: the cited `nextgraph-rs` revision carries only `useShape` in that file, and `provenance.test.ts` checks that a citation RESOLVES there. It cites `useShape` with the version gap named in the note, which is what `OrmSubscriptionHandle` already does for the `OrmSubscription`/`RdfOrmSubscription` rename.
- **`1.0.0-dev.11` is a major and the entry says why**, including the sentence a consumer needs most: if they were reading `.data`, they were reading unfiltered.
### Follow-ups, out of scope
- T01.p item 1 gets easier: the published `useShape` now returns `ShapeResult`, which is NOT published from `index.ts`. Either publish it or decide the return type stays unnameable, and `DeepSignalSet` is still exported with no published signature using it.
- `e2e/polyfill-entry.ts` and `test/wallet-fake.ts` still inject `(() => {}) as unknown as UseShapeLike` at the call sites that never read a shape. Pre-existing, harmless, and now inconsistent with the one real fake.
- The collection view answers `has` as a filtering FUNCTION even on the array form, where the target has no such method. Not a leak (it filters), a shape it grows.
---
## Round 2 - the review refuted the first delivery, and it was right
**FINDING 1 was a real rights leak and it is closed.** `Set.prototype.add` returns `this`, so the three mutations - forwarded BOUND TO THE TARGET - handed the unfiltered collection back as `add`'s return value. Every way IN was filtered and the way OUT gave the whole set away, through the FIRST member the ORM's own documentation example uses (`expenses.add({...})`).
Verified before changing anything, by running it rather than reading it:
```
Set.add returns this: true DeepSignalSet.add returns === the deepSignal proxy: true
Set.delete returns: boolean DeepSignalSet.delete returns: boolean true
Set.clear returns: undefined DeepSignalSet.clear returns: undefined
```
So one of the three was unsafe in the way the reviewer said, and the other two were safe in their RETURN and unsafe in their REACH - which the reviewer's own repro had shown (`delete(aliceItem)` returned `true` and removed it). The rule is therefore one sentence covering all three: **a mutation reaches the collection underneath, but neither hands back nor acts past what the view shows.**
- `add` hands back the VIEW, so chaining lands on the filtered collection.
- `delete` acts only on an item the view shows - the same question `has` already declines to answer, and the two used to disagree.
- `clear` empties the view, not the collection every identity shares.
Writing still reaches the document: `e2e/orm-live-read.ts` `5 passed, 0 failed` against the deployed broker, with `.delete()` removing the triples.
**The reviewer's own repro, replayed through the published call**: `add returns the raw collection : false`, and what it hands back holds only bob's items.
**THE PATTERN, taken seriously.** Five leaks, one cause: a WRITTEN JUSTIFICATION ABOUT A RETURN VALUE NOBODY EXECUTED. So the first thing done here was to run the return values, and the second was to run a SWEEP of every handled member on both proxies asking "does this hand back the target?". It found one more, on the other proxy: a page command of the ENVELOPE that returned the collection handed it back RAW. `nextPage` is declared `() => void` and yields nothing today, so it is not a live defect - it is the same unexecuted ground on the other side, and it is now closed with a test that pins the RULE. Three copies of that reasoning are now ONE function, `handBack`.
**THE THREE MINORS, all fixed:**
- The array form answered `keys`/`entries`/`forEach` with SET semantics. It now yields indices, `[index, item]`, and the index as `forEach`'s second argument. Owner-path output, no leak.
- **The shared fake was more correct than the target** - `isLoading: !loaded` against the shipped `ormSubscription.isReady`. That re-creates the exact blindness this task removes, and the reviewer is right that it is worse than minor in kind. The fake now reproduces the shipped expression, the file says the inversion is upstream's, and the test asserts the inverted value on purpose. The contract carries it as a non-guarantee telling a consumer to branch on `data === undefined` instead.
- Ten stale `as unknown as UseShapeLike`, not two. All ten take the shared fake, with no cast.
**The contract was FALSE and is now true, verified by running it.** Every clause of `:263` has a test that fails if the clause is false; the two newest were demonstrated failing against the pre-fix code before the fix was written. "mutations pass through" is gone, replaced by what the mutations actually do.
### Round 2 gate, verbatim
```
480 pass / 0 fail / 1393 expect() calls, six consecutive runs (baseline 465 pass / 0 fail)
src exit=0 e2e exit=0 concept lint: 0 ERROR
== Polyfill e2e summary: 47 passed, 0 failed, 47 total ==
== ORM live-read summary: 5 passed, 0 failed, 5 declared - run 0.6 min ==
```
### What round 2 did NOT re-open
The twenty-five access routes the review found closed were left alone, and the sweep re-confirmed that no member of the collection view hands back the target.
### Still open, named rather than fixed
`delete` and `clear` now act only on what the view shows. That is a WRITE-side narrowing justified by the read boundary, and it is the one place this task went beyond the finding: it was done because `has` already refuses the same question, so the view was contradicting itself. A fresh adversary should weigh whether a holder losing the ability to clear a collection they cannot fully see is the right trade.
---
## Round 3 - one leak, three outages, and a correction I owe on my own reporting
**FIRST, A CORRECTION TO ROUND 2's REPORT.** I called the e2e gate green on the SUMMARY LINE and never captured the process's exit code. Sometimes it is `1`: the runner prints its clean summary and then HANGS in teardown until its own 25-minute wall clock aborts it (`[e2e deadline] ... This is a HANG, not a verdict`). **Intermittent, and pre-existing - not mine.** Counted over every run recorded here, the abort follows a clean summary in **3 of 7**, and one of the three is `scratchpad/e2e-run1.log`, written at 14:33 on 2026-09-24 against `46 passed` and predating this task's starting commit `5993a8c` (17:50). The CHECKS are the verdict and they pass in all seven. Reported rather than fixed: it is the harness's teardown, outside this task.
**F1, the leak, closed - and it settles the trade I had asked to have weighed.** `delete` and `clear` asked `keep`; `add` did not. That asymmetry made the sentence I had just published true by coincidence, because the ungated `add` MANUFACTURED the state the other two then handled partially: bob plants an object into ALICE's document, the view filters it straight back out so he never sees it again, the two-way binding commits it there, and `clear()` leaves it behind. Reproduced, then fixed, then re-run.
The arbitration, adopted: **all three ask the same question.** What they ANSWER differs because the members differ, and that is not a residual asymmetry but the members' own contracts - `delete` has `false`, which is the Set contract's word for "it was not there" and is true of the view; `add` returns the collection whether or not it did anything, so it has no value meaning "that did not happen" and therefore THROWS.
**F3/F4, the outage, closed.** Eighteen declared array members plus `includes` and the seven ES2025 set combinators were throwing for the holder whose data it was - a leak traded for an outage, which is not a repair. The view now serves **everything `@ng-org/alien-deepsignals` declares non-mutating**, computed over the readable items. The three key sets are restated (the package exports no subpath for them, and `src/` may not import an optional peer at runtime), and a test READS the shipped `dist/iteratorHelpers.js` and fails if the lists have parted company - so the restatement cannot drift silently. The misnamed `iteratorHelperKeys` comment is corrected by the same change.
**F2, latent, closed.** The descriptor trap returned an ACCESSOR untouched, so its getter - which reads the target - was a door around the view. Not reachable on the shape that ships; reachable on the next one, which is the whole claim this file makes. Getter routed through the view, setter refused.
**F5, closed on the view's own account, and the fake no longer diverges.** There were no `set`/`deleteProperty`/`defineProperty` traps, so against a plain ordered array `view[0] = x`, `delete view[0]` and `view.length = 0` all went through, the last defeating the gated `clear`. Production was protected BY THE TARGET, which is resting on a guarantee this view does not make about a shape it does not control. The view carries it now, proved at the unit level against a PLAIN array so that what refuses is unambiguously the view. And the fake builds the REAL collections - `deepSignal(new Set(...))` and `readOnlyArray(deepSignal([...]))` - so it is neither more correct than the target (last round) nor more permissive (this round).
**MY OWN SWEEP OF THIS ROUND'S REPAIR found two more**, which is the discipline working rather than a surprise: `Object.setPrototypeOf(view, other)` replaced the REAL collection's prototype, after which iterating it throws for every identity - the `clear` problem in another key - and `Object.preventExtensions(view)` (what `Object.freeze` begins with) wrote to the target and would have stranded the index projection, which is only lawful while the target is extensible. Both trapped, both pinned.
**The memo claim corrected**, not removed: it buys `result.data === result.data` within one call, and the cross-render stability it used to claim is unattainable because `useDeepSignal` replaces its root proxy on every relevant rerender.
### Round 3 gate, verbatim
```
486 pass / 0 fail / 1450 expect() calls, six consecutive runs (baseline 465 pass / 0 fail)
src exit=0 e2e exit=0 concept lint: 0 ERROR
== ORM live-read summary: 5 passed, 0 failed, 5 declared - run 0.6 min == (process exit 0)
== Polyfill e2e summary: 47 passed, 0 failed, 47 total == (process exit 0)
```
### Follow-up opened
`e2e/run.ts` INTERMITTENTLY fails to exit after printing its verdict - 3 runs in 7 here, one of them predating this task - and is then killed by its own 25-minute deadline, exiting 1 on a batch whose every check passed. Pre-existing and outside this task, but it makes the exit code an unreliable CI signal, and it is the kind of flake that gets read as a failure of whatever changed last.
---
## Round 4 - the principle was wrong, and the suite was pinning the leak
Three findings, all confirmed by running them before anything was changed. Two of the three are mine rather than inherited, and both are the shape this batch has been chasing: **a rule replaced by a list, and a fix applied to one of two places.**
**F1 - the type-based policy was FALSE, and this is the one that needed a new answer rather than a new name.** "A primitive cannot reach an item, so it passes" - but a COUNT is a primitive that DESCRIBES the items. The file already knew it, because `size` and `length` were computed by an explicit two-name list, which is the forbidden name list reappearing as the fallback the moment the principle failed. Measured: a holder who may read 1 of 3 read `count` and `totalCount` as 3.
**And the suite asserted that as correct** - `expect(result.totalCount).toBe(7)` for a holder of one item, six lines above a test whose own comment calls an unfiltered count the defect. A suite that asserts both would have kept the leak green for ever. That test is rewritten and now asserts the refusal.
**The honest answer, stated plainly because the previous one pretended otherwise: it CANNOT be done by type.** A number is a number, and nothing observable about a getter says whether its 7 counts items or echoes a page size. So the default inverts: **what this file has DECIDED it serves; everything else refuses, primitive or not.** That is safe for an unseen shape BY CONSTRUCTION, which a passing default never was. The prohibition worth keeping was on a list that lets unknowns THROUGH - that breaks silently on a bump; one that refuses them breaks loudly, which is the property that matters. Two carve-outs, neither an SDK spelling: a member the target does not carry answers `undefined` (nothing is behind it), and the LANGUAGE's own primitive protocol keys, closed by the specification rather than by any package. `isLoading` is now DECIDED and served with its reason, rather than passing because it happened to be a boolean.
**F2 - the envelope had two traps to the collection's nine.** `delete result.data`, `result.isLoading = x`, `setPrototypeOf` and `preventExtensions` all reached the SDK's own object - and the last two are the pair round 3 found and trapped, on one view of two. The five write traps now live in ONE object spread into both, so one cannot be repaired without the other.
**F3 - the write gate was vacuous for an object naming no document.** `keep` answers `true` when `@graph` is absent, which is right for a READ (nothing to check) and meaningless for a WRITE: the object landed in the collection every identity shares. Only upstream's "@graph is missing" stopped it - the borrowed target guarantee this file removed for `set`/`deleteProperty` one round earlier and left standing here. A write must now NAME a document and the holder must hold it.
**Minor - `copyWithin`** is the one member upstream's own list mislabels as non-mutating, and its own `readOnlyArray` throws on reading it. Serving it over the snapshot made it succeed and discard in silence, so it is no longer served; the parse gate checks for exactly that one exception and nothing more.
**My own sweep of this round's repair** found nothing further: every count route on both views answers the READABLE count (1 of 3, never 3) - `size`, `Object.keys`, `Reflect.ownKeys`, `JSON.stringify`, spread, `toArray`, `Array.from`, the four combinators, `reduce` - and all four write routes refuse on both views with both targets intact.
### Round 4 gate, with EXIT CODES rather than summary lines
```
bun test, six consecutive runs, exit=0 each: 489 pass / 0 fail / 1465 expect() calls
(baseline 465 pass / 0 fail)
src tsc exit=0 e2e tsc exit=0 concept lint exit=0, 0 ERROR
orm-live-read exit=0: 5 passed, 0 failed, 5 declared
```