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
@@ -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);