groundwork-method 0.10.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +42 -0
- package/bin/groundwork.js +86 -17
- package/dist/src/generators/system-test-runner/generator.js +52 -4
- package/dist/src/generators/system-test-runner/generator.js.map +1 -1
- package/package.json +1 -1
- package/src/docs/principles/design/usability-and-ux.md +11 -0
- package/src/docs/principles/foundations/testing.md +32 -6
- package/src/docs/principles/index.md +2 -1
- package/src/docs/principles/quality/observability.md +2 -2
- package/src/engineer-skills/groundwork-electron-engineer/SKILL.md +6 -1
- package/src/engineer-skills/groundwork-electron-engineer/references/documentation.md +126 -0
- package/src/engineer-skills/groundwork-electron-engineer/references/observability.md +37 -0
- package/src/engineer-skills/groundwork-electron-engineer/references/performance-and-reliability.md +80 -0
- package/src/engineer-skills/groundwork-electron-engineer/references/testing-and-smoke.md +22 -0
- package/src/engineer-skills/groundwork-electron-engineer/sync-anchor.md +12 -4
- package/src/engineer-skills/groundwork-flutter-engineer/SKILL.md +7 -1
- package/src/engineer-skills/groundwork-flutter-engineer/references/documentation.md +122 -0
- package/src/engineer-skills/groundwork-flutter-engineer/references/observability.md +37 -0
- package/src/engineer-skills/groundwork-flutter-engineer/references/performance-and-reliability.md +100 -0
- package/src/engineer-skills/groundwork-flutter-engineer/references/security.md +96 -0
- package/src/engineer-skills/groundwork-flutter-engineer/references/testing.md +25 -0
- package/src/engineer-skills/groundwork-flutter-engineer/sync-anchor.md +13 -4
- package/src/engineer-skills/groundwork-go-engineer/SKILL.md +5 -2
- package/src/engineer-skills/groundwork-go-engineer/references/documentation.md +130 -0
- package/src/engineer-skills/groundwork-go-engineer/references/testing.md +63 -1
- package/src/engineer-skills/groundwork-go-engineer/sync-anchor.md +13 -4
- package/src/engineer-skills/groundwork-nextjs-engineer/SKILL.md +6 -1
- package/src/engineer-skills/groundwork-nextjs-engineer/references/accessibility.md +111 -0
- package/src/engineer-skills/groundwork-nextjs-engineer/references/observability.md +48 -0
- package/src/engineer-skills/groundwork-nextjs-engineer/references/security.md +131 -0
- package/src/engineer-skills/groundwork-nextjs-engineer/references/testing.md +59 -1
- package/src/engineer-skills/groundwork-nextjs-engineer/references/ux-principles.md +1 -49
- package/src/engineer-skills/groundwork-nextjs-engineer/sync-anchor.md +10 -3
- package/src/engineer-skills/groundwork-python-engineer/SKILL.md +5 -2
- package/src/engineer-skills/groundwork-python-engineer/references/security.md +148 -0
- package/src/engineer-skills/groundwork-python-engineer/references/testing.md +40 -1
- package/src/engineer-skills/groundwork-python-engineer/sync-anchor.md +11 -4
- package/src/generators/electron-app/docs/principles/stack/electron/index.md +2 -0
- package/src/generators/electron-app/files/tests/smoke/app.spec.ts.template +73 -8
- package/src/generators/flutter-app/docs/principles/stack/flutter/testing.md +14 -2
- package/src/generators/flutter-app/files/integration_test/app_test.dart.template +46 -12
- package/src/generators/go-microservice/docs/principles/stack/go/testing.md +17 -1
- package/src/generators/python-microservice/docs/principles/stack/python/testing.md +41 -0
- package/src/generators/system-test-runner/NATIVE-CHECK-CONTRACT.md +20 -0
- package/src/generators/system-test-runner/files/tests/system/test_render_smoke.py.template +30 -0
- package/src/generators/system-test-runner/generator.ts +58 -4
- package/src/generators/workspace-dev-cli/cli-src/dist/dev-bundle.js +1 -1
- package/src/hidden-skills/code-intelligence.md +6 -0
- package/src/hidden-skills/groundwork-architect/SKILL.md +1 -1
- package/src/hidden-skills/groundwork-architect/sync-anchor.md +2 -2
- package/src/hidden-skills/groundwork-bet/briefs/acceptance-auditor.md +68 -0
- package/src/hidden-skills/groundwork-bet/briefs/blind-reviewer.md +56 -0
- package/src/hidden-skills/groundwork-bet/briefs/coverage-auditor.md +95 -0
- package/src/hidden-skills/groundwork-bet/briefs/edge-case-tracer.md +64 -0
- package/src/hidden-skills/groundwork-bet/briefs/experience-auditor.md +83 -0
- package/src/hidden-skills/groundwork-bet/briefs/slice-worker.md +92 -26
- package/src/hidden-skills/groundwork-bet/instructions.md +4 -4
- package/src/hidden-skills/groundwork-bet/templates/bet-progress-test.md +16 -27
- package/src/hidden-skills/groundwork-bet/templates/change-proposal.md +1 -1
- package/src/hidden-skills/groundwork-bet/templates/decomposition/milestone-index.md +12 -16
- package/src/hidden-skills/groundwork-bet/templates/decomposition/slice.md +4 -8
- package/src/hidden-skills/groundwork-bet/templates/technical-design/03-api-design.md +1 -1
- package/src/hidden-skills/groundwork-bet/workflows/01-discovery.md +3 -1
- package/src/hidden-skills/groundwork-bet/workflows/02-design.md +11 -1
- package/src/hidden-skills/groundwork-bet/workflows/03-decomposition.md +60 -64
- package/src/hidden-skills/groundwork-bet/workflows/04-delivery.md +75 -42
- package/src/hidden-skills/groundwork-bet/workflows/05-validation.md +18 -7
- package/src/hidden-skills/groundwork-designer/sync-anchor.md +1 -1
- package/src/hidden-skills/groundwork-persona/instructions.md +11 -0
- package/src/hidden-skills/groundwork-review/checklists/implementation-readiness.md +1 -0
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# Documentation
|
|
2
|
+
|
|
3
|
+
## Table of Contents
|
|
4
|
+
- [Hierarchy](#hierarchy)
|
|
5
|
+
- [The Contract Type Is the Surface Documentation](#the-contract-type-is-the-surface-documentation)
|
|
6
|
+
- [TSDoc on the Bridge Contract](#tsdoc-on-the-bridge-contract)
|
|
7
|
+
- [The Process Boundary Documents Itself](#the-process-boundary-documents-itself)
|
|
8
|
+
- [Why-Comments on Security Policy](#why-comments-on-security-policy)
|
|
9
|
+
- [Inline Comments](#inline-comments)
|
|
10
|
+
- [A Comment Is Often a Smell](#a-comment-is-often-a-smell)
|
|
11
|
+
- [In-Code Markers](#in-code-markers)
|
|
12
|
+
- [What NOT to Document](#what-not-to-document)
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
A desktop shell has a documentation problem the web app does not: the same TypeScript runs in three processes at three privilege levels, and a reader must know which one a file belongs to before a single line makes sense. The architecture answers that in structure — the folder boundary, the shared contract, the typed bridge — so the documentation is mostly the code's shape, not prose around it.
|
|
17
|
+
|
|
18
|
+
## Hierarchy
|
|
19
|
+
|
|
20
|
+
Structure documents more reliably than comments. A comment is a promise no compiler checks, and a stale comment about *which process runs this* is worse than none — it misleads about a security boundary. Documentation priority — the foundations principle (`docs/principles/foundations/documentation.md`) written the Electron way:
|
|
21
|
+
|
|
22
|
+
1. **Types and the shared contract** — `IpcContract`/`RendererApi` in `src/shared/ipc.ts`; a drifted signature is a compile error in both processes (`references/ipc-contracts.md`).
|
|
23
|
+
2. **The folder boundary** — `main/`, `preload/`, `renderer/`, `shared/` declare where code runs; lint and two tsconfigs enforce it (`references/process-model.md`).
|
|
24
|
+
3. **Zod schemas at the trust boundary** — runtime-checked validation that documents the accepted payload and flows into errors.
|
|
25
|
+
4. **Test names** — a smoke or unit test named for its behaviour is executable documentation verified by CI.
|
|
26
|
+
5. **TSDoc on the bridge and security policy** — written only where types and structure cannot carry the intent.
|
|
27
|
+
6. **Inline "why" comments** — last resort for a genuinely non-obvious decision.
|
|
28
|
+
|
|
29
|
+
Levels 1–4 are verified by tooling. Levels 5–6 are human promises that drift. Minimise them.
|
|
30
|
+
|
|
31
|
+
## The Contract Type Is the Surface Documentation
|
|
32
|
+
|
|
33
|
+
`src/shared/ipc.ts` is the single document of what the renderer may ask the privileged side to do. `IpcContract` enumerates every channel; `RendererApi` is the capability surface the UI sees. Read together they are the complete, compiler-verified description of the trust boundary — no prose inventory of "available IPC calls" stays as accurate, because this one fails the build when it lies.
|
|
34
|
+
|
|
35
|
+
```ts
|
|
36
|
+
// src/shared/ipc.ts — the surface, documented by its types
|
|
37
|
+
export type RendererApi = {
|
|
38
|
+
openProject: (path: string) => Promise<ProjectSummary>;
|
|
39
|
+
onProjectChanged: (cb: () => void) => () => void;
|
|
40
|
+
};
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
The naming carries the documentation: bridge methods name **capabilities** (`openProject`), never transport (`invoke`, `send`). A method called `invoke` documents nothing about what crosses the seam (`references/ipc-contracts.md`).
|
|
44
|
+
|
|
45
|
+
## TSDoc on the Bridge Contract
|
|
46
|
+
|
|
47
|
+
Most channels are self-documenting through their contract types. Add TSDoc only where a method carries a consequence the signature cannot — a privileged side effect, a security implication, an OS interaction the caller must understand:
|
|
48
|
+
|
|
49
|
+
```ts
|
|
50
|
+
export type RendererApi = {
|
|
51
|
+
/**
|
|
52
|
+
* Opens a path in the OS file manager. Main validates it is inside an
|
|
53
|
+
* allowed root before the shell call — a renderer cannot open arbitrary
|
|
54
|
+
* paths. Rejects if the path escapes the sandboxed roots.
|
|
55
|
+
*/
|
|
56
|
+
openInFileManager: (path: string) => Promise<void>;
|
|
57
|
+
};
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
The note documents the trust decision, not the mechanics. A plain `getStatus(): Promise<AppStatus>` needs nothing — the name and return type are the whole contract.
|
|
61
|
+
|
|
62
|
+
## The Process Boundary Documents Itself
|
|
63
|
+
|
|
64
|
+
The folder a file lives in is its most important documentation: it declares the privilege level and what the file may import (`references/process-model.md`). That boundary is physical and enforced — `no-restricted-imports` fails the build when `renderer/` or `shared/` imports `electron` or a Node built-in, and the two tsconfigs reject a renderer file touching `process` or a main file touching `document`.
|
|
65
|
+
|
|
66
|
+
This is why a comment like `// runs in the renderer, no Node here` is a smell: the folder already says it, the lint already enforces it, and the comment will outlive the file's move to another directory. Trust the structure. When a file's process is genuinely ambiguous, the fix is to move it to the right folder, not to annotate it.
|
|
67
|
+
|
|
68
|
+
`src/shared/` documents a stronger rule by what it contains: types only, no runtime behaviour. A function body in a shared file silently drags Node-flavoured code toward the sandbox — the absence of runtime code there is itself the contract (`references/process-model.md`).
|
|
69
|
+
|
|
70
|
+
## Why-Comments on Security Policy
|
|
71
|
+
|
|
72
|
+
Security configuration is the one place inline prose earns its keep, because the *reason* a setting holds is invisible in the value and dangerous to guess at. A future reader tempted to loosen a policy must find the threat model next to the line:
|
|
73
|
+
|
|
74
|
+
```ts
|
|
75
|
+
// sandbox + contextIsolation are non-negotiable: the renderer loads UI
|
|
76
|
+
// that can be XSS'd; without these, an injection reaches Node and the OS.
|
|
77
|
+
webPreferences: { sandbox: true, contextIsolation: true, nodeIntegration: false },
|
|
78
|
+
|
|
79
|
+
// CSP forbids connect-src: the renderer fetches nothing directly — core
|
|
80
|
+
// calls go through main over IPC, which holds the credentials.
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Keep `src/main/policy.ts` a pure module so the rules are unit-tested rather than narrated (`references/process-model.md`). The test documents the policy's behaviour; the comment documents only the threat the test cannot express.
|
|
84
|
+
|
|
85
|
+
## Inline Comments
|
|
86
|
+
|
|
87
|
+
Inline comments explain **why**, never **what**. In a handler, the validation and delegation are visible; the comment captures the reason.
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
ipcMain.handle('project:open', async (event, rawPath: unknown) => {
|
|
91
|
+
assertTrustedSender(event, devServerUrl); // reject navigated-away frames
|
|
92
|
+
const path = openProjectPayload.parse(rawPath);
|
|
93
|
+
return openProject(path);
|
|
94
|
+
});
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
A comment narrating the obvious is noise — the reader can see the `parse` call.
|
|
98
|
+
|
|
99
|
+
## A Comment Is Often a Smell
|
|
100
|
+
|
|
101
|
+
When you reach for a comment to explain *what* a block does, the code is asking to be refactored:
|
|
102
|
+
|
|
103
|
+
- A comment naming which process a file runs in → move it to the right folder.
|
|
104
|
+
- A comment explaining a generic `invoke(channel, ...)` passthrough → name the capability; the passthrough is itself an anti-pattern (`references/ipc-contracts.md`).
|
|
105
|
+
- A comment decoding an untyped channel string → add it to `IpcContract`.
|
|
106
|
+
- A comment summarising what a handler does → the handler does too much; delegate to a named function in main.
|
|
107
|
+
|
|
108
|
+
Delete the comment and fix the code. The refactor cannot drift; the comment can.
|
|
109
|
+
|
|
110
|
+
## In-Code Markers
|
|
111
|
+
|
|
112
|
+
```ts
|
|
113
|
+
// TODO(bob): move indexing to a utilityProcess once payloads grow. Issue #231.
|
|
114
|
+
// FIXME(carol): unsubscribe leak when a window closes mid-stream. Issue #245.
|
|
115
|
+
// HACK(dave): re-assert CSP after a dev-only HMR reload until upstream fix.
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
Always include `(username)` and an issue reference. A marker without one will never be resolved.
|
|
119
|
+
|
|
120
|
+
## What NOT to Document
|
|
121
|
+
|
|
122
|
+
- Channels whose contract type and capability name tell the whole story.
|
|
123
|
+
- Which process a file runs in — the folder and the lint already say it.
|
|
124
|
+
- Boilerplate window construction — the hardened `webPreferences` quartet is the documented standard (`references/process-model.md`), not a per-window note.
|
|
125
|
+
- TanStack Query `queryFn`s that call the typed bridge — the types are the contract.
|
|
126
|
+
- Generated or framework code — never hand-edit comments into it.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Observability
|
|
2
|
+
|
|
3
|
+
An Electron app has two processes and no backend span surface. Distributed tracing lives at the capability core — the services the app talks to — which is why trace-assertions are N/A for the desktop shell (`references/testing-and-smoke.md`). The shell's observability job is **crash reporting and structured logs across the main/renderer boundary**, plus update telemetry. Instrument for the questions you will ask when a user reports a crash you cannot reproduce; the discipline is the framework canon adapted to a binary running on someone else's machine (`docs/principles/quality/observability.md`).
|
|
4
|
+
|
|
5
|
+
## Crash Reporting — Both Processes
|
|
6
|
+
|
|
7
|
+
Two signals, both required, because they catch different failures:
|
|
8
|
+
|
|
9
|
+
- **Native crashes.** Electron's built-in `crashReporter` (Crashpad) captures native crashes in main and renderer and uploads minidumps. Start it in main *before* `app` is ready — a reporter started late misses the early crash.
|
|
10
|
+
- **JS exceptions.** A sink (Sentry-for-Electron shape) captures uncaught JS in both processes — main's `process.on('uncaughtException')`/`'unhandledRejection'` and the renderer's `window` handlers. A native crash and a JS exception are different events; you need both wired.
|
|
11
|
+
|
|
12
|
+
## Structured Logs Across the Process Boundary
|
|
13
|
+
|
|
14
|
+
- **Main-process logging** goes to a rotating file sink (electron-log shape) as JSON, severity-tagged. Main has no devtools console, so an unstructured main log is invisible in the field.
|
|
15
|
+
- **Renderer errors forward to main** over IPC, so a single log stream reconstructs an incident that spans both processes. An error that lands only in the renderer devtools is gone the moment the window closes.
|
|
16
|
+
- Tag every line with the process (`main`/`renderer`) and a session id, so a multi-process incident reads as one timeline.
|
|
17
|
+
|
|
18
|
+
## App and Update Telemetry
|
|
19
|
+
|
|
20
|
+
`autoUpdater` lifecycle events — update available, downloaded, applied, failed — are the signal for whether the fleet is actually on the new build. A silent update failure strands users on an old version with nothing to alert on, the desktop equivalent of a botched rollout. Carry app version, OS, and channel as context on every event.
|
|
21
|
+
|
|
22
|
+
## What to Capture vs PII
|
|
23
|
+
|
|
24
|
+
- **Capture** process, session id, error type and stack, app/OS/channel version, and update events.
|
|
25
|
+
- **Never** log tokens, file paths containing usernames, or PII into logs, crash context, or minidumps. The upload is third-party — redact at the edge.
|
|
26
|
+
|
|
27
|
+
## Where Distributed Tracing Lives
|
|
28
|
+
|
|
29
|
+
The end-to-end request trace belongs to the services the app calls and is asserted at the capability core. The shell does not emit spans; it correlates by attaching a request id it can log on both sides of the IPC boundary.
|
|
30
|
+
|
|
31
|
+
## Anti-Patterns
|
|
32
|
+
|
|
33
|
+
- **`console.log` in main.** There is no console to read in the field — log structured to the sink.
|
|
34
|
+
- **Renderer errors that never cross to main.** Lost when the window closes.
|
|
35
|
+
- **A late `crashReporter`.** Started after `app` ready, it misses the crashes that happen at startup.
|
|
36
|
+
- **A collector in the shell.** The app reports to a sink; it does not run backend OTel infrastructure.
|
|
37
|
+
- **PII in logs or minidumps.** Usernames in paths and tokens in context follow the upload off-machine.
|
package/src/engineer-skills/groundwork-electron-engineer/references/performance-and-reliability.md
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Performance & Reliability
|
|
2
|
+
|
|
3
|
+
## Table of Contents
|
|
4
|
+
- [Where a Desktop App Spends Its Budget](#where-a-desktop-app-spends-its-budget)
|
|
5
|
+
- [The Main Process Is Never Blocked](#the-main-process-is-never-blocked)
|
|
6
|
+
- [Renderer Performance Is Web Performance](#renderer-performance-is-web-performance)
|
|
7
|
+
- [IPC Efficiency](#ipc-efficiency)
|
|
8
|
+
- [Memory Across Long-Lived Windows](#memory-across-long-lived-windows)
|
|
9
|
+
- [Cold Boot](#cold-boot)
|
|
10
|
+
- [Reliability of the IPC Layer](#reliability-of-the-ipc-layer)
|
|
11
|
+
- [A Backend That Is Unreachable](#a-backend-that-is-unreachable)
|
|
12
|
+
- [What Lives in the Core, Not Here](#what-lives-in-the-core-not-here)
|
|
13
|
+
- [Anti-Patterns](#anti-patterns)
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Where a Desktop App Spends Its Budget
|
|
18
|
+
|
|
19
|
+
Performance is a budget spent deliberately, allocated top-down and measured at the tail, not the average (`docs/principles/quality/performance.md`). A desktop shell spends it across three surfaces with different failure modes: the **main process**, where blocking work freezes every window at once; the **renderer**, which is a web app and pays the web's bundle and frame costs; and the **bridge** between them, where a chatty IPC pattern turns a cheap call into a per-frame tax. The process boundaries that contain these costs are the process-model's subject (`references/process-model.md`); this is the performance and reliability lens on them.
|
|
20
|
+
|
|
21
|
+
## The Main Process Is Never Blocked
|
|
22
|
+
|
|
23
|
+
One main process serves every window. A synchronous parse, hash, or file walk on it freezes all of them simultaneously — there is no per-window isolation to fall back on (`references/process-model.md`). The test for any main-process code is the one the process model states: **can this take longer than a frame?** Reading a config file or registering a handler — no, main is fine. Parsing a large file, indexing, image work — yes, and it goes to a `utilityProcess` with its ports wired renderer↔utility directly so the heavy traffic never transits or blocks main (`references/process-model.md`).
|
|
24
|
+
|
|
25
|
+
`sendSync` over IPC is forbidden for the same reason from the other side: it blocks the renderer's event loop for the full round trip (`references/ipc-contracts.md`). Every privileged call is an async `invoke`.
|
|
26
|
+
|
|
27
|
+
## Renderer Performance Is Web Performance
|
|
28
|
+
|
|
29
|
+
The renderer is a normal Vite + React app, so the web stack's performance discipline applies unchanged: lazy-load routes and heavy components behind code-split boundaries, keep the bundle lean, and gate deterministic budgets — bundle size, not noisy wall-clock — in CI (`docs/principles/quality/performance.md`). Two desktop-specific notes:
|
|
30
|
+
|
|
31
|
+
- **The bundle is local, but it is not free.** It loads from the bundle protocol rather than a network, so transfer cost is near zero — but parse and execute cost is not, and a bloated main chunk still slows cold boot. Code-split anyway.
|
|
32
|
+
- **One window is one renderer.** A second window (settings, about) is a second renderer with its own bundle and memory; share chunks through the build, and do not spawn windows the app does not need.
|
|
33
|
+
|
|
34
|
+
## IPC Efficiency
|
|
35
|
+
|
|
36
|
+
The bridge is a serialization boundary: every `invoke` structured-clones its arguments and result across the process line. That cost is invisible per call and ruinous in a loop. Two rules keep it cheap:
|
|
37
|
+
|
|
38
|
+
- **Never call the bridge in a hot loop.** A per-row or per-frame `invoke` pays the clone cost every iteration. Fetch the collection in one call and iterate in the renderer.
|
|
39
|
+
- **Batch and coarsen the contract.** Design channels around the renderer's actual unit of work — `items:list` returning a page, not `item:get` called N times. A coarse channel is one clone; a chatty one is N (`references/ipc-contracts.md`).
|
|
40
|
+
|
|
41
|
+
Push channels (main → renderer) carry coarse events too: a file-watcher that fires per-keystroke should coalesce before it `webContents.send`s, so the renderer invalidates once, not a hundred times (`references/ipc-contracts.md`).
|
|
42
|
+
|
|
43
|
+
## Memory Across Long-Lived Windows
|
|
44
|
+
|
|
45
|
+
A desktop window lives for hours or days, so a leak that a page reload would have swept never gets swept. The discipline is lifecycle hygiene at the boundaries:
|
|
46
|
+
|
|
47
|
+
- **Every subscription returns its unsubscribe, and the renderer calls it.** A bridge push subscription returns a remover; an effect that registers one must return it for cleanup, or the listener and its closure outlive the component (`references/ipc-contracts.md`).
|
|
48
|
+
- **Bound caches.** TanStack Query's cache is bounded by its garbage-collection time; an ad-hoc `Map` accumulating per-result entries is not — it is the unbounded queue the performance canon rejects, in client form.
|
|
49
|
+
- **Tear down `utilityProcess` workers** when their work is done. A spawned worker that is never killed is retained memory and a retained handle.
|
|
50
|
+
|
|
51
|
+
## Cold Boot
|
|
52
|
+
|
|
53
|
+
Time-to-first-window is the desktop app's first impression. Keep main's startup to the four things it must do — register the protocol, apply the security policy, register handlers, create the window — and defer everything else until after the window is visible (`references/process-model.md`). Show the window with a skeleton and let data arrive into it over IPC; do not block window creation on a gateway call or a heavy index. Build the index in a `utilityProcess` after the first frame, not before it.
|
|
54
|
+
|
|
55
|
+
## Reliability of the IPC Layer
|
|
56
|
+
|
|
57
|
+
Reliability is designed in, not hoped for, and for the renderer the IPC seam is the dependency that fails (`docs/principles/quality/reliability.md`). TanStack Query is the renderer's resilience layer over that seam, exactly as it would be over HTTP: its `queryFn`s call the typed bridge, and its caching, retry, and invalidation give the renderer bounded retries with backoff and a served-from-cache fallback for free (`references/ipc-contracts.md`). Configure retry to back off and to retry only transient failures — a rejected privileged call from a validation failure is a bug or an attack, not a state to retry into (`references/ipc-contracts.md`). A failed `invoke` rejects the query, and the component renders that error state rather than letting the rejection escape.
|
|
58
|
+
|
|
59
|
+
## A Backend That Is Unreachable
|
|
60
|
+
|
|
61
|
+
When the workspace has a hosted core, main holds the HTTP client and the renderer reaches the gateway only through main (`references/ipc-contracts.md`). That places the gateway's reliability in main, and the discipline is the one the resilience patterns describe at any client edge:
|
|
62
|
+
|
|
63
|
+
- **Timeout and bounded retry on main's HTTP client.** Every outbound call to the gateway has a timeout and a jittered, bounded retry for transient failures — a hung gateway connection otherwise stalls the IPC call that is waiting on it.
|
|
64
|
+
- **Map unreachable to a domain result.** Main returns a typed "unreachable" result the renderer can render, not a raw thrown error — the gateway being down is an expected state with a designed UI, decided at design time alongside the happy path (`docs/principles/quality/reliability.md`).
|
|
65
|
+
- **Degrade, do not blank.** A feature whose gateway data is unavailable serves cached data or an explicit unavailable state while the rest of the app works; the window stays usable when one capability is down.
|
|
66
|
+
|
|
67
|
+
## What Lives in the Core, Not Here
|
|
68
|
+
|
|
69
|
+
Server reliability patterns belong to the capability core and its services, not the desktop shell (`docs/principles/quality/reliability.md`). **SLOs and error budgets** are defined and measured server-side. **Load shedding** protects the server from overload regardless of how clients behave — a backstop the server owns, not something a client implements for it. **Server-side circuit breakers** are earned against slow downstreams and tuned against real traffic in the core. The client's share is the right amount of resilience at the edge: timeout, bounded retry, a cache fallback, and a designed degraded state. The business rules about a failure — recoverability, retry budget, what a result means — are proven in the core, and the renderer renders the result.
|
|
70
|
+
|
|
71
|
+
## Anti-Patterns
|
|
72
|
+
|
|
73
|
+
- **Blocking the main process.** A synchronous parse or hash freezes every window — `utilityProcess` it.
|
|
74
|
+
- **`sendSync`.** Blocks the renderer event loop for the round trip; always async.
|
|
75
|
+
- **IPC in a hot loop.** Per-row or per-frame `invoke` pays the clone cost every iteration — fetch once, iterate locally.
|
|
76
|
+
- **Chatty fine-grained channels.** N calls where one coarse channel would do; design channels around the renderer's unit of work.
|
|
77
|
+
- **Leaked subscriptions.** A push listener registered without its unsubscribe outlives the component across a multi-day session.
|
|
78
|
+
- **Unbounded ad-hoc caches.** A `Map` that only grows is the latency bomb in client form; let TanStack Query bound it.
|
|
79
|
+
- **Blank on unreachable.** No designed state for a down gateway ships a frozen or empty window.
|
|
80
|
+
- **Reimplementing server reliability in the shell.** Load shedding, SLOs, and reflexive circuit breakers live in the core.
|
|
@@ -22,6 +22,8 @@
|
|
|
22
22
|
|
|
23
23
|
This maps onto the multi-surface verification contract: generation (snapshot, framework-side), compilation (`tsc` + lint), boot (the smoke). Business rules are **not** on this list — they are proven once at the capability core's contract; surface tests assert wiring and rendering only.
|
|
24
24
|
|
|
25
|
+
These tiers are the Electron idiom of the framework testing canon (`docs/principles/foundations/testing.md`): the renderer and main unit tests are the fat middle the canon's honeycomb puts the weight on, and the boot smoke is the thin top — a fat smoke is the fat-integration-suite antipattern wearing a desktop coat. When this file and the canon disagree, the canon wins and this file is the one to fix.
|
|
26
|
+
|
|
25
27
|
`vitest.config.ts` defines the two unit projects with per-process environments. Test placement follows the process split: `src/main/**/*.test.ts` runs in Node, `src/renderer/**/*.test.tsx` runs in jsdom. A test that needs the wrong environment is in the wrong process.
|
|
26
28
|
|
|
27
29
|
## Unit: Node Project (Main Policy)
|
|
@@ -96,6 +98,18 @@ Boot minutes are this stack's expensive test currency. The smoke proves the app
|
|
|
96
98
|
- New IPC channels get unit tests (policy + renderer fake) by default; extend the smoke only when a channel's *wiring* is novel (new push mechanism, new window).
|
|
97
99
|
- Feature behaviour belongs in renderer unit tests; business rules belong at the core's contract. A fat smoke is the fat-integration-suite antipattern wearing a desktop coat.
|
|
98
100
|
|
|
101
|
+
## Mutation Testing — the assertion-quality read-out
|
|
102
|
+
|
|
103
|
+
The main-process policy modules (`policy.ts` — URL allow-listing, sender validation, IPC guards) are dense security logic, exactly where a covered-but-unasserted line is a real risk. **StrykerJS** is the read-out that proves those tests bite: it mutates the rule and confirms a test fails. Treat it as a **signal, never a gate**, run it incrementally on changed code (`stryker run --incremental`), and point it at the pure policy modules first — a surviving mutant on a security rule is the missing assertion to add. The renderer's pure logic earns the same spot check; the Electron glue and the smoke do not (they prove wiring, not branches).
|
|
104
|
+
|
|
105
|
+
## Generate the Inputs You Can't Enumerate
|
|
106
|
+
|
|
107
|
+
The same pure policy modules are the prime target for property-based testing (canon principle 7). A hand-written `it.each` list of malicious URLs checks the cases you thought of; an allow-list rule that ingests untrusted strings is exactly where the dangerous input is the one you didn't enumerate. Drive `isAllowedExternalUrl` and sender-validation guards with **`fast-check`** generators — arbitrary URLs, schemes, and host shapes — and assert the security invariant holds (`file:`/`javascript:`/credential-bearing URLs always rejected; only the allow-listed origins pass). One property closes a class of bypass the example list never reaches. The renderer's pure logic earns the same treatment; the Electron glue and the boot smoke do not — they prove wiring, not branches. Service-boundary tools (Schemathesis, coverage-guided fuzzing) belong at the capability core's contract, not the desktop shell.
|
|
108
|
+
|
|
109
|
+
## Naming Tests by Behaviour
|
|
110
|
+
|
|
111
|
+
A policy test name must state the rule and the condition from the failure log alone — `rejects file:// URLs` and `rejects credential-bearing hosts`, not `policy test 3`. The generated `it.each('rejects %s', ...)` shape already encodes this; keep it. Renderer component naming follows the web stack idiom (`groundwork-nextjs-engineer/references/testing.md`), unchanged.
|
|
112
|
+
|
|
99
113
|
## Test Commands
|
|
100
114
|
|
|
101
115
|
```bash
|
|
@@ -105,3 +119,11 @@ npx nx run <app>:smoke # build + Playwright _electron (display-guarded)
|
|
|
105
119
|
npx nx run <app>:typecheck # tsc, both process tsconfigs
|
|
106
120
|
npx nx run <app>:lint # eslint incl. process-boundary rules
|
|
107
121
|
```
|
|
122
|
+
|
|
123
|
+
## Bet Slice Rollout — the permanent tests a slice owes
|
|
124
|
+
|
|
125
|
+
When a bet slice's progress tests go green, the slice rolls out permanent coverage before it closes (bet workflow, Delivery). The bet-progress tests prove the capability once and are archived; these stay. Test placement follows the process split, and surface tests assert wiring and rendering only — never a business rule the capability core already owns.
|
|
126
|
+
|
|
127
|
+
- **Main policy unit tests (when the slice added privileged logic).** Every new security or policy decision the slice introduced gets a pure-module test in the `main` project, with the rejection cases exercised, not just the allow case — this is the densest risk surface in the stack.
|
|
128
|
+
- **Renderer unit tests (when the slice added a component or state).** Components the slice introduced with conditional rendering, async pending states, or error handling get jsdom tests against the faked `window.api` bridge; the typed fake keeps the bridge contract honest.
|
|
129
|
+
- **Smoke extension (only when wiring is novel).** A new IPC channel gets unit tests by default; extend the boot smoke only when the channel's *wiring* is genuinely new — a new push mechanism or window — never for feature behaviour. Trace assertions do not apply — an Electron app emits no OpenTelemetry traces, so there is no span surface to assert on.
|
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
# Sync Anchor
|
|
2
2
|
|
|
3
|
-
This file pins the principle files this skill embeds
|
|
4
|
-
|
|
5
|
-
|
|
3
|
+
This file pins the principle files this skill embeds — both the per-stack
|
|
4
|
+
Electron / TypeScript idiom docs and the cross-cutting central canon this skill
|
|
5
|
+
distils. When any listed file changes, this skill must be reviewed in the same
|
|
6
|
+
commit (and the matching per-stack idiom doc reconciled to the canon). CI
|
|
7
|
+
verifies the hashes match.
|
|
6
8
|
|
|
7
9
|
| Principle file | SHA-256 | Last reviewed |
|
|
8
10
|
|---|---|---|
|
|
9
|
-
| src/generators/electron-app/docs/principles/stack/electron/index.md |
|
|
11
|
+
| src/generators/electron-app/docs/principles/stack/electron/index.md | e80808eecbda59c97cd5b7870d621fc07b77ec4a4a0d1b812f4990de02be2675 | 2026-06-27 |
|
|
10
12
|
| src/generators/electron-app/docs/principles/stack/electron/process-model.md | d510797d59a06786fb6bd35f537566ee7f1024ce35a8985c9c94d305e3de5c43 | 2026-06-12 |
|
|
11
13
|
| src/generators/electron-app/docs/principles/stack/electron/ipc-contracts.md | 11d728db5d33c0c9cb3a082a58a55c96d6195f61558ca38fae91806db72da9e3 | 2026-06-12 |
|
|
12
14
|
| src/generators/electron-app/docs/principles/stack/electron/security.md | 316c118dcfb6de110d3d62b8a4c95ca79b58f5368e1b41881cd842628b3902f8 | 2026-06-12 |
|
|
13
15
|
| src/generators/electron-app/docs/principles/stack/electron/packaging-and-updates.md | b5f91ed102290dd73e52890fd389bce5be825a3d76a0f0353673cfe32ae09871 | 2026-06-12 |
|
|
14
16
|
| src/generators/electron-app/docs/principles/stack/typescript/frontend.md | 98232d067ad03c08d6c1ca5f2caec30e7c3400da55c3afb7754482bc121d7554 | 2026-06-12 |
|
|
17
|
+
| src/docs/principles/foundations/testing.md | 205ac40d4c643e7b61cf1e4295df8a7b8b46dcd7c81b857aa8c642ea353f62ef | 2026-06-27 |
|
|
18
|
+
| src/docs/principles/quality/observability.md | 8aa60e213ba03e989c93263153e3a1ac10b2336f6d0360c394f473660d565a0b | 2026-06-26 |
|
|
19
|
+
| src/docs/principles/quality/security.md | 61157d97677142737ec537954dc5aaad7a04012cc8a3dcc855e2d324287fdc64 | 2026-06-26 |
|
|
20
|
+
| src/docs/principles/quality/performance.md | 18b6d3391c57d97342068f9f1da732b24de4221489d0459bb6ad8900fac0a02e | 2026-06-26 |
|
|
21
|
+
| src/docs/principles/quality/reliability.md | 9c9788504e0963458667d2727c3fc2359776108be593a2efc6603f6470002252 | 2026-06-26 |
|
|
22
|
+
| src/docs/principles/foundations/documentation.md | 8b576072eaf4970f1251b560781e3e755c864a7920faa599b2834c921cbb8734 | 2026-06-26 |
|
|
@@ -41,7 +41,7 @@ GroundWork gives you a deterministic **repo map** (`npx groundwork-method repo-m
|
|
|
41
41
|
|
|
42
42
|
## How to Use This Skill
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
**Orient first.** On any non-trivial task, refresh the repo map (`npx groundwork-method repo-map`), read its `centrality` ranking to find the hubs, and navigate them with Serena before reading widely (see Code intelligence above) — this is the first step, not optional; fall back to ordinary reads only when those tools are unavailable. Then match the user's task to the smallest relevant reference set. Most tasks touch one or two references.
|
|
45
45
|
|
|
46
46
|
| Topic | Reference | Load When |
|
|
47
47
|
|-------|-----------|-----------|
|
|
@@ -55,6 +55,10 @@ Match the user's task to the smallest relevant reference set. Most tasks touch o
|
|
|
55
55
|
| Platform Channels | `references/platform-channels.md` | Pigeon APIs, native modules, when to drop to Swift/Kotlin, SwiftPM wiring. |
|
|
56
56
|
| Releases & Distribution | `references/releases-and-distribution.md` | Signing, CI/CD pipelines, versioning, forced upgrade, Shorebird code push. |
|
|
57
57
|
| Accessibility | `references/accessibility.md` | Semantics, tap targets, dynamic type, contrast, a11y-driven testing. |
|
|
58
|
+
| Security | `references/security.md` | Secure storage vs SharedPreferences, no secrets in the bundle, cert pinning, deep-link/intent validation, biometrics. |
|
|
59
|
+
| Performance & Reliability | `references/performance-and-reliability.md` | Frame budget, rebuild discipline, list/image memory, isolates, retry/offline, graceful degradation. |
|
|
60
|
+
| Observability | `references/observability.md` | Crash/error reporting, structured breadcrumbs, client performance traces, PII discipline. |
|
|
61
|
+
| Documentation | `references/documentation.md` | Dartdoc on public API, naming-as-documentation, structure-as-documentation, why-comments. |
|
|
58
62
|
|
|
59
63
|
## Task Routing
|
|
60
64
|
|
|
@@ -66,6 +70,8 @@ Match the user's task to the smallest relevant reference set. Most tasks touch o
|
|
|
66
70
|
- **Test work** → Load `references/testing.md`. Pick the cheapest tier that can carry the assertion.
|
|
67
71
|
- **Native capability work** → Load `references/platform-channels.md`. Check pub.dev for a maintained plugin before writing native code.
|
|
68
72
|
- **Release/store work** → Load `references/releases-and-distribution.md`. Signing material never enters the repo.
|
|
73
|
+
- **Security / secure-storage work** → Load `references/security.md`. The client is hostile territory; secrets and trust live server-side.
|
|
74
|
+
- **Performance or offline/resilience work** → Load `references/performance-and-reliability.md`. SLOs and load shedding live in the core, not the client.
|
|
69
75
|
|
|
70
76
|
## Safety Gates
|
|
71
77
|
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# Documentation
|
|
2
|
+
|
|
3
|
+
## Table of Contents
|
|
4
|
+
- [Hierarchy](#hierarchy)
|
|
5
|
+
- [Dartdoc on Public API](#dartdoc-on-public-api)
|
|
6
|
+
- [Names Are the Documentation](#names-are-the-documentation)
|
|
7
|
+
- [Structure as Documentation](#structure-as-documentation)
|
|
8
|
+
- [Inline Comments](#inline-comments)
|
|
9
|
+
- [A Comment Is Often a Smell](#a-comment-is-often-a-smell)
|
|
10
|
+
- [In-Code Markers](#in-code-markers)
|
|
11
|
+
- [What NOT to Document](#what-not-to-document)
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
Dart ships its documentation discipline in the toolchain: `dart doc` renders `///` comments to API pages, the formatter normalises them, and the `public_member_api_docs` lint flags missing ones on a public surface. The language — sound null safety, immutable widgets, `const` — is built so that well-shaped code carries most of its own meaning. Lean on that before reaching for prose.
|
|
16
|
+
|
|
17
|
+
## Hierarchy
|
|
18
|
+
|
|
19
|
+
Structure documents more reliably than comments. A comment is a promise no analyzer checks; when the widget tree changes, the comment silently lies. Documentation priority — the foundations principle (`docs/principles/foundations/documentation.md`) written the Flutter way:
|
|
20
|
+
|
|
21
|
+
1. **Types and null safety** — the analyzer rejects incorrect types and unhandled nulls. Zero drift risk.
|
|
22
|
+
2. **Naming** — the official View/ViewModel/Repository conventions, which are load-bearing (`references/architecture.md`). Rename before you comment.
|
|
23
|
+
3. **`const` and immutability** — an immutable model and a `const` constructor declare intent the analyzer enforces (`references/widgets-and-composition.md`).
|
|
24
|
+
4. **Test names** — a widget test named for its behaviour is executable documentation verified by CI.
|
|
25
|
+
5. **Dartdoc `///` on public API** — rendered by `dart doc`; written only when types cannot carry the contract.
|
|
26
|
+
6. **Inline "why" comments** — last resort for a genuinely non-obvious decision.
|
|
27
|
+
|
|
28
|
+
Levels 1–4 are verified by tooling. Levels 5–6 are human promises that drift. Minimise them.
|
|
29
|
+
|
|
30
|
+
## Dartdoc on Public API
|
|
31
|
+
|
|
32
|
+
A dartdoc comment uses `///`, leads with a one-sentence summary, and references other API in square brackets so `dart doc` links them:
|
|
33
|
+
|
|
34
|
+
```dart
|
|
35
|
+
/// Holds stock for an [order] until the reservation TTL expires.
|
|
36
|
+
///
|
|
37
|
+
/// Throws [OutOfStockException] when the requested quantity is unavailable.
|
|
38
|
+
Future<Reservation> reserve(Order order, int quantity);
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Document the **public** surface — the exported API a consumer in another library calls. A private widget (`_Header`) is read with the build method that uses it; a doc comment there usually restates the code below.
|
|
42
|
+
|
|
43
|
+
State what the signature cannot: the exceptions a caller must catch, the lifecycle of a returned `Stream`, the units of a parameter. Do not narrate the type.
|
|
44
|
+
|
|
45
|
+
```dart
|
|
46
|
+
// BAD — restates the signature
|
|
47
|
+
/// Returns the user with the given id.
|
|
48
|
+
Future<User> getUser(String id);
|
|
49
|
+
|
|
50
|
+
// GOOD — skip it; the name + types already say this
|
|
51
|
+
Future<User> getUser(String id);
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Names Are the Documentation
|
|
55
|
+
|
|
56
|
+
The cheapest documentation is the official name. The architecture conventions are not style preferences — `ProfileView`/`ProfileViewModel`, `OrderRepository`/`RemoteOrderRepository`, `AuthService` are how the next agent finds the right file on the first try (`references/architecture.md`). A `HomeController` or `UserStore` documents nothing because it matches no convention.
|
|
57
|
+
|
|
58
|
+
A small, single-purpose widget names its job in its class declaration:
|
|
59
|
+
|
|
60
|
+
```dart
|
|
61
|
+
// The name is the contract; no comment adds anything.
|
|
62
|
+
class OrderStatusBadge extends StatelessWidget {
|
|
63
|
+
const OrderStatusBadge({super.key, required this.status});
|
|
64
|
+
final OrderStatus status;
|
|
65
|
+
// ...
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Structure as Documentation
|
|
70
|
+
|
|
71
|
+
A composed widget tree documents itself when it is built from small, named, `const` pieces. A `build` method that reads as a list of well-named child widgets needs no comment to explain its layout — the composition *is* the explanation (`references/widgets-and-composition.md`).
|
|
72
|
+
|
|
73
|
+
This is why `Widget _buildHeader()` helpers are a documentation failure as much as a performance one: a method named `_buildHeader` hides a subtree behind a verb, where an extracted `_Header` widget names the thing. Extract the widget; the name replaces the comment.
|
|
74
|
+
|
|
75
|
+
Immutability documents the data flow. A `freezed` sealed union spells out every state a value can hold, exhaustively matched at the call site — prose listing "the order can be draft, placed, or cancelled" is the type, written worse (`references/architecture.md`).
|
|
76
|
+
|
|
77
|
+
## Inline Comments
|
|
78
|
+
|
|
79
|
+
Inline comments explain **why**, never **what**. The widget already says what it renders; the comment captures the reason the next reader cannot recover from the tree.
|
|
80
|
+
|
|
81
|
+
```dart
|
|
82
|
+
// LayoutBuilder, not MediaQuery: this card also renders inside a side
|
|
83
|
+
// pane and a test harness, where screen size is the wrong signal.
|
|
84
|
+
return LayoutBuilder(builder: (context, constraints) { /* ... */ });
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
A comment narrating the obvious is noise — the reader can see the `Column`.
|
|
88
|
+
|
|
89
|
+
```dart
|
|
90
|
+
// BAD — narrates the tree
|
|
91
|
+
// a column with a title and a body
|
|
92
|
+
return Column(children: [Title(), Body()]);
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## A Comment Is Often a Smell
|
|
96
|
+
|
|
97
|
+
When you reach for a comment to explain *what* a block does, the widget is asking to be refactored. The comment is debt; the fix is in the code:
|
|
98
|
+
|
|
99
|
+
- A comment heading a chunk of a long `build` → extract a named widget.
|
|
100
|
+
- A comment explaining a magic number → move it to the theme/density system as a named token (`references/theming-and-design-tokens.md`).
|
|
101
|
+
- A comment decoding a boolean argument → name the parameter or introduce an enum.
|
|
102
|
+
- A comment explaining why a field is mutable → it should be immutable; the comment is covering a broken data flow.
|
|
103
|
+
|
|
104
|
+
Delete the comment and fix the structure. The refactor cannot drift; the comment can.
|
|
105
|
+
|
|
106
|
+
## In-Code Markers
|
|
107
|
+
|
|
108
|
+
```dart
|
|
109
|
+
// TODO(bob): paginate once the repository exposes a cursor. Issue #231.
|
|
110
|
+
// FIXME(carol): rebuild storm when the parent re-themes. Issue #245.
|
|
111
|
+
// HACK(dave): clamp negative durations from the platform clock until SDK fix.
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Always include `(username)` and an issue reference. A marker without one will never be resolved.
|
|
115
|
+
|
|
116
|
+
## What NOT to Document
|
|
117
|
+
|
|
118
|
+
- Self-evident widgets where the class name and props tell the whole story.
|
|
119
|
+
- Private widgets (`_Header`) read in context with their parent's build method.
|
|
120
|
+
- `@override Widget build(...)` — never document an override the framework defines.
|
|
121
|
+
- Provider declarations whose name states what they expose (`orderRepositoryProvider`).
|
|
122
|
+
- Generated code (`*.g.dart`, `*.freezed.dart`) — never hand-edit comments into it.
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# Observability
|
|
2
|
+
|
|
3
|
+
A Flutter client emits no OpenTelemetry server spans. Distributed tracing lives at the capability core, where the gateway owns the request trace and asserts it with an in-memory exporter — which is why trace-assertions are N/A on the client (`references/testing.md`). The client's observability job is **crash and UX telemetry**: what broke, where, and what the user was doing when it did. Instrument for the questions you will ask when a crash report lands; the discipline is the framework canon adapted to a device you do not control (`docs/principles/quality/observability.md`).
|
|
4
|
+
|
|
5
|
+
## Crash and Error Reporting
|
|
6
|
+
|
|
7
|
+
A crash/error sink (Sentry or Firebase Crashlytics shape) captures unhandled failures. Two handlers are mandatory, because they catch different errors:
|
|
8
|
+
|
|
9
|
+
- `FlutterError.onError` — errors raised inside the Flutter framework (build, layout, paint).
|
|
10
|
+
- `PlatformDispatcher.instance.onError` — uncaught async and platform errors that never enter the framework.
|
|
11
|
+
|
|
12
|
+
Wiring only the first leaves the async crash invisible — the one you never see is the one outside the handler you wired. Native engine and platform-channel crashes report through the sink's native layer.
|
|
13
|
+
|
|
14
|
+
## Structured Breadcrumb Logging
|
|
15
|
+
|
|
16
|
+
Breadcrumbs, not `print`. A trail of structured events — navigation, command fired, repository call, state transition — lets a crash report reconstruct the path to the failure. `print()` is dropped in release builds and carries no structure even in debug. Attach the screen/route and the operation to each event; emit at an agreed severity rather than as free text.
|
|
17
|
+
|
|
18
|
+
## Performance Traces
|
|
19
|
+
|
|
20
|
+
Custom performance traces (Firebase Performance shape) wrap the spans the user *feels*: app start, screen render, and the gateway round-trip as seen from the device. This is client-perceived latency — a different number from the server span the core owns, and the only one that includes the user's network and frame budget. Add frame/jank metrics where smoothness is the product.
|
|
21
|
+
|
|
22
|
+
## What to Capture vs PII
|
|
23
|
+
|
|
24
|
+
- **Capture** error type and stack, the breadcrumb trail, screen, operation, client-perceived latency, and device/OS/app-version context.
|
|
25
|
+
- **Never** put tokens, PII, or full payloads in breadcrumbs or crash context. The sink is third-party — scrub before the event leaves the device.
|
|
26
|
+
|
|
27
|
+
## Where Distributed Tracing Lives
|
|
28
|
+
|
|
29
|
+
The end-to-end request trace is the capability core's, asserted there. The client does not mirror it with spans of its own; it correlates by attaching a request/correlation id it can surface in a breadcrumb, so a crash report points back at the server trace.
|
|
30
|
+
|
|
31
|
+
## Anti-Patterns
|
|
32
|
+
|
|
33
|
+
- **`print` as telemetry.** Dropped in release, structureless in debug.
|
|
34
|
+
- **One error handler.** `FlutterError.onError` alone lets async errors escape uncaught.
|
|
35
|
+
- **PII in crash context.** Emails, tokens, and payloads following the event off-device.
|
|
36
|
+
- **A client tracing story.** Re-implementing spans to mirror the backend — there is no client span surface; correlate, don't duplicate.
|
|
37
|
+
- **Over-instrumenting.** Custom traces nobody reads are cost without a question behind them.
|
package/src/engineer-skills/groundwork-flutter-engineer/references/performance-and-reliability.md
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# Performance & Reliability
|
|
2
|
+
|
|
3
|
+
## Table of Contents
|
|
4
|
+
- [Two Budgets a Client Spends](#two-budgets-a-client-spends)
|
|
5
|
+
- [The Frame Budget](#the-frame-budget)
|
|
6
|
+
- [List Virtualization](#list-virtualization)
|
|
7
|
+
- [Image and Asset Memory](#image-and-asset-memory)
|
|
8
|
+
- [Isolates for Heavy Compute](#isolates-for-heavy-compute)
|
|
9
|
+
- [Startup Time](#startup-time)
|
|
10
|
+
- [Resilience on the Typed Client](#resilience-on-the-typed-client)
|
|
11
|
+
- [Optimistic UI and Offline](#optimistic-ui-and-offline)
|
|
12
|
+
- [Graceful Degradation](#graceful-degradation)
|
|
13
|
+
- [What Lives in the Core, Not Here](#what-lives-in-the-core-not-here)
|
|
14
|
+
- [Anti-Patterns](#anti-patterns)
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Two Budgets a Client Spends
|
|
19
|
+
|
|
20
|
+
Performance is a budget spent deliberately, not "fast enough" tuned afterward (`docs/principles/quality/performance.md`). A client spends two budgets. The **frame budget** is local: 16ms to produce a frame at 60Hz, 8ms at 120Hz, and a build that overruns it drops the frame the user feels as jank. The **round-trip budget** is remote: the time from a tap to a rendered result, most of which is the gateway call the app does not control. Allocate both top-down — decide the interaction's target before building it — and measure the tail, not the average: the one stutter in a scroll is the experience users remember (`docs/principles/quality/performance.md`).
|
|
21
|
+
|
|
22
|
+
## The Frame Budget
|
|
23
|
+
|
|
24
|
+
Jank is a build that does too much, too often. The mechanics of keeping builds cheap live in the widget and state references; this is the performance lens on them:
|
|
25
|
+
|
|
26
|
+
- **`const` and extracted widgets** canonicalise subtrees the framework skips on rebuild — the cheapest performance work available (`references/widgets-and-composition.md`).
|
|
27
|
+
- **`RepaintBoundary`** isolates a frequently-repainting subtree (an animation, a progress indicator) so its repaint does not invalidate the rest of the layer tree. Wrap the moving part, not the whole screen — a boundary has its own cost, so it earns its place only where repaint actually churns.
|
|
28
|
+
- **Narrow rebuild scope.** A god-provider per screen rebuilds everything when one field changes; split providers by concern so the graph recomputes at the right granularity (`references/state-management.md`). Watch the narrowest provider a widget needs, never the whole state object for one field.
|
|
29
|
+
- **Pure builds.** A `build` that fires work or mutates state turns rebuild cadence into behaviour and into cost (`references/widgets-and-composition.md`).
|
|
30
|
+
|
|
31
|
+
Profile before you optimise — the obvious cause of a jank is usually wrong. Flutter DevTools' timeline and the raster/UI thread split tell you whether a frame overran in build or in paint; the "obvious" bottleneck almost always isn't (`docs/principles/quality/performance.md`).
|
|
32
|
+
|
|
33
|
+
## List Virtualization
|
|
34
|
+
|
|
35
|
+
A scrollable collection of unknown or large length is built lazily — `ListView.builder` / `GridView.builder` / `SliverList`, never a `ListView(children: [...])` that constructs every row up front. Eager construction allocates the whole list before the first frame and is the most common source of scroll jank and startup memory spikes. Give reorderable or filterable rows a `ValueKey(item.id)` so element state follows identity through rebuilds (`references/widgets-and-composition.md`).
|
|
36
|
+
|
|
37
|
+
## Image and Asset Memory
|
|
38
|
+
|
|
39
|
+
Decoded images dominate a client's memory; a full-resolution image rendered into a thumbnail wastes most of what it decoded. Constrain the decode, not just the display:
|
|
40
|
+
|
|
41
|
+
```dart
|
|
42
|
+
Image.network(
|
|
43
|
+
url,
|
|
44
|
+
cacheWidth: 320, // decode to the size actually shown, not the source resolution
|
|
45
|
+
fit: BoxFit.cover,
|
|
46
|
+
);
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Size on-disk and remote assets to their rendered footprint, and let the framework's image cache evict — do not hold decoded bytes in app state. Oversized decodes are a memory budget overrun the same way an unbounded queue is a latency bomb.
|
|
50
|
+
|
|
51
|
+
## Isolates for Heavy Compute
|
|
52
|
+
|
|
53
|
+
The UI isolate renders frames; anything that can take longer than a frame must not run on it. Parsing a large payload, hashing, cryptographic work, image transforms — move them to a background isolate with `compute()` or a long-lived `Isolate`, so the work never blocks the frame loop:
|
|
54
|
+
|
|
55
|
+
```dart
|
|
56
|
+
final parsed = await compute(parseLargeReport, rawBytes);
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
This is the client's version of the hot-path discipline: the scoped, profiled paths that demand care, not every function (`docs/principles/quality/performance.md`). Repository translation of an ordinary payload stays on the UI isolate; reach for an isolate when a profile shows the work itself overrunning the frame.
|
|
60
|
+
|
|
61
|
+
## Startup Time
|
|
62
|
+
|
|
63
|
+
Cold start is the first interaction, and it spends the round-trip and frame budgets at once. Keep `main()` and the first frame thin: `ProviderScope` at the root and nothing else, no synchronous I/O before `runApp`, and defer non-critical provider initialisation until after the first frame paints. Render a skeleton from the first frame and let real data arrive into it — the app should be visible before it is fully loaded, never blank while it fetches.
|
|
64
|
+
|
|
65
|
+
## Resilience on the Typed Client
|
|
66
|
+
|
|
67
|
+
Reliability is designed in, not hoped for (`docs/principles/quality/reliability.md`). For a client, that discipline lives at the one seam that talks to the gateway — the `Dio` instance and the repositories above it (`references/data-and-contracts.md`):
|
|
68
|
+
|
|
69
|
+
- **Timeout always.** Every outbound call has connect and receive timeouts set on the one configured `Dio` instance — a hung connection otherwise holds a loading spinner forever.
|
|
70
|
+
- **Retry transient failures only, with jitter.** A retry interceptor retries connection errors and timeouts with bounded, jittered exponential backoff; jitter is not optional, because a fleet of clients retrying in lockstep is a thundering herd against the gateway. A `4xx` is permanent — retrying it wastes the budget. Retry at this one layer, not in every repository on top of it.
|
|
71
|
+
- **Map transport failures to domain states.** `DioException` never crosses above the data layer; the repository turns an unreachable gateway into a domain state the UI renders, and lets a contract violation surface as an error (`references/data-and-contracts.md`).
|
|
72
|
+
|
|
73
|
+
## Optimistic UI and Offline
|
|
74
|
+
|
|
75
|
+
A mobile process dies constantly and a network drops mid-interaction; state the app cannot rebuild from the core or the route is state it silently loses (`references/state-management.md`). Two patterns follow:
|
|
76
|
+
|
|
77
|
+
- **Optimistic update.** Reflect the user's action in state immediately, fire the mutation, and reconcile on the result — invalidate the provider to re-derive from the repository on success, roll back on failure. A Riverpod `Mutation` surfaces the pending/error lifecycle without a hand-rolled boolean (`references/state-management.md`).
|
|
78
|
+
- **Offline read-through.** A repository that caches can serve its last-known value while a refresh is in flight, so a dropped connection degrades to stale-but-present rather than empty. Mark served-stale state so the UI can show it honestly.
|
|
79
|
+
|
|
80
|
+
## Graceful Degradation
|
|
81
|
+
|
|
82
|
+
Every feature that depends on the gateway has a defined behaviour when the gateway is down — decided at design time, built alongside the happy path, never "we'll figure out what to show later" (`docs/principles/quality/reliability.md`). A view renders `loading` / `error` / `data` exhaustively, and the error case is a real designed state with a retry affordance, not a thrown exception reaching the user (`references/state-management.md`). A non-critical panel whose data is unavailable renders its own empty or "unavailable" state while the rest of the screen works — partial function beats a blank screen.
|
|
83
|
+
|
|
84
|
+
## What Lives in the Core, Not Here
|
|
85
|
+
|
|
86
|
+
Server reliability patterns do not belong on a client, and importing them here is miscategorised work. They live in the capability core and its services (`docs/principles/quality/reliability.md`):
|
|
87
|
+
|
|
88
|
+
- **SLOs and error budgets** are defined per service in the core, measured server-side. The client contributes client-observed latency to that picture; it does not own the objective.
|
|
89
|
+
- **Load shedding** protects the server from overload and must work regardless of client behaviour — it is a server-side backstop, not something a well-behaved client implements for it.
|
|
90
|
+
- **Circuit breakers** are earned against slow downstreams and tuned against real traffic at the service layer. A client's bounded, budgeted retry is the right amount of resilience at the edge; a breaker estimated locally by one app trips on noise. The business rules about a failure — whether it is recoverable, the retry budget — are proven in the core, and the surface renders the result.
|
|
91
|
+
|
|
92
|
+
## Anti-Patterns
|
|
93
|
+
|
|
94
|
+
- **Jank shipped, fixed later.** If you ship a stuttering scroll, users remember slow.
|
|
95
|
+
- **`ListView(children: [...])` for a dynamic or long list.** Eager construction of every row — use the `.builder` constructor.
|
|
96
|
+
- **Heavy work on the UI isolate.** A parse or hash that overruns a frame freezes the app — `compute()` it.
|
|
97
|
+
- **Full-resolution decode for a thumbnail.** Decoded image memory, wasted — constrain `cacheWidth`/`cacheHeight`.
|
|
98
|
+
- **Retry without jitter, or at every layer.** Lockstep retries are a thundering herd; layered retries multiply one tap into a storm.
|
|
99
|
+
- **No degraded state.** A feature with no defined behaviour when the gateway is down ships a blank screen or a raw exception.
|
|
100
|
+
- **Reimplementing server reliability on the client.** Load shedding, SLOs, and reflexive circuit breakers belong in the core, not the surface.
|