refactor(data): per-doc anchored reads over the virtual wallet

Read each by-need entity document with its own anchored query (bounded to the
current account's virtual wallet), never an anchorless scan of the physical shared
wallet. The 75s ORM hang stays gone; a non-empty PHYSICAL wallet now costs nothing
(never scanned). Removed the throwaway anchorless-union probe.

Known remaining (test-infra, not the product): the @data suite still times out
because THIS test account's VIRTUAL wallet is bloated (hundreds of docs
accumulated across this session's many runs) → per-doc reads are O(my docs), and
`clearWallet` still enumerates all accounts. Needs per-scenario test isolation
(fresh/small virtual wallet) + a virtual-wallet-scoped clear to validate green.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sylvain Duchesne
2026-07-05 22:50:15 +02:00
parent 8bb19b687b
commit 8ca79c6d16
6 changed files with 31 additions and 104 deletions
+6 -46
View File
@@ -128,12 +128,13 @@ function ConnectedHarness() {
// The app writes ONE DOCUMENT PER ENTITY (events → public per-entity docs,
// participations/users → protected per-entity docs) via `createEntityDoc`,
// and READS by the union model (T03.k): resolve the by-need doc NURIs (my own
// scope docs + the discovery index) then run ONE anchorless union
// `sparql_query` (`readEntities` → `readModel.readUnion`), re-querying on a
// change signal — never the reactive per-entity ORM fan-out (that HANGS). The
// and READS by need: resolve the bounded by-need doc NURIs (my own scope docs
// + the discovery index) then read EACH doc with its OWN anchored `sparql_query`
// (`readEntities` → `readModel.readUnion`), re-querying on a change signal —
// never the reactive per-entity ORM fan-out (that HANGS), and never an
// anchorless scan of all graphs (O(wallet), times out on a bloated wallet). The
// step-facing `events/users/participations` + mutations/queries delegate to the
// APP data context (`appData`), i.e. the exact union-read path the screens use.
// APP data context (`appData`), i.e. the exact read path the screens use.
// The step contract (`[...td.events]` with `@id`/`title`/`participantCount`,
// `.size`, `p.user`/`p.event`) is preserved by mapping the app types to that
// shape in a Set-like adapter.
@@ -510,47 +511,6 @@ function ConnectedHarness() {
return nuri;
},
/**
* T03.k PROBE — pins down the read-model union premise against the REAL
* broker (docs/read-model.md § Minimal broker probe). Creates two graph
* docs A and B, writes a DISTINCT triple into each (anchored per-doc),
* then queries GRAPH ?g { ?s ?p ?o } twice: once with NO anchor (expect
* BOTH A and B — the LOCAL UNION) and once anchored to A (expect ONLY A).
* Returns the graphs seen in each mode so the step can assert the model.
*/
async runUnionProbe() {
const sid = session.session_id;
const docA = await docs.docCreate(sid, 'Graph', 'data:graph', 'store', undefined);
const docB = await docs.docCreate(sid, 'Graph', 'data:graph', 'store', undefined);
const sA = `urn:probe:s:${Date.now().toString(36)}:a`;
const sB = `urn:probe:s:${Date.now().toString(36)}:b`;
await docs.sparqlUpdate(sid, `INSERT DATA { GRAPH <${docA}> { <${sA}> <urn:probe:p> "A" } }`, docA);
await docs.sparqlUpdate(sid, `INSERT DATA { GRAPH <${docB}> { <${sB}> <urn:probe:p> "B" } }`, docB);
// Query our OWN probe subjects (sA/sB) so the assertion is by triple,
// not by the repo_graph_name (which carries an overlay suffix and won't
// string-equal the doc NURI). ?g is still selected for observability.
const q = `SELECT ?g ?s ?o WHERE { GRAPH ?g { ?s <urn:probe:p> ?o . FILTER(?s IN (<${sA}>, <${sB}>)) } }`;
const readObjs = (res: any): string[] => {
const rows = Array.isArray(res) ? res : res?.results?.bindings ?? [];
return rows.map((r: any) => r?.o?.value).filter(Boolean);
};
// NO anchor → local union across all opened graphs.
const unionRes = await docs.sparqlQuery(sid, q, undefined, undefined);
const unionObjs = readObjs(unionRes);
// Anchor = A → one repo only.
const anchorRes = await docs.sparqlQuery(sid, q, undefined, docA);
const anchorObjs = readObjs(anchorRes);
return {
docA, docB,
unionObjs,
anchorObjs,
unionHasA: unionObjs.includes('A'),
unionHasB: unionObjs.includes('B'),
anchorHasA: anchorObjs.includes('A'),
anchorHasB: anchorObjs.includes('B'),
};
},
/**
* Round-trip the sharedWalletShim through the wallet: create an account
* (3 docs + SPARQL INSERT), drop the cache, reload from the wallet via