Files
ng-eventually/.project/concepts/app-contract/knowledge_what-a-read-scope-is-upstream.md
Sylvain Duchesne e16fd6a707 docs: ce que le hook du SDK rend vraiment, la ou le depot disait un set nu
Six endroits affirmaient au present que `useShape` rend un `DeepSignalSet` nu.
C-est vrai de la revision citee, faux de celle contre laquelle ce paquet est
construit - et deux de ces six PORTAIENT une decision de provenance :

- `ShapeQuery kind=invention` etait justifie par "aucun type amont ne porte
  d-etat de chargement", alors que `isLoading` en porte un. Le kind tient, mais
  pour l-autre raison : ce qui n-a de contrepartie nulle part est le troisieme
  etat, la lecture ECHOUEE, parce qu-un `orm_start_graph` refuse n-est qu-un
  `console.error` et laisse une attente eternelle a tous les niveaux.
- l-annotation `announced-not-written` de `useShapeQuery` posait la meme
  premisse fausse. La direction annoncee ne bouge pas ; sa premisse si.

Ajoutee aussi l-instruction d-injection qui ne resolvait pas : l-export `.` de
`@ng-org/orm` pointe sur `core`, qui n-exporte aucun `useShape`. Il est sur
`/react` (export par defaut) ou `reactUseShape` sur `/all`.
2026-09-24 18:28:24 +02:00

9.9 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-24

What a read scope is upstream

Read at the source in nextgraph-rs (213338f6), which declares OLDER releases than this repo installs - the ANCHOR in packages/polyfill/test/provenance.test.ts holds the gap and checks both its ends. 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 at 213338f6), normalised by normalizeScope into { graphs: string[]; subjects: string[] }. In the installed @ng-org/orm@0.1.2-alpha.21, graphs is REQUIRED (graphs: string[] | string, dist/types.d.ts); subjects? stays optional. A caller that omitted graphs no longer compiles.

The two axes read [] in OPPOSITE directions, and this is the trap. Upstream documents each field separately, and they do not agree:

field ["did:ng:i"] or [""] [] or undefined
graphs the whole dataset no objects at all - the EMPTY set
subjects n/a no filtering - every subject

So { graphs: [], subjects: [] } returns nothing, and it is graphs alone that decides it. Reading the pair as symmetrical is how a read that should have been unscoped becomes a read that matches nothing, silently, since a shape query matching nothing is indistinguishable from a scope holding nothing. 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.

How far where, orderBy and pageSize are known to travel

OrmConfig (engine/net/src/orm.rs) carries where_, order_by, select, page_size and max_active_pages; OrmConfig::from_json parses all five and refuses a pageSize greater than zero without an orderBy; and every one of them is consumed downstream - schema_shape_to_sparql takes the where config, the order-by config and a (limit, offset) pair and emits FILTER, ORDER BY, LIMIT and OFFSET from them, while initialize.rs drives its paging window and its page eviction from page_size and max_active_pages. The config machinery is implemented, not stubbed.

One hop is missing, at the ENTRY: Verifier::start_orm takes no config argument at all - request_processor.rs calls it with three - so it builds one from json!({}) behind a // TODO. OrmConfig::from_json names the missing hop in its own TODO, "Use json in libwasm context", and that is exactly what is absent there: orm_start_graph (sdk/js/lib-wasm/src/lib.rs) declares FIVE parameters at this commit, with no slot for a config.

That absence describes 213338f6 and nothing later, and the installed releases are past it. @ng-org/web@0.1.2-alpha.14 ships orm_start_graph with SIX parameters, config at index 4, and @ng-org/orm@0.1.2-alpha.21 calls it with six, passing its whole options object into that slot - both read in the shipped artifacts, not inferred. So at the releases this repo installs the config LEAVES level 3 and is ACCEPTED at level 2. Whether the engine behind that binding feeds it to from_json is settled by nothing reachable: the shipped wasm carries no source, and this tree is demonstrably older than the binding it would be asked about. Only a live measurement answers it, and none has been made.

Reading the json!({}) as "level 1 discards the richness" is inference from an absent implementation, which no-divergence-from-nextgraph forbids - and it was written here once, as "no caller can reach any of it".

"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. In the installed alpha.21 both the symbol and its file are renamed, to RdfOrmSubscription and connector/RdfOrmSubscription.ts, and it gains an isReady getter that answers the same question without awaiting. So at that level the two states are distinguishable. The React useShape adapter drops the distinction: what it publishes beside the data is an isLoading, which says whether the initial payload has arrived and has no value for a read that will never answer - and its own doc still describes the data 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.