docs: chaque symbole dit d'où il vient

98 annotations posées à côté des déclarations, et un test qui les exige sur la
surface publiée. Elles portent trois choses : le niveau qui répond, la référence
amont, et la catégorie parmi les cinq.

La cinquième est celle qui manquait : declared-not-wired, quand la cible DÉFINIT
la forme et ne la câble pas. Neuf symboles en relèvent, dont readLinks — que
j'avais classé « notre invention » en raisonnant depuis l'absence, alors que
c'est le meilleur alignement disponible.

Les références citent un SYMBOLE, jamais une ligne : trois citations du document
avaient déjà pourri. Cinq corrections au passage, toutes vérifiées à la source —
un chemin ORM qui n'existe pas, deux plages de lignes fausses, et surtout
docs.* et subscribeDoc étiquetés PASSTHROUGH alors qu'ils sont alignés : nos
noms, plus un argument jamais transmis. La sémantique survit à la migration,
les sites d'appel non, et la nuance disparaissait sous une étiquette trop
flatteuse.

Le test échoue à l'annotation retirée, à la catégorie mal orthographiée, et à
une invention qui prétendrait citer une référence — vérifié en cassant les
trois. Il a aussi attrapé un défaut en lui-même : le gabarit de format placé
dans index.ts se faisait analyser comme une annotation.

La classification couvre l'interne qui prétend ressembler à la cible — tout
emulated-verifier — et exclut ce qui ne le prétend pas. La faute d'origine
portait sur une fonction non exportée ; n'être pas publié n'a protégé personne.

Quatre symboles ont résisté et sont annotés avec leur catégorie dominante, la
seconde nommée dans la note plutôt que lissée.
This commit is contained in:
Sylvain Duchesne
2026-08-16 22:53:50 +02:00
parent 6138d831da
commit 43aadbeb45
26 changed files with 353 additions and 8 deletions
+8
View File
@@ -0,0 +1,8 @@
# Doc-debt — app-contract
> Presence of a block = doc to update. Processed → delete the block; no blocks left → delete this file.
> One block = one "big change": `why` + `files` + `verify` (leaves to review).
## Raw markers (consolidate into blocks, then delete)
- TOUCHED docs/api-contract.md @2026-08-16 (session f93872b5-293a-4916-a353-181409a96d42)
- TOUCHED packages/polyfill/src/index.ts @2026-08-16 (session f93872b5-293a-4916-a353-181409a96d42)
+32 -6
View File
@@ -15,6 +15,28 @@
Per the design principle (`README.md` § *Design principle*): an absent implementation is never treated as evidence about the future — "the engine does not do X" and "the SDK will not offer X" are kept apart throughout.
### The per-symbol annotations are the authority; this document is the reasoning
Since 2026-08-16 every published symbol — and every internal one that claims to mirror the target — carries its provenance **in the source, beside the declaration**, pinned by `packages/polyfill/test/provenance.test.ts`:
```text
// @provenance <name> kind=<kind> level=<1|2|3|none> ref=<path:symbol|none> — <note>
```
It exists because this document could not stop drifting from the code: it claimed a "1:1 passthrough" for `docs.*` that was false in two ways, and `emulated-verifier/branch-registers.readLinks` was classified as this library's own invention when it implements `AddLinkV0` — a type the engine **defines** and constructs nowhere. That second mistake is the one the annotations are designed against: *nothing constructs this upstream* is an **absence**, and an absence is not evidence that a thing is ours.
The `ref` cites a **symbol**, never a line number — three citations in this document had already rotted (§ 2, § 4, § 12 below).
The prose labels above map onto the annotation's closed `kind` set, and where they disagree **the annotation wins**:
| label here | annotation `kind` | |
|---|---|---|
| PASSTHROUGH | `passthrough` | only when the name and shape are upstream's OWN, so the call site survives migration untouched |
| PASSTHROUGH | `aligned` | when the semantics are upstream's but the name or the ergonomics are ours (`docCreate` for `doc_create`, a `label` parameter that is not forwarded) — § 7, § 8 |
| LEVEL-1 SHAPE | `aligned` / `declared-not-wired` | the two are NOT the same claim: `aligned` means the engine does this and we follow it; `declared-not-wired` means the engine **defines** the type and constructs it nowhere |
| ASSUMPTION / NO COUNTERPART | `invention` | nothing at any level answers; `ref` is `none`, deliberately |
| *(no prose label)* | `divergent` | upstream does this and we do it differently — the inbox-address publication, the re-readable deposit list |
---
## 1. Bootstrap and configuration
@@ -84,7 +106,7 @@ export declare const init: (callback: Function | null, singleton: boolean, acces
// level 3 — @ng-org/orm: sdk/js/orm/src/connector/initNg.ts:51 (exported as initNg from core.ts)
export function initNgSignals(ngImpl: NG, session: Session): void;
// level 2 — the Session initNg consumes: index.d.ts:264-272
// level 2 — the Session initNg consumes: index.d.ts:265-273 (source sdk/js/web/src/index.ts:15-22)
export declare type Session = {
session_id: string | number;
protected_store_id: string;
@@ -179,7 +201,11 @@ const useShape = <T extends BaseType>(
scope: Scope | string | undefined
) => DeepSignalSet<T>;
// its Scope — sdk/js/orm/src/model/types.ts:25-38 (NOT this lib's Scope, see § 12)
// its Scope — sdk/js/orm/src/types.ts:25-38 (NOT this lib's Scope, see § 12)
// *(Corrected 2026-08-16: this said `sdk/js/orm/src/model/types.ts`, a path that does not
// exist in the clone. The type is at `sdk/js/orm/src/types.ts:25`. A citation nobody can
// follow reads as verified and is not — the reason the per-symbol annotations below cite a
// SYMBOL rather than a line.)*
export type Scope = {
graphs?: string[] | string;
subjects?: string[];
@@ -309,7 +335,7 @@ export async function sparqlQuery(
### Target
**PASSTHROUGH (level 2, VERIFIED)** — these forward to the real methods, and the ONLY argument they do not pass on is the trailing `label` (a lib-internal access-log tag). Until 2026-08-14 this entry claimed a 1:1 mirror, which was false in two ways, both now fixed rather than documented as deltas:
**Annotated `aligned` (level 2, VERIFIED) — not `passthrough`, and the distinction is the point.** These forward to the real methods with the same arguments in the same order, but the names are ours (`docCreate` for `doc_create`) and they take a trailing `label` (a lib-internal access-log tag) the real signatures have no room for. So the *semantics* survive migration and the *call site* does not — which is exactly what `aligned` says and what a blanket "PASSTHROUGH" hid. *(This entry said PASSTHROUGH until 2026-08-16; it had already been caught claiming a 1:1 mirror, false in two ways, both fixed rather than documented as deltas:)*
- **The session id is `string | number`, upstream's own declared type for it** (`Session.session_id`, `sdk/js/web/src/index.ts:16` and the installed `index.d.ts:266`) — it used to be narrowed to `string` here, which made the value the SDK hands an application impossible to pass back into this library. It is RELAYED, never converted: the wasm side deserializes a `u64` (`sdk/js/lib-wasm/src/lib.rs:352-358` `sparql_query`, `:452-457` `sparql_update`, `:1575` `doc_create`), and stringifying it fails that deserialization for real — observed as `Deserialization error of session_id JsValue("1")`.
- **`sparqlUpdate` returns what the real method returns** — the commits the update produced (`lib.rs:481-483` serialises `AppResponseV0::Commits`; the installed `index.d.ts:297` types it `Promise<any>`). It was declared `Promise<void>` while already relaying the value at runtime, so the answer was thrown away for every caller. Typed `unknown` rather than `any`, exactly as `sparqlQuery` already renders the same upstream `Promise<any>`. A caller that ignores it is unaffected.
@@ -361,7 +387,7 @@ export function subscribeDocs(
### Target
**PASSTHROUGH (level 2, VERIFIED) with two deliberate ergonomic deltas:**
**Annotated `aligned` (level 2, VERIFIED) — the deltas below are what makes it `aligned` rather than `passthrough`:**
```ts
// index.d.ts:66, source sdk/js/lib-wasm/src/lib.rs:1908
@@ -569,10 +595,10 @@ export async function openDocumentInbox(doc: NuriLike): Promise<Nuri>;
- **`createEntityDoc(id, scope)` → level 2, VERIFIED direction.** Target: `doc_create(session_id, crdt, class_name, destination, store_repo)` aimed at the identity's real per-scope store (see § 7 for the store-targeting nuance — the nodejs SDK already takes `store_type`/`store_repo` strings). The two writes the lib performs by hand are **native side effects** of `doc_create` upstream: the `ldp:contains` listing on the store's Main branch and the `AddRepo { read_cap }` on its Store branch (`engine/verifier/src/request_processor.rs:697-710`). The `id` parameter is already gone from the published call (2026-08-10); expect `createEntityDoc(scope)` to become `doc_create(sid, …, storeOf(scope))` with no listing/cap bookkeeping.
- **`listMyEntityDocs(id, scope)` → level 1/2, VERIFIED mechanism.** Upstream the listing is the store's `ldp:contains` graph (written at `request_processor.rs:706-708`), readable with an anchored `sparql_query` on the store; the caps come back by replaying the Store branch (`AddRepo::verify``load_repo_from_read_cap`). The function's shape (give me my per-scope doc NURIs) survives; its implementation becomes one native read.
- **`userStoreDoc(id, scope)` / `resolveScopeGraph(scope)` / `resolveWriteGraph(id, scope)` → level 2, VERIFIED.** The target answers these from the session: `did:ng:` + `session.private_store_id | protected_store_id | public_store_id` (`Session`, `index.d.ts:264-272`). The store IS the container; the per-scope index document disappears.
- **`userStoreDoc(id, scope)` / `resolveScopeGraph(scope)` / `resolveWriteGraph(id, scope)` → level 2, VERIFIED.** The target answers these from the session: `did:ng:` + `session.private_store_id | protected_store_id | public_store_id` (`Session`, `sdk/js/web/src/index.ts:Session`, installed `index.d.ts:265-273`). The store IS the container; the per-scope index document disappears.
- **`userInbox(id)` → level 1, VERIFIED counterpart with a different granularity.** Upstream a user's inboxes are their public and protected STORE repos' inboxes — the only two `AddInboxCap` commits in the engine (`engine/verifier/src/site.rs:128,149`). An identity-level "my inbox" therefore maps to a store inbox; the resolution moves into the lib/SDK and the consumer's act (deposit to an address, process my own) is unchanged.
- **`openDocumentInbox(doc)` / `documentInboxAddress(doc)` → level 1, VERIFIED support, no exerciser.** Every `Repo` carries `inbox: Option<PrivKey>` (`engine/repo/src/repo.rs:126`); `AddInboxCapV0` is keyed by `repo_id` with no is-store restriction (`engine/repo/src/types.rs:1973`; applied at `engine/verifier/src/verifier.rs:1920-1928`); but no code path creates one for a plain document (`doc_create``new_repo_default``Store::create_repo_default``create_repo_with_keys`, which builds the `Repo` with `inbox: None``engine/verifier/src/verifier.rs:3004`, `engine/repo/src/store.rs:264,284,691`) and no level-2/3 API exposes any of it. So: the *capability* is engine-verified; the *functions* are invented surface; and the **address publication is a real, deliberate divergence** (upstream transmits addresses, never publishes them — § 9), with the ownership guard compensating our design, not mirroring an upstream rule.
- **`addLink(cap)` / `readLinks()`level 1, VERIFIED model, no JS surface.** The emulated `AddLink { read_cap }` register (`engine/repo/src/types.rs:1939-1948`*"so that a user can share with all its device a new Link they received"*, external repos only). Upstream this filing happens inside the verifier when it processes the inbox; the future SDK most likely never exposes these as calls, so consumers should not code against them (§ 15).
- **`addLink(cap)` / `readLinks()`annotated `declared-not-wired` (level 1).** The emulated `AddLink { read_cap }` register (`engine/repo/src/types.rs:AddLinkV0`*"so that a user can share with all its device a new Link they received"*, external repos only). **Verified 2026-08-16, and this is the load-bearing fact:** the type is DEFINED, its `CommitVerifier::verify` is a no-op `Ok(())` (`engine/verifier/src/commits/mod.rs`), and **nothing in the engine constructs one** — the only other occurrences are the `CommitBodyV0::AddLink` enum variant and two match arms classifying body kinds. Its sibling `AddRepoV0` *is* constructed (`Verifier::send_add_repo_to_store`), which is why the Store-branch register next door is annotated `aligned` and this one is not. A maintainer read that absence and concluded the function was this library's own invention; it is not — an absent implementation says nothing about the target. Upstream this filing happens inside the verifier when it processes the inbox; the future SDK most likely never exposes these as calls, so consumers should not code against them (§ 15).
- **`resolveAccount` / `ensureAccount` / `VirtualUserRecord` / `RegistrySession` / `reservedAccount` / `resetRegistryCache` → NO COUNTERPART.** The shared-wallet shim (accounts directory, pointer → doc-shim indirection) has no image in the target — the target has no central directory of identities (`docs/migration-guide.md` § 3). The whole group disappears with the shim.
- **`isOwnInbox` / `myInboxes` → NO COUNTERPART as API.** Upstream the question "which inboxes may I read" is answered inside the verifier by the User branch's `AddInboxCap` records; nothing suggests a JS API for it. These exist for the emulated read guard and the connection drain.
@@ -68,6 +68,7 @@ import type { InboxScope, Nuri, NuriLike, PrincipalId, ReadCap, Scope } from "..
* inbox read guard consults (`inbox.ts`). Anonymous holds no inbox, so it is false
* for everyone until an identity is set.
*/
// @provenance isOwnInbox kind=aligned level=1 ref=engine/repo/src/types.rs:AddInboxCapV0 — 'which inboxes may I read' is answered by the User branch's records
export async function isOwnInbox(nuri: Nuri): Promise<boolean> {
const holder = getCurrentUser();
if (holder === null) return false;
@@ -100,6 +101,7 @@ export async function isOwnInbox(nuri: Nuri): Promise<boolean> {
* not ours to hold. `id` is compared through the shim key, so it matches however
* the consumer spells the identity.
*/
// @provenance holdOwnCap kind=aligned level=1 ref=engine/repo/src/types.rs:AddRepoV0 — the cap of a document you created, filed on the store's Store branch. WIRED upstream — `send_add_repo_to_store` constructs it
export function holdOwnCap(id: string, scope: Scope, doc: Nuri, cap: ReadCap): void {
const holder = getCurrentUser();
if (holder === null || accountKey(holder) !== accountKey(id)) return;
@@ -124,6 +126,7 @@ export function holdOwnCap(id: string, scope: Scope, doc: Nuri, cap: ReadCap): v
* Scoped to the current holder, like {@link holdOwnCap}: another user's stores are
* emphatically not ours to hold.
*/
// @provenance fileOwnStructure kind=aligned level=1 ref=engine/verifier/src/site.rs:create_individual_ — a user's three stores are its own repos; it holds their caps by BEING a user
export function fileOwnStructure(id: string, record: VirtualUserRecord): void {
const holder = getCurrentUser();
if (holder === null || accountKey(holder) !== accountKey(id)) return;
@@ -135,6 +138,7 @@ export function fileOwnStructure(id: string, record: VirtualUserRecord): void {
/** Same, for the user's own inbox — it is its document, and it must be able to
* read it. Depositing into someone else's needs no cap (see `register-write.depositInto`). */
// @provenance fileOwnInbox kind=aligned level=1 ref=engine/repo/src/types.rs:AddInboxCapV0 — upstream the record IS the key (`priv_key`); here the cap is minted for the holder the record belongs to
export function fileOwnInbox(id: string, inbox: Nuri): void {
const holder = getCurrentUser();
if (holder === null || accountKey(holder) !== accountKey(id)) return;
@@ -160,6 +164,7 @@ export function fileOwnInbox(id: string, inbox: Nuri): void {
* did not: the address that WAS published is gone, and every future deposit on that
* document is refused as "this document has no inbox".
*/
// @provenance publishInboxAddress kind=divergent level=1 ref=engine/net/src/types.rs:ContactDetails — upstream an inbox address is TRANSMITTED (a message, a profile QR) and lands in the verifier's own per-user `inboxes` table; PUBLISHING it on the document is this library's deliberate divergence
export async function publishInboxAddress(doc: Nuri, inbox: Nuri): Promise<void> {
const s = await session();
try {
@@ -197,6 +202,7 @@ export async function publishInboxAddress(doc: Nuri, inbox: Nuri): Promise<void>
* its whole contract is that the connection either did the restore or says it did not.
* A caller that genuinely prefers to carry on catches it itself, and one of them does.
*/
// @provenance readStoreCaps kind=aligned level=1 ref=engine/repo/src/types.rs:AddRepoV0 — the replay of the Store branch — what reloads a store's documents with their keys
export async function readStoreCaps(storeDoc: Nuri): Promise<ReadCap[]> {
const s = await session();
const out: ReadCap[] = [];
@@ -246,6 +252,7 @@ export async function readStoreCaps(storeDoc: Nuri): Promise<ReadCap[]> {
* read. A failed READ is one, and it propagates — the caller is `connect.connectedUser`,
* whose contract is that it did the work or said it did not.
*/
// @provenance restoreOwnCaps kind=aligned level=1 ref=engine/verifier/src/verifier.rs:load_repo_from_read_cap — `AddRepo::verify` loads each repo from its read cap at replay; this is that replay, at connection
export async function restoreOwnCaps(holderKey: string): Promise<void> {
const holder = getCurrentUser();
if (holder === null) return;
@@ -294,6 +301,7 @@ export async function restoreOwnCaps(holderKey: string): Promise<void> {
* **Never creates.** Asking where to deposit must not bring an inbox into existence —
* only its owner opens one, and only on its own document.
*/
// @provenance documentInboxAddress kind=divergent level=1 ref=engine/net/src/types.rs:ContactDetails — the deposit-side half of the same divergence: upstream you are TOLD an address, you never read one off a document
export async function documentInboxAddress(doc: Nuri): Promise<Nuri | undefined> {
// RULE 2 — do not even attempt. Not holding the document, we have no address to
// find: upstream the inbox pubkey travels WITH what you can read, so "where do I
@@ -333,6 +341,7 @@ export async function documentInboxAddress(doc: Nuri): Promise<Nuri | undefined>
* place that records authorship. Holding a cap is NOT ownership: a cap can be
* received, and a recipient must not be able to open an inbox on what it merely reads.
*/
// @provenance ownsDocument kind=aligned level=1 ref=engine/repo/src/types.rs:AddRepoV0 — authorship is what the Store branch records; holding a cap is NOT ownership, since a cap can be received
export async function ownsDocument(doc: Nuri): Promise<boolean> {
const holder = getCurrentUser();
if (holder === null) return false;
@@ -381,6 +390,7 @@ function encodeInboxCap(doc: Nuri, inbox: Nuri): string {
* make a document's queue silently un-drained — a share that was delivered and never
* applied, with nothing to see anywhere.
*/
// @provenance readInboxCapPairs kind=declared-not-wired level=1 ref=engine/repo/src/types.rs:AddInboxCapV0 — the record is keyed by `repo_id` and `update_inbox_cap_v0` applies it with no is-store check — but the engine only ever commits one for the two STORE repos, never for a plain document
export async function readInboxCapPairs(): Promise<Array<{ doc: Nuri; inbox: Nuri }>> {
const holder = getCurrentUser();
if (holder === null) return [];
@@ -411,6 +421,7 @@ export async function readInboxCapPairs(): Promise<Array<{ doc: Nuri; inbox: Nur
}
/** The inbox recorded for one document, if this user opened one. */
// @provenance readInboxCapsFor kind=declared-not-wired level=1 ref=engine/repo/src/types.rs:AddInboxCapV0 — the per-document lookup over the same unwired record
export async function readInboxCapsFor(doc: Nuri): Promise<Nuri | undefined> {
return (await readInboxCapPairs()).find((p) => p.doc === doc)?.inbox;
}
@@ -437,6 +448,7 @@ export async function readInboxCapsFor(doc: Nuri): Promise<Nuri | undefined> {
* sign-in, on a document the application had never named, repeating at every connection
* because the record is durable. Reported live, three sign-ins out of three.
*/
// @provenance myInboxes kind=aligned level=1 ref=engine/repo/src/types.rs:AddInboxCapV0 — the User branch answers 'which inboxes may I read'. The document-inbox half of this list is declared-not-wired — see `readInboxCapPairs`
export async function myInboxes(): Promise<Nuri[]> {
const holder = getCurrentUser();
if (holder === null) return [];
@@ -473,6 +485,7 @@ export async function myInboxes(): Promise<Nuri[]> {
* wrong is not a near-miss: a Link filed under the wrong holder gives one user another's
* capability and leaves the real recipient with nothing.
*/
// @provenance addLink kind=declared-not-wired level=1 ref=engine/repo/src/types.rs:AddLinkV0 — the type is DEFINED and its `CommitVerifier::verify` is a no-op `Ok(())`; nothing in the engine constructs one
export async function addLink(cap: ReadCap, forHolder?: PrincipalId): Promise<void> {
const holder = forHolder ?? getCurrentUser();
if (holder === null) return;
@@ -525,6 +538,7 @@ function holderRing(forHolder: PrincipalId | undefined): string | undefined {
* `forHolder` names whose User branch is read when it is not the connected identity's —
* see {@link addLink}, whose idempotence check is the caller that needs it.
*/
// @provenance readLinks kind=declared-not-wired level=1 ref=engine/repo/src/types.rs:AddLinkV0 — same: defined, verifier is a no-op, constructed nowhere. NOT this library's invention — an absent implementation says nothing about the target
export async function readLinks(forHolder?: PrincipalId): Promise<ReadCap[]> {
const holder = forHolder ?? getCurrentUser();
if (holder === null) return [];
@@ -608,6 +622,7 @@ export async function readLinks(forHolder?: PrincipalId): Promise<ReadCap[]> {
* owner. To deposit into someone else's document, resolve
* {@link documentInboxAddress} and `inbox.post` into it.
*/
// @provenance storeRegistry.openDocumentInbox kind=declared-not-wired level=1 ref=engine/repo/src/types.rs:AddInboxCapV0 — every `Repo` carries `inbox: Option<PrivKey>` and the record is keyed by any `repo_id`, but `new_store_default` attaches one only to non-private STORES and `doc_create` leaves `inbox: None`. PUBLISHING the address is a separate, divergent act — see `publishInboxAddress`
export async function openDocumentInbox(docLike: NuriLike): Promise<Nuri> {
// Permissive in, precise out — see `model/nuri.ts`. Published through
// `surface/placement.ts`, so it is a door an application types against.
@@ -83,6 +83,7 @@ const STAND_IN_CAP = "OK";
* Build the cap-bearing form of `nuri` — `{target}:r:OK`. Passing an already
* cap-bearing reference yields the same value. INTERNAL to the emulated verifier.
*/
// @provenance mintCap kind=divergent level=1 ref=engine/repo/src/types.rs:readcap_nuri — upstream a cap is SERIALIZED from the ObjectRef at repo creation; here it is DERIVED from a NURI with a stand-in key. Unreachable from outside on purpose
export function mintCap(nuri: Nuri): ReadCap {
return `${targetOf(nuri)}${CAP_SEGMENT}${STAND_IN_CAP}`;
}
@@ -90,6 +91,7 @@ export function mintCap(nuri: Nuri): ReadCap {
/** The map key of the anonymous holder (no identity established yet). */
const ANONYMOUS = "";
// @provenance CapRegistry kind=aligned level=1 ref=engine/repo/src/repo.rs:Repo — the session's record of which repos it holds a `read_cap` for; upstream that record is the verifier's opened repos, never a queryable registry
export class CapRegistry {
/** holder → the caps they hold, indexed by the cap-less NURI. */
private heldByHolder = new Map<string, Map<Nuri, ReadCap>>();
@@ -126,6 +126,7 @@ const inFlight = new Map<string, Promise<void>>();
* Not fixed with a retry, a timeout or a flag on purpose: deciding *what to do* about a
* broker that cannot answer belongs to the caller, and it can only decide if it is told.
*/
// @provenance connectedUser kind=aligned level=1 ref=engine/verifier/src/verifier.rs:inbox — upstream the verifier restores and processes with nothing for a caller to await; this is the awaitable form, for a deterministic start
export async function connectedUser(): Promise<void> {
const holder = getCurrentUser();
if (holder === null) return;
@@ -223,6 +224,7 @@ export async function connectedUser(): Promise<void> {
* inherits it, failure included. That is what `ensureIdentity()` does, and it is the path
* on which the guarantee is published.
*/
// @provenance startConnect kind=aligned level=1 ref=engine/verifier/src/verifier.rs:inbox — the fire-and-forget form of the same — upstream no application asks for it at all
export function startConnect(): void {
void connectedUser().catch((error: unknown) => {
console.error(accessLogPrefix() + " connect failed:", error);
@@ -126,6 +126,7 @@ function fire(inbox: Nuri, process: () => Promise<void>): void {
*
* Idempotent inside the window: a second deposit into the same inbox joins the pending run.
*/
// @provenance scheduleInboxProcessing kind=divergent level=1 ref=engine/verifier/src/verifier.rs:inbox — upstream the RECIPIENT's own verifier applies its inbox; here another identity's session drains it on a timer. Deliberate, unpublished, and only possible on one shared wallet
export function scheduleInboxProcessing(inbox: Nuri, process: () => Promise<void>): void {
if (scheduled.has(inbox)) return;
const handle = setTimeout(() => fire(inbox, process), PROCESSING_DELAY_MS);
@@ -145,6 +146,7 @@ export function scheduleInboxProcessing(inbox: Nuri, process: () => Promise<void
* Resolves rather than rejects, like the timer path it stands in for: a drain's failure is
* reported where failures go, and a caller of this is not the party that asked for the work.
*/
// @provenance runScheduledInboxProcessingNow kind=divergent level=1 ref=engine/verifier/src/verifier.rs:inbox — the same drain, run at once; a test seam over the divergence above
export async function runScheduledInboxProcessingNow(): Promise<void> {
for (const [inbox, entry] of [...scheduled]) {
clearTimeout(entry.handle);
@@ -161,12 +163,14 @@ export async function runScheduledInboxProcessingNow(): Promise<void> {
* session that no longer exists. In-flight runs are not cancelled (nothing can un-issue a
* write already sent); they end where they end.
*/
// @provenance cancelScheduledInboxProcessing kind=divergent level=1 ref=engine/verifier/src/verifier.rs:inbox — cancels a pending drain — upstream there is no pending anything to cancel
export function cancelScheduledInboxProcessing(): void {
for (const entry of scheduled.values()) clearTimeout(entry.handle);
scheduled.clear();
}
/** Trace one completed drain — diagnostics, so gated by the access-log flag. */
// @provenance traceProcessed kind=invention level=none ref=none — a diagnostic line in this package's own log stream
export function traceProcessed(inbox: Nuri, holder: string, applied: number): void {
logStage(
"DEFERRED PROCESS " + shortNuri(inbox) + " for " + holder + " → " + applied + " link(s) applied",
@@ -29,6 +29,7 @@
* its reads. That is a deliberate reservation, not a hazard to guard against: the
* namespace names this library.
*/
// @provenance MACHINERY_NS kind=invention level=none ref=none — upstream a branch is a distinct CRDT with its own topic; a reserved subject prefix exists only because this library has no branches
export const MACHINERY_NS = "urn:ng-eventually:";
/**
@@ -37,6 +38,7 @@ export const MACHINERY_NS = "urn:ng-eventually:";
* Tolerant of `undefined` so a read path can hand it a possibly-absent binding
* without a preliminary check — an absent subject is not machinery.
*/
// @provenance isMachinerySubject kind=invention level=none ref=none — same — upstream nothing has to be filtered out of a document's own graph
export function isMachinerySubject(subject: string | undefined): boolean {
return subject !== undefined && subject.startsWith(MACHINERY_NS);
}
@@ -85,6 +85,7 @@ import type { Nuri } from "../model/types";
* - `"timed-out"` fallback fired without a `State` — open proceeded, sync UNconfirmed;
* `"unknown"` (from {@link getSyncState}) means "never requested / no `State` semantics".
*/
// @provenance SyncState kind=aligned level=1 ref=engine/net/src/app_protocol.rs:AppResponseV0 — the first `State` push IS the barrier; `timed-out` names the bounded fallback and is deliberately not `synced`
export type SyncState = "syncing" | "synced" | "timed-out";
/** Repos whose bootstrap open has completed (first `State` received OR timed out). */
@@ -114,12 +115,14 @@ let OPEN_TIMEOUT_MS = 8000;
* 8s production wait, too slow for a unit test. Production never calls this — the
* default stands. `resetOpenedRepos` restores the default.
*/
// @provenance setOpenTimeoutForTests kind=invention level=none ref=none — a test seam over the bounded fallback
export function setOpenTimeoutForTests(ms: number): void {
OPEN_TIMEOUT_MS = ms;
}
/** Reset the open registry (mainly for tests / a switched wallet). Tears down the
* held bootstrap subscriptions so a subsequent open re-subscribes cleanly. */
// @provenance resetOpenedRepos kind=invention level=none ref=none — per-session bookkeeping reset; upstream the verifier owns `self.repos` and nothing resets it from outside
export function resetOpenedRepos(): void {
for (const unsub of held.values()) {
try {
@@ -145,6 +148,7 @@ export function resetOpenedRepos(): void {
* `"timed-out"` (fallback fired without a `State`). `"synced"` and `"timed-out"`
* are deliberately distinct — a later readiness signal must not confuse them.
*/
// @provenance getSyncState kind=aligned level=1 ref=engine/net/src/app_protocol.rs:AppResponseV0 — reports whether the first `State` arrived; upstream this is not observable from JS
export function getSyncState(nuri: Nuri): SyncState | "unknown" {
return syncState.get(nuri) ?? "unknown";
}
@@ -184,6 +188,7 @@ async function syncSession(): Promise<void> {
* another holder would put a cap in the wrong ring, and a caller on that path has already
* established what its holder holds.
*/
// @provenance ensureRepoOpen kind=aligned level=2 ref=sdk/js/lib-wasm/src/lib.rs:doc_subscribe — a real subscription held for the session — upstream `doc_subscribe` is what brings a repo into `self.repos`, which `resolve_target_for_sparql` then requires
export async function ensureRepoOpen(nuri: Nuri, holderKey?: string): Promise<void> {
if (!nuri) return;
// A repo in a PUBLIC store hands its cap to whoever asks — upstream by serving it on
@@ -204,6 +209,7 @@ export async function ensureRepoOpen(nuri: Nuri, holderKey?: string): Promise<vo
* for nobody else; neither entry point re-exports it. The `Unguarded` suffix is the
* warning, and the single importer is what keeps it honest.
*/
// @provenance openRepoUnguarded kind=aligned level=2 ref=sdk/js/lib-wasm/src/lib.rs:doc_subscribe — the same open without the reach guard; internal
export async function openRepoUnguarded(nuri: Nuri): Promise<void> {
// Drop the registry if the session changed (in-page re-login → fresh verifier).
await syncSession();
@@ -274,6 +280,7 @@ export async function openRepoUnguarded(nuri: Nuri): Promise<void> {
* Open a SET of repos before an anchored batch read, in parallel, each tolerant
* ({@link ensureRepoOpen} never throws). Empty / falsy entries are ignored.
*/
// @provenance ensureReposOpen kind=aligned level=2 ref=sdk/js/lib-wasm/src/lib.rs:doc_subscribe — the batch form, with per-doc isolation
export async function ensureReposOpen(nuris: Nuri[]): Promise<void> {
const unique = [...new Set(nuris.filter(Boolean))];
if (unique.length === 0) return;
@@ -110,6 +110,7 @@ import type { Nuri, ReadCap } from "../model/types";
const attempted = new Map<Nuri, Promise<ReadCap | null>>();
/** Forget every outer-overlay fetch (tests / a switched session or wallet). */
// @provenance resetPublicStoreFetches kind=invention level=none ref=none — forgets the memoised fetches; upstream there is nothing to memoise, the broker answers
export function resetPublicStoreFetches(): void {
attempted.clear();
}
@@ -130,6 +131,7 @@ export function resetPublicStoreFetches(): void {
* caller `createEntityDoc` already refuses to hand back a reference whose bookkeeping did
* not land, for the same reason and in the same words.
*/
// @provenance exposeReadCap kind=divergent level=1 ref=engine/net/src/types.rs:PublicRepoLinkV0 — upstream NOTHING is written to make a repo public: the store is public and the broker exposes its outer overlay. Here the cap is written on the document's Header branch
export async function exposeReadCap(doc: Nuri, cap: ReadCap): Promise<void> {
const s = await session();
try {
@@ -164,6 +166,7 @@ export async function exposeReadCap(doc: Nuri, cap: ReadCap): Promise<void> {
* Never throws — a document that is not in a public store simply answers nothing, which
* is not an error but the normal case.
*/
// @provenance fetchReadCap kind=declared-not-wired level=1 ref=engine/net/src/types.rs:PublicRepoLinkV0 — the type DECLARES the cap is downloaded from the outer overlay if the brokers allow; it is constructed nowhere, both `PinRepo` constructors hard-code `expose_outer: false`, and `ExtTopicSyncReq` is `unimplemented!()`
export async function fetchReadCap(docLike: Nuri): Promise<boolean> {
const doc = targetOf(docLike);
const caps = getCaps();
@@ -221,6 +224,7 @@ async function downloadReadCap(doc: Nuri): Promise<ReadCap | null> {
* Ask for a SET of documents' caps, in parallel — what a batch read does before it
* decides which documents it may touch. Each fetch is independent and tolerant.
*/
// @provenance fetchReadCaps kind=declared-not-wired level=1 ref=engine/net/src/types.rs:PublicRepoLinkV0 — the batch form of the same unwired mechanism
export async function fetchReadCaps(docs: Nuri[]): Promise<void> {
const unique = [...new Set(docs.filter(Boolean))];
if (unique.length === 0) return;
@@ -71,6 +71,7 @@ import type { Nuri } from "../model/types";
* fixes is which ring the question is about, so that work decided for one identity is not
* judged against another's possession.
*/
// @provenance mayReach kind=aligned level=1 ref=engine/verifier/src/verifier.rs:load_repo_from_read_cap — upstream possession decides by whether the key opens the repo at all; here it is a lookup, because nothing is encrypted yet
export function mayReach(nuri: Nuri, holderKey?: string): boolean {
const caps = getCaps();
if (!caps.isEnforcing()) return true;
@@ -94,6 +95,7 @@ export function mayReach(nuri: Nuri, holderKey?: string): boolean {
* {@link mayReach}. The refusal names it too, or a processor's refusal would read as the
* connected identity's, which is the one thing nobody would then go and check.
*/
// @provenance assertMayReach kind=aligned level=1 ref=engine/verifier/src/verifier.rs:load_repo_from_read_cap — the throwing form, at the passage points; upstream the equivalent failure is a repo that simply never decrypts
export function assertMayReach(nuri: Nuri, op: string, holderKey?: string): void {
if (mayReach(nuri, holderKey)) return;
const who = holderKey === undefined ? "the connected user" : `holder ${JSON.stringify(holderKey)}`;
@@ -144,6 +146,7 @@ export function assertMayReach(nuri: Nuri, op: string, holderKey?: string): void
* `docs.registerUpdate`, because a store document is not OWNED in this sense — it IS a
* store, and the verifier commits to it on its own behalf.
*/
// @provenance assertMayWrite kind=aligned level=1 ref=engine/repo/src/commit.rs:verify_perm — upstream the right to write is membership of the repo, checked on commits only; ownership stands in for it here
export async function assertMayWrite(nuri: Nuri, op: string): Promise<void> {
const caps = getCaps();
if (!caps.isEnforcing()) return;
@@ -175,6 +178,7 @@ export async function assertMayWrite(nuri: Nuri, op: string): Promise<void> {
*
* `holderKey` names the ring the question is about — see {@link mayReach}.
*/
// @provenance mustNotAttempt kind=aligned level=1 ref=engine/verifier/src/request_processor.rs:resolve_target — upstream a repo you cannot open answers `RepoNotFound`; this is the do-not-even-attempt form of the same absence
export function mustNotAttempt(nuri: Nuri, holderKey?: string): boolean {
return !mayReach(nuri, holderKey);
}
@@ -54,6 +54,7 @@ function readable(item: unknown, caps: CapRegistry): boolean {
}
/** Pure: keep only the items whose document the current holder holds. */
// @provenance filterReadable kind=aligned level=1 ref=engine/verifier/src/verifier.rs:load_repo_from_read_cap — upstream the broker only delivers what the wallet's keys decrypt; the access unit is the DOCUMENT, which is why this filters per `@graph`
export function filterReadable<T>(items: Iterable<T>, caps: CapRegistry): T[] {
const out: T[] = [];
for (const item of items) if (readable(item, caps)) out.push(item);
@@ -85,6 +86,7 @@ export function filterReadable<T>(items: Iterable<T>, caps: CapRegistry): T[] {
* Everything that is not a function passes through untouched (`size` is handled above):
* a plain property carries no items.
*/
// @provenance makeReadFilteredView kind=aligned level=1 ref=engine/verifier/src/verifier.rs:load_repo_from_read_cap — the same filter as a live view over a reactive set; removed at migration
export function makeReadFilteredView<S extends object>(set: S, caps: CapRegistry): S {
const keep = (item: unknown): boolean => readable(item, caps);
/** The readable items, as a plain array — what every handled member works from. */
@@ -41,6 +41,7 @@ import type { Nuri } from "../model/types";
* during someone else's session. The guard still fires, against the named ring; see
* {@link assertMayReach}.
*/
// @provenance registerUpdate kind=aligned level=1 ref=engine/repo/src/types.rs:BranchCrdt — upstream these are service commits on branches whose CRDT is `None`, made by the verifier on its own behalf; here they are SPARQL writes that skip the ownership guard
export async function registerUpdate(
sessionId: string | number,
query: string,
@@ -79,6 +80,7 @@ function forHolderSuffix(holderKey: string | undefined): string {
*
* Never exported from the package — it reads a document the connected identity may not.
*/
// @provenance readForHolder kind=invention level=none ref=none — reading on behalf of an identity other than the session's exists only because one wallet hosts every identity
export async function readForHolder(
sessionId: string | number,
query: string,
@@ -115,6 +117,7 @@ export async function readForHolder(
* The recipient's ownership of the inbox is what bounds this: `inbox.post` is the
* only caller, and reading is guarded separately (`inbox.read`).
*/
// @provenance depositInto kind=aligned level=1 ref=engine/verifier/src/inbox_processor.rs:post_to_inbox — the one write that legitimately crosses: upstream a deposit is an anonymous sealed box, so it holds no cap and gets nothing back
export async function depositInto(
sessionId: string | number,
query: string,
+20 -2
View File
@@ -43,8 +43,20 @@
*/
// ── SDK-SHAPED — a target counterpart for every symbol ──────────────────────
// At migration the build alias is removed and these resolve to the real SDK. The
// per-symbol ruling, with its epistemic label, is in `docs/api-contract.md`.
// At migration the build alias is removed and these resolve to the real SDK.
//
// WHAT each symbol is aligned on is recorded BESIDE IT, not in a document that drifts:
//
// // @provenance <name> kind=<kind> level=<1|2|3|none> ref=<path:symbol|none> — <note>
//
// `level` numbers the target's stack from the bottom (1 engine, 2 wasm binding /
// `@ng-org/web`, 3 JS ORM); `ref` cites a SYMBOL in `nextgraph-rs`, never a line number.
// `kind` is closed: `passthrough` (upstream's own symbol, name and shape), `aligned` (our
// name or ergonomics, upstream's semantics), `declared-not-wired` (upstream DEFINES the
// shape and constructs it nowhere — an absence, which is NOT evidence the thing is ours),
// `invention` (nothing answers, a documented bet), `divergent` (upstream does this, we do
// it differently). Pinned by `test/provenance.test.ts`; the reasoning behind each ruling
// stays in `docs/api-contract.md`, and where the two disagree the annotation wins.
// A type is published only when a PUBLISHED SIGNATURE uses it. `export *` published
// eight in one gesture (2026-08-10: it was a blanket re-export), of which two named
@@ -81,8 +93,13 @@ export * as storeRegistry from "./surface/placement";
// SDK type re-exports — so the app imports these from @ng-eventually/polyfill too, not from
// @ng-org. `export type` is ERASED at build, so this adds NO runtime @ng-org import to
// the lib (no risk of a duplicate SDK copy in the bundle).
// @provenance ShapeType kind=passthrough level=3 ref=sdk/js/shex-orm/src/types.ts:ShapeType
// @provenance BaseType kind=passthrough level=3 ref=sdk/js/shex-orm/src/types.ts:BaseType
// @provenance Schema kind=passthrough level=3 ref=sdk/js/shex-orm/src/types.ts:Schema
export type { ShapeType, BaseType, Schema } from "@ng-org/shex-orm";
// @provenance DeepSignalSet kind=passthrough level=3 ref=sdk/js/alien-deepsignals/src/types.ts:DeepSignalSet
export type { DeepSignalSet } from "@ng-org/alien-deepsignals";
// @provenance NG kind=passthrough level=2 ref=sdk/js/web/src/index.ts:NG — 88 members, all forwarded; the proxy adds none and removes none
export type { NG } from "@ng-org/web";
// ── POLYFILL-ERA — THE DELETION LIST ────────────────────────────────────────
@@ -150,4 +167,5 @@ import { makeNg } from "./surface/ng-proxy";
* `Record<string, any>` until 2026-08-14, which announced a different surface from the one
* it forwards to: an application got no completion and no check on any of the 88 members.
*/
// @provenance ng kind=passthrough level=2 ref=sdk/js/web/src/index.ts:ng — a Proxy over the injected real `ng`; only `sparql_update` is overridden (the emulated write guard)
export const ng: NG = makeNg();
+8
View File
@@ -8,6 +8,7 @@
* `did:ng:` is the URI scheme prefix, not a "without cap" marker; the discriminant
* is the `:r:` segment (see {@link ReadCap}).
*/
// @provenance Nuri kind=aligned level=1 ref=engine/net/src/app_protocol.rs:NuriV0 — the cap-less reference form upstream's parser accepts; `:r:` is the cap-bearing discriminant
export type Nuri = `did:ng:${string}`;
/**
@@ -31,14 +32,17 @@ export type Nuri = `did:ng:${string}`;
* out. The runtime checks stay regardless — a JavaScript consumer never meets the
* compiler, and a cast bypasses it.
*/
// @provenance ReadCap kind=aligned level=1 ref=engine/repo/src/types.rs:readcap_nuri — the `r:` segment and its one-opaque-segment encoding are upstream's; the key material is a stand-in constant
export type ReadCap = `did:ng:${string}:r:${string}`;
/** NextGraph-native store scopes. The *mapping* of entities to scopes is the
* consumer's concern; this layer only knows the three scopes exist. */
// @provenance Scope kind=aligned level=1 ref=engine/repo/src/types.rs:from_type_and_repo — upstream's store types; `group` is omitted here and `dialog` is `unimplemented!()` upstream
export type Scope = "public" | "protected" | "private";
/** The current identity id. Target: the wallet user (`session.user`). Polyfill:
* a chosen id, because everyone shares one wallet. */
// @provenance PrincipalId kind=divergent level=1 ref=engine/repo/src/types.rs:UserId — upstream a user id is a PubKey obtained FROM the wallet; here it is a chosen name, because one wallet hosts every identity
export type PrincipalId = string;
/**
@@ -48,9 +52,11 @@ export type PrincipalId = string;
* makes the wrapper testable with a fake. Permissive on purpose: the real `ng`
* carries non-function members too, so we accept any property bag.
*/
// @provenance NgLike kind=invention level=none ref=none — nothing upstream is injected; the type exists only to describe what `configure` receives
export type NgLike = Record<string, any>;
/** Loose shape of `@ng-org/orm`'s `useShape` (a generic hook). */
// @provenance UseShapeLike kind=invention level=none ref=none — same, for the injected `useShape`
export type UseShapeLike = (...args: any[]) => any;
/**
@@ -76,6 +82,8 @@ export type UseShapeLike = (...args: any[]) => any;
* So: precise on the way out, permissive on the way in, and validated inside
* (`assertNuri`). The guards remain, internal, where the validation happens.
*/
// @provenance NuriLike kind=aligned level=2 ref=sdk/js/lib-wasm/src/lib.rs:doc_subscribe — the binding takes `repo_o: String`, so permissive-in matches the target's own door
export type NuriLike = Nuri | string;
// @provenance InboxScope kind=aligned level=1 ref=engine/verifier/src/verifier.rs:new_store_default — an inbox is attached only `if !private`, so the private scope cannot carry one
export type InboxScope = Extract<Scope, "public" | "protected">;
@@ -150,6 +150,7 @@ const URL_PARAM = "ng-id";
* passes the VALUES here. A library that read `process.env` would impose its build
* system on every consumer, and would be untestable with other values.
*/
// @provenance SharedWalletConfig kind=invention level=none ref=none — describes what a DEPLOYMENT hands out for the one shared wallet; it goes with the gate
export interface SharedWalletConfig {
/** URL of the `.ngw` file served by the application's own bundle. */
fileUrl: string;
@@ -555,6 +556,7 @@ async function resolveIdentity(): Promise<PrincipalId> {
* application had to read the gate's own private storage key — a boundary no consumer
* should be able to see, let alone depend on.
*/
// @provenance ensureIdentity kind=invention level=none ref=none — nothing answers 'gate a shared wallet'. Upstream a user opens THEIR wallet and `session_start(wallet_name, user_id)` takes an id that came from it — so the CALL SITE survives, the substance does not
export async function ensureIdentity(): Promise<PrincipalId> {
const settled = await settleIdentity();
refuseAWaitNothingCanEnd();
@@ -968,6 +968,7 @@ async function scopeStoreNuri(scope: Scope): Promise<Nuri> {
* store-id and builds no NURI. Use the returned value as both the read scope
* (`useShape(shape, nuri)`) and the `@graph` write target.
*/
// @provenance storeRegistry.resolveScopeGraph kind=aligned level=2 ref=sdk/js/web/src/index.ts:Session — `did:ng:` + `private_store_id | protected_store_id | public_store_id`, straight off the session
export async function resolveScopeGraph(scope: Scope): Promise<Nuri> {
return scopeStoreNuri(scope);
}
@@ -1043,6 +1044,7 @@ const inboxCache = new Map<string, Nuri>();
* At migration these become the site's native store inboxes and the resolution moves
* there the consumer-facing act (deposit to an inbox, process my own) is unchanged.
*/
// @provenance userInbox kind=aligned level=1 ref=engine/verifier/src/site.rs:create_individual_ — a user's inboxes are its public and protected STORE repos' — the engine's only two `AddInboxCap` commits
export async function userInbox(id: string, scope: InboxScope): Promise<Nuri> {
const key = `${accountKey(id)}\u0000${scope}`;
const cached = inboxCache.get(key);
@@ -1261,6 +1263,7 @@ export async function readUserStore(indexDoc: Nuri): Promise<Nuri[]> {
* here). Idempotent via `ensureAccount`'s cache. At migration this becomes the
* user's real per-scope store NURI (the container the store itself provides).
*/
// @provenance userStoreDoc kind=aligned level=2 ref=sdk/js/web/src/index.ts:Session — the per-scope index document stands in for the store the session names directly
export async function userStoreDoc(id: string, scope: Scope): Promise<Nuri> {
const record = await ensureAccount(id);
return storeOf(record, scope);
@@ -105,6 +105,7 @@ export interface StoreRegistryDeps {
* carried two defects at once. Both remain substitutable through {@link StoreRegistryDeps},
* which the published entry cannot reach.
*/
// @provenance EventuallyConfig kind=invention level=none ref=none — nothing upstream takes an injected SDK; this is the shape of the build-alias, and it is on the deletion list
export interface EventuallyConfig {
/** The REAL `@ng-org/web` `ng` (injected to avoid a hard import / alias loop). */
ng: NgLike;
@@ -188,6 +189,7 @@ function capsHolder(): PrincipalId | null {
*/
let caps = new CapRegistry(capsHolder);
// @provenance configure kind=invention level=none ref=none — same — the whole call disappears at migration, when the consumer initialises the real SDK directly
export function configure(c: EventuallyConfig): void {
cfg = c;
// Not taken from the config: an application never supplies its own identity — upstream
+3
View File
@@ -51,6 +51,7 @@ function rowCount(result: unknown): number {
// through this chain — never normalise it, and above all never stringify it: a JS string
// fails that deserialization, observed live as
// `Deserialization error of session_id JsValue("1")`.
// @provenance docs.docCreate kind=aligned level=2 ref=sdk/js/lib-wasm/src/lib.rs:doc_create — same arguments in the same order; JS-cased name, and the broker's answer is validated into a `Nuri`
export async function docCreate(
sessionId: string | number,
crdt: string,
@@ -98,6 +99,7 @@ export async function docCreate(
* {@link sparqlQuery} already renders the same upstream `Promise<any>`: the value is
* the broker's to shape, and a caller that ignores it is unaffected.
*/
// @provenance docs.sparqlUpdate kind=aligned level=2 ref=sdk/js/lib-wasm/src/lib.rs:sparql_update — same arguments; the trailing `label` is a lib-internal access-log tag, never forwarded
export async function sparqlUpdate(
sessionId: string | number,
query: string,
@@ -131,6 +133,7 @@ export async function sparqlUpdate(
* Mirrors `ng.sparql_query(session_id, query, base?, anchor?)`. `base` is the
* query base IRI (usually `undefined`); `anchor` is the document NURI to query.
*/
// @provenance docs.sparqlQuery kind=aligned level=2 ref=sdk/js/lib-wasm/src/lib.rs:sparql_query — same arguments; the trailing `label` is a lib-internal access-log tag, never forwarded
export async function sparqlQuery(
sessionId: string | number,
query: string,
+10
View File
@@ -58,6 +58,7 @@ import type { Nuri, NuriLike, PrincipalId, ReadCap } from "../model/types";
// --- deposit model --------------------------------------------------------
/** One deposit as materialized from an inbox document. */
// @provenance inbox.Deposit kind=aligned level=1 ref=engine/net/src/types.rs:InboxMsgBody — `from` optional like `from_inbox`, and NO target-document field because the address identifies the recipient repo; `payload`/`ts` have no upstream field
export interface Deposit {
/** The sender, if identified; `null` when the deposit was anonymous. */
from: PrincipalId | null;
@@ -68,6 +69,7 @@ export interface Deposit {
}
/** Options for {@link post}. `from` and `ts` are both optional. */
// @provenance inbox.PostOptions kind=aligned level=1 ref=engine/net/src/types.rs:InboxMsgBody — same shape as Deposit on the way in; `from: null` mirrors `from_inbox: None`
export interface PostOptions {
/**
* Who is depositing. Omit (or pass `null`) for an ANONYMOUS deposit; pass a
@@ -169,6 +171,7 @@ function readBindings(result: unknown): Array<Record<string, { value: string }>>
* another's identity. This check is redundant once the seal enforces it, but
* until then it closes the spoof the shared wallet would otherwise allow.
*/
// @provenance inbox.post kind=aligned level=1 ref=engine/verifier/src/inbox_processor.rs:post_to_inbox — the act exists at level 1 and the broker routes it (`inbox_post`); no JS surface at level 2/3. An arbitrary `payload` is a BET — `InboxMsgContent` has no generic variant
export async function post(targetInboxLike: NuriLike, opts: PostOptions): Promise<void> {
const targetInbox = toNuri(targetInboxLike, "inbox.post");
const current = getCurrentUser();
@@ -269,6 +272,7 @@ export async function post(targetInboxLike: NuriLike, opts: PostOptions): Promis
* address in advance, because an application must name a document or a person, never an
* inbox.
*/
// @provenance inbox.postToDocument kind=declared-not-wired level=1 ref=engine/repo/src/repo.rs:Repo — every repo carries `inbox: Option<PrivKey>`, but nothing opens one for a plain document. Resolving the address by READING the document is our divergence — see `publishInboxAddress`
export async function postToDocument(docLike: NuriLike, opts: PostOptions): Promise<void> {
const doc = toNuri(docLike, "inbox.postToDocument");
const target = await documentInboxAddress(doc);
@@ -364,6 +368,7 @@ function capOfPayload(payload: unknown): ReadCap | null {
* The shape is right; the implementation is absent at both ends, so we emulate it
* meanwhile.
*/
// @provenance inbox.share kind=declared-not-wired level=1 ref=engine/net/src/types.rs:ContactDetails — `read_cap: Option<ReadCap>` is defined; building the message is `unimplemented!()`, its only caller passes `with_readcap: false`, and the receiving arm never reads the field
export async function share(doc: NuriLike, toUser: string): Promise<void> {
const target = toNuri(doc, "inbox.share");
// Names the DOCUMENT and the PERSON — the two things an application has. Neither the
@@ -431,6 +436,7 @@ export async function share(doc: NuriLike, toUser: string): Promise<void> {
* no more reason to handle an inbox address than a depositor does. Empty when the
* document has no inbox, which is a state and not an error.
*/
// @provenance inbox.readForDocument kind=divergent level=1 ref=engine/verifier/src/inbox_processor.rs:process_inbox — upstream an inbox is a queue the verifier consumes and APPLIES; this enumerates it instead, non-destructively
export async function readForDocument(docLike: NuriLike): Promise<Deposit[]> {
const doc = toNuri(docLike, "inbox.readForDocument");
const address = await documentInboxAddress(doc);
@@ -484,6 +490,7 @@ async function assertOwnInbox(targetInbox: Nuri, op: string): Promise<void> {
* watching its inbox gets them, and the resulting change re-triggers the
* reads that were empty for want of that cap.
*/
// @provenance inbox.read kind=divergent level=1 ref=engine/verifier/src/inbox_processor.rs:process_inbox — same: a re-readable list where upstream has a consumed queue. Coding a mailbox UI on this is coding against emulation detail
export async function read(targetInboxLike: NuriLike): Promise<Deposit[]> {
const targetInbox = toNuri(targetInboxLike, "inbox.read");
await assertOwnInbox(targetInbox, "read");
@@ -578,6 +585,7 @@ export async function read(targetInboxLike: NuriLike): Promise<Deposit[]> {
* `discovery.readIndex` does. Idempotent per session (no polling); a no-op open on
* the unit fake-ng path (no `doc_subscribe`) so `bun test` is unaffected.
*/
// @provenance inbox.readSynced kind=divergent level=1 ref=engine/verifier/src/inbox_processor.rs:process_inbox — same as `read`, gated on the sync barrier first
export async function readSynced(targetInboxLike: NuriLike): Promise<Deposit[]> {
const targetInbox = toNuri(targetInboxLike, "inbox.readSynced");
// Marks the cold, connection-triggered entry point in the trace — the BARRIER
@@ -604,6 +612,7 @@ export async function readSynced(targetInboxLike: NuriLike): Promise<Deposit[]>
* second tab, a reconnect) costs nothing. Returns the consumer deposits, exactly as
* {@link read} does Links are never surfaced.
*/
// @provenance inbox.processInbox kind=aligned level=1 ref=engine/verifier/src/inbox_processor.rs:process_inbox — unseal-and-apply: Links are filed durably and never surfaced. Returning the remaining deposits is the divergent half it shares with `read`
export async function processInbox(targetInboxLike: NuriLike): Promise<Deposit[]> {
const targetInbox = toNuri(targetInboxLike, "inbox.processInbox");
const deposits = await readSynced(targetInbox);
@@ -711,6 +720,7 @@ async function processForOwner(targetInbox: Nuri): Promise<void> {
* there is no polling. (The inbox document is a single doc, so this is immune to
* the ORM fan-out hang see {@link subscribeDoc}.)
*/
// @provenance inbox.watch kind=divergent level=1 ref=engine/verifier/src/verifier.rs:inbox — upstream a message is applied on arrival and there is nothing to subscribe to; this pushes the raw deposit list on every change
export function watch(
targetInboxLike: NuriLike,
onDeposits: (deposits: Deposit[]) => void,
@@ -58,6 +58,7 @@ import { captureSession, expectSession } from "../shared-wallet/session";
* it (`callback: Function | null`, and the call site is guarded). The wrapper still goes in,
* so the package gets its session either way, and calls nothing afterwards.
*/
// @provenance init kind=passthrough level=2 ref=sdk/js/web/src/index.ts:init — arguments and return pass straight through; the callback in position 0 is wrapped to keep the session it carries
export function init(...args: any[]): any {
const f = getConfig().init;
if (!f) throw new Error("[ng-eventually] init() not injected — pass it to configure()");
@@ -90,6 +91,7 @@ export function init(...args: any[]): any {
}
/** Forwards to the real `@ng-org/orm` `initNg` (ORM signals). */
// @provenance initNg kind=passthrough level=3 ref=sdk/js/orm/src/core.ts:initNg — upstream's own exported name for `initNgSignals`
export function initNg(...args: any[]): any {
const f = getConfig().initNg;
if (!f) throw new Error("[ng-eventually] initNg() not injected — pass it to configure()");
@@ -61,16 +61,19 @@ function connectedIdentity(op: string): string {
}
/** Create a document for ONE entity in `scope`, and record it in that scope's store. */
// @provenance storeRegistry.createEntityDoc kind=aligned level=2 ref=sdk/js/lib-wasm/src/lib.rs:doc_create — upstream `doc_create` performs the `ldp:contains` listing and the `AddRepo { read_cap }` natively; here they are two explicit writes
export async function createEntityDoc(scope: Scope): Promise<Nuri> {
return registryCreateEntityDoc(connectedIdentity("createEntityDoc"), scope);
}
/** The entity documents this user owns in `scope` — with their caps recovered. */
// @provenance storeRegistry.listMyEntityDocs kind=aligned level=1 ref=engine/verifier/src/request_processor.rs:doc_create — reads back the `ldp:contains` listing that `doc_create` writes on the store's Main branch
export async function listMyEntityDocs(scope: Scope): Promise<Nuri[]> {
return registryListMyEntityDocs(connectedIdentity("listMyEntityDocs"), scope);
}
/** The NURI where GROUPED entities of `scope` are written (no per-entity document). */
// @provenance storeRegistry.resolveWriteGraph kind=aligned level=2 ref=sdk/js/web/src/index.ts:Session — a scope resolves to the session's own store id; upstream the store IS the container and the per-scope index document disappears
export async function resolveWriteGraph(scope: Scope): Promise<Nuri> {
return registryResolveWriteGraph(connectedIdentity("resolveWriteGraph"), scope);
}
@@ -57,6 +57,7 @@ void docCreate;
void sparqlUpdate;
/** One subject read from a doc, with its properties (predicate → values). */
// @provenance UnionSubject kind=invention level=none ref=none — no upstream type groups triples per (document, subject); a polyfill property bag, to be mapped into app types at the boundary
export interface UnionSubject {
/**
* The subject IRI (`?s`), exactly as the document carries it.
@@ -163,6 +164,7 @@ async function readDoc(
* read with an anchored default-graph query, O(1) per doc, independent of wallet
* size a non-empty wallet no longer matters. Reads run in parallel via `Promise.all`.
*/
// @provenance readUnion kind=aligned level=2 ref=sdk/js/lib-wasm/src/lib.rs:sparql_query — one anchored query per document — upstream's own anchoring (`resolve_target_for_sparql`), composed client-side
export async function readUnion(docsLike: NuriLike[]): Promise<UnionSubject[]> {
const sid = await sessionId();
// Drop the empties BEFORE validating, not after: this call has always tolerated a
@@ -45,6 +45,7 @@ import type { Nuri, NuriLike } from "../model/types";
* change SIGNAL (re-query on change the read-model pattern), so this stays
* permissive rather than modelling every AppResponse variant.
*/
// @provenance DocChange kind=aligned level=1 ref=engine/net/src/app_protocol.rs:AppResponseV0 — the raw serialized response, left `unknown` rather than modelling every variant
export type DocChange = unknown;
/**
@@ -58,6 +59,7 @@ export type DocChange = unknown;
* `TabInfo`/`Patch` pushes see `open-repo.ts`. Most callers ignore it and use
* any push as a plain change signal.
*/
// @provenance DocChangeType kind=aligned level=1 ref=engine/net/src/app_protocol.rs:AppResponseV0 — the variant discriminant; a bare string because the enum is open (State | Patch | TabInfo | …)
export type DocChangeType = string | undefined;
/**
@@ -67,6 +69,7 @@ export type DocChangeType = string | undefined;
* is not a recognisable `{ V0: { <Variant>: … } }` object. Inspects the variant
* proplerly (no `any`-cast to force it) so a `State` push is identifiable.
*/
// @provenance docChangeType kind=aligned level=1 ref=engine/net/src/app_protocol.rs:AppResponseV0 — reads the variant key out of the payload — a convenience over a verified shape, not an upstream call
export function docChangeType(resp: DocChange): DocChangeType {
if (!resp || typeof resp !== "object") return undefined;
const outer = resp as { V0?: unknown; v0?: unknown };
@@ -77,6 +80,7 @@ export function docChangeType(resp: DocChange): DocChangeType {
}
/** An unsubscribe function — idempotent (calling it twice is a no-op). */
// @provenance Unsubscribe kind=aligned level=2 ref=sdk/js/lib-wasm/src/lib.rs:doc_subscribe — upstream resolves to this function; ours is returned synchronously
export type Unsubscribe = () => void;
async function sessionId(): Promise<string | number> {
@@ -102,6 +106,7 @@ async function sessionId(): Promise<string | number> {
*
* Calls the REAL injected `ng.doc_subscribe` directly (never `makeNg`).
*/
// @provenance subscribeDoc kind=aligned level=2 ref=sdk/js/lib-wasm/src/lib.rs:doc_subscribe — two deliberate deltas: the unsubscribe is returned synchronously, and the callback gets a pre-extracted `type`
export function subscribeDoc(
nuriLike: NuriLike,
onChange: (r: DocChange, type: DocChangeType) => void,
@@ -118,6 +123,7 @@ export function subscribeDoc(
* owns the machinery's entire privileged door and for nobody else. It is not
* re-exported by either entry point; the `Unguarded` suffix is the warning.
*/
// @provenance subscribeDocUnguarded kind=aligned level=2 ref=sdk/js/lib-wasm/src/lib.rs:doc_subscribe — the same call without the reach guard; internal, never published
export function subscribeDocUnguarded(
nuri: Nuri,
onChange: (r: DocChange, type: DocChangeType) => void,
@@ -177,6 +183,7 @@ export function subscribeDocUnguarded(
* ORM fan-out hang do NOT replace this with `orm_start_graph(graphs:[…])`). The
* set is deduplicated; an empty set returns a no-op unsubscribe.
*/
// @provenance subscribeDocs kind=aligned level=2 ref=sdk/js/lib-wasm/src/lib.rs:doc_subscribe — client-side composition with per-doc error isolation; `orm_start_graph` is deliberately unused — it aborts wholesale on one RepoNotFound
export function subscribeDocs(
nuris: Nuri[],
onChange: (nuri: Nuri, r: DocChange, type: DocChangeType) => void,
@@ -9,6 +9,7 @@
import { getConfig, getCaps } from "../shared-wallet/bootstrap";
import { makeReadFilteredView } from "../emulated-verifier/read-filter";
// @provenance useShape kind=passthrough level=3 ref=sdk/js/orm/src/frontendAdapters/react/useShape.ts:useShape — forwards to the injected real hook; the read-filtered view emulates cryptographic isolation and the generic `T` is widened to `unknown`
export function useShape(shapeType: unknown, scope: unknown): unknown {
const set = getConfig().useShape(shapeType, scope) as object;
const caps = getCaps();
@@ -70,6 +70,7 @@ const RDF_TYPE = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type";
* the generic per-subject property bags of the read-model (NO application domain);
* the app maps them to its own entity types in phase B.
*/
// @provenance ShapeQuery kind=invention level=none ref=none — no upstream type carries load state; the distinction IS expressible (`OrmSubscription.readyPromise`) but this vocabulary is ours
export interface ShapeQuery<T = UnionSubject> {
/** The subjects of the requested shape/scope. Empty array when none (never undefined). */
data: T[];
@@ -87,6 +88,7 @@ export interface ShapeQuery<T = UnionSubject> {
}
/** The observable a caller binds with `useSyncExternalStore` (phase B). */
// @provenance ShapeObservable kind=invention level=none ref=none — no upstream observable exists; the ORM exposes a hook returning a DeepSignalSet and nothing else
export interface ShapeObservable<T = UnionSubject> {
/** The current snapshot. STABLE across calls until it actually changes (so
* `useSyncExternalStore` does not loop): the same reference is returned until a
@@ -163,6 +165,7 @@ function barrierReached(docs: Nuri[]): boolean {
* what kicks off resolution, opening and the first read; before then `getSnapshot`
* reports the initial pending snapshot.
*/
// @provenance watchShape kind=invention level=none ref=none — nothing upstream distinguishes syncing from synced-empty at the hook; the header's 'planned useShape upgrade' has NO upstream provenance
export function watchShape<T = UnionSubject>(
shapeType: unknown,
scope: Scope,
+202
View File
@@ -0,0 +1,202 @@
/**
* Every published symbol must SAY what it is aligned on in a machine-checkable line
* beside the code, not in a document that drifts away from it.
*
* Why this is a test, and why the line lives in the source
* The library's governing rule is that nothing may diverge from the target. Nothing in
* the code recorded *what each symbol is aligned on*, and it cost twice in one week:
* `docs/api-contract.md` claimed a "1:1 passthrough" that was false in two ways, and a
* maintainer classified `branch-registers.readLinks` as this library's own invention when
* it implements `AddLinkV0` a type NextGraph **defines** (`engine/repo/src/types.rs`)
* and constructs nowhere. Reading "nothing constructs this upstream" and concluding "so
* it is ours" is inference from an ABSENT IMPLEMENTATION, which the project's own rule
* forbids: a definition is a fact, its being unwired is an absence, and the absence says
* nothing about the target.
*
* So the provenance is recorded per symbol, next to it, and this test pins it.
*
* The format
*
* // @provenance <name> kind=<kind> level=<1|2|3|none> ref=<path:symbol|none> — <note>
*
* - **name** the PUBLISHED name, qualified for a namespace member (`inbox.post`,
* `storeRegistry.createEntityDoc`). Internal symbols carry their own name. It is in
* the line so this file can map an annotation to a symbol without a TypeScript parser.
* - **level** which layer of the target answers, numbered from the bottom like the
* stack: `1` the engine (`engine/`), `2` the wasm binding / `@ng-org/web`
* (`sdk/js/lib-wasm`, `sdk/js/web`), `3` the JS ORM (`sdk/js/orm`). `none` only when
* nothing answers.
* - **ref** `path:symbol` into `nextgraph-rs` (a SYMBOL, never a line number: line
* numbers rot and three citations in `docs/api-contract.md` already had). `none` only
* for `invention`.
* - **kind** from a closed set:
*
* | kind | meaning |
* |----------------------|----------------------------------------------------------------|
* | `passthrough` | upstream's own symbol, same name and shape |
* | `aligned` | our name or ergonomics, upstream's semantics and model |
* | `declared-not-wired` | upstream DEFINES the type or shape; nothing constructs it yet |
* | `invention` | nothing at any level answers a deliberate, documented bet |
* | `divergent` | upstream does this, and we do it differently |
*
* What this test deliberately does NOT check
* That the `ref` path exists on disk. `nextgraph-rs` is a sibling checkout that may be
* absent on another machine, and a test that goes red for that reason teaches nothing
* it just trains people to skip it. The claim is verified by a human reading the source;
* this file pins that a claim was MADE, is well-formed, and covers the whole surface.
*/
import { test, expect } from "bun:test";
import * as fs from "node:fs";
import * as path from "node:path";
const SRC = path.join(import.meta.dir, "..", "src");
const KINDS = new Set(["passthrough", "aligned", "declared-not-wired", "invention", "divergent"]);
const LEVELS = new Set(["1", "2", "3", "none"]);
/** `path/to/file.ext:Symbol` — a path and a symbol, never a line number. */
const REF_SHAPE = /^[A-Za-z0-9_.@/-]+\.[a-z]+:[A-Za-z0-9_]+$/;
interface Provenance {
name: string;
kind: string;
level: string;
ref: string;
file: string;
}
/**
* The names a module really EXPORTS declarations and named re-exports, comments
* stripped first so a name that only appears in prose (an explained removal, a
* `{@link}`) is not mistaken for a live export. Same rule as `vocabulary.test.ts`,
* for the same reason: a gate that says yes to a name that is not there reads as
* verified while proving nothing.
*/
function moduleExports(file: string): Set<string> {
const out = new Set<string>();
if (!fs.existsSync(file)) return out;
const text = fs
.readFileSync(file, "utf8")
.replace(/\/\*[\s\S]*?\*\//g, "")
.replace(/^\s*\/\/.*$/gm, "");
for (const m of text.matchAll(
/^export (?:declare )?(?:async )?(?:const|function|class|interface|type) (\w+)/gm,
)) {
out.add(m[1]!);
}
for (const m of text.matchAll(/export (?:type )?\{([^}]*)\}/g)) {
for (const raw of m[1]!.split(",")) {
const name = raw.trim().replace(/^type /, "").split(/\s+as\s+/).pop()?.trim();
if (name) out.add(name);
}
}
return out;
}
/** namespace name → the names it really carries (`export * as ns from "./x"`). */
function namespaces(): Map<string, Set<string>> {
const text = fs.readFileSync(path.join(SRC, "index.ts"), "utf8");
const out = new Map<string, Set<string>>();
for (const m of text.matchAll(/export \* as (\w+) from "\.\/([^"]+)"/g)) {
out.set(m[1]!, moduleExports(path.join(SRC, m[2]! + ".ts")));
}
return out;
}
/**
* Every symbol the entry publishes, as a consumer names it: bare for a direct export,
* `ns.member` for a namespace member. The namespace NAMES themselves are groupings, not
* symbols a consumer never holds `docs`, it calls `docs.docCreate` so they carry no
* provenance of their own and are excluded.
*/
function publishedSymbols(): string[] {
const ns = namespaces();
const out = new Set<string>();
for (const name of moduleExports(path.join(SRC, "index.ts"))) {
if (!ns.has(name)) out.add(name);
}
for (const [name, members] of ns) for (const m of members) out.add(`${name}.${m}`);
return [...out].sort();
}
/** Every `@provenance` line in the source tree, parsed. */
function annotations(): Provenance[] {
const out: Provenance[] = [];
const walk = (dir: string): void => {
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
const full = path.join(dir, entry.name);
if (entry.isDirectory()) walk(full);
else if (entry.name.endsWith(".ts")) {
for (const line of fs.readFileSync(full, "utf8").split("\n")) {
// Anchored to a REAL annotation line — a comment that IS the annotation, not
// one that quotes the format. `src/index.ts` documents the shape in its header
// (a `// @provenance <name> kind=<kind> …` template nested inside a `//` block);
// an unanchored match read that template as a symbol called `<name>` with a kind
// called `<kind>`, and the well-formedness check went red on the documentation
// of its own format. A `<…>` name is skipped for the same reason, belt and
// braces: a placeholder is never a symbol.
const m = line.match(
/^\s*\/\/ @provenance\s+(\S+)\s+kind=(\S+)\s+level=(\S+)\s+ref=(\S+)/,
);
if (m && !m[1]!.startsWith("<")) {
out.push({
name: m[1]!,
kind: m[2]!,
level: m[3]!,
ref: m[4]!,
file: path.relative(SRC, full),
});
}
}
}
}
};
walk(SRC);
return out;
}
test("every published symbol carries a provenance annotation", () => {
const annotated = new Set(annotations().map((a) => a.name));
const missing = publishedSymbols().filter((s) => !annotated.has(s));
// A failure is a question, not a formality: on what does this symbol align? Read the
// nextgraph-rs source and say so — including "nothing answers", which is `invention`.
expect(missing).toEqual([]);
});
test("no symbol is annotated twice", () => {
const seen = new Map<string, string[]>();
for (const a of annotations()) seen.set(a.name, [...(seen.get(a.name) ?? []), a.file]);
const duplicated = Object.fromEntries([...seen].filter(([, files]) => files.length > 1));
// Two annotations for one name is two answers to one question, and nothing says which
// is current — the drift this file exists to prevent, reintroduced inside it.
expect(duplicated).toEqual({});
});
test("every provenance annotation is well-formed", () => {
const malformed: string[] = [];
for (const a of annotations()) {
const where = `${a.file} :: ${a.name}`;
if (!KINDS.has(a.kind)) malformed.push(`${where} — kind '${a.kind}' is not one of ${[...KINDS].join(" | ")}`);
if (!LEVELS.has(a.level)) malformed.push(`${where} — level '${a.level}' is not 1 | 2 | 3 | none`);
if (a.kind === "invention") {
// Nothing answers, at any level. A ref here would name a counterpart the kind
// denies exists — the two halves have to agree or the label means nothing.
if (a.ref !== "none") malformed.push(`${where} — kind=invention must carry ref=none, not '${a.ref}'`);
if (a.level !== "none") malformed.push(`${where} — kind=invention must carry level=none, not '${a.level}'`);
} else {
// Every other kind CLAIMS a counterpart, so it must cite one.
if (a.ref === "none") malformed.push(`${where} — kind=${a.kind} claims a counterpart, so ref may not be 'none'`);
else if (!REF_SHAPE.test(a.ref)) malformed.push(`${where} — ref '${a.ref}' is not 'path:symbol' (a symbol, never a line number)`);
if (a.level === "none") malformed.push(`${where} — kind=${a.kind} claims a counterpart, so level must be 1, 2 or 3`);
}
}
expect(malformed).toEqual([]);
});
test("the published surface is annotated at every kind that claims a counterpart", () => {
// Not a quota — a smoke check that the closed set is actually being USED. If every
// published symbol ever lands on one kind, the vocabulary has stopped discriminating
// and the annotations have become decoration.
const published = new Set(publishedSymbols());
const kinds = new Set(annotations().filter((a) => published.has(a.name)).map((a) => a.kind));
expect(kinds.size).toBeGreaterThan(1);
});