From 9e00a89fe88945d27a76ce00757b26f166c93d75 Mon Sep 17 00:00:00 2001 From: Sylvain Duchesne Date: Thu, 24 Sep 2026 20:47:46 +0200 Subject: [PATCH] chore: T01.o est close, cinq rounds adverses et la garde sait echouer --- ...-filter-is-bypassed-by-plain-properties.md | 43 ++++++++++++++++++- 1 file changed, 41 insertions(+), 2 deletions(-) rename .tasks/{doing => done}/T01.o-the-read-filter-is-bypassed-by-plain-properties.md (83%) diff --git a/.tasks/doing/T01.o-the-read-filter-is-bypassed-by-plain-properties.md b/.tasks/done/T01.o-the-read-filter-is-bypassed-by-plain-properties.md similarity index 83% rename from .tasks/doing/T01.o-the-read-filter-is-bypassed-by-plain-properties.md rename to .tasks/done/T01.o-the-read-filter-is-bypassed-by-plain-properties.md index 22fb1d5..a837cee 100644 --- a/.tasks/doing/T01.o-the-read-filter-is-bypassed-by-plain-properties.md +++ b/.tasks/done/T01.o-the-read-filter-is-bypassed-by-plain-properties.md @@ -255,13 +255,13 @@ Three findings, all confirmed by running them before anything was changed. Two o **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. +**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, 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. +**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. @@ -271,6 +271,45 @@ Three findings, all confirmed by running them before anything was changed. Two o 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 ```