docs: la surface publiée reprend les noms, paramètres et retours du SDK

This commit is contained in:
Sylvain Duchesne
2026-09-16 12:12:01 +02:00
parent 88ce8d17c5
commit 820552d263
4 changed files with 101 additions and 0 deletions
@@ -39,5 +39,6 @@ This repo is the **provider** of `polyfill-surface`; consuming applications live
- `.project/rules/nextgraph/no-divergence-from-nextgraph.md` — the test that decides what may be built at all. - `.project/rules/nextgraph/no-divergence-from-nextgraph.md` — the test that decides what may be built at all.
- `knowledge_what-an-app-deletes-at-migration` — the two fates a published symbol can have. - `knowledge_what-an-app-deletes-at-migration` — the two fates a published symbol can have.
- `knowledge_what-an-iri-says-upstream` — the same two fates for a written *triple*: NextGraph's own vocabulary, ours, and which of our IRIs must outlive the migration. - `knowledge_what-an-iri-says-upstream` — the same two fates for a written *triple*: NextGraph's own vocabulary, ours, and which of our IRIs must outlive the migration.
- `knowledge_what-a-read-scope-is-upstream` — what the target offers for reading many objects at once, and the one question it cannot express.
- `knowledge_what-an-inbox-is-upstream` — what the target actually offers around inboxes, and where a choice of ours begins. - `knowledge_what-an-inbox-is-upstream` — what the target actually offers around inboxes, and where a choice of ours begins.
- `knowledge_who-processes-an-inbox` — the division of labour along a deposit's path: what this package applies, what it merely delivers, and what a layer above owes itself. - `knowledge_who-processes-an-inbox` — the division of labour along a deposit's path: what this package applies, what it merely delivers, and what a layer above owes itself.
@@ -0,0 +1,63 @@
---
type: knowledge
summary: 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
last_checked: 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.
+1
View File
@@ -5,3 +5,4 @@ Each rule lists its one-line summary. **Read the full file** when a rule looks r
## nextgraph ## nextgraph
- **no-divergence-from-nextgraph** — Never diverge from NextGraph — ask "does the target do this?" FIRST, and only then how badly a caller would be misled → `.project/rules/nextgraph/no-divergence-from-nextgraph.md` - **no-divergence-from-nextgraph** — Never diverge from NextGraph — ask "does the target do this?" FIRST, and only then how badly a caller would be misled → `.project/rules/nextgraph/no-divergence-from-nextgraph.md`
- **surface-mirrors-the-sdk** — Publish the SDK's names, parameters and returns — a difference must be justified beside the code → `.project/rules/nextgraph/surface-mirrors-the-sdk.md`
@@ -0,0 +1,36 @@
---
name: surface-mirrors-the-sdk
summary: Publish the SDK's names, parameters and returns — a difference must be justified beside the code
group: nextgraph
scope: local
created: 2026-09-16
---
# The published surface mirrors the SDK
**Rule:** What this package publishes exposes, as far as possible, functions of the **same NAMES**, taking the **same PARAMETERS**, returning the **same VALUES** as the target SDK. A difference is allowed — but it must be **duly justified**, and the best justification of all is that the difference is **NextGraph's own direction, as announced by its developers**. The justification lives in the symbol's provenance annotation, beside the code, never in a conversation or a prose document.
This is the surface-level sibling of [[no-divergence-from-nextgraph]], which asks *does the target do this?* of the model and the semantics. That question can be answered correctly and this one still failed: a symbol may carry the target's semantics exactly and still be published under a name the target never uses, taking a parameter the target cannot express. An application does not consume semantics. It types a name, passes an argument, reads a return — those three are what it will have to unlearn.
**Why.** The abstract statement of this rule was available for weeks and changed nothing, so what follows is what actually happened, verified in the tree.
**A name the target never uses.** `watchShape` has **zero** occurrences in `nextgraph-rs`. Worse, it spends `watch`, a verb the ORM has already spent on signal observation — `watch as watchDeepSignal` in `sdk/js/orm/src/connector/GraphOrmSubscription.ts`, `watchEffect` in the Vue adapter. So the name is not merely absent upstream, it reads as something upstream already means. The target's stated name is `useShapeQuery`.
**A parameter that had to be translated.** Its `scope` is `Scope = "public" | "protected" | "private"` (`packages/polyfill/src/model/types.ts`) — three invented literals. The SDK's own `Scope` is `{ graphs?: string[] | string; subjects?: string[] }` (`sdk/js/orm/src/types.ts`). Not equivalent, therefore **not passable through**: the polyfill has to translate one into the other, and that translation is what forced an entire home-grown read pipeline beneath it — scope resolution, document enumeration, a sync barrier, a batch read. **A parameter that must be translated is the tell that a mechanism below it is about to become ours.** The cost of a re-invented parameter is never the parameter.
**A true citation attached to the wrong claim.** `readUnion` has no counterpart upstream — it is N calls plus a batch policy — yet its head annotation reads `kind=aligned level=2 ref=sdk/js/lib-wasm/src/lib.rs:sparql_query`. That symbol exists and does what the annotation says it does. The citation is exact; the claim is false. A *composition* cannot be "aligned" on a single function. **This is the tell to watch for**, because it defeats the very thing annotations are for: a reader checks the citation, finds it correct, and moves on. The rest of the surface offers the honest form by contrast — `useShape` is annotated `kind=passthrough level=3` and the cited symbol *is* the function it forwards to. (The annotation has since been split so that the enumeration and the failure policy are marked `invention`; the head claim of alignment is still there.)
**The legitimate deviation, so nobody concludes every difference is a fault.** `watchShape` returns a `ShapeQuery` shaped like TanStack Query's `useQuery` — `{ data, isPending, isSuccess, isError, error }` — where the SDK's `useShape` returns a bare `DeepSignalSet`. That is a real difference from what the SDK ships today, and it is **justified**: NextGraph's direction, as reported by its developers, is a `useShapeQuery` returning exactly that shape. Deviating *toward* the announced target is alignment, not divergence — it is the rest of the surface that will have to catch up.
**The gap that same example exposes — a known weakness of the annotation format.** A target **stated by its developers but not yet written** has no `file:symbol` to cite: `useShapeQuery` has zero occurrences in the tree. The taxonomy in [[no-divergence-from-nextgraph]] has no kind for it either — `declared-not-wired` needs a declaration, `reserved-name` needs a name present in the tree. So an author holding the strongest possible justification has only two bad moves left: cite an approximate symbol, which is precisely the `readUnion` failure, or declare `invention`, which understates the alignment and tells a later reader the shape is ours to change. Recorded here as a weakness to fix, deliberately not fixed here.
**How to apply.**
- **Before naming a published symbol**, find the target's own name and use it. If the target has not named it, check the verb is *free* at the target: one it already spends elsewhere is taken, and borrowing it says something false.
- **Prefer passing the target's type through to translating it.** When a parameter cannot be passed through, stop and ask what will have to be built beneath it — that is the real decision, and it is being made by the parameter.
- **Return what the SDK returns**, unless the difference is the target's announced direction — then say whose statement it is.
- **"Duly justified" has a fixed cost**: one annotation line, beside the code, naming what the target does, what was chosen instead, and why. The test is refutability — a reader must be able to check it by opening one file.
- **Check every citation against the CLAIM, not just against the tree.** Same granularity on both sides: a function may cite a function; a composition, a pipeline or a policy cites nothing at level 2 and must say so. "The reference resolves" is not "the annotation is true".
- **Applies to what is published.** An internal helper that claims to resemble the target falls under [[no-divergence-from-nextgraph]]; this rule is about the symbols an application types.
**Pending, not done.** Renaming `watchShape` to `useShapeQuery` and removing `readUnion` are **decided and not yet applied**. Read the two as live examples, not as history.