feat: la fuite du filtre de lecture devient la tache prioritaire

This commit is contained in:
Sylvain Duchesne
2026-09-24 17:50:28 +02:00
parent a7f9f4e18a
commit 5993a8c4a2
5 changed files with 183 additions and 32 deletions
@@ -1,32 +0,0 @@
---
id: T01.g
title: Adversarial review of the whole published surface
parent: T01
depends_on: [T01.a, T01.b, T01.c, T01.d, T01.e, T01.f, T01.h, T01.i, T01.j, T01.k, T01.l, T01.m, T01.n]
created: 2026-09-16
claimed_by: null
claimed_at: null
blocked: false
---
## Description
An adversarial pass over the **whole** published surface — every symbol exported from `packages/polyfill/src/index.ts`, not only the ones this batch touched — trying to **refute** the parent task's success criterion: *every published symbol either mirrors an SDK symbol in name, parameters and return, or carries a justified annotation*.
**Method, in this order.** For each published symbol ask the two questions of `.project/rules/nextgraph/no-divergence-from-nextgraph.md` in their stated order: *does the target do this?* **first**, verified in `nextgraph-rs` or stated by its author; *would an application have to unlearn it?* second, and only to rank severity — never to authorise a divergence. Then, for the name and the shape (`.project/rules/nextgraph/surface-mirrors-the-sdk.md`): is this the target's own name, does the verb belong to something else upstream, is the parameter passed through or translated, is the return the SDK's.
**Check every `@provenance` line against the CLAIM, not just against the tree.** Same granularity on both sides: a function may cite a function; a composition, a pipeline or a policy cites nothing at level 2 and must say so. *"The reference resolves"* is not *"the annotation is true"* — that is precisely how `readUnion` survived. Use the mechanism as T01.a left it, including whatever the new kind for an announced-but-unwritten target requires as evidence.
**Known, and not findings.** The three open `bug_` leaves in `.project/concepts/app-contract/`: `bug_useshape-live-read-needs-a-newer-broker` (MAJOR), `bug_a-lost-inbox-watch-is-never-retried` (MAJOR), `bug_an-empty-inbox-list-can-mean-a-failure`. Do not re-report them as new. **Do** report it if a change in this batch altered their status, or invalidated a workaround they or the contract publish — the `useShape` bug leaf and the contract's `## Non-guarantees` both name `watchShape` and `readUnion` as the unaffected paths, and both were changed here.
**Check the contract as delivered**, since three children edited it in sequence: `## Surface` matches the actual exports symbol for symbol; `## Change policy` records a version whose bump level follows the contract's own rules for what actually changed; no section was half-edited or edited twice for the same change; nothing published is left described by a sentence that is no longer true.
**Findings are leads, not mandates, and this child fixes nothing.** A finding without a reproducible failure scenario or a checkable citation does not count. Report symptom, precise location (`file:line`, the failing assertion, the offending extract) and, where it is already known, the cause — but do not instrument, bisect or repair. Each fix is a new task.
**Deliverable.** In `## Result`: findings by severity (blocking / major / minor), atomic, numbered `1.1`, `1.2`, … so they can be answered precisely. Neutral tone, no first or second person. One line of verdict on the test suites, not a list of what passes.
**Pass condition for this child** (it writes no code): run `bun test`, `bunx tsc --noEmit -p packages/polyfill/tsconfig.json`, `bunx tsc --noEmit -p packages/polyfill/e2e/tsconfig.json` and `python3 ~/.claude/skills/concept/lint.py` to **establish** the delivered state and report it verbatim; the review covers every symbol exported from `packages/polyfill/src/index.ts`, with the ones found clean stated as a count rather than an inventory.
## Progress
## Result
@@ -0,0 +1,57 @@
---
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: null
claimed_at: null
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
## Result
@@ -0,0 +1,25 @@
---
id: T01.p
title: Four minor faults on the published surface
parent: T01
depends_on: [T01.o]
created: 2026-09-24
claimed_by: null
claimed_at: null
blocked: false
---
## Description
The four minor findings of T01.g, all CONFIRMED there. None is a rights or correctness defect; each is a published-surface or annotation inaccuracy. Verify each still stands before changing it - T01.o may have moved some.
1. `Schema` and `DeepSignalSet` are published with no published signature using them, against the rule stated at `packages/polyfill/src/index.ts:78` (the same `ReadCap`/`InboxScope` rule, so this is a recurrence). Either a published signature uses them or they are unpublished.
2. Seven symbols annotated `kind=invention level=none ref=none` sit ABOVE the polyfill-era divider, under the heading that promises a target counterpart for every symbol (`index.ts:45`, `:111-123`). Either the annotation is wrong or they are on the wrong side of the divider.
3. `docChangeType` is published but absent from the concept contract's `## Surface`, and nothing pins that block against the actual exports. Add it, and consider whether the block should be pinned by a test the way the deletion list already is.
4. `account-registry.ts:971` annotates `storeRegistry.resolveScopeGraph`, unpublished since `1.0.0-dev.5`. A provenance annotation on an unpublished symbol claims a surface that does not exist.
**Hard gate:** `bun test` -> 0 fail; both typechecks exit 0; `python3 ~/.claude/skills/concept/lint.py` -> 0 ERROR. Anti-cheat as usual. Any symbol you unpublish is a MAJOR by the contract's own change policy - follow it rather than deciding the level yourself.
## Progress
## Result