9e4374b6780710e5afe5f63fe7d6d53468d53db5
91 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
9e4374b678 |
Persist the dates the form always collected and always threw away
The creation form has always offered real date and time pickers, and the write kept only the label derived from them. The user chose a date; the broker never saw it. Five fields went that way, on create and on update alike, because the shape defined nowhere for them to land. `startDate`, `endDate`, `startTime` and `endTime` are now on the Event shape and carried end to end. `date` is untouched -- it is the label the lists display, and nothing derives one from the other. `themes` is deliberately left out: it is a repeated value, so it needs a cardinality decision rather than one more optional string, and nothing consumes it. Proven on the running application, both paths, asserted only after a reconnect because the optimistic overlay shows the values before anything is durable: written 2026-12-05 / 09:15, read back identical; edited to 2027-01-22 / 08:05, read back identical. An event missing the fields renders clean -- the time line simply absent, nothing thrown. The recorded generator drift no longer reproduces. Regenerating from the UNCHANGED shape produced bytes identical to the committed bindings, which is how the additive diff was separated from the generator's own; that separation was the point of running it twice, and it turned a warning into a measurement. The doctrine still carries the old warning and is now wrong about it. Also worth having: the form composes its label from the ISO values joined, not from the French wording the doctrine gives as its example. |
||
|
|
32c2302c91 |
Deployed, and it signs people in
First deployment carrying the injected wallet and the external data layer. A user signs in and sees their own data. That settles the one link in the chain nothing here had ever exercised: a deployed origin CAN be embedded in the hosted broker's iframe. The identity flow runs the application inside that iframe, so data reaching a signed-in user is proof the embedding works for this origin. It is settled by the deployment, not by a test, and the doctrine now says so: no scenario covers it, so a change of origin, of proxy, or of the broker's embedding policy would be found by a person rather than by the suite. Worth stating plainly what this deployment is: the first one able to sign anybody in at all. The production bundle never asked for its runtime configuration under NODE_ENV=production, so it had neither password nor wallet to hand the data layer, and the failure was invisible from outside — the server answered, the page rendered, and nothing was wrong except that nobody could get in. |
||
|
|
95479ebe77 |
The image builds, serves the injected wallet, and asks for its configuration
Built and run for the first time in this shape. `pnpm install --frozen-lockfile` fetches the external data layer from Gitea inside the install stage and succeeds -- the step that refused an hour ago. Run with only the password and the base64 wallet set, and no path variable, the container serves 810 bytes at /shared-wallet.ngw with the same sha256 as the original file. The check that mattered most: the production bundle contains exactly one call to /festipod-config.json. That call being absent under NODE_ENV=production is what made the deployed application unable to sign anybody in, and its absence is invisible from outside -- the server answers, the page renders, the endpoint works, and nothing asks it anything. Two doctrine claims the build disproved, corrected rather than left standing: The `bun` peer shim the deployment notes describe as a live hazard does not appear at all in a built image -- `bun` is absent from node_modules/.bin and resolves to the base image's binary. The `onlyBuiltDependencies` approval is currently inert. Kept, since it costs nothing and the shape can return, but the paragraph now says dormant instead of describing a mechanism that is not running. Asset paths are emitted as `/../chunk-*.js`. Browsers normalise that at the root and the existing deployment already passes it through its proxy, so it works -- but a proxy that treats `..` segments differently would break asset loading, and the symptom would be a blank page with 404s on chunks, naming nothing. |
||
|
|
c3d64555d9 |
The data layer is a real external dependency, pinned to a commit
`package.json` named `#main`, where the renamed `packages/polyfill` does not exist, and `pnpm-lock.yaml` still named the package as it was called in July. `pnpm install --frozen-lockfile` -- the Dockerfile's install step -- refused on exactly that mismatch, so the image could not build at all. Both now name the commit the branch was pushed at, and the frozen install passes. The dependency is now genuinely external: installed from git into pnpm's store, with no local overlay anywhere in the resolution. The invariant that justified the overlay in the first place holds -- `@ng-org/web` resolves to the same realpath from the application and from the installed package, so one instance and one verifier. A git-installed package does not carry its own `@ng-org`, where a symlink to the checkout would have. `.project/contracts.yaml` names the same commit as the dependency, so the surface the app is coded against and the package it installs designate one state, and a drift between them becomes visible instead of silent. A tag will replace the commit once the provider settles a convention; the invariant is that the two agree, whatever the ref. Deployment doctrine now carries what a deployer needs and did not have: the pinning discipline, that the frozen install only verifies, the wallet arriving through the environment with its one-directional precedence and its loud failure on a malformed value, that neither the wallet nor its password is a secret, that tailwindcss is a devDependency the server needs at serve time, and that a deployed origin's embedding in the broker's iframe is covered by no test. Nothing has been deployed in this shape yet, and the doctrine says so. |
||
|
|
ac29735d20 |
A deployment can carry the wallet in its environment, not on a mounted disk
The contract requires the application to serve a wallet file and hand its URL and password to the data layer. Until now the only source was a filesystem path, which no container has: `*.ngw` is gitignored, `COPY . .` brings none, and nothing mounts one -- so a deployed instance served 404 where the contract expects bytes. `FESTIPOD_SHARED_WALLET_FILE_BASE64` carries the file itself. It is 810 bytes, and it is not a secret: by design the application hands it to every user who opens the app, so provisioning persistent storage would be guarding something public. One channel for both values, nothing to mount, and a new host needs only its variables. Precedence is deliberate and one-directional: the path always wins when set, and an unreadable path does NOT fall through to the base64 form. A deployment sets exactly one; both set is a leftover, not a fallback chain. Local development and the test harness only ever set the path, so they are untouched. A malformed value answers 500 and names the variable. Answering 404 would have made "configured wrong" indistinguishable from "not configured at all" -- the confusion this codebase has spent two days removing. Proven by serving the same wallet from each source in turn and comparing: identical size, identical sha256. The first attempt at that proof silently exercised the path branch, because Bun auto-loads `.env` and the variable was already there; the checksums matched for the wrong reason. Caught, cleared, and measured again. |
||
|
|
e780c5246c |
The count defect was two things; only the one that is not ours is left
`bug_participant-count-stays-at-zero` described a failure that no longer exists and an open question that turned out to be the same failure seen from the other side. It becomes `caveat_participant-count-one-connection-lag`, and the type change is the point: what remains is a bounded delay whose cause sits entirely outside the application, not a defect this repo can act on. Gone from it: the multi-inbox race, "never converges", and the withdrawal asymmetry recorded as unexplained. That asymmetry WAS the race — with one inbox per document both paths now share the identical one-connection lag, so nothing is left unaccounted for. What it says now: the count needs one connection more than the write that produced it, because the layer does not notify you of your own actions — a deposit into an inbox you watch raises no push, and a write to your own document is not re-read in the writing session. Both measured, both raised with the provider, and neither compensated here: a retry or a poll is precisely what the doctrine forbids. Repaired alongside: the leaves that still described DELETE-then-INSERT, the materializer's old shape, and the probe cookbook's tally of open defects. The two new primitives are recorded where the shared utilities are listed, marked unit-tested. |
||
|
|
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. |
||
|
|
ff26f26e60 |
Close the fixed blocker, and record what withdrawal converging faster does NOT mean
Six variations against the real application, one thing varied at a time: the sign-up-breaks-the-next-connection defect is gone. New identity, immediate reload or delayed; an identity already used for several cycles; and the very identity poisoned by the pre-fix code, which now reconnects cleanly three times out of three. So nothing needed healing -- the fix does not merely stop writing the bad state, it makes what was already written harmless. The leaf is deleted rather than graduated: its reproduction method, the stale-server trap it led to, and "a provider gap is not worked around here" all live elsewhere already. The count defect stays open, with its shape now known: it does converge across connections, but takes one more than expected -- the first reconnect still shows zero, the second shows the value. And a correction worth keeping. The explanation offered for why withdrawal converges in a single connection -- that it writes straight to the owner's document instead of depositing -- is contradicted by the code: it deposits its own marker into the same inbox, drained by the same owner routine, "symmetric" by the code's own comment. The asymmetry is real and recorded as verified; its cause is recorded as unknown rather than filled with a plausible story. Same mechanism, same inbox, two connections against one, is a lead worth having honestly. The dev-server caveat is upgraded from inferred to verified, and it is worse than it read: an edit to application source triggers a genuine rebuild, new bundle hash and all, and that rebuild still carries the stale dependency. Only a restart works, and "touch a file to force a rebuild" is now explicitly ruled out as a substitute. |
||
|
|
ac55dc96a4 |
A refreshed data-layer package never reaches a running dev server
`link:polyfill` overlays the package into node_modules and keeps it current. That is all it does. A `bun run dev` already running goes on serving what it loaded at startup, however many times the overlay is rewritten underneath it: node_modules is excluded by file watchers, so the change lands in the one place nothing is looking. The script promised the opposite -- that `bun --hot` would reload the copied file live. That promise is now removed, and replaced by the instruction to restart. It cost about an hour. A defect had been fixed upstream, the overlay refreshed, and a probe on a freshly launched server confirmed the fix 3 runs out of 3. The same sequence by hand reproduced the defect at once. The two observations looked irreconcilable and the hunt went to the wallet, to prior state, to timing. The dev server had been up for six days. It predated the package rename and the whole migration; the browser was running a different application from the one under test. Nothing warned: a stale server looks exactly like a current one, and the script's own header ruled out the true cause for anyone who trusted it. The reflex is written down with the leaf: when a fix seems not to take, or when a hand-run and an automated run disagree, check how long the server has been up before anything else. One command, cheapest hypothesis first. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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.
|
||
|
|
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.
|
||
|
|
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. |
||
|
|
47af46fd09 |
Probe a protected ENTITY, not the protected store, in the connections scenario
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.
|
||
|
|
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. |
||
|
|
05ee576d7d |
refactor(comments): retirer les raisonnements sur l'état de NextGraph du code app
Application de la règle qu'on vient de durcir : l'app IGNORE entièrement l'état d'implémentation de NextGraph. Le CODE était légitime — `inbox.readSynced` est une surface SDK exportée ; ce sont les JUSTIFICATIONS qui fautaient, en expliquant les choix par des internes du cœur. Réécrit en termes de CONTRAT : - registration.ts / FestipodDataContext : « barrier-gated read, le repo d'inbox n'est pas encore ouvert dans le verifier, un read ancré renverrait 0 » devient « `read` rend ce qui est connu localement maintenant, `readSynced` rend une fois les dépôts synchronisés visibles ; ce site a besoin du second parce qu'il lit depuis une session froide ». - ngBootstrap : « le verifier sérialise les créations » devient « `docCreate` est un aller-retour qui ne recouvre pas le suivant, donc le coût du seed croît LINÉAIREMENT avec le nombre de documents ». Le ~2s mesuré est conservé, mais explicitement comme une observation, pas comme un contrat. - entityWrites : description de lecture périmée (ORM fan-out, ngSet couplé au scope) remplacée par la vue réactive. La distinction read/readSynced vit désormais là où elle est légitime : knowledge_sdk-surface, avec le critère de choix (`read` dans une session qui observe déjà l'inbox, `readSynced` dès que la justesse dépend d'une session froide voyant le dépôt d'une autre identité). knowledge_context-internals cesse d'expliquer le fix par `ensureRepoOpen`/premier `State` et pointe le contrat. Laissé tel quel : `src/shared/support/hooks.ts` et les steps e2e — le harness de test connaît légitimement la plomberie ; la règle vise l'app. Et le « no cross-account fan-out » de FestipodDataContext, qui décrit le périmètre de l'app et non NextGraph. tsc : 0 erreur. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014GbGgNEHRejVKoREvFuDFg |
||
|
|
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 |
||
|
|
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 |
||
|
|
3ec3b37a65 |
docs(concept): poser le principe de lecture publique non récursive
Remplace le point « validation d'existence impossible » — qui traînait une préoccupation de forgerie hors périmètre — par le principe qui fait réellement tenir le modèle : Un élément du store public est public : qui a l'URL lit le contenu. Mais PAS récursivement — un contenu public peut référencer du contenu privé. C'est exactement notre cas. Le créateur lit la Participation (publique) et ne peut pas suivre la référence vers le profil (protected). Lecture par le créateur ET anonymat vis-à-vis de lui, sans aucun mécanisme supplémentaire. Le compteur est Set.size. Rien d'autre à en dire. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014GbGgNEHRejVKoREvFuDFg |
||
|
|
e2adfacb0b |
docs(concept): la validation d'existence est IMPOSSIBLE, pas optionnelle
Le brief inscriptions disait que le créateur pouvait vérifier qu'un did pointe sur un objet réel sans détenir la clé, et rangeait ça en durcissement optionnel. Faux : le contrôle d'accès en lecture laisse bien passer, mais l'ADRESSAGE présuppose le cap — aucune commande d'existence au niveau SDK, et une référence cap-less n'a ni les identifiants de blocs ni l'overlay nécessaires. Conséquence assumée, écrite noir sur blanc : le créateur ajoute la référence SUR PAROLE, donc le compteur est déclaratif et forgeable. Hors périmètre sécurité, mais cette étape ne doit pas être présentée comme une validation. Le modèle lui-même n'en dépendait pas — il était déjà noté comme non requis. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014GbGgNEHRejVKoREvFuDFg |
||
|
|
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
|
||
|
|
a8401bd143 |
docs(concept): inscriptions — Participation lisible + drapeau active, purge par le créateur
Affinement PO du 2026-07-27. La Participation devient LISIBLE par tous et se réduit à trois choses : référence à l'événement, booléen `active`, did cap-less vers le profil protected du participant. Pas de description pour l'instant. Ce que ça débloque : une suppression n'est pas détectable sans la clé (vérifié), ce qui imposait un nudge forgeable pour la désinscription. Un objet lisible avec un drapeau change la nature du problème — l'annulation n'est plus à DÉTECTER, elle est à LIRE. Le blocage disparaît au lieu d'être contourné. Le principe qui tient l'ensemble : la vérité est dans l'objet que le participant contrôle, tout message n'est qu'un indice. Un faux « purge X » conduit le créateur à lire X, la voir active, et ne rien faire. La forgerie devient structurellement inoffensive — d'où l'absence de besoin de signer les dépôts d'inbox, ce qui tombe bien : NextGraph ne l'offre pas (inbox non authentifiée, vérification de signature non implémentée et exigeant de déchiffrer). Le pointeur d'identité vise le profil protected existant, pas un second document par participation : les connexions en détiennent déjà le cap. Ajouter une connexion ne réécrit donc rien — on scelle une fois, durablement. Un champ chiffré dans la Participation aurait exigé de re-sceller à N destinataires et de réécrire à chaque nouvelle connexion (et n'est pas un primitif NextGraph : la granularité de chiffrement est le document, en tout-ou-rien). Arbitrages assumés : pas de filtrage à la lecture (Set.size est une borne haute, exacte après purge — obsolescence acceptée pour garder la lecture en O(1)) ; la purge incombe au créateur ; pas de description. Point ouvert noté : Participation passe en scope public alors que la doctrine produit la place en protected. Ce leaf décrit l'implémenté — à mettre à jour à la graduation du brief, pas avant. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014GbGgNEHRejVKoREvFuDFg |
||
|
|
ab077d8080 |
docs(concept): réserve durable sur le pseudonyme permanent de l'overlay
app-security/caveat_stable-overlay-pseudonym (nouveau) — toute référence cap-less vers un document protected expose le `✌️` du store, identique partout et pour toujours. BLAKE3 non inversible le rend OPAQUE, d'où la tentation de le croire INOFFENSIF : ce sont deux choses différentes. C'est la constance qui expose, pas la lisibilité. Un seul recoupement, une seule fois, et tout l'historique bascule — y compris ce qui a été publié des années plus tôt. Aucune porte de sortie, vérifié sur quatre axes : pas de rotation d'overlay, store id généré une fois pour toutes, aucune migration de contenu, aucune forme de référence n'évitant d'exposer l'overlay. Le renouvellement de capabilities ne toucherait que l'inner ; l'outer y survit. Placé en app-security et non dans le brief inscriptions : un brief se dissout à sa graduation, la réserve doit lui survivre. Le brief n'en garde qu'un résumé et pointe dessus. Déclencheurs élargis (anonymat, pseudonyme, traçage, corrélation, overlay, cap-less) pour qu'elle remonte quand on s'apprête à concevoir de l'« anonyme ». Consigne pratique qui en découle : ne jamais présenter une action comme « anonyme » si elle fait circuler une référence cap-less — c'est pseudonyme, et le pseudonyme est permanent. Dédup par `✌️` validée par le PO ; le brief le note. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014GbGgNEHRejVKoREvFuDFg |
||
|
|
42dbfd0c34 |
docs(concept): modèle d'inscription recalé + l'inbox NextGraph suit aussi les manques
Le brief inscriptions est réécrit sur le modèle reposé par le PO : tout est clés et URLs, sans notion d'appartenance. Le participant crée une Participation chiffrée, dépose son did (URI sans ReadCap) dans l'inbox de l'événement ; le créateur traite l'inbox automatiquement, déduplique sans pouvoir lire, et range la référence dans un Set porté par l'événement ; compteur = Set.size ; seules les connexions détiennent la clé et reconnaissent la personne. La dédup s'appuie sur un fait vérifié dans nextgraph-rs : l'overlay (segment `✌️` d'un NURI) est STORE-scopé, jamais document-scopé. Deux Participations d'une même personne portent donc le même `✌️`. Contrepartie actée dans le brief : ce `✌️` est un pseudonyme stable et permanent — c'est le MÊME bit d'information qui permet de dédupliquer sans lire et de tracer d'un événement à l'autre ; on ne peut pas garder l'un sans l'autre. Retiré du brief : le trilemme et la piste de dédup par vérification de signature. Ils reposaient sur une notion de membership importée de l'état courant du source Rust, où elle est un échafaudage inerte — erreur de méthode désormais consignée en règle. Règles : - rule_capture-nextgraph-findings (nouvelle) — toute connaissance établie sur le fonctionnement réel de NextGraph se consigne AU MOMENT de la découverte dans la doc du polyfill ; distinguer VÉRIFIÉ d'INFÉRÉ ; ne jamais déduire la forme cible de l'état courant du source. - rule_file-nextgraph-bugs → rule_nextgraph-inbox — l'inbox reçoit désormais DEUX familles : les dysfonctionnements ET les manques dont on a besoin. Une fiche de manque dit ce que le polyfill émule en attendant et ce qu'il faudra en RETIRER quand ça atterrit en amont : l'inbox devient un suivi de l'avancement de NextGraph, pas un simple bug-tracker. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014GbGgNEHRejVKoREvFuDFg |
||
|
|
5b536ff981 |
docs(concept): brief inscriptions par Set + règle de report des bugs NextGraph
- data-layer/brief_2026-07-20_attendance-set-model : réaligner les inscriptions sur la vision initiale — objet participation auto-possédé (la vérité) + Set curé de références cap-less sur l'événement + cap scellé aux seules connexions ⇒ compteur = `Set.size`, présence anonyme par défaut, personne ne désinscrit autrui. Inclut la revue adverse (trilemme anonyme/dédup/inviolable) et les verdicts du spike P0 vérifiés dans `nextgraph-rs` : fetch d'existence sans clé = OUI, détection de suppression sans clé = NON (⇒ la désinscription passe par un nudge), confidentialité = OUI. Statut : direction cible, PAS un pivot immédiat. - data-layer/rule_file-nextgraph-bugs : tout dysfonctionnement NextGraph identifié donne lieu à une fiche dans `../../nextgraph/orm-tests/INBOX/`. - to-discuss : alignement ReadCap/WriteCap, terminologie identité NextGraph. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014GbGgNEHRejVKoREvFuDFg |
||
|
|
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 |
||
|
|
7e65a83d42 |
fix(test): cucumber via l'entrée JS réelle (@cucumber/cucumber/bin) — pnpm casse le shim .bin
pnpm installe node_modules/.bin/cucumber-js comme shim shell (pas du JS) → 'node --import tsx/esm node_modules/.bin/cucumber-js' échoue. Pointer sur l'entrée JS réelle du paquet. Répare test:data et cucumber:run. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014GbGgNEHRejVKoREvFuDFg |
||
|
|
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 |
||
|
|
f6fc3c262e |
chore: install via pnpm + polyfill depuis Gitea (git-https) + lien local réactif
Passe l'installation de bun à pnpm (runtime/build/test restent bun). Dépendance prod @ng-eventually/client résolue depuis le Gitea public en git+https (committée, reproductible via pnpm-lock.yaml). Script link:polyfill (S2 copie-overlay + watcher) pour un lien local réactif préservant l'instance @ng-org unique. bun (peer de bun-plugin-tailwind) approuvé au build (pnpm.onlyBuiltDependencies) pour que node_modules/.bin/bun soit un vrai binaire. Dockerfile: install pnpm avec git + node dans l'image, runtime bun inchangé. Doctrine tech-stack (deployment, stack-and-commands) mise à jour. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014GbGgNEHRejVKoREvFuDFg |
||
|
|
82004a30b0 |
fix(data): participantCount fiable à la connexion du propriétaire + source unique
Bug: user2 crée un événement, user1 s'inscrit et voit "1", mais user2 (créateur) reste à 0. Recadrage (spec existante): l'exigence est "le propriétaire traite son inbox à sa PROCHAINE CONNEXION", pas une notif live temps-réel. Cause: le owner-materializer lisait l'inbox AVANT sa synchronisation → active=0 → écrit 0 → mémoïse 0 → ne retraite plus. Fix: - Lecture inbox gated sur barrière: inbox.readSynced (ensureRepoOpen attend le 1er State, puis read — comme discovery.readIndex) au lieu de inbox.read. Un dépôt déjà synchronisé EST vu à la connexion. Pas de polling. - Materializer déclenché directement à la connexion ([ready, ownedKey]). - materializedCountRef ne verrouille plus un 0 prématuré (rôle = anti-boucle seul). - Source UNIQUE du nombre = event.participantCount: le littéral participantCount:1 de CreateEventScreen retiré (démarre à 0), l'affichage ne calcule plus de nombre local (ParticipantsListScreen). Le statut "Je participe" optimiste est intact. - Logs [Attendance] sur tout le chemin dépôt→matérialisation→écriture. Test: e2e-multibrowser "converge à la prochaine connexion" reframé + dé-@wip, ROUGE avant / VERT après sur profil frais. Non-régression @multibrowser 4/4, @data 7/7. Doctrine: knowledge_context-internals (caveat BUG ACTIF → CORRIGÉ), brief_2026-07-06 (cadrage "sans reload" = sur-cadrage; exigence = fiable à la connexion). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
0958d70132 |
docs(tech-stack): caveat Firefox 151+ LNA bloque l'iframe app du broker en dev
Piège coûteux : iframe blanche + zéro log app + aucune erreur = pas un bug Festipod, c'est Local Network Access de Firefox qui bloque le broker public d'embarquer l'app locale. Fix navigateur (network.lna.enabled=false). HTTPS n'y change rien ; le top-level charge quand même ; le smoke ne peut pas l'attraper. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
c0fd69344b |
feat(data): auto-seed opt-in (FESTIPOD_AUTO_SEED) + logs data lisibles; diag bug participantCount
Seed: l'auto-seed sur wallet vide est désormais OPT-IN, OFF par défaut — ne se déclenche que si FESTIPOD_AUTO_SEED=1 (livré en dev via /festipod-config.json + define build.ts, comme le shared-wallet). Le seed répété bloatait le wallet (lenteurs de lecture). Seed explicite (loadTestData, tests @data) inchangé. Logs: chaque useShapeQuery logge à la réception du set le nombre d'objets + le type + des compteurs globaux cumulés : [FestipodData] set reçu: 9 objets Event (public) en 1234ms [FestipodData] totaux — Event: 9, Participation: 3, UserProfile: 10 (5 sets) (polyfill docs.ts: "N rows" -> "N triple-rows" pour clarifier que ce sont des triplets RDF, pas des objets métier.) Diagnostic bug participantCount (NON corrigé, design-sensible): le propriétaire d'un événement reste à participantCount=0 quand un inscrit d'un AUTRE verifier dépose. Cause: le owner-materializer n'est re-déclenché que par ownedKey, jamais par un push d'inbox — doc_subscribe ne délivre aucun Patch cross-session. Le bloat de wallet MASQUAIT le bug (faux-vert). La théorie "StorageError" était fausse. Scénario réactif @wip = test ROUGE qui documente le bug. Doctrine: knowledge_context-internals (caveat BUG ACTIF + auto-seed opt-in), brief_2026-07-06 (claim D.2 "prouvé vert" REFUTÉ), build-pipeline (nouvelle var). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
39b67feea0 |
feat(ui): spinner global près du titre Festipod + log du délai des requêtes
Chaque useShapeQuery s'enregistre dans un store module-level pendingQueries au début de son cycle et se résout à son premier résultat (isPending→isSuccess|isError, équivalent readPromise). HomeScreen affiche un Spinner à côté du titre "Festipod" tant qu'au moins une requête est en attente ; il ne s'arrête que quand TOUTES ont reçu leur premier résultat. Toute future useShapeQuery y contribue automatiquement. À la 1re résolution, chaque cycle logge son délai : [FestipodData] <shape>/<scope> premier résultat en <N>ms (n=<len>) → le délai d'obtention des événements (Event/public) est visible nommément. Store idempotent (Set d'ids, sûr sous StrictMode) ; cycleId mémoïsé sur [shapeKey, scope] → re-begin sur switch d'identité, cleanup résout au démontage (spinner jamais bloqué). Spinner = Loader2 lucide + @keyframes app-spin dans index.css. Tests: pendingQueries.test.ts (6, dont "off seulement quand toutes résolues"). Doctrine: data-layer/knowledge_context-internals. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
c7e924abe7 |
fix(auth): porter l'identité par param d'URL (?id=), pas localStorage; renommer username→identifier
Cause racine du décalage d'identité : l'app tourne dans DEUX contextes avec DEUX partitions de localStorage — top-level (127.0.0.1:3000 direct, barrière) et iframe (embarquée sous nextgraph.net après le round-trip broker). Le navigateur partitionne le storage par site top-level, donc l'identifiant saisi en top-level n'est jamais celui que l'app connectée lit dans l'iframe (symptôme: deux valeurs divergentes). Fix : le param d'URL ?id= devient la SOURCE DE VÉRITÉ. Le SDK redirige avec encodeURIComponent(window.location.href) (URL app complète, query comprise), donc un param d'URL TRAVERSE la frontière contrairement à localStorage. AuthGate écrit ?id=<identifiant> (replaceState) avant connect(); AccountContext résout par priorité (1) ?id= puis (2) localStorage (préremplissage same-partition seulement). Renommage username→identifier (champ useAccount, normalizeIdentifier, clé festipod.account.identifier) — c'est un id technique d'espace, pas un username. Le username de PROFIL (nom d'affichage) est laissé intact. Test garde-fou @ui (identifiant-resolution.feature) : la priorité param>localStorage, rouge si on l'inverse. Le flux de barrière étant désactivé en @e2e, ces @ui sont la seule couche qui le garde. Doctrine: knowledge_authentication (porteur URL + partition) + knowledge_context-internals (vocab). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
13da2d9e03 |
fix(auth): préremplir l'identifiant à la barrière — plus de re-saisie à l'arrivée
Symptôme (vraie app) : au retour dans Festipod, la barrière redemandait un
identifiant NU et VIDE alors qu'il était déjà choisi/stocké.
Cause racine (pas une perte de localStorage — l'identifiant survit au round-trip) :
au rechargement, AccountProvider restaure `username` depuis le store, mais
NextGraphContext repart en `disconnected`, donc AuthGate réaffiche la barrière ;
et AccessGateScreen initialisait son champ à useState('') → vide malgré le stocké.
Fix : AuthGate passe `initialIdentifier={username}` ; AccessGateScreen préremplit
le champ. L'identifiant est saisi UNE FOIS au premier accès, persisté, puis
prérempli au retour — jamais retapé.
Test garde-fou @ui (barriere-acces-identifiant.feature) : prérempli / vide au
premier accès / Entrer remonte la valeur. Rouge si on remet useState(''). Utile
car le flux de barrière est désactivé en @e2e (__FESTIPOD_ACCESS_GATE_DISABLED__),
donc invisible à cette couche. renderElement() ajouté au harness @ui pour rendre
un composant prop-driven hors registre/providers.
Doctrine: app-security/knowledge_authentication documente la saisie-unique + prérempli.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
7302936502 |
test(e2e): smoke @smoke garde la classe "page blanche une fois connecté"
Boote le VRAI App via le broker (hook Before @e2e existant), navigue vers l'accueil connecté et asserte deux choses fortes : HomeScreen a réellement monté (.app-navbar + bouton "Relayer", absents d'un spinner/bandeau broker) ET aucune erreur runtime (pageerror/console.error) n'a été émise pendant le boot connecté. Le World collecte désormais les pageErrors (réinitialisés par scénario, logging existant préservé). Câblé dans `bun run validate` (run par défaut) via @smoke and not @wip, avec nettoyage Chromium. Preuve: un throw dans HomeScreen fait virer le smoke au rouge; sans lui, vert. Comble le trou qui laissait passer la régression page-blanche (aucune suite n'exécutait @e2e et aucune assertion ne gardait le rendu connecté). Doctrine: bdd-testing/knowledge_e2e-layer documente le smoke @smoke + pageErrors. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
f366ee29a7 |
doctrine(data-layer): context-internals — lecture via watchShape + overlay optimiste + auto-seed sur isSuccess
Rafraîchit les sections périmées : la lecture passe par `useShapeQuery`/`watchShape` (plus readEntities/subscribeDocs/bumpRead/relist) ; visibilité immédiate des mutations par overlay optimiste (plus registerDoc) ; auto-seed gardé sur `isSuccess` (plus le setTimeout 3s qui causait le re-seed à chaque reconnexion). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
38266d96f8 |
refactor(app): l'app lit via watchShape (useShapeQuery), plus de machinerie bespoke
Phase B — FestipodDataContext lit désormais via la surface SDK `watchShape` (binding `useSyncExternalStore` dans `useShapeQuery`) + adaptateurs Fp (`shapeAdapters.ts`), au lieu de sa machinerie maison. Applique rule_app-uses-sdk-surface-only : l'app ne consomme que la surface SDK. Supprimé : `readEntities.ts`, `subscribeDocs`+`bumpRead`+`readTick`+`readDocKey`, le listing manuel (`publicDocs`/`protectedDocs`/`registerDoc` pour la lecture, `readDiscoveredEvents`), et les commentaires raisonnant sur le hang ORM. Gardé découplé : `listMyEntityDocs(owner,'public')` → `ownedEventIds` pour le seul matérialiseur propriétaire. Auto-seed : chronomètre 3 s → gate `isSuccess` (seed uniquement si synchronisé ET vide) — fix du re-seed « First time… » au 3ᵉ connect. Mode démo inchangé. Non-régression VÉRIFIÉE (broker réel, wallet frais) : inscription (1 passed), isolation « identité fraîche ne voit pas » (re-run local, 5 steps passed), compteur dérivé/Q4 (1 passed). tsc propre, build OK. Résiduel PRÉ-EXISTANT (pas causé par ce refactor, vérifié par stash sur baseline) : - reconnexion « relit ses propres données » → RE-@wip : défaut cold-read de l'index de scope PUBLIC côté lib (une page fraîche relit vide) — prochaine cible. - un @AUTH « données pas rechargées » (timing loadFire-and-forget vs step 30 s). Doctrine : rule_app-uses-sdk-surface-only « déviation résolue ». Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
2295af610a |
doctrine+dev: règle « app = surface SDK seule » + access-log par défaut
- rule_app-uses-sdk-surface-only : l'app se comporte comme si NextGraph était fini et sans défaut ; elle lit via `useShape` (scopé wallet virtuel, fourni par le polyfill), jamais via des internes (readModel/subscribeDoc) ni en raisonnant sur un problème NextGraph. Raison d'être du polyfill = le WALLET VIRTUEL (pas le hang ORM, qui n'est qu'un détail interne). Cible : `useShape` polyfill à la forme TanStack useQuery (data + isPending/isSuccess…), en anticipation de la mise à jour prévue de useShape par NextGraph — distingue nativement sync-en-cours de vide. Déviation actuelle notée : readEntities/subscribeDocs/bumpRead côté app. - ngSession : access-log ON par défaut (le toggle opt-in était fragile), opt-out via localStorage festipod.debug.accessLog=0 ; ligne de diagnostic au démarrage. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
4c80ada3de |
doctrine(bdd-testing): remplacer le caveat poll par la règle « ne jamais poller »
L'ancien caveat_poll-broker-reads érigeait à tort le POLLING en pratique de test. Remarque utilisateur : le polling est un anti-pattern dans le contexte NextGraph (par abonnement). Remplacé par rule_no-broker-polling : attendre le push réactif / la barrière du 1er State ; ne JAMAIS re-interroger le broker en boucle. Fallback pragmatique admis : un intervalle court qui OBSERVE l'état réactif déjà mis à jour (pas une re-lecture broker) — au plus près de l'utilisateur qui attend. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
517045c257 |
test(@multibrowser): le compteur converge AUSSI côté inscrit B (Q4) + caveat polling
Q4 — le scénario réactif ne vérifiait la convergence de participantCount que du POV du PROPRIÉTAIRE A. Ajout des assertions symétriques côté INSCRIT B : après que B rejoint, B voit le compteur passer à 1 réactivement (sans reload) ; après désinscription, il revient à 0 côté B. Le doc public mis à jour par A (seul matérialiseur) se propage via le broker jusqu'au doc_subscribe de B. Ferme « A et B ont-ils tous les deux le compteur incrémenté ? » — oui. Vert wallet frais (16 steps). Doctrine : nouveau caveat bdd-testing/caveat_poll-broker-reads — asserter les lectures broker en POLLING borné (lag de sync ~1s), jamais en one-shot ; vaut pour les lectures à froid (reconnexion) et la propagation réactive (compteur cross-navigateur). Consolide la doc-debt des features touchées cette session. Non couvert (suivi) : observateur TIERS (découverte publique, flaky). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> |
||
|
|
65bd67cc20 |
Isolation deux-identités: test permanent + le créateur ne participe plus
Deux corrections produit/tests demandées, empiriquement validées au broker réel.
1. Créateur ≠ hôte (décision produit). Il n'y a PAS de notion d'hôte : un
événement est public, simplement signalé par le créateur, qui n'est PAS
obligé de participer. `createEvent` n'écrit plus de participation-hôte et
`participantCount` démarre à 0 ; le matérialiseur du propriétaire dérive
`participantCount = |inscriptions actives|` (plus de base « +1 hôte »).
2. Isolation deux-identités : le trou réel était l'ABSENCE d'un test de
régression, pas un bug de code actif. Reproduction empirique (DIAG instrumenté,
retiré) : la fuite n'apparaît QUE si le reset `useEffect([username])` est
désactivé ET les caps vides (docs persistés d'une session antérieure sur wallet
gonflé) — le reset en place la neutralise. La sighting live venait d'un état
wallet pré-fix + identifiant réutilisé. Ajout du test permanent manquant :
- isolation-deux-identites.feature (@data) : A crée+rejoint E, une identité
fraîche B sur le même wallet ne voit E ni sur son accueil, ni via
isParticipating(E,B), et ne lit aucune participation portant le principal de A.
- us-13 : « Le créateur ne participe pas automatiquement » (count 0,
isParticipating false autoritatif, puis join→1, leave→0).
Harness: 4 helpers permanents (switchIdentity, currentIdentifier, homeEventTitles,
currentParticipations) pour piloter/observer l'identité en test.
Scénarios @multibrowser/us-7 réalignés (compteur 0→1 au lieu de 1→2).
Doctrine mise à jour (context-internals, actors-and-concepts).
Gates: build OK, tsc propre, @data verts (inscription, désinscription,
idempotence, compteur dérivé, auth ×4), lib @ng-eventually/client non touchée.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
||
|
|
0f164300f0 |
refactor(data): canonical event-id matching for the owner-materializer (defensive)
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> |
||
|
|
cd2a45c254 |
feat(data): participantCount via Option B (deposit + owner materialization)
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> |
||
|
|
e62a17e5a2 |
docs(data-layer): correct the graph-round-trip claim (it was the bloat hang)
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>
|
||
|
|
4e96659bd7 |
feat(data): reactive cross-session reads (doc_subscribe), + real 2-browser e2e
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> |
||
|
|
84bc87d13c |
docs(data-layer): brief update — P1/P2 done, owner-offline decided, hooks
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> |