docs: le nommage de NextGraph l'emporte toujours — y compris sur nos propres règles
La règle que je venais d'écrire bannissait « publish » sans réserve. Or le moteur emploie `publisher` 126 fois — `as_publisher`, `publisher_advert` — pour un rôle pub/sub sur un topic. Une règle appliquée à la lettre aurait fait rejeter du vocabulaire amont, ce qui est exactement l'inverse du but. Le principe est donc posé au-dessus, et il prime sur tout : là où la cible a un mot, c'est le mot, point. Ce qui reste banni est NOTRE « publier un document », qui n'a aucun référent en amont et recouvre trois actes distincts — placer dans un store public, rendre trouvable, remettre une clé. Ça n'autorise jamais à renommer ce que l'amont appelle `publisher`. `publisher`, `topic` et `advert` rejoignent le vocabulaire déclaré du contrôle de noms, avec la raison en commentaire. Le renommage `publishRepoLink` → `recordInPublicStore` reste justifié : cette méthode n'a pas de pendant amont, elle enregistre un document en store public (et, artefact d'émulation, lui frappe une clé).
This commit is contained in:
@@ -16,6 +16,8 @@ Purpose: to give the ground truth of NextGraph's access-rights model, in order t
|
|||||||
>
|
>
|
||||||
> - **A comment describing the CURRENT state is not the intent.** §3's DIRECTION block exists because `RepoLinkV0`'s comment was read as the target model. It is not.
|
> - **A comment describing the CURRENT state is not the intent.** §3's DIRECTION block exists because `RepoLinkV0`'s comment was read as the target model. It is not.
|
||||||
> - **A word you recognise probably does not mean what you think.** `branch` is not git's. `wallet` is only a keyring — what we call a virtual user is a **user** (a *site*). Check the type before using the word.
|
> - **A word you recognise probably does not mean what you think.** `branch` is not git's. `wallet` is only a keyring — what we call a virtual user is a **user** (a *site*). Check the type before using the word.
|
||||||
|
> - **NextGraph's naming ALWAYS wins over any rule of ours — including the one that follows.** Where upstream has a word for something, that is the word, full stop. `publisher` is a case in point: it appears 126 times in the engine (`as_publisher`, `publisher_advert`) for a **pub/sub role on a topic**, and it must be used, unchanged, whenever that is what is meant.
|
||||||
|
> - **What is banned is OUR loose "publish a document"**, which has no upstream referent and covers three different acts: *placing a document in a public store*, *making it findable*, and (in this emulation only) *handing out a key*. Every design discussion that used it drifted between them. NextGraph knows only the first — a document IS IN a public store, and brokers serve it accordingly. Say which act you mean. If you catch yourself writing "publish a document", you have not yet decided which one. This never licenses renaming something upstream calls `publisher`.
|
||||||
> - **"I looked and it is not there" is not a finding.** §4quinquies once stated that no register existed for received caps, after checking one code path. `AddLink` had been sitting next to `AddRepo` in the same file the whole time. Absence needs at least as much evidence as presence — and an implementation *cache* (like local user storage) is never the model: it is what the model fills.
|
> - **"I looked and it is not there" is not a finding.** §4quinquies once stated that no register existed for received caps, after checking one code path. `AddLink` had been sitting next to `AddRepo` in the same file the whole time. Absence needs at least as much evidence as presence — and an implementation *cache* (like local user storage) is never the model: it is what the model fills.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@@ -107,7 +107,7 @@ export function holdOwnCap(id: string, scope: Scope, doc: Nuri, cap: ReadCap): v
|
|||||||
// by the very session that created it. Mint once, store it, hold that one.
|
// by the very session that created it. Mint once, store it, hold that one.
|
||||||
caps.learn(cap);
|
caps.learn(cap);
|
||||||
// Publication is a registry fact, not a stored one, so it is applied separately.
|
// Publication is a registry fact, not a stored one, so it is applied separately.
|
||||||
if (scope === "public") caps.publishRepoLink(doc);
|
if (scope === "public") caps.recordInPublicStore(doc);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -83,14 +83,14 @@ export class CapRegistry {
|
|||||||
/** holder → the caps they hold, indexed by the cap-less NURI. */
|
/** holder → the caps they hold, indexed by the cap-less NURI. */
|
||||||
private heldByHolder = new Map<string, Map<Nuri, ReadCap>>();
|
private heldByHolder = new Map<string, Map<Nuri, ReadCap>>();
|
||||||
/**
|
/**
|
||||||
* Documents published as a shareable repo link (`RepoLinkV0`) — the emulated
|
* Documents in a PUBLIC store, as this emulation records it. This is NOT a read grant:
|
||||||
* public store. This is NOT a read grant: a published document is read by
|
* such a document is read by
|
||||||
* whoever HOLDS the link, exactly like §5 of the brief says ("whoever has the
|
* whoever HOLDS the link, exactly like §5 of the brief says ("whoever has the
|
||||||
* URL reads the content"), and holding it means having received it. The set
|
* URL reads the content"), and holding it means having received it. The set
|
||||||
* exists so the library can refuse to surface a document its holder never
|
* exists so the library can refuse to surface a document its holder never
|
||||||
* published. *(This fed `discovery.submitToIndex`, removed 2026-07-30; the flag is kept because publishing is still what turns a document into a shareable link.)*
|
* in a public store. *(This fed `discovery.submitToIndex`, removed 2026-07-30.)*
|
||||||
*/
|
*/
|
||||||
private published = new Set<Nuri>();
|
private inPublicStore = new Set<Nuri>();
|
||||||
/** doc NURI → principals holding its WRITE cap. Decorative until P1b. */
|
/** doc NURI → principals holding its WRITE cap. Decorative until P1b. */
|
||||||
private writers = new Map<Nuri, Set<PrincipalId>>();
|
private writers = new Map<Nuri, Set<PrincipalId>>();
|
||||||
/** Fired whenever a holder gains a cap — a cap delivered asynchronously must
|
/** Fired whenever a holder gains a cap — a cap delivered asynchronously must
|
||||||
@@ -181,23 +181,34 @@ export class CapRegistry {
|
|||||||
// --- publication (the public store) -------------------------------------
|
// --- publication (the public store) -------------------------------------
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Publish `nuri` as a shareable repo link and return it — the upstream
|
* Record that `nuri` sits in a PUBLIC store, and mint its cap.
|
||||||
* `RepoLinkV0 { read_cap }`, which whoever receives it can open. The consumer
|
|
||||||
* puts this link (not the bare NURI) in what it makes discoverable.
|
|
||||||
*
|
*
|
||||||
* NOT recursive: the published document may REFERENCE private documents, and the
|
* **Named for what it does here, not for what it means upstream** — and the gap is the
|
||||||
* reference grants nothing on what it references — that non-recursiveness is
|
* point. It was called `publishRepoLink`, and "publish" is banned in this repo
|
||||||
* what lets a public object point at a private identity without disclosing it.
|
* (`docs/readcap-and-nuri-model.md`, the traps block) precisely because it blurs three
|
||||||
|
* acts: placing a document in a public store, making it findable, and handing out a
|
||||||
|
* key. This method does the first and, as an emulation artefact, the third.
|
||||||
|
*
|
||||||
|
* Upstream a document in a public store is readable because the STORE is public and
|
||||||
|
* brokers serve it accordingly (`expose_outer`); a `PublicRepoLinkV0` carries no
|
||||||
|
* `read_cap` at all (`engine/net/src/types.rs:5105-5127`). Here there is no broker that
|
||||||
|
* serves differently, so possession stands in for it — the emulation is OVER-strict,
|
||||||
|
* not inverted: it under-grants, and "circulate the reference" remains the right
|
||||||
|
* gesture at migration. See `readcap-and-nuri-model.md` §4sexies.
|
||||||
|
*
|
||||||
|
* NOT recursive: a document in a public store may REFERENCE private ones, and the
|
||||||
|
* reference grants nothing on what it references. That non-recursiveness is what lets
|
||||||
|
* a public object point at private content without disclosing it.
|
||||||
*/
|
*/
|
||||||
publishRepoLink(nuri: Nuri): ReadCap {
|
recordInPublicStore(nuri: Nuri): ReadCap {
|
||||||
const target = targetOf(nuri);
|
const target = targetOf(nuri);
|
||||||
this.published.add(target);
|
this.inPublicStore.add(target);
|
||||||
return this.mint(target);
|
return this.mint(target);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Was `nuri` published as a repo link? (An emitter-side guard, not a right.) */
|
/** Is `nuri` recorded as sitting in a public store? An emitter-side fact, not a right. */
|
||||||
isPublished(nuri: Nuri): boolean {
|
isInPublicStore(nuri: Nuri): boolean {
|
||||||
return this.published.has(targetOf(nuri));
|
return this.inPublicStore.has(targetOf(nuri));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -211,7 +222,7 @@ export class CapRegistry {
|
|||||||
* arming their guard here would be enforcement this batch does not do.
|
* arming their guard here would be enforcement this batch does not do.
|
||||||
*/
|
*/
|
||||||
open(nuri: Nuri, scope: Scope): ReadCap {
|
open(nuri: Nuri, scope: Scope): ReadCap {
|
||||||
return scope === "public" ? this.publishRepoLink(nuri) : this.mint(nuri);
|
return scope === "public" ? this.recordInPublicStore(nuri) : this.mint(nuri);
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- enforcement gate ---------------------------------------------------
|
// --- enforcement gate ---------------------------------------------------
|
||||||
@@ -280,7 +291,7 @@ export class CapRegistry {
|
|||||||
* NOT what an identity change does (that switches heldByHolder, see the header). */
|
* NOT what an identity change does (that switches heldByHolder, see the header). */
|
||||||
clear(): void {
|
clear(): void {
|
||||||
this.heldByHolder.clear();
|
this.heldByHolder.clear();
|
||||||
this.published.clear();
|
this.inPublicStore.clear();
|
||||||
this.writers.clear();
|
this.writers.clear();
|
||||||
this.issued = false;
|
this.issued = false;
|
||||||
this.notify();
|
this.notify();
|
||||||
|
|||||||
@@ -1038,7 +1038,7 @@ export async function listMyEntityDocs(id: string, scope: Scope): Promise<Nuri[]
|
|||||||
for (const cap of await readStoreCaps(store)) caps.learn(cap);
|
for (const cap of await readStoreCaps(store)) caps.learn(cap);
|
||||||
// A `public` store's documents are also published links — the publication fact
|
// A `public` store's documents are also published links — the publication fact
|
||||||
// lives in the registry, not in the store, so it is re-applied here.
|
// lives in the registry, not in the store, so it is re-applied here.
|
||||||
if (scope === "public") for (const d of docs) caps.publishRepoLink(d);
|
if (scope === "public") for (const d of docs) caps.recordInPublicStore(d);
|
||||||
}
|
}
|
||||||
return docs;
|
return docs;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,15 +87,15 @@ test("a cap received (learn) reads, exactly like one minted", () => {
|
|||||||
expect(bob.caps.capFor(doc)).toBe(cap);
|
expect(bob.caps.capFor(doc)).toBe(cap);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("publishRepoLink returns a cap-bearing link; reading it still means HOLDING it", () => {
|
test("recordInPublicStore returns a cap-bearing link; reading it still means HOLDING it", () => {
|
||||||
const { caps, become } = registry("alice");
|
const { caps, become } = registry("alice");
|
||||||
const doc = "did:ng:o:public-doc";
|
const doc = "did:ng:o:public-doc";
|
||||||
const link = caps.publishRepoLink(doc);
|
const link = caps.recordInPublicStore(doc);
|
||||||
|
|
||||||
expect(hasReadCap(link)).toBe(true);
|
expect(hasReadCap(link)).toBe(true);
|
||||||
expect(targetOf(link)).toBe(doc);
|
expect(targetOf(link)).toBe(doc);
|
||||||
expect(caps.isPublished(doc)).toBe(true);
|
expect(caps.isInPublicStore(doc)).toBe(true);
|
||||||
expect(caps.isPublished("did:ng:o:other")).toBe(false);
|
expect(caps.isInPublicStore("did:ng:o:other")).toBe(false);
|
||||||
|
|
||||||
// Publication is not a world-wide read grant: whoever HAS the URL reads it.
|
// Publication is not a world-wide read grant: whoever HAS the URL reads it.
|
||||||
become("bob");
|
become("bob");
|
||||||
@@ -110,9 +110,9 @@ test("open(): a public document is published as a link, a private one is not", (
|
|||||||
const prot = caps.open("did:ng:o:prot", "protected");
|
const prot = caps.open("did:ng:o:prot", "protected");
|
||||||
const priv = caps.open("did:ng:o:priv", "private");
|
const priv = caps.open("did:ng:o:priv", "private");
|
||||||
|
|
||||||
expect(caps.isPublished("did:ng:o:pub")).toBe(true);
|
expect(caps.isInPublicStore("did:ng:o:pub")).toBe(true);
|
||||||
expect(caps.isPublished("did:ng:o:prot")).toBe(false);
|
expect(caps.isInPublicStore("did:ng:o:prot")).toBe(false);
|
||||||
expect(caps.isPublished("did:ng:o:priv")).toBe(false);
|
expect(caps.isInPublicStore("did:ng:o:priv")).toBe(false);
|
||||||
// All three are readable BY THEIR OWNER — a creator is never locked out.
|
// All three are readable BY THEIR OWNER — a creator is never locked out.
|
||||||
for (const [doc, cap] of [["did:ng:o:pub", pub], ["did:ng:o:prot", prot], ["did:ng:o:priv", priv]] as const) {
|
for (const [doc, cap] of [["did:ng:o:pub", pub], ["did:ng:o:prot", prot], ["did:ng:o:priv", priv]] as const) {
|
||||||
expect(caps.capFor(doc)).toBe(cap);
|
expect(caps.capFor(doc)).toBe(cap);
|
||||||
|
|||||||
@@ -254,7 +254,7 @@ test("(b) a bare reference reads nothing; the repo link of a published document
|
|||||||
setCurrentUser("alice");
|
setCurrentUser("alice");
|
||||||
const pub = await createEntityDoc("alice", "public");
|
const pub = await createEntityDoc("alice", "public");
|
||||||
const items = [item(pub, "u1")];
|
const items = [item(pub, "u1")];
|
||||||
expect(getCaps().isPublished(pub)).toBe(true);
|
expect(getCaps().isInPublicStore(pub)).toBe(true);
|
||||||
const link = linkTo(pub);
|
const link = linkTo(pub);
|
||||||
|
|
||||||
// bob HAS the document's bare NURI (it is right there in `items`) and reads nothing.
|
// bob HAS the document's bare NURI (it is right there in `items`) and reads nothing.
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ function setup(initial: string | null = "alice") {
|
|||||||
const before = holder;
|
const before = holder;
|
||||||
holder = "alice";
|
holder = "alice";
|
||||||
caps.mint("did:ng:o:alice");
|
caps.mint("did:ng:o:alice");
|
||||||
const link = caps.publishRepoLink("did:ng:o:public");
|
const link = caps.recordInPublicStore("did:ng:o:public");
|
||||||
holder = before;
|
holder = before;
|
||||||
return { caps, link, become: (id: string | null) => (holder = id) };
|
return { caps, link, become: (id: string | null) => (holder = id) };
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,11 @@ const TARGET_WORDS = new Set([
|
|||||||
"fetch", "init", "watch", "sparql", "ng", "orm", "type", "types",
|
"fetch", "init", "watch", "sparql", "ng", "orm", "type", "types",
|
||||||
// RDF / SPARQL terms the engine's own query paths use
|
// RDF / SPARQL terms the engine's own query paths use
|
||||||
"subject", "base", "schema", "connected", "identity", "identities",
|
"subject", "base", "schema", "connected", "identity", "identities",
|
||||||
|
// `publisher` is upstream's word for a pub/sub role on a topic (`as_publisher`,
|
||||||
|
// `publisher_advert`, 126 occurrences in the engine). Our own "publish a document" is
|
||||||
|
// banned as ambiguous, but that ban never reaches upstream's term — see the traps
|
||||||
|
// block in `docs/readcap-and-nuri-model.md`.
|
||||||
|
"publisher", "topic", "advert",
|
||||||
// the reactive model the ORM exposes (`OrmSubscription`, `DeepSignalSet`)
|
// the reactive model the ORM exposes (`OrmSubscription`, `DeepSignalSet`)
|
||||||
"observable", "deep", "signal", "set",
|
"observable", "deep", "signal", "set",
|
||||||
]);
|
]);
|
||||||
|
|||||||
Reference in New Issue
Block a user