7.3 KiB
type, summary, last_checked
| type | summary | last_checked |
|---|---|---|
| knowledge | What the target offers for reading many objects — a graph is one document, a store's graph holds only its containment triples, and scoping a read by store is not expressible at all | 2026-09-16 |
What a read scope is upstream
Read at the source in nextgraph-rs (213338f6). Every claim points at a symbol or a module rather than a line, so git grep settles any doubt after the code moves. The question answered here is the one a caller actually asks: how do I ask for many objects at once, and what can the request say?
The type, and the two axes it filters on
Scope = { graphs?: string[] | string; subjects?: string[] } (sdk/js/orm/src/types.ts), normalised by normalizeScope into { graphs: string[]; subjects: string[] }. Its own documentation states the edges: ["did:ng:i"] or [""] means the whole dataset, while [] or undefined returns no objects at all. Two axes, no third.
A graphs entry is one document's graph, never a set of them
At the binding, orm_start_graph (sdk/js/lib-wasm/src/lib.rs) passes each string through NuriV0::new_from (engine/net/src/app_protocol.rs). That function accepts exactly five forms and rejects everything else: did:ng:i; a repo id did:ng:o:<repo>; a file read-cap did:ng:j:<id>:k:<key>; a repo graph did:ng:o:<repo>:v:<overlay>; a branch did:ng:o:<repo>:v:<overlay>:b:<branch>. No string parses to a store target. And nuri_to_string (engine/verifier/src/orm/graph/utils.rs) then collapses every non-UserSite NURI to a single repo graph name, did:ng:o:<repo>:v:<overlay> — one document's graph, whatever went in.
Naming a store's graph reaches its containment triples, not its documents
Verifier::doc_create (engine/verifier/src/request_processor.rs) writes <store> <http://www.w3.org/ns/ldp#contains> <did:ng:o:doc> into the store's graph, while the new document's own triples live in its repo graph. The object of that triple is the bare repo-id form, not even a graph name.
So the obvious formulation — "search my private store" — resolves to the store's graph, matches the containment triples, and returns an empty set for the shape being asked about. Silently: no error exists for it, because a shape query matching nothing is indistinguishable from a scope holding nothing.
did:ng:i and "" replace the whole list
In orm_start_graph, an entry equal to "" or did:ng:i discards every other entry and substitutes NuriV0::new_entire_user_site(); nuri_to_string renders that back as the string did:ng:i. Downstream, query_quads_for_shape and query_graph_subjects (engine/verifier/src/orm/graph/query.rs) test nuris[0] == "did:ng:i" — positional, which holds because the binding has already collapsed the list to a singleton — and when it does, they drop the graph filter entirely: the emitted SELECT carries no FILTER(?g …) and runs over the whole local dataset.
Nothing is added to the query to narrow it by what the reader may open. query_sparql_select parses the SPARQL and runs it against the verifier's dataset as-is. The only bound is what the verifier has materialised locally — documents opened from a held ReadCap, load_repo_from_read_cap (engine/verifier/src/verifier.rs) — not a check performed when the query runs.
The graph filter is literal
schema_shape_to_sparql (engine/verifier/src/orm/graph/query.rs) emits FILTER(?g IN (<g1>, <g2>, …)) from the list verbatim. No prefix, pattern or hierarchy matching: a graph is in the list or it is not. StorageReader::repos_in_store (engine/oxigraph/src/oxigraph/storage/mod.rs) could enumerate a store's repos — and has zero callers.
entire_store is the shape to align on, and nothing wires it
NuriV0.entire_store (engine/net/src/app_protocol.rs) is documented "If it is a store, will include all the docs belonging to the store". Every constructor sets it false, nothing anywhere sets it true, and its only three readings are guards (is_branch_identifier, is_valid_for_sparql_update, is_valid_for_discrete_update) that require it false. No code expands it into a graph list.
That is declared-not-wired in the taxonomy of no-divergence-from-nextgraph: the target has written down the shape of the question even though nothing answers it yet — and a declared shape is the strongest thing to align on, far stronger than the absence of an implementation is evidence against it.
subjects filters the root shape only
query_quads_for_shape seeds the caller's subject list as the pending set of the root shape; every nested shape gets its subjects from what the root query discovered, not from the caller. The emitted filter is FILTER(?s IN (…)) over root-subject IRIs.
The engine takes more than the surface hands it
OrmConfig (engine/net/src/orm.rs) carries where_, order_by, select, page_size and max_active_pages, and schema_shape_to_sparql accepts a where config, an order-by config and a (limit, offset) pair — it emits ORDER BY, LIMIT and OFFSET from them. But Verifier::start_orm (engine/verifier/src/orm/graph/initialize.rs) builds its config from json!({}) behind a // TODO, so the config every query reads is always empty: no caller can reach any of it. The richness is real and unreachable.
"Not ready" and "ready and empty" are distinguishable — one level down
OrmSubscription (sdk/js/orm/src/connector/GraphOrmSubscription.ts) is exported from the package entry — core.ts, which @ng-org/orm resolves . to — and carries a public readyPromise getter. So at that level the two states are distinguishable. The React useShape adapter drops the distinction: it returns a DeepSignalSet and documents it as "the orm objects or an empty set, if still loading". The gap is between level 3's class and level 3's hook, not between us and the target.
What this package does instead
useShape here is a passthrough (packages/polyfill/src/surface/use-shape.ts, annotated kind=passthrough level=3): the scope goes to the injected SDK hook unchanged, and the rights filter is applied to the results before they reach the caller. makeReadFilteredView (packages/polyfill/src/emulated-verifier/read-filter.ts) keeps only items whose document — their @graph — the current holder has a cap for, annotated kind=aligned level=1 ref=engine/verifier/src/verifier.rs:load_repo_from_read_cap.
That is the emulation of what the broker will bound at delivery, done where the target does it: per document, on what comes back. It disappears at migration, with the module — see knowledge_what-an-app-deletes-at-migration.
What follows
Scoping a read by store is a question the SDK cannot express. No string names a store as a read target; the store's own graph holds containment triples and nothing else; the one field that would mean it is declared and unwired. Nor is it a question an application needs to ask: an application names the documents it holds — it cannot discover, it can only follow a reference (knowledge_what-an-iri-says-upstream for what those references say). Enumerating documents is only forced if you insist on asking by store.
Where the target does answer, it answers on two axes — by document (graphs) and by subject (subjects) — and those are the two to build on.