docs: fix ARCHITECTURE.md mermaid diagrams + document the autosave last-mile guard
CI / frontend (push) Failing after 1m6s
CI / storybook-a11y (push) Successful in 5m3s
CI / backend (push) Successful in 1m20s
CI / codeql (csharp) (push) Has been cancelled
CI / codeql (javascript-typescript) (push) Has been cancelled
CI / api-client-drift (push) Has been cancelled
CI / e2e (push) Has been cancelled

Rewrite all five mermaid blocks to portable syntax so they render (verified via
a mermaid-native render): split two `classDef`/`class` statements that shared a
line (the "why not just signals" graph); drop the `reduce() — PURE` participant
alias and the `;` inside a Note (§2c sequence); remove `<br/>` and parenthetical
transition labels from the two stateDiagram-v2 blocks (RemoteData, intake),
moving the aside into a note; normalize `<br/>`→`<br>` and drop `<b>` in the
atomic-hierarchy flowchart.

Extend §2g to document the now-closed gap: a CanDeactivate guard flushes the
pending debounce before in-app navigation, and beforeunload warns + best-effort
flushes on hard close (noting the HttpClient/keepalive limitation).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
eho
2026-07-21 16:29:09 +02:00
co-authored by Claude Opus 4.8
parent 645fad088e
commit 7dfbd4501f
+43 -22
View File
@@ -43,11 +43,11 @@ ever uses the level(s) below it — so anything you build is reusable by everyth
```mermaid ```mermaid
graph TD graph TD
P["<b>Pages</b><br/>dashboard.page · login.page · intake.page"] P["Pages<br>dashboard.page · login.page · intake.page"]
T["<b>Templates</b><br/>page-shell · shell"] T["Templates<br>page-shell · shell"]
O["<b>Organisms</b><br/>login-form · registration-table · intake-wizard"] O["Organisms<br>login-form · registration-table · intake-wizard"]
M["<b>Molecules</b><br/>form-field · data-row · async"] M["Molecules<br>form-field · data-row · async"]
A["<b>Atoms</b><br/>button · text-input · radio-group · alert · heading"] A["Atoms<br>button · text-input · radio-group · alert · heading"]
P --> T --> O --> M --> A P --> T --> O --> M --> A
classDef l fill:#e5f1fb,stroke:#007bc7,color:#00567d; classDef l fill:#e5f1fb,stroke:#007bc7,color:#00567d;
class P,T,O,M,A l; class P,T,O,M,A l;
@@ -110,12 +110,12 @@ the 4 states that are real** — the illegal ones can't be expressed at all.
graph LR graph LR
subgraph bad["3 booleans = 8 states (most illegal)"] subgraph bad["3 booleans = 8 states (most illegal)"]
direction TB direction TB
b1["loading ✓ · error ✗ · data ✗ "] b1["loading ✓ · error ✗ · data ✗ — legal"]
b2["loading ✗ · error ✓ · data ✗ "] b2["loading ✗ · error ✓ · data ✗ — legal"]
b3["loading ✗ · error ✗ · data ✓ "] b3["loading ✗ · error ✗ · data ✓ — legal"]
b4["loading ✓ · error ✓ · data ✓ nonsense"] b4["loading ✓ · error ✓ · data ✓ nonsense"]
b5["loading ✓ · error ✗ · data ✓ nonsense"] b5["loading ✓ · error ✗ · data ✓ nonsense"]
b6["… 3 more illegal combos"] b6["… 3 more illegal combos"]
end end
subgraph good["1 union = 4 legal states"] subgraph good["1 union = 4 legal states"]
direction TB direction TB
@@ -125,8 +125,10 @@ graph LR
g4["Success (carries value)"] g4["Success (carries value)"]
end end
bad -->|"choose a better type"| good bad -->|"choose a better type"| good
classDef ok fill:#e8f5e9,stroke:#39870c; classDef no fill:#fdecea,stroke:#d52b1e; classDef ok fill:#e8f5e9,stroke:#39870c;
class b1,b2,b3,g1,g2,g3,g4 ok; class b4,b5,b6 no; classDef no fill:#fdecea,stroke:#d52b1e;
class b1,b2,b3,g1,g2,g3,g4 ok;
class b4,b5,b6 no;
``` ```
The same argument applies to forms (a `submitting` boolean that can be true _with_ The same argument applies to forms (a `submitting` boolean that can be true _with_
@@ -175,7 +177,7 @@ stateDiagram-v2
Loading --> Success: data arrived Loading --> Success: data arrived
Loading --> Empty: arrived, but no rows Loading --> Empty: arrived, but no rows
Loading --> Failure: request failed Loading --> Failure: request failed
Failure --> Loading: reload() Failure --> Loading: reload
note right of Success note right of Success
value lives ONLY here value lives ONLY here
end note end note
@@ -230,15 +232,15 @@ _one_ function. No state is mutated anywhere else.
```mermaid ```mermaid
sequenceDiagram sequenceDiagram
actor User actor User
participant View as View (template) participant View
participant Store as createStore (signal) participant Store
participant Reduce as reduce() — PURE participant Reduce
User->>View: clicks / types User->>View: clicks / types
View->>Store: dispatch(msg) View->>Store: dispatch(msg)
Store->>Reduce: reduce(model, msg) Store->>Reduce: reduce(model, msg) — PURE
Reduce-->>Store: next model Reduce-->>Store: next model
Store-->>View: signal updates re-render Store-->>View: signal updates, re-render
Note over Reduce: the ONLY place state changes;<br/>no HTTP, no timers, no mutation Note over Reduce: the ONLY place state changes<br>no HTTP, no timers, no mutation
``` ```
Side effects (HTTP) sit _outside_ this loop: a command does the I/O, then `dispatch`es a Side effects (HTTP) sit _outside_ this loop: a command does the I/O, then `dispatch`es a
@@ -366,6 +368,22 @@ So it _feels_ like save-on-blur only because you usually stop typing when you le
field, and the debounce fires ~600 ms later. The trigger is **"stopped changing," not field, and the debounce fires ~600 ms later. The trigger is **"stopped changing," not
"lost focus."** Submit is a separate, explicit action (§2d). "lost focus."** Submit is a separate, explicit action (§2d).
**The last-mile guard (leaving mid-debounce).** A debounce means an edit made in the final
&lt;600 ms before you leave hasn't been written yet. Two seams close that window
([`pending-saves.ts`](../../../src/app/shared/application/pending-saves.ts)): every autosave
owner (the brief/org-template root stores and each wizard's `draft-sync`) registers in a
`PendingSaves` registry, and
- **in-app navigation** — a `CanDeactivate` guard (`flushPendingGuard`, on the autosave
routes) flushes the pending write and _awaits_ it before the route changes, so the page
can't tear down with an unsaved keystroke;
- **hard close / reload** — a `beforeunload` handler fires the flush best-effort and triggers
the browser's native "unsaved changes" prompt. It is deliberately _not_ a guaranteed sync
save: the HTTP seam is Angular `HttpClient` (no `keepalive`/`sendBeacon`), so an async write
can't be promised to finish as the page unloads — the prompt lets the debounce land if the
user stays. The authoritative _submit_ path already force-flushes first, so only unsent
draft keystrokes are ever at risk.
--- ---
## 3. "Parse, don't validate" — value objects ## 3. "Parse, don't validate" — value objects
@@ -443,11 +461,14 @@ where the user left off.
```mermaid ```mermaid
stateDiagram-v2 stateDiagram-v2
[*] --> Answering [*] --> Answering
Answering --> Answering: SetAnswer / Next / Back<br/>(steps re-derived each time) Answering --> Answering: SetAnswer / Next / Back
Answering --> Submitting: Submit (all answers valid) Answering --> Submitting: Submit when all answers valid
Submitting --> Submitted: ok Submitting --> Submitted: ok
Submitting --> Failed: error Submitting --> Failed: error
Failed --> Submitting: Retry Failed --> Submitting: Retry
note right of Answering
steps re-derived each time
end note
``` ```
See it live on `/concepts` (section 5) — the step list and the "stap N van M" counter See it live on `/concepts` (section 5) — the step list and the "stap N van M" counter