The scenario reads "an account not connected to another does not read its
protected ENTITY, then reads it after connecting", but the probe was reading
`didđ${protected_store_id}` â the STORE document â and writing its test
entities straight into it.
Under an ACL that shortcut was harmless. Under key possession it is wrong, and
for a reason the model states outright: sharing a store capability would hand
over everything the store contains, present and future. The unit of sharing is
the document. `declareConnections` therefore shares the keys of entity
documents, the store is not one of them, and the reader legitimately saw
nothing. The code was right; the probe was standing in the store for an entity.
Writing several entities into a store-level document also broke this repo's own
one-document-per-entity rule.
The probe now creates a real protected entity document, writes the entity there,
and mounts its subscription on THAT document.
A second defect surfaced while fixing the first: `connect()` asserted both
directions from the READER's session, but a capability can only be shared by
whoever holds it, and `capFor` answers for the connected identity alone â so the
owner-side call returned early having shared nothing. Each direction is now
asserted from its own session, and the reader drains its inbox afterwards.
The reader is a genuine second identity (per-run identifiers give it its own
account, stores, inbox and keyring), not the same one in disguise â a test that
passes because the state is unreal proves nothing. Checked by breaking it on
purpose: without `connect` it fails with `expected +0 to equal 1`.
Also recorded, and worth knowing before writing another probe: `resetCaps()`
clears the "a capability was issued" flag, which disarms the read filter
entirely â it has to run BEFORE the first mint, or reads go straight through and
the reader sees everything.
tsc 0, @ui 7/7, target scenario green, read-filter not regressed.
The SDK was rebuilt: reading is possession instead of an ACL, `Nuri` and
`ReadCap` are template literal types, the cross-account fan-out is gone, and so
is the global discovery index.
Repair the typecheck gate FIRST â it was checking nothing. Under TypeScript 6 the
deprecated `baseUrl` is reported as an ERROR that aborts compilation, so
`tsc --noEmit` exited 0 having verified nothing, behind a single line that reads
like a harmless warning. Dropping `baseUrl` (paths resolve relative to the file
since 4.4) makes the gate real again â and it immediately surfaced 33 errors,
three of which had been dormant for a long time.
Types: 18 sites fixed AT THE SOURCE â the functions that produce a NURI now
return `Nuri` â with `isNuri` guards only at genuine boundaries (an `@id` read
back from a document, an argument coming from a Cucumber step). No cast, no
`@ts-ignore`: silencing the compiler here would have removed the very guarantee
the new types provide.
Capabilities: the ACL is gone. `grantRead`/`protectedDocsOf`/`canRead`/
`makePublic` give way to `capFor`/`shareCap`/`publishRepoLink`, and `open` loses
its `owner` argument. `declareConnections` now shares the caps of its OWN
protected documents to each neighbour's wallet inbox.
Discovery is REMOVED, not postponed: there is no discovery in the target model,
a reader reaches a document only by following a link it was given. The module and
its call sites are gone; the scenario is suspended with a comment saying what
will bring it back â a Festipod DIRECTORY document, whose link the app knows.
Kept rather than deleted: the product need has not gone away.
Verification, and a correction to how it was measured. The @data baseline (20/22)
had been taken on a bloated test wallet: 93 MB against a threshold documented
around 99 MB, with the run stretching from 18 to 23 minutes. Restarting from a
fresh profile drops it to 9m37 and turns BOTH baseline failures green â including
the cold-reconnection one, which confirms the SDK's claim that a fresh session
reads its own documents back with nothing re-declared. So the reference itself was
degraded, on both sides of the comparison.
Real state: typecheck 0, @ui 7/7, @data 20/21. The single failure is understood
and left standing: the protected-connections probe reads the protected STORE
document as a stand-in for an entity. Sharing a store cap would hand over its
entire contents, present and future â precisely the gesture the model refuses. The
scenario's own title says "the protected ENTITY"; the probe is what took the
shortcut, and it is what has to change.
Guard the Option-B owner-materializer against overlay-form drift: match inbox
deposits to owned events on the CANONICAL base repo id (canonicalEventId strips any
âïž<overlay> suffix), applied at the matching boundary in materializeAttendance /
readRegistrationNotifications and to dedup ownedEventIds (ownedKey). The count is
still WRITTEN on the real owned NURI â a stripped id is never a write/anchor target.
Honest framing: this is DEFENSIVE, not a fix for an active bug. On the current tree
create-time, listMyEntityDocs and the read @id already carry the identical NURI
(readUnion pins the subject to the input NURI, 63ecfee) â verified: the count
converges for an event owned via listMyEntityDocs. A prior investigation's 'never
matches' reading was the seeded-but-not-owned artifact (a prior-run identity owned
the seed â reached via discovery, not ownedEventIds â correct behavior).
Un-@wip the @data convergence scenario (asserts the just-joined uid enters the
owner-derived active set â deterministic despite shared-inbox accumulation); it
now passes. Fix authParticipationCount already landed separately. Doctrine:
knowledge_context-internals (canonical id-form invariant). Build + tsc clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Remove the write-isolation violation: joinEvent/leaveEvent no longer write
participantCount on the event doc (a non-owner writing the owner's public doc â
illegitimate in NextGraph). The joiner/leaver only write their own protected
participation doc and DEPOSIT a marker into the event inbox (depositRegistration /
depositLeave).
The event OWNER's session materializes: it subscribes (inbox.watch, doc_subscribe â
no polling) to the inboxes of its OWNED events (ownedEventIds), and on each deposit
recomputes participantCount on its OWN event doc. The count is DERIVED, not
incremented: materializeAttendance derives the SET of distinct active registrations
(new-participant deduped by uid, MINUS leave-participant by regUid/fallback
eventId+userId), count = 1 (host self) + |active set|. A pure function of the inbox
â broker re-syncs converge, never double-count nor resurrect (idempotent); the write
is guarded (only on change â no loop). Authoritative deleteParticipation preserved
(caveat_participation-deletion).
Because the owner writes its own PUBLIC event doc and every session subscribes to it
(P3), the count round-trips reactively to all â no reload. Owner-offline = eventual
(V1; a future @ng-eventually/service materializes on the owner's behalf).
Real 2-browser e2e (e2e-multibrowser.feature): B registers â A materializes â count
1â2 reactively (no reload) + unknown participant; B leaves â count â1. 14/14 green.
Gates: @data auth 4/4, @data isolation 4/4, build + tsc clean. Doctrine:
knowledge_context-internals (Option B section).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The lib e2e harness proves that on the current broker an anchored
INSERT DATA { GRAPH <plainNuri> {âŠ} } DOES round-trip â the earlier 'explicit GRAPH
writes a phantom named graph the read never sees' claim was false; the '0 entity'
symptom was actually the wallet-bloat hang (caveat_wallet-bloat-hang), not a graph
mismatch. Reframe the no-GRAPH default-graph rule as a simplicity/safety convention,
not a round-trip necessity. Lib/app inline comments asserting the phantom-graph
claim remain to reconcile.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Wire the app read path to the lib's per-doc reactive subscription so a change made
in ANOTHER session propagates without a reload or local action:
- useNgData subscribes the by-need set via subscribeDocs(allReadDocs, bumpRead) â
one doc_subscribe per NURI, per-doc error isolation (never the ORM fan-out). Any
patch (own write or broker-synced from a remote peer) re-runs readUnion.
- Reactive discovery: watchDiscoveredEvents(relist) subscribes the global index â
a new public event from another session enters the read set (and gets its own sub).
- Loop-safe: the sub effect is keyed on a stable sorted-NURI key (readDocKey); a
fireâbumpReadâread never changes the doc set, so no re-subscribe loop. Identity
switch empties the set â clean unsubscribe â rebuild â re-subscribe (no leak).
- readUnion stays the one-shot tolerant reader; subscriptions only trigger re-reads.
Real 2-browser e2e (e2e-multibrowser.feature): B registers â A's EventDetailScreen
shows participantCount 1â2 and an 'unknown' participant WITHOUT A reloading, via A's
doc_subscribe on the public event doc (event-driven). Isolated run 12/12 green.
Count mechanism unchanged (P4/Option-B is next); the joiner still writes the public
event doc's participantCount â which is exactly what the observer sees change live.
Gates: @data auth 4/4, @data isolation 4/4, build + tsc clean.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
P1/P2 (lib subscribeDoc + drop polling) landed in @ng-eventually/client c0498a6.
Owner-offline count = eventual for V1, a future @ng-eventually/service takes over
when the owner is disconnected. Reactive hooks are useShape + useDiscrete (no
useQuery); the union-of-N-docs read stays subscribeDocs + re-readUnion (useShape
fan-out hangs). Next: P3 (wire per-doc subscription into the app read path).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Implementation design brief (grounded in current code): reactive reads via a typed
per-doc doc_subscribe wrapper (no polling, no ORM fan-out -> avoids the historical
hang); participant count via option B (joiner deposits into the event inbox, the
event owner materializes into its own event doc's count; option A ruled out --
non-owner append is impossible in NextGraph). Connection-gated identity (else
'inconnu'). Test plan: polyfill low-level doc_subscribe + real 2-browser e2e
reactivity. Phased P1-P6. Open product question: owner-offline eventual count.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add a pointer in knowledge_nextgraph-stack: the SDK's recommended read is its
reactive useShape hook (subscribe/push, one-shot is the exception); full contract
in @ng-eventually/client packages/client/docs/sdk-reference.md. No NextGraph
internals copied into the app repo â just the pointer.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The shared-wallet stopgap keeps ONE React tree across a faux-logout + re-login
under a different identifier (AccountContext.login only rewrites a localStorage
id; AuthGate never remounts, no page reload). FestipodDataContext's by-need read
set accumulates the current identity's scope docs and was never reset on identity
change, so the PREVIOUS identity's PROTECTED docs (its participations) survived in
the new identity's read set and leaked through the union read â the in-memory cap
gate can't filter a doc it doesn't govern this session. Symptom: user B saw A's
participation, and A's event surfaced on B's home (home = getUserEvents(currentUserId)).
Treat every identifier change as a fresh session: a ref-guarded useEffect([username])
clears publicDocs/protectedDocs, resetCaps(), resetRegistryCache(), then bumps the
read tick so the listing effect rebuilds the set bounded to the new identity.
Isolation stays per-document/emulated; the reset only drops cross-identity carryover.
Documented in knowledge_context-internals.
Validated (@data, real broker): after an AâB switch, B does not participate and
does not read A's participation; protected-isolation/read-filter/auth scenarios pass.
tsc + build green; lib untouched.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
A dummy FESTIPOD_SHARED_WALLET_PASSWORD=1 only makes the screen appear; the import
fails because the displayed password must match the imported .ngw. Document the
working invocation with the real e2e wallet (festipod-e2e-tests) + its file.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Removing ConnexionScreen dropped its post-login navigate('/home'). Since the
identifier is now entered at the barrier (before the broker round-trip), on return
the app can load at '/' (WelcomeScreen) with a session already open. AuthGate now
redirects welcomeâ/home once connected AND identified (gate-disabled paths, i.e.
@e2e/@data harness, are exempt).
Update the @humain assisted-import e2e (the real staging flow, the coverage for
this page) to the new UX: the tester types an identifier then clicks « Entrer »
(one act), and lands directly on home â the 'choisir un nom d'utilisateur'
(ConnexionScreen) steps are removed. Step bindings verified; tsc + build green.
Doctrine: knowledge_multibrowser-harness.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The access barrier's shared-wallet steps are gated on hasSharedWallet(), which
reads a global set only by build.ts's compile-time `define`. The src-served paths
(bun run dev AND bun run start) bundle index.html via Bun's HTML import, which
applies no define and inlines neither `process.env` nor `bun --define` (verified) â
so FESTIPOD_SHARED_WALLET_PASSWORD passed to `bun run dev` never reached the
frontend, and the barrier showed the identifier-only variant.
Expose the config at runtime instead: src/index.ts serves /festipod-config.json
(+ /shared-wallet.ngw), and the entry (frontend.tsx) fetches it, sets the global,
then dynamically imports App so sharedWallet.ts reads it on eval. In a build.ts
bundle the value is inlined via define, so the fetch is skipped (NODE_ENV).
Verified in a headless browser: FESTIPOD_SHARED_WALLET_PASSWORD=1 bun run dev now
renders the download + import steps AND the identifier field, no console errors.
Also: only show the download/import steps when status !== 'connected' â after a
faux-logout the wallet is still open, so re-import must not be offered (just the
identifier). Documents the build-define-vs-runtime-config pitfall in tech-stack.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Consumer-side of the @ng-eventually/client fidelity pass, plus the identifier UX:
- Identity: the user types an IDENTIFIER at the access barrier (AccessGateScreen),
in the same act that opens the shared wallet â the separate 'pick a username'
screen (ConnexionScreen) is removed. The identifier is a technical id (a pseudo
in practice, not a Festipod username), normalized (trim, @-stripped, lowercased)
and persisted before the broker redirect, then handed to the SDK as the identity.
AccountContext keeps its API but its stored value is now this normalized id.
- Relationship/connections are app-owned: new src/shared/utils/connections.ts holds
the bilateral registry and maps each link to the SDK's directed grantRead(doc,
grantee); the lib no longer carries a connection concept. Rewired FestipodData
and the @data harness to it.
- Login removed: accounts use the SDK's IdentityStore (set/clear/get); no faux
login/logout framing in the SDK boundary.
Doctrine reconciled: app-security (knowledge_authentication flow, knowledge_trust-model
directed grants, decision_2026-07-06_identifier-at-access-barrier), data-layer
(knowledge_context-internals: stable id principal + single-seed), app-architecture
(knowledge_screens auth inventory), bdd-testing (caveat_wallet-bloat-hang).
App gates: tsc no new errors, build OK. @data path unaffected (harness bypasses the
gate and sets identity directly; login() is not on that path).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Multiple compounding defects kept the connected @data read at 0 entities:
- writeEntity/updateEntityField and registration helpers wrote into an explicit
GRAPH <plainNuri> named graph, invisible to the anchored default-graph read
(read-model.readDoc) after the read switched to per-doc anchored. Drop the
wrapper so writes land in the repo's default graph (matches the read).
- Seed entities are now owned by the CURRENT account, so protected seed docs
(user profiles) pass the per-document ReadCap gate and round-trip.
- Suppress the double seed (explicit loadTestData + 3s dev auto-seed) and add a
re-list signal so freshly-seeded protected docs enter the read set.
- @data step awaits the seed result and waits for events AND users > 0.
Documents the anchored-default-graph write pitfall in rule_document-per-entity.
Validated: connexion-nextgraph.feature @data = 4 scenarios / 13 steps green.
NB: the shared test wallet's private store bloats across runs and makes anchored
queries hang (>15s); a fresh .playwright-profile restores ~1.5s â durable wallet
hygiene is a follow-up.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- @data Before hook sets a UNIQUE virtual-wallet id (username) per scenario so each
scenario starts on a fresh, empty virtual wallet â isolation without touching the
physical wallet; "le portefeuille est vide" is now a fast check, not a full scan.
resetDataState / clearWallet fan-out dropped.
- bootstrapWallet now submits each seeded PUBLIC event to the discovery index
(mirrors the product createEvent), so a fresh virtual wallet can see seeded events
through discovery rather than as its own docs.
Note: @data still red â seeded/published events do not surface in the discovery
read (submitâreadIndex round-trip against the real broker), and some publish steps
time out. The 75s ORM hang is gone; this is a distinct discovery-index integration
issue, still under diagnosis.
Read each by-need entity document with its own anchored query (bounded to the
current account's virtual wallet), never an anchorless scan of the physical shared
wallet. The 75s ORM hang stays gone; a non-empty PHYSICAL wallet now costs nothing
(never scanned). Removed the throwaway anchorless-union probe.
Known remaining (test-infra, not the product): the @data suite still times out
because THIS test account's VIRTUAL wallet is bloated (hundreds of docs
accumulated across this session's many runs) â per-doc reads are O(my docs), and
`clearWallet` still enumerates all accounts. Needs per-scenario test isolation
(fresh/small virtual wallet) + a virtual-wallet-scoped clear to validate green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Replace the reactive-ORM per-entity fan-out read (which HUNG 75s: orm_start_graph
opened every scope graph and RepoNotFound on any fresh/unsynced doc aborted the
subscription) with the read model:
- readEntities.ts â lib readUnion: resolve the by-need doc set (my own scope docs
via listMyEntityDocs + public events via the discovery index â NOT all-accounts
fan-out), then ONE anchorless union sparql_query (GRAPH ?g, VALUES-pinned). Map
to app types. Re-query on a change signal (no reactive union query).
- countUserParticipations no longer fans out over all accounts (own docs only).
- await loadTestData in the seed step; deleted orphaned useShapeWithDefaults;
removed the old multistore-stopgap fan-out scenarios; added the read-model-probe.
- Doctrine: rule_document-per-entity read half + _overview rewritten to the union
model (write half unchanged).
Result: the 75s ORM hang is ELIMINATED (0 hangs; build/tsc/lib-93-tests green;
boundary clean). @data is NOT yet fully green: remaining failures are 90s step
timeouts in the test-harness broker data ops (clearWallet / runUnionProbe / seed)
this run â a harness/broker-op issue, not the read path. To finish separately.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@data oscillated 15-20/21 because the persistent test wallet accumulated data
across scenarios, growing the read fan-out. Add a cheap per-scenario reset
(resetDataState): a single SPARQL DELETE on the shim anchor graph clears the
account records, so allAccounts() collapses and the fan-out is bounded to what
the current scenario re-provisions (accounts recreated lazily). O(1) on one
graph â not a fan-out delete (which saturated the browser before). Called in the
@data Before hook, time-boxed so it can't starve the broker login budget.
Test-infra only â product model, boundary and app read path untouched.
Note: not yet re-measured to stable-green â the broker was degraded during the
bounded validation window (DNS/timeout flakiness). To re-measure when the broker
is stable. knowledge_data-layer-broker updated.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The per-document isolation refactor (one doc per entity) broke every @data
round-trip against the real broker (0 events readable) â fake-ng unit tests
missed it. Root causes + fixes:
- ngSet.add cannot write to an empty subscription scope ("Set is readonly
because scope is empty") â write each entity DIRECTLY into its own document via
SPARQL (new data/entityWrites.ts: writeEntity/updateEntityField), typing each
field with the correct RDF term per the SHEX shape (else the ORM drops the
entity on read). Reactive set stays read-only; the doc NURI is registered into
useShape({graphs}) for reactive reads.
- Current principal made STABLE and username-derived (urn:festipod:user:<name>),
available immediately at login and invariant â so a Participation's mandatory
fp:user is never empty and identity/cap-owner/connections all key on the same
value.
- Discovery deposits AS the current identity (harness sets current user first).
- Idempotence/deregistration checks made authoritative against the broker;
participantCount persisted via SPARQL. rule_document-per-entity enriched with
these write/read + stable-principal lessons.
Round-trip restored (seed readable, inscription+notif, persistent deregistration,
public discovery all pass in isolation). NOT yet stably green as a full suite:
@data oscillates 15â20/21 â residual failures are environmental (participation-
read fan-out lag on an accumulating persistent test wallet), same class as the
Chromium saturation; not a logic bug. Durable fix (follow-up): non-fan-out
materialized read + per-scenario test-wallet isolation. app build+tsc + lib 89
tests green.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Festipod persists each entity as its own document (via the SDK), placed in its
scope. The document is the SDK's unit of sharing/permission, so per-document
isolation (privateâowner, protectedâowner+connections, publicâall) is only
possible when each entity has its own document. Writing several entities into a
store-level document defeats per-scope isolation. Framed as SDK usage; the SDK
owns enforcement (app-security/knowledge_trust-model).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
On creating a public event, Festipod submits it to the discovery index (an SDK
call); the discovery screen reads the index instead of enumerating accounts. The
app knows nothing of the index's owner, inbox, or materialization â it treats the
lib as a finished SDK whose discovery is a global index. No store ids.
Unit-validated in the lib (79 tests). @data broker validation deferred: the
NextGraph broker (nextgraph.net/eu) was unreachable at run time â to be re-run
in T03.d once the broker recovers.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>