36 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.mdinstructs injecting "the REAL @ng-org/orm useShape" (seedocs/api-contract.md:50), but@ng-org/orm's.export maps todist/core.d.ts, which exports NOuseShape- it lives at/react, or asreactUseShapefrom/all. The instruction does not resolve. CONFIRMED by T01.g.- Four present-tense claims that the SDK's
useShapereturns a bareDeepSignalSetare false of alpha.21, and TWO are load-bearing provenance grounds:use-shape-query.ts:284'sannounced-not-writtenpremise, and:193'sShapeQuery kind=invention level=none ref=none, justified as "no upstream type carries load state" whenUseShapeResult.isLoadingdoes. This is thereadUnionfailure mode again - a citation that resolves, attached to a claim that is false - andprovenance.test.tscannot 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
UseShapeResultand aDeepSignalSethave nothing in common - the collection view threw on[...result]and.sizebecause an envelope is not iterable. One object cannot be both, so there are two views andsurface/use-shape.tsapplies the envelope one. - Wrapping
subscriptionrather than refusing it. It reaches the items three ways (signalObject,readyPromise,addChangeListener's raw arrays) and this surface already saysRdfOrmSubscriptionis 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
undefinedfor 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
isLoadingcontradicts its own documentation.useShape.d.tssays "truewhen no data is available yet";useShape.jscomputesisLoading: !ormSubscription ? false : ormSubscription.isReady, which istrueonce it IS ready. VERIFIED on both artifacts. Upstream's, not ours; reproduced as written ine2e/orm-live-entry.tsand flagged there, because a fake that quietly corrects the target measures something nobody ships. UseShapeResultis exported from NO subpath of@ng-org/orm..iscore.d.ts,/reactre-exports the hook alone,/allre-exportsOrderByConfig/RdfOrmConfig/SubscriptionData/WhereConfigonly. VERIFIED on the manifest and the three.d.ts. The type is restated inmodel/types.tsfor that reason.- The provenance gate forced a citation choice.
ShapeResultcannot citeUseShapeResult: the citednextgraph-rsrevision carries onlyuseShapein that file, andprovenance.test.tschecks that a citation RESOLVES there. It citesuseShapewith the version gap named in the note, which is whatOrmSubscriptionHandlealready does for theOrmSubscription/RdfOrmSubscriptionrename. 1.0.0-dev.11is 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
useShapenow returnsShapeResult, which is NOT published fromindex.ts. Either publish it or decide the return type stays unnameable, andDeepSignalSetis still exported with no published signature using it. e2e/polyfill-entry.tsandtest/wallet-fake.tsstill inject(() => {}) as unknown as UseShapeLikeat the call sites that never read a shape. Pre-existing, harmless, and now inconsistent with the one real fake.- The collection view answers
hasas 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.
addhands back the VIEW, so chaining lands on the filtered collection.deleteacts only on an item the view shows - the same questionhasalready declines to answer, and the two used to disagree.clearempties 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/forEachwith SET semantics. It now yields indices,[index, item], and the index asforEach's second argument. Owner-path output, no leak. - The shared fake was more correct than the target -
isLoading: !loadedagainst the shippedormSubscription.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 ondata === undefinedinstead. - 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. (The second was UNSOUND as reasoned - the specification closes the keys, not the values behind them - and is gone in round 5 below; the first stands, with what it discloses now stated exactly.) 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. (This sentence continued "and its own readOnlyArray throws on reading it" - FALSE, and reasoned rather than run; corrected in round 5 below, where the measurement shows it runs and destroys an element.) 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
polyfill e2e exit=0: 47 passed, 0 failed, 47 total - 0 FAIL rows, no hang abort
Status: done. Five adversarial rounds; the rights boundary closed after four, the record and the gate after five. What the whole task is worth remembering for is one sentence: every leak in this family, and both remaining faults in the record, came from a justification about a value that nobody executed. rule_justify-a-return-value-by-running-it was written in this batch and its first real application was to its own neighbour, which it failed - so the rule is right and it needs a habit behind it, not a second statement.
Round 5 - the record and the gate, not the boundary
The rights position on the shipped shapes is closed, confirmed independently across ~40 routes on both views with both live suites green on their EXIT CODES. The five findings were about whether the record tells the truth about the code and whether the gate can fail. Both were wrong in my favour, which is the worst direction. Each was reproduced before anything changed.
F2 - MY RECORDED GROUND WAS FALSE, and it was reasoned rather than run. I had written - in the code, the contract, a test comment and this Result - that upstream's readOnlyArray throws on reading copyWithin, so refusing it merely matched the target. Measured against the installed package:
ro.copyWithin -> function (no throw on the read)
ro.copyWithin(0, 1) -> ["B","C","C"] from ["A","B","C"]: the first element
is DESTROYED
ro.push / splice / sort / reverse / fill / shift -> THROW on the READ
readOnlyArray refuses every mutator it knows about, and copyWithin escapes for exactly the reason it is named in this file: it sits in the same mislabelled list the refusal is built from. Refusing it is STRICTER than the target, not equal to it - a deliberate divergence, now recorded as one with the measurement beside it in all four places. This is the first thing rule_justify-a-return-value-by-running-it should have caught, and its first application was to its own neighbour.
F4 - the file still documented the rule this round DELETED. Two headers described the type-based default ("a count, a flag passes untouched", "nothing below names isLoading") while the code names isLoading a few lines on, and the contract carried both policies at once. Both headers now state the current rule and record the two defaults that were tried and failed, since each read as obviously sound when written; the contract bullet states one policy.
F3 - the gate could not fail. Proved by running the experiment: a verbatim copy with {copyWithin, slice} as the exception set and slice dropped from the served list gave 1 pass / 0 fail. It asserted filtered comparisons, which any number of exceptions satisfies. It now asserts a PARTITION - upstream's effective set equals the union, the three parts are disjoint, the exception set is exactly the one member named - and reads the SYMBOL entries the string-only regex could not see. Verified red in three directions: dropping a served member (2 fail), smuggling a second exception (2 fail), adding a phantom helper (1 fail).
F1 - the ECMAScript carve-out was unsound as reasoned. The specification closes the KEY set, not the values behind it, so a Symbol.toStringTag implemented as a getter returning Set(${size}) published the unfiltered count through a key this file had called harmless. The carve-out is gone: both keys are DECIDED, with values the view computes from the shape it wraps, answered only when the target carries one. Measured on the hostile case - [object Set] where the raw count is 2 and the readable count 1. The existence oracle is not defended but STATED: the view discloses whether the target carries a key, which is the SDK's published shape rather than the holder's data, and it is not a count channel - measured, on every shipped shape no key's presence depends on the items.
F5 - reduce branched on the accumulator's VALUE instead of on arity. An explicit reduce(fn, undefined) was read as the one-argument form: an owner with no readable items got a TypeError where a Set or Array answers undefined, and with one item got that item back without fn running. Both measured against the native behaviour they must match, both fixed, both pinned.
The two sentences that were unsafe advice. The contract said "Log data" and use-shape.ts advertised that the spread reads through the view. Neither survives: a console is the HOST's - console.log(result) prints every identity's items from behind the proxy and no trap can prevent it - and the envelope's spread THROWS. The contract now says not to inspect what a shape read hands you when several identities share a page, and says why that is not a hole a version can close.
Round 5 gate, with EXIT CODES
bun test, six consecutive runs, exit=0 each: 491 pass / 0 fail / 1479 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