From 97699c232e32389b8f6f8507a1272d52364d6312 Mon Sep 17 00:00:00 2001 From: Sylvain Duchesne Date: Thu, 24 Sep 2026 20:16:02 +0200 Subject: [PATCH] chore: le Result porte le round 4 --- ...-filter-is-bypassed-by-plain-properties.md | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/.tasks/doing/T01.o-the-read-filter-is-bypassed-by-plain-properties.md b/.tasks/doing/T01.o-the-read-filter-is-bypassed-by-plain-properties.md index 0ab5156..22fb1d5 100644 --- a/.tasks/doing/T01.o-the-read-filter-is-bypassed-by-plain-properties.md +++ b/.tasks/doing/T01.o-the-read-filter-is-bypassed-by-plain-properties.md @@ -245,3 +245,32 @@ src exit=0 e2e exit=0 concept lint: 0 ERROR `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 +``` +