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:
+27
-14
@@ -89,31 +89,44 @@ async function main(): Promise<void> {
|
|||||||
const nuri = await sdk<string>(frame, "docCreate");
|
const nuri = await sdk<string>(frame, "docCreate");
|
||||||
check("docCreate returns a usable NURI", typeof nuri === "string" && nuri.length > 0, nuri);
|
check("docCreate returns a usable NURI", typeof nuri === "string" && nuri.length > 0, nuri);
|
||||||
});
|
});
|
||||||
await step("SPARQL anchored default-graph round-trip", async () => {
|
await step("SPARQL graph-behavior characterization (a/b/c)", async () => {
|
||||||
const rt = await sdk<any>(frame, "docRoundTrip");
|
const rt = await sdk<any>(frame, "docRoundTrip");
|
||||||
// THE load-bearing assertion fake-ng cannot verify: the anchored default-
|
// (a) THE load-bearing assertion fake-ng cannot verify: the anchored default-
|
||||||
// graph write (no GRAPH clause) round-trips through the real broker's
|
// graph write (no GRAPH clause) round-trips through the real broker's
|
||||||
// repo_graph_name overlay. This is what read-model / inbox / store-registry
|
// repo_graph_name overlay. This is the canonical shape the lib writes, and
|
||||||
// all rely on.
|
// what read-model / inbox / store-registry all rely on.
|
||||||
check(
|
check(
|
||||||
"anchored default-graph write ROUND-TRIPS (the shape the lib writes)",
|
"(a) anchored default-graph write (no GRAPH) ROUND-TRIPS",
|
||||||
rt.anchoredPresent === true,
|
rt.anchoredPresent === true,
|
||||||
`predicates=${JSON.stringify(rt.predicates)}`,
|
`predicates=${JSON.stringify(rt.predicates)}`,
|
||||||
);
|
);
|
||||||
// FINDING (reported, not asserted): on this broker version, an explicit
|
// (b) FINDING (reported, not gating): on THIS broker version an explicit
|
||||||
// `INSERT DATA { GRAPH <plainNuri> {…} }` ANCHORED to the same doc ALSO
|
// `INSERT DATA { GRAPH <plainNuri> {…} }` ANCHORED to the same doc ALSO
|
||||||
// round-trips via the anchored default-graph read (explicitGraphPresent),
|
// round-trips — readable both via the anchored default-graph read
|
||||||
// AND is reachable via an explicit `GRAPH <plainNuri>` read
|
// (explicitGraphPresent) AND via an explicit `GRAPH <plainNuri>` read
|
||||||
// (explicitViaNamedGraph). i.e. when anchored, the plain NURI resolves to
|
// (explicitViaNamedGraph). i.e. when anchored, the plain NURI resolves to the
|
||||||
// the same repo graph — it is NOT a phantom graph here. The lib still writes
|
// SAME repo graph — there is NO "phantom graph" here. The lib still writes the
|
||||||
// the no-GRAPH default-graph shape (the always-safe one); this records what
|
// no-GRAPH default-graph shape as the always-safe canonical convention; this
|
||||||
// the broker actually does, so the lib's "phantom graph" comments can be
|
// records what the broker actually does so the "phantom graph" comments can be
|
||||||
// re-checked against this broker version.
|
// re-checked against this broker version by re-running this harness.
|
||||||
record(
|
record(
|
||||||
"[finding] explicit GRAPH <plainNuri> when anchored resolves to the same repo",
|
"(b) [finding] explicit GRAPH <plainNuri> ANCHORED resolves to the same repo (no phantom graph)",
|
||||||
true,
|
true,
|
||||||
`defaultGraphRead=${rt.explicitGraphPresent} namedGraphRead=${rt.explicitViaNamedGraph} (informational)`,
|
`defaultGraphRead=${rt.explicitGraphPresent} namedGraphRead=${rt.explicitViaNamedGraph} (informational)`,
|
||||||
);
|
);
|
||||||
|
// (c) FINDING: the ANCHORLESS `GRAPH ?g { … }` union scan spans EVERY named
|
||||||
|
// graph in the session store (it saw BOTH doc A's and doc B's graphs). This is
|
||||||
|
// the O(wallet-size) cost the read path avoids by reading each doc with its own
|
||||||
|
// anchored default-graph query. Reported, not gating (it is a perf property of
|
||||||
|
// the union, not a correctness assertion of the lib's write/read shape).
|
||||||
|
const us = rt.unionSpan ?? {};
|
||||||
|
record(
|
||||||
|
"(c) [finding] anchorless GRAPH ?g scan spans ALL named graphs (O(wallet) union)",
|
||||||
|
true,
|
||||||
|
us.graphCount === -1
|
||||||
|
? `anchorless scan errored: ${us.error} (union claim NOT re-verified here)`
|
||||||
|
: `sawDocA=${us.sawDocA} sawDocB=${us.sawDocB} graphCount=${us.graphCount} (both ⇒ union spans all graphs)`,
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// ── read-model ──────────────────────────────────────────────────────────
|
// ── read-model ──────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -121,30 +121,34 @@ const identity = new IdentityStore(
|
|||||||
return docs.sparqlQuery(s.session_id, query, undefined, anchor);
|
return docs.sparqlQuery(s.session_id, query, undefined, anchor);
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* The load-bearing docs test: prove the anchored DEFAULT-graph write is what
|
* The load-bearing graph-behavior characterization against the REAL broker.
|
||||||
* round-trips, and an explicit `GRAPH <plainNuri>` write is NOT (fake-ng cannot
|
* fake-ng cannot verify any of this — it needs the broker's repo_graph_name
|
||||||
* see this — it needs the real broker's repo_graph_name overlay).
|
* overlay. This is the ground truth the lib's write/read comments cite.
|
||||||
* - write a triple with NO GRAPH clause, anchored to the doc → default graph
|
*
|
||||||
* - write a DIFFERENT triple wrapped in `GRAPH <plainNuri>` → phantom graph
|
* Anchored to doc D, it measures each write shape:
|
||||||
* - read back (anchored, default graph): only the anchored write is present
|
* (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() {
|
async docRoundTrip() {
|
||||||
const s = await sessionReady;
|
const s = await sessionReady;
|
||||||
const doc = await docs.docCreate(s.session_id, "Graph", "data:graph", "store", undefined);
|
const doc = await docs.docCreate(s.session_id, "Graph", "data:graph", "store", undefined);
|
||||||
const subj = "urn:e2e:s";
|
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(
|
await docs.sparqlUpdate(
|
||||||
s.session_id,
|
s.session_id,
|
||||||
`INSERT DATA { <${subj}> <urn:e2e:anchored> "yes" }`,
|
`INSERT DATA { <${subj}> <urn:e2e:anchored> "yes" }`,
|
||||||
doc,
|
doc,
|
||||||
);
|
);
|
||||||
// (B) explicit GRAPH <plainNuri> write — targets a phantom graph.
|
// (b) explicit `GRAPH <plainNuri>` write, anchored to the same doc.
|
||||||
await docs.sparqlUpdate(
|
await docs.sparqlUpdate(
|
||||||
s.session_id,
|
s.session_id,
|
||||||
`INSERT DATA { GRAPH <${doc}> { <${subj}> <urn:e2e:explicitGraph> "yes" } }`,
|
`INSERT DATA { GRAPH <${doc}> { <${subj}> <urn:e2e:explicitGraph> "yes" } }`,
|
||||||
doc,
|
doc,
|
||||||
);
|
);
|
||||||
// read back anchored default graph
|
// read back anchored default graph — both predicates expected if (b) round-trips.
|
||||||
const res: any = await docs.sparqlQuery(
|
const res: any = await docs.sparqlQuery(
|
||||||
s.session_id,
|
s.session_id,
|
||||||
`SELECT ?p ?o WHERE { <${subj}> ?p ?o }`,
|
`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 rows = Array.isArray(res) ? res : res?.results?.bindings ?? [];
|
||||||
const preds = rows.map((r: any) => r.p?.value).filter(Boolean);
|
const preds = rows.map((r: any) => r.p?.value).filter(Boolean);
|
||||||
// (C) probe: does an explicit `GRAPH <plainNuri>` write, when the query is
|
// (b, cont.) read the explicit-graph triple back via its OWN named graph, to
|
||||||
// NOT anchored to the doc, land where the anchored default-graph read sees it?
|
// confirm `GRAPH <plainNuri>` when anchored resolves to the same repo graph.
|
||||||
// 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.
|
|
||||||
const explicitNamed: any = await docs.sparqlQuery(
|
const explicitNamed: any = await docs.sparqlQuery(
|
||||||
s.session_id,
|
s.session_id,
|
||||||
`SELECT ?o WHERE { GRAPH <${doc}> { <${subj}> <urn:e2e:explicitGraph> ?o } }`,
|
`SELECT ?o WHERE { GRAPH <${doc}> { <${subj}> <urn:e2e:explicitGraph> ?o } }`,
|
||||||
@@ -165,12 +166,51 @@ const identity = new IdentityStore(
|
|||||||
doc,
|
doc,
|
||||||
);
|
);
|
||||||
const enRows = Array.isArray(explicitNamed) ? explicitNamed : explicitNamed?.results?.bindings ?? [];
|
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 {
|
return {
|
||||||
doc,
|
doc,
|
||||||
predicates: preds,
|
predicates: preds,
|
||||||
anchoredPresent: preds.includes("urn:e2e:anchored"),
|
anchoredPresent: preds.includes("urn:e2e:anchored"),
|
||||||
explicitGraphPresent: preds.includes("urn:e2e:explicitGraph"),
|
explicitGraphPresent: preds.includes("urn:e2e:explicitGraph"),
|
||||||
explicitViaNamedGraph: enRows.length > 0,
|
explicitViaNamedGraph: enRows.length > 0,
|
||||||
|
unionSpan,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -127,13 +127,15 @@ export async function post(targetInbox: Nuri, opts: PostOptions): Promise<void>
|
|||||||
const fromTriple =
|
const fromTriple =
|
||||||
from == null ? "" : ` ;\n <${P.from}> "${escapeLiteral(from)}"`;
|
from == null ? "" : ` ;\n <${P.from}> "${escapeLiteral(from)}"`;
|
||||||
|
|
||||||
// NO explicit `GRAPH <…>` wrapper: the real broker stores each repo's triples
|
// NO explicit `GRAPH <…>` wrapper — write the anchored DEFAULT graph:
|
||||||
// under repo_graph_name(repo_id, overlay_id) — the doc NURI WITH an overlay
|
|
||||||
// suffix, not the plain NURI. An `INSERT DATA { GRAPH <plainNuri> {…} }`
|
|
||||||
// therefore targets a phantom graph and does NOT round-trip. The only
|
|
||||||
// verified-working shape is the anchored DEFAULT graph (no GRAPH clause):
|
|
||||||
// `sparqlUpdate(sid, update, targetInbox)` scopes the write to that repo's
|
// `sparqlUpdate(sid, update, targetInbox)` scopes the write to that repo's
|
||||||
// default graph (same shape as read-model.ts readDoc/readUnion).
|
// default graph (same shape as read-model.ts readDoc/readUnion). This is the
|
||||||
|
// CANONICAL, always-safe shape and the one the anchored default-graph read
|
||||||
|
// queries. (Not a round-trip necessity on the current broker: the e2e harness
|
||||||
|
// `packages/client/e2e/` verified that an anchored `GRAPH <plainNuri>` write
|
||||||
|
// ALSO round-trips here — it resolves to the same repo graph, no phantom graph.
|
||||||
|
// The no-GRAPH form is kept as a simplicity/safety convention; re-verify with
|
||||||
|
// that harness if the broker version changes.)
|
||||||
const update = `
|
const update = `
|
||||||
INSERT DATA {
|
INSERT DATA {
|
||||||
<${subject}> a <${P.type}> ;
|
<${subject}> a <${P.type}> ;
|
||||||
|
|||||||
@@ -422,11 +422,14 @@ export async function createEntityDoc(id: string, scope: Scope): Promise<Nuri> {
|
|||||||
try {
|
try {
|
||||||
await sparqlUpdate(
|
await sparqlUpdate(
|
||||||
s.sessionId,
|
s.sessionId,
|
||||||
// NO explicit `GRAPH <…>` wrapper: the real broker stores each repo's
|
// NO explicit `GRAPH <…>` wrapper: write the anchored DEFAULT graph (the
|
||||||
// triples under repo_graph_name(repo_id, overlay_id), not the plain NURI,
|
// `indexDoc` anchor scopes it) — the CANONICAL, always-safe shape the
|
||||||
// so an INSERT into `GRAPH <plainNuri>` targets a phantom graph and does
|
// anchored default-graph read queries (readScopeIndex below, same as
|
||||||
// NOT round-trip. Write the anchored DEFAULT graph instead (the `indexDoc`
|
// read-model.ts). Not a round-trip necessity on the current broker: the e2e
|
||||||
// anchor scopes it). entityNuri is a NURI stored as a literal → escapeLiteral.
|
// harness (`packages/client/e2e/`) verified an anchored `GRAPH <plainNuri>`
|
||||||
|
// write ALSO round-trips here (same repo graph, no phantom graph); no-GRAPH
|
||||||
|
// is kept as a simplicity/safety convention. entityNuri is a NURI stored as
|
||||||
|
// a literal → escapeLiteral.
|
||||||
`INSERT DATA { <${INDEX_SUBJECT}> <${P.contains}> "${escapeLiteral(entityNuri)}" }`,
|
`INSERT DATA { <${INDEX_SUBJECT}> <${P.contains}> "${escapeLiteral(entityNuri)}" }`,
|
||||||
indexDoc,
|
indexDoc,
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user