Files
ng-eventually/.tasks/doing/T01.o-the-read-filter-is-bypassed-by-plain-properties.md
T
2026-09-24 20:16:02 +02:00

31 KiB

id, title, parent, depends_on, created, claimed_by, claimed_at, blocked
id title parent depends_on created claimed_by claimed_at blocked
T01.o The read filter is bypassed by plain properties, and no fake shows it T01
2026-09-24 worker-T01.o 2026-09-24 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