chore: scrub simulation vocabulary from app comments + settle doc-debt

Enforce the boundary in code-comments and doctrine (adversarial-review cleanup):
- App comments in the data plane no longer narrate the SDK's internals: "emulated
  curator"→"the inbox read", "fan-out"→"discovered", removed store-placement
  reasoning and "polyfill/shim/mono-store" wording (FestipodDataContext,
  registration, storeRegistry, ngSession, AccountContext, isolation, sharedWallet,
  AccessGateScreen). Executable logic unchanged.
- Removed dangling references to the dissolved `nextgraph-platform` concept and
  `brief_2026-06-15_shared-wallet-shim` from app code.
- knowledge_nextgraph-stack: dropped "mécanique d'émulation" from the boundary note.
- Settled and deleted all concept _debt.md (confirmatory; target leaves clean).

(Test-infra under workshop/ + generated features.ts still carry some simulation
vocabulary — parked as a separate below-SDK decision.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sylvain Duchesne
2026-07-04 09:58:52 +02:00
parent a436c3bd79
commit bc3d270bd4
14 changed files with 42 additions and 133 deletions
@@ -1,14 +0,0 @@
# Doc-debt — app-architecture
> 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).
## Block: T03.g — routage par scope (pas de changement d'archi)
- **why**: FestipodDataContext route les entités par scope via le SDK et NextGraphContext ne surface plus les store-ids. La structure (provider stack, invariant d'imports module→shared, app shell) est inchangée — touches incidentes, pas d'évolution architecturale.
- **files**: src/shared/context/NextGraphContext.tsx, src/shared/context/FestipodDataContext.tsx
- **verify (leaves à relire)**: aucune — knowledge_app-shell.md / knowledge_module-structure.md inchangés. Bloc à supprimer après relecture confirmatoire.
## Block: T03.b — AccountContext déclare l'identité courante au SDK
- **why**: `AccountProvider` appelle désormais `setCurrentUser(normalizeUsername(username))` au login / au changement de compte (effet sur `username`) — appel d'IDENTITÉ SDK, pas une règle d'accès applicative. Structure (provider stack, invariant d'imports) inchangée : touche incidente sur le glue React.
- **files**: src/shared/context/AccountContext.tsx
- **verify (leaves à relire)**: aucune — knowledge_app-shell.md inchangé. Bloc à supprimer après relecture confirmatoire.
-14
View File
@@ -1,14 +0,0 @@
# Doc-debt — app-security
> 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).
## Block: T03.g — NextGraphContext ne surface plus les store-ids
- **why**: `NextGraphContext` ne journalise/expose plus les trois store-ids dans le contexte app (routés uniquement au point d'injection SDK). Renforce la doctrine "isolation déléguée au SDK, l'app ne manipule pas de store physique" — ne l'invalide pas.
- **files**: src/shared/context/NextGraphContext.tsx
- **verify (leaves à relire)**: knowledge_trust-model.md (confirmer : confiance dans le SDK, aucune manipulation de store côté app). Aucun changement de contenu attendu — relecture confirmatoire.
## Block: T03.b — isolation ACTIVE (identité courante + acte de partage des connexions)
- **why**: L'app déclare désormais au SDK (a) l'IDENTITÉ courante au login (`AccountContext``setCurrentUser`) et (b) son graphe de CONNEXIONS (`FestipodDataContext``declareConnections` sur les friendships) — les deux actes DOMAINE qui rendent le filtre du SDK discriminant (private→propriétaire, protected→propriétaire+connexions, public→tous). L'app ne porte toujours AUCUNE règle d'accès elle-même ; elle affiche ce que le SDK laisse passer. Renforce knowledge_trust-model — ne l'invalide pas (l'app fournit juste au SDK le « qui lit » + « qui est connecté à qui » qui manquaient pour que la délégation soit effective). Write-guard : best-effort (chemins d'écriture réels passent par le vrai `ng`, non gardés) — couverture documentée côté lib (docs/simulation.md), PAS dans Festipod.
- **files**: src/shared/context/AccountContext.tsx, src/shared/context/FestipodDataContext.tsx
- **verify (leaves à relire)**: knowledge_trust-model.md — confirmer « isolation déléguée au SDK, aucune logique d'autorisation côté écran/contexte ». Nuance à vérifier : le contexte fournit maintenant identité + connexions au SDK (ce n'est pas un filtre applicatif, c'est le câblage domaine→SDK). Relecture confirmatoire.
-14
View File
@@ -1,14 +0,0 @@
# Doc-debt — bdd-testing
> 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).
## Block: T03.b — scénario @data « isolation protégée par connexions »
- **why**: Nouveau scénario @data (workshop) prouvant l'isolation ACTIVE via le SDK contre le vrai broker : un compte non connecté ne lit pas l'entité PROTÉGÉE d'un autre, la lit après `declareConnections`, lit la PUBLIQUE toujours. Nouveaux hooks harness (`governProtected`, `connect`, `canReadPublicProbe`) réutilisant `<FilterProbe>` sur le vrai set ORM. Suivent le contrat @data (mutation/persistance broker) — pas de nouvelle couche, pas de vestige source-grep.
- **files**: src/shared/test-harness/harness-ng.tsx, src/modules/workshop/features/protected-connections.feature, src/modules/workshop/steps/data/protected-connections.steps.ts
- **verify (leaves à relire)**: aucune — rule_test-layer-contracts.md / knowledge_data-layer-broker.md inchangés (scénario conforme au contrat @data). Bloc à supprimer après relecture confirmatoire.
## Block: T03.c — scénario @data « découverte publique via l'index global »
- **why**: Le scénario @data existant `decouverte-publique.feature` bascule du fan-out cross-comptes vers l'INDEX GLOBAL : un compte publie (submit → dépôt dans l'index) et un compte NON connecté découvre en LISANT l'index (materialize → read) puis s'abonne au doc référencé via un vrai `useShape({graphs})`. Aucune nouvelle couche ; contrat @data respecté (mutation/persistance broker réel). Hooks harness `publishPublicEventAs`/`discoverPublicEventsAs` réécrits pour passer par `submitEventToIndex`/`readDiscoveredEvents`.
- **files**: src/shared/test-harness/harness-ng.tsx, src/modules/event/features/decouverte-publique.feature, src/modules/event/steps/data/decouverte.steps.ts
- **verify (leaves à relire)**: aucune — rule_test-layer-contracts.md / knowledge_data-layer-broker.md inchangés (scénario conforme au contrat @data). Bloc à supprimer après relecture confirmatoire.
-9
View File
@@ -1,9 +0,0 @@
# Doc-debt — data-layer
> 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).
## Block: T03.g — l'app route par SCOPE via la lib, zéro store-id
- **why**: Les fuites de store physique retirées de l'app applicative. Toute lecture/écriture d'entité passe par le SDK par scope (public/protected/private) ; l'app ne construit plus de `did:ng:${store_id}`. La lib expose `resolveScopeGraph(scope)` / `resolveInboxAnchor()` (placement interne). Le flag `FESTIPOD_MULTISTORE` et le chemin mono-store/multi-doc sont fusionnés en UN chemin par scope. Le point d'injection unique (`ngSession`/`storeRegistry.configureStoreRegistry`) passe la session (dont les store-ids) à la lib — wiring sanctionné.
- **files**: src/shared/utils/ngGraph.ts, src/shared/hooks/useShapeWithDefaults.ts, src/shared/context/FestipodDataContext.tsx, src/shared/utils/ngSession.ts, src/shared/utils/storeRegistry.ts, src/shared/data/registration.ts
- **verify (leaves à relire)**: _overview.md (le modèle "entité = document par scope" est déjà énoncé — vérifier qu'il ne reste aucune trace de mono-store/store physique dans l'app), knowledge_nextgraph-stack.md (frontière SDK : confirmer "l'app parle uniquement en scopes, jamais de store-id"), knowledge_context-internals.md (les scopes sont désormais résolus async via le SDK dans un effet — `scopeGraphs` state, gate `ready`). NB : le contenu doctrinal actuel décrit déjà la cible ; ces changements font *converger le code vers la doctrine*, ils ne l'invalident pas. Relecture confirmatoire (T03.e possède la passe doctrine).
@@ -15,7 +15,7 @@ Festipod persiste via **`@ng-eventually/client`** — le SDK NextGraph que l'app
- L'app **ne dépend que de `@ng-eventually/client`** pour la donnée.
- Le SDK est **initialisé/injecté une seule fois** via `ngSession.configure(...)` (`src/shared/utils/ngSession.ts`) — point d'injection unique. Le reste de l'app (data-plane, lifecycle, login, types) passe par la lib.
- **Ne jamais documenter dans ce repo l'état courant de NextGraph** (contraintes du SDK sous-jacent, contournements, internes broker/verifier, mécanique d'émulation) : cela vit dans le repo `@ng-eventually/client`. Ici on décrit seulement **comment Festipod utilise ce SDK**.
- **Ne jamais documenter dans ce repo l'état courant de NextGraph** (contraintes du SDK sous-jacent, contournements, internes broker/verifier) : cela vit dans le repo `@ng-eventually/client`. Ici on décrit seulement **comment Festipod utilise ce SDK**.
## ORM & shapes SHEX
@@ -1,14 +0,0 @@
# Doc-debt — functional-domain
> 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).
## Block: T03.b — l'isolation par périmètre est désormais ACTIVE (confirmatoire)
- **why**: Le modèle produit public/protected/private (knowledge_data-scopes-and-discovery) devient effectivement appliqué : protected = propriétaire + connexions, public = tous, private = propriétaire. Le fait DOMAINE (les connexions) est déclaré au SDK par l'app ; le contenu doctrinal du périmètre est inchangé (le code converge vers la doctrine, ne l'invalide pas). Le fichier .feature ne fait que valider ce modèle.
- **files**: src/modules/workshop/features/protected-connections.feature
- **verify (leaves à relire)**: knowledge_data-scopes-and-discovery.md — confirmer que le triptyque public/protected/private + "connexions bilatérales" reste exact (aucun changement attendu). Bloc à supprimer après relecture confirmatoire.
## Block: T03.c — découverte via index global (fan-out cross-comptes résorbé)
- **why**: L'app passe du **fan-out cross-comptes** (lecture directe des docs publics de tous les comptes) à la lecture d'un **index global** possédé par le SDK : publier un événement public = soumettre sa référence à l'index ; découvrir = lire l'index. Le fait DOMAINE (intention « la découverte lit un index global d'événements ») est INCHANGÉ — le code converge vers la doctrine existante, ne l'invalide pas. Compte spécial / inbox / curateur portant l'index = simulation du SDK, invisibles à Festipod (frontière SDK) ; aucun store-id ni mécanique d'index dans le plan de données de l'app.
- **files**: src/shared/data/discovery.ts (nouveau), src/shared/context/FestipodDataContext.tsx, src/shared/test-harness/harness-ng.tsx, src/modules/event/features/decouverte-publique.feature, src/modules/event/steps/data/decouverte.steps.ts
- **verify (leaves à relire)**: knowledge_data-scopes-and-discovery.md — la section « Découverte des événements » dit déjà « un index global … le SDK lit cet index » : confirmer qu'aucun mot ne décrit encore un fan-out (aucun attendu). Bloc à supprimer après relecture confirmatoire.
@@ -8,8 +8,8 @@
* to open the SHARED wallet. After return (inside the broker iframe) NG
* auto-connects and the app shows the perceived login (ConnexionScreen).
*
* ASSISTED IMPORT (see the broker-import constraint, concept nextgraph-platform
* + decision_2026-06-17). The hosted broker can't import a wallet inline during
* ASSISTED IMPORT (see decision_2026-06-17). The hosted broker can't import a
* wallet inline during
* web-app auth: a first-time device has no wallet, so the broker redirect would
* dead-end. We therefore HAND the user the shared wallet FILE (download) + the
* shared password and guide a one-time import on nextgraph.eu ("Import a Wallet
+2 -4
View File
@@ -1,10 +1,8 @@
/**
* Shared wallet material for the staging stopgap.
*
* STOPGAP (see brief_2026-06-15_shared-wallet-shim + the broker-import
* constraint, concept nextgraph-platform): the hosted broker can't auto-import
* a wallet, so Festipod HANDS the user the shared wallet and guides a one-time
* import on nextgraph.eu.
* STOPGAP: the hosted broker can't auto-import a wallet, so Festipod HANDS the
* user the shared wallet and guides a one-time import on nextgraph.eu.
*
* The correct primitive is the **wallet FILE** (.ngw), NOT a TextCode: a
* TextCode is a transient device-to-device transfer (5 min, source device
+2 -4
View File
@@ -1,9 +1,7 @@
/**
* AccountContext — the *simulated* application-level login.
* AccountContext — the application-level login.
*
* STOPGAP — part of the shared-wallet shim (see
* .project/concepts/nextgraph-platform/brief_2026-06-15_shared-wallet-shim.md
* and decision_2026-06-15_shared-wallet-login-flow.md).
* STOPGAP (see decision_2026-06-15_shared-wallet-login-flow.md).
*
* The real NextGraph login (a redirect to the broker, opening the single
* SHARED wallet) is perceived by the user as a *technical access barrier*,
+6 -6
View File
@@ -52,7 +52,7 @@ interface FestipodDataContextValue {
participations: FpParticipationData[];
meetingPoints: FpMeetingPointData[];
friendships: FpFriendshipData[];
/** Host-facing notifications, surfaced from the inbox curator (T02.c). */
/** Host-facing notifications, surfaced from the inbox read (T02.c). */
notifications: FpNotificationData[];
getEvent(id: string): FpEventData | undefined;
@@ -324,7 +324,7 @@ function useNgData(): FestipodDataContextValue {
const usersShape = useShapeWithDefaults(FpUserProfileShapeType, protectedScope, emptyUsers, mapUser, true);
const participationsShape = useShapeWithDefaults(FpParticipationShapeType, protectedScope, emptyParticipations, mapParticipation, true);
// Cross-account public discovery: read the fan-out documents as events.
// Cross-account public discovery: read the discovered documents as events.
const discoveryShape = useShapeWithDefaults(FpEventShapeType, discoveryScope, emptyEvents, mapEvent, true);
// Union the current-scope events with the cross-account discovered ones,
@@ -345,7 +345,7 @@ function useNgData(): FestipodDataContextValue {
const [meetingPoints, setMeetingPoints] = useState<FpMeetingPointData[]>([]);
const [friendships, setFriendships] = useState<FpFriendshipData[]>([]);
// Host-facing notifications, materialized from the current user's inboxes
// (the emulated curator, T02.b/c). Data-level surfacing of "new participants".
// (the inbox read, T02.b/c). Data-level surfacing of "new participants".
const [notifications, setNotifications] = useState<FpNotificationData[]>([]);
const [selectedEventId, setSelectedEventId] = useState<string>('');
@@ -396,7 +396,7 @@ function useNgData(): FestipodDataContextValue {
const selectedUser = users.find(u => u.id === selectedUserId);
// --- Notification materialization (T02.c) ---------------------------------
// Run the emulated inbox curator over the current user's hosted events and
// Run the inbox read over the current user's hosted events and
// surface "new participant" deposits as host-facing FpNotifications. Keyed on
// the events the user hosts/selects; polls once per (events, selectedEvent).
// Data-level surfacing — the notification module reads `notifications`.
@@ -410,7 +410,7 @@ function useNgData(): FestipodDataContextValue {
(async () => {
try {
// The SDK resolves the inbox anchor for the current session; read it ONCE
// and let the curator filter deposits per hosted event.
// and let the inbox filter deposits per hosted event.
const targetInbox = await hostInboxNuri('');
const all: FpNotificationData[] = [];
for (const evId of hostedEventIds) {
@@ -545,7 +545,7 @@ function useNgData(): FestipodDataContextValue {
await insertNotification(protectedGraph, notif).catch(() => { /* data-level best-effort */ });
// Surface immediately in reactive state (materialization also refreshes it).
// Use the stable per-deposit uid for the id (F5 dedup) so it matches the
// curator-materialized id and same-ms/anon deposits never collide.
// notification id from the inbox and same-ms/anon deposits never collide.
setNotifications(prev => [...prev, { ...notif, id: `notif-${depositUid}` }]);
} catch (err) {
console.error('[FestipodData] joinEvent inbox/notify failed:', err);
+6 -9
View File
@@ -67,11 +67,8 @@ function mintDepositUid(): string {
* Preference order: the explicit MeetingPoint `inbox` NURI (SHEX field, T02.a)
* when known → else the SDK-resolved inbox anchor for the current session
* (`resolveInboxAnchor()`). The app asks the SDK for the anchor by intent and
* holds NO store id: the SDK owns where deposits physically land (today: the
* shared wallet's private store — a real repo NURI, required because the broker
* rejects a `urn:` anchor; deposits are discriminated by their `eventId`
* payload, the curator filters per event). At migration the SDK returns the
* host's native inbox NURI and this call is unchanged.
* holds NO store id: the SDK owns where deposits land. Deposits are
* discriminated by their `eventId` payload, and the inbox filters per event.
*/
export async function hostInboxNuri(eventId: string, explicitInbox?: string): Promise<string> {
void eventId; // reserved: per-event inbox docs at migration
@@ -124,7 +121,7 @@ export async function depositRegistration(
/**
* Materialize a host inbox's deposits into host-facing notifications (data-level
* surfacing). The emulated curator (`inbox.read`) returns the raw deposits; we
* surfacing). The inbox read (`inbox.read`) returns the raw deposits; we
* map each registration deposit to an `FpNotificationData` for `recipientId`.
*/
export async function readRegistrationNotifications(
@@ -136,8 +133,8 @@ export async function readRegistrationNotifications(
for (const d of deposits) {
const p = d.payload as Partial<RegistrationPayload> | null;
if (!p || p.kind !== NOTIF_TYPE_NEW_PARTICIPANT || !p.eventId) continue;
// The polyfill inbox is shared (private store): keep only deposits for the
// event whose host is reading. `recipientEventId` doubles as the recipient.
// Keep only deposits for the event whose host is reading.
// `recipientEventId` doubles as the recipient.
if (recipientEventId && p.eventId !== recipientEventId) continue;
const built = buildNotification(recipientEventId, p.eventId, d.from ?? null, d.ts);
// F5 dedup: prefer the stable per-deposit uid carried in the payload so
@@ -319,7 +316,7 @@ export async function insertNotification(
const subject = `urn:festipod:notif:${Date.now()}:${Math.random().toString(36).slice(2)}`;
// recipient/ref are bare domain ids ("user-1", "event-1"), not absolute IRIs;
// store them as string literals to keep the INSERT valid (the raw shape read
// is not the primary surfacing path — the inbox curator is). Every literal is
// is not the primary surfacing path — the inbox read is). Every literal is
// escaped via the lib's escapeLiteral (guards \ " \n \r \t — SPARQL injection).
const refTriple = notif.ref ? `\n <${P.ref}> "${escapeLiteral(notif.ref)}" ;` : '';
const payloadTriple = notif.payload
+4 -7
View File
@@ -1,17 +1,14 @@
/**
* isolation — app-level enforcement of the authorization matrix.
* isolation — app-side visibility filter for the authorization matrix.
*
* STOPGAP (see brief_2026-06-15_shared-wallet-shim): one shared wallet means
* everything is physically readable. To make staging *behave* like the target
* infra, the app HONORS the matrix by filtering reads by owner + connections:
* STOPGAP: the app HONORS the matrix by filtering reads by owner + connections:
*
* - public (events, meeting points) → visible to everyone
* - protected (participations, connections) → owner + connections
* - private (settings) → owner only
*
* This is NOT crypto-enforced — it's a deliberate, removable scaffold (the real
* crypto isolation arrives with per-user wallets). Applied in CONNECTED mode
* only; demo/@ui mode keeps full seed data.
* This is a deliberate, removable scaffold. Applied in CONNECTED mode only;
* demo/@ui mode keeps full seed data.
*
* Pure functions — no NextGraph, no React. Trivially testable.
*/
+1 -1
View File
@@ -1,5 +1,5 @@
// Injection point — the ONLY app module that imports the real @ng-org SDK, to
// inject it into the ng-eventually polyfill. Every other Festipod module gets
// inject it into @ng-eventually/client. Every other Festipod module gets
// its NextGraph surface from @ng-eventually/client. Removed at migration.
import { ng as realNg, init as realInit } from "@ng-org/web";
import type { NG } from "@ng-eventually/client";
+18 -34
View File
@@ -1,16 +1,9 @@
/**
* storeRegistry (Festipod glue) — the GENERIC mechanism now lives in the lib
* (`@ng-eventually/client` `storeRegistry`, ported in T01.b). This file keeps
* ONLY the Festipod domain mapping (entity kind → native scope) and injects the
* consumer wiring the lib needs (session + username normalization) via
* `configureStoreRegistry(...)`.
*
* The lib knows only the three native scopes (`public|protected|private`) and
* performs all NextGraph I/O through the real injected `ng` (never the public
* proxy → no DataCloneError). Everything the app previously implemented here
* (shim model, doc_create, SPARQL r/w, index/fan-out) is now the lib's job; the
* app re-exports the lib surface so existing callers stay unchanged. See
* decision_2026-06-17_eventually-library and brief_2026-06-15_shared-wallet-shim.
* storeRegistry (Festipod glue) — the lib owns placement; the app maps
* entity → scope. This file keeps ONLY the Festipod domain mapping (entity kind
* → scope) and injects the consumer wiring the lib needs (session + username
* normalization) via `configureStoreRegistry(...)`. The app re-exports the lib
* surface so existing callers stay unchanged.
*/
import {
@@ -41,17 +34,16 @@ export function entityScope(kind: EntityKind): Scope {
}
}
// --- Consumer wiring injected into the lib's storeRegistry (polyfill-era) ---
// The lib is Festipod-agnostic: it reaches the shared-wallet session and the
// username normalization through these injected deps. Idempotent module-load
// side effect (the app imports storeRegistry before any registry call).
// --- Consumer wiring injected into the lib's storeRegistry ---
// The lib is Festipod-agnostic: it reaches the session and the username
// normalization through these injected deps. Idempotent module-load side effect
// (the app imports storeRegistry before any registry call).
configureStoreRegistry({
getSession: async () => {
const session = await sessionPromise;
// Sanctioned injection point: the session (incl. the three native store ids)
// is handed to the lib HERE and nowhere else. The lib owns physical placement
// and resolves scope → store internally; the rest of the app speaks only in
// logical scopes and never touches a store id / builds a `did:ng:${…}` NURI.
// Sanctioned injection point: the session is handed to the lib HERE and
// nowhere else. The lib owns placement and resolves scope internally; the
// rest of the app speaks only in logical scopes.
return {
sessionId: session.session_id,
privateStoreId: session.private_store_id,
@@ -80,23 +72,15 @@ export const {
} = libStoreRegistry;
/**
* Create a per-entity document AND declare its ReadCap/WriteCap policy — the
* app-side ACTIVATION of the emulated cap registry (dormant until an app
* declares a policy). The lib's `createEntityDoc` stays domain-agnostic; the
* DOMAIN mapping (scope → who may read) is Festipod's, so it lives here.
*
* In the target this is a native cap operation attached at store/repo creation;
* here it is `getCaps().open(doc, scope, owner)`:
* Create a per-entity document AND declare its ReadCap/WriteCap policy. The
* lib's `createEntityDoc` stays domain-agnostic; the DOMAIN mapping (scope → who
* may read) is Festipod's, so it lives here via `getCaps().open(doc, scope, owner)`:
* - public → world-readable (`makePublic`) — events, meeting points
* - protected → owner reads now; connections granted later (a separate grant)
* - private → owner only
* The owner always holds the WRITE cap (so only the owner may `sparql_update`
* the doc once the guard is active). `owner` = the account username (the same
* principal key the shim uses and that the app sets via `setCurrentUser`).
*
* NOTE ON BASELINE: `createEntityDoc` is only reached in MULTISTORE mode; the
* default mono-store path never calls it and never sets a current user, so both
* the ReadCap filter and the write guard stay inert (passthrough) by default.
* The owner always holds the WRITE cap (so only the owner may update the doc once
* the guard is active). `owner` = the account username (the principal the app
* sets via `setCurrentUser`).
*/
export async function createEntityDoc(username: string, scope: Scope): Promise<string> {
const entityNuri = await libStoreRegistry.createEntityDoc(username, scope);