Commit Graph

20 Commits

Author SHA1 Message Date
Sylvain Duchesne 0d925c7cb9 One inbox per document: the sign-up's deposit now lands where the owner is looking
Creating an event resolved its inbox four times at once -- from `createEvent`,
from the materializer, from the watch callback and from the watch wiring --
with nothing serialising them. Three inboxes were registered for that one
document inside 0.3 s, so the owner watched one while the sign-up deposited into
another. That is the whole of the asymmetry: on any later connection nothing
re-registers, both sides agree, and withdrawal converged immediately while a
sign-up never did.

Measured before: 2 of 3 fresh sign-ups NEVER converged, the deposit unfindable
on every later connection. Measured after, twice: one inbox, one caller joining
the in-flight resolution instead of opening a second, the deposit read back, and
nothing failing to converge.

The fix is two primitives rather than a lock in the middle of the data context,
each unit-tested on its own: a resolve-once-per-key whose rejection is NOT
memoized (unknown is not absent), and a serial task whose mid-run requests
coalesce into one follow-up and which a failure cannot wedge. The single-flight
wrapper is now the only caller of the underlying entry, so every call site is
covered without touching any of them.

Also closed on the same path: the write guard carries a monotonic cycle number,
so a cycle from an earlier effect run cannot overwrite a fresher count; the
field update is one statement instead of DELETE-then-INSERT, closing the window
where a reader saw the field absent and read zero; and the materializer's
before-value comes from a ref instead of a stale closure.

What is NOT fixed, deliberately: the count still takes one connection to appear.
A deposit you make into an inbox you watch produces no push, and neither does a
write to your own document -- both are questions for the provider, and any
app-side substitute would be the polling the doctrine forbids.
2026-08-17 00:02:27 +02:00
Sylvain Duchesne 4148df8fcb Record what running the flow proved, and fix the leaf that caused one defect
The create-and-participate flow was driven in a real browser for the first time.
It had been called correct by construction -- typecheck, build, reading -- and
the probe found three defects none of those could see.

Two open bugs, both major, both filed rather than worked around:
signing up to your own event makes the NEXT connection fail outright
(`ensureIdentity()` rejects inside the data layer's own inbox processing, 3/3,
reproduced on a fresh origin and identity), and the participant count does not
converge in the same session (2/2, 120 s and 75 s). Whether it converges at the
next connection is recorded as UNKNOWN and unmeasurable, because the first bug
stops the app from getting there.

The doctrine defect is the one worth the trouble. `knowledge_build-pipeline`
said production builds into `dist/`; `knowledge_deployment` said the container
runs `bun run start` from `src/`. Both were written down, they contradicted each
other, and the code followed the wrong one -- which is how a deployed app that
could sign nobody in was shipped. The three paths now live in one table whose
discriminating column is what is actually served: dev `src/`, production `src/`,
and `dist/` served by nothing at all. A build artefact nobody serves is a trap
for the next reader who assumes otherwise.

The probe method itself is written down: the suite cannot run, a targeted probe
can, and the difference is worth knowing before concluding that nothing is
measurable.

Stated once where a reader meets it: honest steps do not add up to an honest
flow. Every gesture in the sign-up reports correctly, and the user is still told
they participate while the count never moves and the next connection fails.
2026-08-16 16:46:10 +02:00
Sylvain Duchesne fa934ccdc6 Production serves from source, so it must fetch its config like everything else
The deployed application could never sign anybody in.

`loadRuntimeConfig` skipped fetching `/festipod-config.json` under
`NODE_ENV=production`, reasoning that a production build has the value inlined
by `build.ts`'s `define`. This project's production does not build: the
container copies the sources and runs `bun run start`
(= `NODE_ENV=production bun src/index.ts`), serving from `src/` exactly as dev
does. Nothing serves `dist/` at all. So the one step that could supply a wallet
was skipped, `ensureIdentity()` threw for want of one, and the endpoint sat
there — served, and never asked.

The condition tested the wrong thing. "Was the value inlined?" is a question the
global itself answers; "am I in production?" only ever stood in for it, and the
stand-in was false on the very path that matters.

Verified on the served bundle rather than on the endpoint: under
`NODE_ENV=production` it now carries the fetch, where it carried none.

The same combination is what `hooks.ts` spawns for the @e2e app server, so that
layer could not sign in either.
2026-08-16 16:27:00 +02:00
Sylvain Duchesne cebd54c978 The doctrine says what the code does again
Eighteen leaves had drifted behind today's changes, and several taught the exact
mistakes that were just removed.

Corrected, among others: the identity and the profile were conflated, and
`knowledge_context-internals` still described the impersonation fallback and the
principal-to-username join as current mechanisms. `caveat_identity-ids-in-screens`
and `knowledge_data-modes` still had `joinEvent` logging and returning where it
now throws. The shape listings still carried the event host. And
`knowledge_screen-pattern`'s canonical sample taught a toast written beside the
call rather than after the write -- the very bug fixed this afternoon, sitting in
the file a new screen is copied from.

New leaves for what had no home: write rights read from the owned-document
listing, with its three states and its deliberate residual; the owner's ruling
that no "may I write this?" call is coming, so the list is the answer for good;
and the `@data` suite losing its fixtures now that the seed writes nothing into a
connected wallet.

Four doc-debt files settled, including one the hook opened mid-pass.

Worth recording how one leaf died: a caveat was written for the unguarded edit
screen exactly as briefed, then deleted on finding the fix had landed while the
pass ran. Doctrine tracks the tree, not the instructions it was given.
2026-08-16 15:25:34 +02:00
Sylvain Duchesne 13eb2c4a15 Waits that say ten seconds now wait ten seconds, and editing checks you may
Two things that announced what they had not verified.

Seventeen `waitForFunction` calls passed their timeout in Playwright's ARGUMENT
slot instead of its options slot, so every one of them silently used the 30 s
default while the code read 5, 10, 15 or 60. The inventory said sixteen: one was
a false positive and two more were found that it never listed.

All seventeen are corrected, including the nine whose written value is SHORTER
than the default. Honouring the author's number is the point: a wait that is too
short fails loudly and names its step, where thirty seconds obtained by accident
hides a real slowness and reads as a lie in the source. Which of them need
raising is a question for the day the suite can run again -- it will be answered
on an honest number.

The event edit screen awaited nothing: the success toast fired and the screen
navigated away whether or not the write resolved. It now confirms after the
write, keeps the user on their edits when it fails, and says so.

That route was also unguarded -- anyone reaching the URL got the form, for any
event. It is now decided by ownership, read from the list of my own documents,
with the same three-state answer the pencil icon uses. UNKNOWN renders neither
the form nor a bounce: both would present a guess as a fact, and the guess that
matters here is telling a genuine owner their event is not theirs.
2026-08-16 15:16:46 +02:00
Sylvain Duchesne db3dbba294 No fixture seed, no event host, and the edit affordance stops lying
Three changes the product model asked for.

The fixture seed no longer writes anything into a connected wallet, by any
route. `bootstrapWallet` is the single enforcement point -- both call sites
funnel through it -- so the switch cannot be walked around by a screen or a
bridge. The fixtures, the seeding code, the demo path and the rendering tests
are untouched; a unit test now fails if a document is created after all.

An event has no host. The domain says so -- the meeting point has a host, the
event is only the anchor -- while the shape carried `hostName`/`hostInitials`
and every created event was written with the fabricated `'Moi'` / `'MD'`. Gone
from the shape, the ORM bindings, the type, the adapters, the writes and the
screens. `fp:MeetingPoint.host` stays: that one is real.

Regenerating the ORM revealed the committed bindings had drifted from what the
generator emits -- stylistic, verified predicate by predicate, plus the loss of
the `Fp` prefix. The prefix cannot be restored at the generator: the name comes
from the shape IRI, and those IRIs are the persisted RDF classes. Aliased at the
three import sites instead, so nothing downstream moved and the DOM `Event` and
`Notification` types are never shadowed.

Write rights are ownership, read from the list
The contract leaves no other reading -- only an owner writes, and no call adds a
writer -- so `listMyEntityDocs('public')` is what says which events are mine.
The hard-coded `isOwner = true` is replaced by a three-state answer, and the
UNKNOWN state renders neither a pencil nor a greyed one: a disabled look-alike
invites a dead click.

Two adversarial passes refuted the first attempt and both defects are fixed. A
latched boolean denied an owner their own event forever once a listing had
missed it; the ruling is now rebuilt rather than accumulated, so a later listing
overturns an earlier one.

Residual, deliberate and commented: "not mine" is inferred from absence, and the
reactive read and the listing are separate mechanisms, so a freshly arrived
event is ruled out for the window between them. Closing it needs a timer, which
the doctrine forbids.
2026-08-16 14:50:34 +02:00
Sylvain Duchesne df971df135 Who I am comes from signing in; my profile is the document I own
The app derived its identity from a profile lookup and, when nothing matched,
picked somebody else. That is backwards: signing in returns who I am, and the
profile is looked up by it.

- Identity and profile are now two things. The identity is what
  `ensureIdentity()` returns: opaque, never rendered, never written, never
  passed to a data-layer call. The profile is Festipod's own object -- pseudo,
  name, initials -- in a document we create and write.
- "My profile" is the profile document I own, resolved through
  `listMyEntityDocs('protected')`. No username matching, no positional pick. A
  failed listing leaves the answer UNKNOWN rather than collapsing to "none".
- Having no profile now resolves to having no profile. Two impersonation
  fallbacks are gone, including one in `updateProfile` that would have written
  your pseudo into a stranger's document.
- A profile is created at sign-in when none exists. The shape makes name,
  initials and username mandatory, so it is written with placeholders that read
  as instructions -- never a plausible human name, never anything derived from
  the opaque identity.

Nothing succeeds in silence any more
`joinEvent` used to return without writing and without throwing when it could
not attribute the participation, while the screen announced success. It rejects
now, and the confirmation follows the write. Withdrawal likewise -- the doctrine
requires it to be authoritative. The host notification stops being written into
the joiner's own store, where its recipient could never read it, and the
optimistic notice shown to the wrong person goes with it.

The creator signs up through the common path: no owner branch anywhere, no
special case, the same deposit and the same derived count.
2026-08-16 13:50:50 +02:00
Sylvain Duchesne 53c0e095cf Code against the polyfill's published contract, and nothing else
The data layer is now reached through one pulled, version-pinned engagement
(`.project/concepts/data-layer/contract_polyfill-surface.md`, @1ecf511e9d).
That copy is the only reference: the provider's sources are never opened, and
what the contract does not answer is a gap raised with it, never worked around
here.

Surface
- `@ng-eventually/sdk` -> `@ng-eventually/polyfill`, one entry point.
- `configure` loses `getSession`, `normalizeId`, `currentUser`; the session
  belongs to the package and its own `init` captures it.
- Placement is named by scope alone -- a session is one user, so the app no
  longer passes an identity it had no way to obtain. This removes a constant
  that made every user collide on one owner's document.
- `init(...)` then `await ensureIdentity()`, in that order, as one sequence:
  React runs child effects first, so the two calls sat in the wrong order and
  the contract now makes that throw.
- `sessionId` relayed as `string | number`, `materialize` -> `read`.

A rejection means "unknown", never "absent"
Four places treated a caught error as an empty result. The worst wrote a
duplicate participation: an unknown count read as zero defeated the idempotence
guard of `joinEvent`. Also fixed: a per-document count, a silently dropped
notification shown optimistically anyway, and a failed listing that left the
owned-event set empty and disabled the materializer for the whole session.

Shared identity is not a Festipod notion
A browser context is one user. The per-scenario identity plant is deleted at
its source and its five sites; what stays is the deployment's wallet file,
which the contract requires an application to serve.

Documentation
The doctrine no longer describes how the data layer works underneath: five
leaves whose subject was internals are gone, a dozen more are re-founded on the
contract's own words, and two frozen arbitrations about a deleted screen were
removed rather than left to mislead a future session.

Test harness
It can sign in at last: cucumber runs under node, which does not load `.env`,
so the harness never received the wallet material and every scenario silently
fell back to an empty local mode. A failed sign-in is now loud on both sides.
The suite also releases what it opens and exits on its own -- runs were still
resident hours after reporting, holding a browser and two servers.

Known red: `@data` cannot be measured. The served wallet accumulates and
nothing resets it; moving the browser profile aside does not, since the data
lives in the wallet file, not the profile.
2026-08-16 12:33:14 +02:00
Sylvain Duchesne c1817607b4 Migrate Festipod onto the rebuilt @ng-eventually/client surface
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.
2026-08-03 13:53:32 +02:00
Sylvain Duchesne 7459d49e83 docs+fix: recadrer le polyfill comme compensateur d'écart, corriger la doctrine périmée
RECADRAGE — la doctrine était trop étroite. rule_app-uses-sdk-surface-only
disait « le polyfill existe pour le WALLET VIRTUEL » : juste sur le fond, mais à
la lettre l'émulation des caps qu'on vient de livrer n'entrait pas dans son
mandat. Nouvelle formulation, portée aussi dans AGENTS.md :

  @ng-eventually/client est un POLYFILL, et ce mot dit toute sa mission :
  compenser l'écart entre le SDK tel qu'il devrait être et ce que NextGraph
  fournit aujourd'hui. Le wallet virtuel en est la plus grosse pièce, pas la
  totalité.

Avec la conséquence opérationnelle : quand quelque chose ne marche pas, la
question n'est jamais « comment contourner dans l'app » mais « qu'est-ce que le
polyfill doit compenser ». Un contournement côté app est une violation même
quand il fonctionne — il grave un état temporaire de NextGraph dans du code qui
doit lui survivre. Et l'ignorance de l'état d'implémentation est durcie :
ENTIÈREMENT, pas « sauf quand ça mord ».

NOUVEAU — data-layer/knowledge_sdk-surface : le contrat SDK cible, écrit dans CE
repo pour qu'un agent n'ait jamais à ouvrir le repo du polyfill. Couvre lectures
réactives, écritures, placement par scope, inbox, discovery, capabilities
(capFor/shareCap/publishRepoLink, livrées avec P1a), identité, sûreté SPARQL —
et les surfaces exportées mais interdites à l'app.

DOCTRINE PÉRIMÉE corrigée, après vérification dans le code :
- rule_document-per-entity décrivait la lecture via readEntities/readUnion/
  registerDoc/bumpRead : ZÉRO site d'appel, readEntities.ts supprimé. Réécrite
  sur watchShape/useShapeQuery. Le fond (un document par entité) est intact.
- brief_2026-07-06 §P3 réaffirmait une phrase que son propre encadré déclare
  fausse : rétractée explicitement.
- knowledge_data-modes citait useShapeWithDefaults(), qui n'existe nulle part.
- ConnectScreen : les fiches avaient raison mais étaient vagues — l'écran existe,
  est routé et monté, et est bien absent du registre. Précisé.

FIX CODE — build:orm était CASSÉ : il pointait ./src/shapes/, qui n'existe pas
(les shapes vivent sous src/shared/shapes/), et sortait en erreur. Donc la
commande que la doctrine prescrit après tout changement de .shex ne marchait
pas. Corrigé et vérifié : exit 0. La fiche avait raison, c'est le code qui était
faux — le point 4 approuvé, simplement situé dans l'autre fichier.

Régénération NON embarquée : lancer build:orm reformate les bindings et retire
l'annotation `: Schema`. C'est une montée de version d'outil, pas une correction
de contenu — elle mérite son propre changement validé, pas un passage clandestin.
Noté dans la fiche.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GbGgNEHRejVKoREvFuDFg
2026-07-28 16:49:27 +02:00
Sylvain Duchesne b6a6b14fad docs(concept): passer les 59 fiches de doctrine en anglais
Convention du projet pour la documentation projet. Traduction fidèle, sans
changement de fond : mêmes fiches, mêmes sections, mêmes liens. Le lint est
identique à la baseline (59 leaves, 0 nouveau lien cassé, wikilinks bit-à-bit
inchangés) et aucun `.feature` n'a été touché.

Le `summary:` du frontmatter est traduit lui aussi — c'est ce que le hook affiche
dans l'index, il porte autant que le corps.

RESTENT EN FRANÇAIS, délibérément :
- les fichiers .feature (convention explicite du projet : Etant donné/Quand/Alors)
  et le bloc Gherkin cité dans brief_2026-07-06 ;
- les libellés d'interface cités en prose (« Entrer », « ✓ Je participe »,
  « Voir tous les participants », « participant inconnu »…) : ce sont des chaînes
  réelles de l'app, pas de la prose ;
- les noms de scénarios BDD ;
- les `triggers.keywords` des _overview : jetons de matching du hook, et la
  conversation reste en français — les traduire aurait cassé la livraison.

EFFET SECONDAIRE UTILE : relire intégralement a fait remonter des contradictions
et des péremptions que personne ne voyait section par section. Notées, non
corrigées (hors périmètre de la traduction) :
- rule_document-per-entity décrit la lecture via readEntities/readUnion/
  registerDoc/bumpRead, que rule_app-uses-sdk-surface-only déclare SUPPRIMÉS au
  profit de watchShape/useShapeQuery. Une règle qui décrit des APIs retirées est
  activement trompeuse — à traiter en priorité.
- brief_2026-07-06 §P3 réaffirme « prouvé par l'e2e D.2, sans reload » juste
  après l'encadré qui déclare cette phrase fausse et sur-cadrée.
- knowledge_data-modes cite useShapeWithDefaults() là où useShapeQuery est
  documenté ailleurs.
- knowledge_stack-and-commands : build:orm pointe ./src/shapes/* alors que les
  shapes vivent sous src/shared/shapes/.
- knowledge_screens / knowledge_routing : ConnectScreen décrit comme absent du
  registre mais présent en route.
- brief_2026-05-18 : « identifié si connecté » était ambigu en français (session
  ouverte vs est une connexion) ; l'anglais a forcé à trancher — rendu par
  « if a connection », à confirmer côté produit.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GbGgNEHRejVKoREvFuDFg
2026-07-28 16:27:24 +02:00
Sylvain Duchesne 96e28a702f docs(concept): solder la doc-debt des 6 concepts
Dette accumulée depuis le 13/07 (27 marqueurs). Au-delà du vidage, trois
corrections de doctrine réellement fausse — c'est ce que le reconcile devait
attraper :

- app-security : `sharedWallet.ts` capture le mot de passe à l'ÉVALUATION du
  module. Tant qu'un repli existait, un global posé trop tard ne faisait que
  dégrader ; depuis que le wallet partagé est l'unique mode, il rend la barrière
  INUTILISABLE (écran d'erreur, aucun champ). Conséquence non anticipée de la
  décision shared-wallet-only → nouveau caveat.
- bdd-testing : la doctrine rendait des tests faux-verts. `ctx.newPage()` sur le
  profil persistant relit l'IndexedDB local et ne prouve JAMAIS la durabilité
  broker ; seul un contexte partagé neuf tranche. Un agent suivant la doctrine
  écrivait un test qui passe sans rien vérifier → nouveau caveat.
- app-architecture : `knowledge_routing` décrivait encore une route `/login`
  disparue, et `knowledge_screen-pattern` citait `LoginScreen` qui n'existe
  plus. Nouveau caveat sur les deux espaces d'id vus depuis un écran.

Aussi : data-layer/knowledge_context-internals décrit la jointure
participation→profil et corrige un mécanisme de changement d'identité périmé ;
tech-stack raccroche la table des scripts au vrai point d'entrée cucumber ;
functional-domain note qu'« implémenté » ≠ « durable ».

Trois marqueurs soldés comme sans objet : ils visaient
`reconnexion-socket-mort.{feature,steps.ts}`, absents de l'arbre ET de tout
l'historique — expérience abandonnée avant tout commit. Ce qu'elle devait
établir est capturé ailleurs (caveat de durabilité, post-mortem polyfill, fiche
INBOX socket-death).

Liens morts vers une décision disparue avec le concept `nextgraph-platform`
réparés. Reste au lint : le brief 07-06 (superseded) porte des file:line et des
références aux internes NextGraph — laissé intact, il décrit l'Option-B encore
implémentée et se dissoudra à la graduation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GbGgNEHRejVKoREvFuDFg
2026-07-27 14:43:09 +02:00
Sylvain Duchesne c5e627c5fc fix(participants): joindre participation→profil à travers les deux espaces d'id
Une Participation stocke son user comme principal stable
`urn:festipod:user:<clé>`, alors qu'un UserProfile a pour `id` son NURI
`did🆖`. La jointure brute `partUserIds.includes(u.id)` ne matchait donc
jamais en mode connecté → chaque participant s'affichait « inconnu ».

- `resolveParticipantUser` : match direct (espace seed demo) puis, à défaut,
  match sur `normalizeIdentifier(username)` après retrait du préfixe principal.
- `USER_PRINCIPAL_PREFIX` : source unique du préfixe, partagée par l'écriture
  (`currentUserId`) et la lecture, pour qu'elles ne divergent pas.
- EventDetailScreen : filtrer soi-même sur `currentUser?.id` (id de profil,
  même espace que `p.id`) et non sur `currentUserId` (principal).

Aussi : épingle `packageManager` pnpm (l'install passe par pnpm, cf.
rule_bun-first) — le runtime/test/build restent Bun.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GbGgNEHRejVKoREvFuDFg
2026-07-27 11:30:00 +02:00
Sylvain Duchesne a21d9b0735 docs(concept): durabilité écriture↔déconnexion, décision wallet-partagé-unique, rule_bun-first (install pnpm)
caveat_write-durability-across-disconnect + decision_2026-07-20 (wallet partagé = seul mode ; identifiant ≠ username profil) + amendement bun-first. Marqueurs _debt.md inclus (voyagent avec la branche, à réconcilier avant push).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014GbGgNEHRejVKoREvFuDFg
2026-07-20 13:16:04 +02:00
Sylvain Duchesne e951eaaf96 feat(auth)+refactor(app): identifier at the access barrier; adopt the lib fidelity refactor
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>
2026-07-06 14:52:40 +02:00
Sylvain Duchesne 8bb19b687b feat(data): union read model — list via anchorless sparql_query, hang eliminated
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>
2026-07-05 20:49:01 +02:00
Sylvain Duchesne bc3d270bd4 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>
2026-07-04 09:58:52 +02:00
Sylvain Duchesne 337a1e000d feat(data): activate isolation — declare identity + connections to the SDK
Festipod performs the domain acts that make isolation real: AccountContext
declares the current identity at login/change; FestipodDataContext declares its
connections (friendships) to the data SDK. Reads then discriminate by scope
through the SDK (private→owner, protected→owner+connections, public→all) — no
app-side filtering, no store ids, no awareness that isolation is emulated. New
@data scenario proves an unconnected account can't read another's protected
entity but can after connecting; public stays visible. @data 21/21.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 23:59:03 +02:00
Sylvain Duchesne 619b94ac0e refactor(data): route entities by scope via the SDK — no store ids in the app
Festipod now treats @ng-eventually/client as a finished NextGraph SDK: the app
decides only each entity's logical scope (events/PdR public, profiles/
participations protected, settings private) and calls the lib by scope. The old
mono-store default and the FESTIPOD_MULTISTORE path collapse into ONE scope path.

Removed every physical-store leak from the app data-plane (ngGraph, registration,
FestipodDataContext, NextGraphContext, useShapeWithDefaults): no more
did🆖${store_id} construction. The session is handed to the lib only at the
sanctioned injection point (ngSession/configureStoreRegistry). Product behavior
unchanged. @data 20/20; build + tsc clean.

(_debt.md included; the T03.e doctrine pass settles accumulated doc-debt.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 23:42:03 +02:00
Sylvain Duchesne 0294e3992f docs(concepts): migrate project docs into 7 concepts + code-grounded audit
Migrate .project/{knowledge,decisions,briefs} and the always-loaded
AGENTS.md/CLAUDE.md into the in-repo `concept` system (hook-delivered,
typed leaves). Then audit the actual code to verify the migrated doctrine
and capture knowledge that lived only in the source.

Concepts (53 leaves):
- functional-domain — produit : point de rencontre greffé, acteurs, déduplication
- app-architecture — modules, invariant d'imports, routing, écrans, styling-system,
  screen-pattern, cookbook d'ajout d'écran
- tech-stack — Bun-first, APIs, build pipeline, deployment (Dockerfile), commandes
- data-layer — NextGraph mono-store, shapes, modes, règles + caveats (suppression,
  champs non persistés, internals du contexte)
- bdd-testing — Cucumber multi-couches, contrat de couches, harness, cookbook
- app-security — posture actuelle (mono-store, confiance broker), auth wallet,
  brief matrice d'autorisations cible
- nextgraph-platform — NextGraph système externe + briefs (multi-store, shim, fork)

Audit corrections:
- décision SPARQL-delete annulée (superseded) → caveat (le code utilise ngSet.delete,
  persistance possiblement partielle)
- divergences relevées : routing path-based (pas hash), thème moderne sous components/sketchy,
  ConnectScreen hors registre, build:orm au chemin périmé, champs d'event perdus en connecté

Strip migrated sources; AGENTS.md/CLAUDE.md réduits au cœur (but, invariants,
carte des concepts) + pointeurs.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-15 14:58:44 +02:00