docs+test(client): correct the phantom-graph claim, verify graph behavior in e2e

The lib e2e harness now characterizes all three SPARQL graph shapes against the real
broker (24/24): (a) no-GRAPH anchored write round-trips; (b) an explicit anchored
GRAPH <plainNuri> ALSO resolves to the same repo (no phantom graph) — the earlier
'targets a phantom graph, does NOT round-trip' claim is FALSE on @ng-org/web
0.1.2-alpha.13; (c) an anchorless GRAPH ?g scan spans every named graph (O(wallet)
union, 32 graphs) — TRUE, re-verified.

Reconcile the comments accordingly: inbox.ts / store-registry.ts drop the false
phantom-graph justification (no-GRAPH stays as the canonical, always-safe shape — a
simplicity choice, not a round-trip necessity; re-verify via e2e if the broker
version changes). read-model.md keeps the anchorless-O(wallet) rationale (the real
reason reads are per-doc anchored) and appends a note distinguishing the variable
GRAPH ?g SCAN from a constant GRAPH <D> WRITE. No new absolute introduced.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sylvain Duchesne
2026-07-07 09:38:59 +02:00
parent 1c475d8c9f
commit 4a2569e243
4 changed files with 97 additions and 39 deletions
+54 -14
View File
@@ -121,30 +121,34 @@ const identity = new IdentityStore(
return docs.sparqlQuery(s.session_id, query, undefined, anchor);
},
/**
* The load-bearing docs test: prove the anchored DEFAULT-graph write is what
* round-trips, and an explicit `GRAPH <plainNuri>` write is NOT (fake-ng cannot
* see this — it needs the real broker's repo_graph_name overlay).
* - write a triple with NO GRAPH clause, anchored to the doc → default graph
* - write a DIFFERENT triple wrapped in `GRAPH <plainNuri>` → phantom graph
* - read back (anchored, default graph): only the anchored write is present
* The load-bearing graph-behavior characterization against the REAL broker.
* fake-ng cannot verify any of this — it needs the broker's repo_graph_name
* overlay. This is the ground truth the lib's write/read comments cite.
*
* Anchored to doc D, it measures each write shape:
* (a) `INSERT DATA { <s> <p> o }` (NO GRAPH) → read anchored default graph
* (b) `INSERT DATA { GRAPH <D> { <s> <p> o } }` (explicit plain NURI) → read
* both the anchored default graph AND `GRAPH <D>` explicitly
* (c) anchorless `SELECT … WHERE { GRAPH ?g { … } }` over TWO docs D and D2 →
* does it see BOTH docs' graphs (the O(wallet) union scan)?
*/
async docRoundTrip() {
const s = await sessionReady;
const doc = await docs.docCreate(s.session_id, "Graph", "data:graph", "store", undefined);
const subj = "urn:e2e:s";
// (A) anchored default-graph write — the verified-working shape.
// (a) anchored default-graph write (no GRAPH) — the canonical shape the lib writes.
await docs.sparqlUpdate(
s.session_id,
`INSERT DATA { <${subj}> <urn:e2e:anchored> "yes" }`,
doc,
);
// (B) explicit GRAPH <plainNuri> write — targets a phantom graph.
// (b) explicit `GRAPH <plainNuri>` write, anchored to the same doc.
await docs.sparqlUpdate(
s.session_id,
`INSERT DATA { GRAPH <${doc}> { <${subj}> <urn:e2e:explicitGraph> "yes" } }`,
doc,
);
// read back anchored default graph
// read back anchored default graph — both predicates expected if (b) round-trips.
const res: any = await docs.sparqlQuery(
s.session_id,
`SELECT ?p ?o WHERE { <${subj}> ?p ?o }`,
@@ -153,11 +157,8 @@ const identity = new IdentityStore(
);
const rows = Array.isArray(res) ? res : res?.results?.bindings ?? [];
const preds = rows.map((r: any) => r.p?.value).filter(Boolean);
// (C) probe: does an explicit `GRAPH <plainNuri>` write, when the query is
// NOT anchored to the doc, land where the anchored default-graph read sees it?
// This distinguishes "explicit GRAPH is a phantom graph" from "explicit GRAPH
// resolves to the same repo when anchored". We also read the explicit-graph
// triple back via its OWN named graph to see where it actually lives.
// (b, cont.) read the explicit-graph triple back via its OWN named graph, to
// confirm `GRAPH <plainNuri>` when anchored resolves to the same repo graph.
const explicitNamed: any = await docs.sparqlQuery(
s.session_id,
`SELECT ?o WHERE { GRAPH <${doc}> { <${subj}> <urn:e2e:explicitGraph> ?o } }`,
@@ -165,12 +166,51 @@ const identity = new IdentityStore(
doc,
);
const enRows = Array.isArray(explicitNamed) ? explicitNamed : explicitNamed?.results?.bindings ?? [];
// (c) anchorless `GRAPH ?g` union scan across TWO docs. Create a second doc
// with a DISTINCT triple, then run an ANCHORLESS scan (anchor undefined →
// UserSite → local union). If it returns rows from BOTH graphs, the anchorless
// union spans every named graph in the session store — the O(wallet-size) cost
// the read path exists to avoid. Two docs is enough to demonstrate the union;
// it does not bloat the wallet.
let unionSpan: { sawDocA: boolean; sawDocB: boolean; graphCount: number } = {
sawDocA: false,
sawDocB: false,
graphCount: 0,
};
try {
const docB = await docs.docCreate(s.session_id, "Graph", "data:graph", "store", undefined);
await docs.sparqlUpdate(
s.session_id,
`INSERT DATA { <urn:e2e:s2> <urn:e2e:anchoredB> "yes" }`,
docB,
);
const union: any = await docs.sparqlQuery(
s.session_id,
`SELECT ?g ?s ?p WHERE { GRAPH ?g { ?s ?p ?o } }`,
undefined,
undefined, // ANCHORLESS → UserSite → local union of all named graphs
);
const uRows = Array.isArray(union) ? union : union?.results?.bindings ?? [];
const subjs = new Set(uRows.map((r: any) => r.s?.value).filter(Boolean));
const graphs = new Set(uRows.map((r: any) => r.g?.value).filter(Boolean));
unionSpan = {
sawDocA: subjs.has(subj),
sawDocB: subjs.has("urn:e2e:s2"),
graphCount: graphs.size,
};
} catch (e: any) {
unionSpan = { sawDocA: false, sawDocB: false, graphCount: -1 };
(unionSpan as any).error = String(e?.message ?? e);
}
return {
doc,
predicates: preds,
anchoredPresent: preds.includes("urn:e2e:anchored"),
explicitGraphPresent: preds.includes("urn:e2e:explicitGraph"),
explicitViaNamedGraph: enRows.length > 0,
unionSpan,
};
},