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.
Files changed (70) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/bin/groundwork.js +86 -17
  3. package/dist/src/generators/system-test-runner/generator.js +52 -4
  4. package/dist/src/generators/system-test-runner/generator.js.map +1 -1
  5. package/package.json +1 -1
  6. package/src/docs/principles/design/usability-and-ux.md +11 -0
  7. package/src/docs/principles/foundations/testing.md +32 -6
  8. package/src/docs/principles/index.md +2 -1
  9. package/src/docs/principles/quality/observability.md +2 -2
  10. package/src/engineer-skills/groundwork-electron-engineer/SKILL.md +6 -1
  11. package/src/engineer-skills/groundwork-electron-engineer/references/documentation.md +126 -0
  12. package/src/engineer-skills/groundwork-electron-engineer/references/observability.md +37 -0
  13. package/src/engineer-skills/groundwork-electron-engineer/references/performance-and-reliability.md +80 -0
  14. package/src/engineer-skills/groundwork-electron-engineer/references/testing-and-smoke.md +22 -0
  15. package/src/engineer-skills/groundwork-electron-engineer/sync-anchor.md +12 -4
  16. package/src/engineer-skills/groundwork-flutter-engineer/SKILL.md +7 -1
  17. package/src/engineer-skills/groundwork-flutter-engineer/references/documentation.md +122 -0
  18. package/src/engineer-skills/groundwork-flutter-engineer/references/observability.md +37 -0
  19. package/src/engineer-skills/groundwork-flutter-engineer/references/performance-and-reliability.md +100 -0
  20. package/src/engineer-skills/groundwork-flutter-engineer/references/security.md +96 -0
  21. package/src/engineer-skills/groundwork-flutter-engineer/references/testing.md +25 -0
  22. package/src/engineer-skills/groundwork-flutter-engineer/sync-anchor.md +13 -4
  23. package/src/engineer-skills/groundwork-go-engineer/SKILL.md +5 -2
  24. package/src/engineer-skills/groundwork-go-engineer/references/documentation.md +130 -0
  25. package/src/engineer-skills/groundwork-go-engineer/references/testing.md +63 -1
  26. package/src/engineer-skills/groundwork-go-engineer/sync-anchor.md +13 -4
  27. package/src/engineer-skills/groundwork-nextjs-engineer/SKILL.md +6 -1
  28. package/src/engineer-skills/groundwork-nextjs-engineer/references/accessibility.md +111 -0
  29. package/src/engineer-skills/groundwork-nextjs-engineer/references/observability.md +48 -0
  30. package/src/engineer-skills/groundwork-nextjs-engineer/references/security.md +131 -0
  31. package/src/engineer-skills/groundwork-nextjs-engineer/references/testing.md +59 -1
  32. package/src/engineer-skills/groundwork-nextjs-engineer/references/ux-principles.md +1 -49
  33. package/src/engineer-skills/groundwork-nextjs-engineer/sync-anchor.md +10 -3
  34. package/src/engineer-skills/groundwork-python-engineer/SKILL.md +5 -2
  35. package/src/engineer-skills/groundwork-python-engineer/references/security.md +148 -0
  36. package/src/engineer-skills/groundwork-python-engineer/references/testing.md +40 -1
  37. package/src/engineer-skills/groundwork-python-engineer/sync-anchor.md +11 -4
  38. package/src/generators/electron-app/docs/principles/stack/electron/index.md +2 -0
  39. package/src/generators/electron-app/files/tests/smoke/app.spec.ts.template +73 -8
  40. package/src/generators/flutter-app/docs/principles/stack/flutter/testing.md +14 -2
  41. package/src/generators/flutter-app/files/integration_test/app_test.dart.template +46 -12
  42. package/src/generators/go-microservice/docs/principles/stack/go/testing.md +17 -1
  43. package/src/generators/python-microservice/docs/principles/stack/python/testing.md +41 -0
  44. package/src/generators/system-test-runner/NATIVE-CHECK-CONTRACT.md +20 -0
  45. package/src/generators/system-test-runner/files/tests/system/test_render_smoke.py.template +30 -0
  46. package/src/generators/system-test-runner/generator.ts +58 -4
  47. package/src/generators/workspace-dev-cli/cli-src/dist/dev-bundle.js +1 -1
  48. package/src/hidden-skills/code-intelligence.md +6 -0
  49. package/src/hidden-skills/groundwork-architect/SKILL.md +1 -1
  50. package/src/hidden-skills/groundwork-architect/sync-anchor.md +2 -2
  51. package/src/hidden-skills/groundwork-bet/briefs/acceptance-auditor.md +68 -0
  52. package/src/hidden-skills/groundwork-bet/briefs/blind-reviewer.md +56 -0
  53. package/src/hidden-skills/groundwork-bet/briefs/coverage-auditor.md +95 -0
  54. package/src/hidden-skills/groundwork-bet/briefs/edge-case-tracer.md +64 -0
  55. package/src/hidden-skills/groundwork-bet/briefs/experience-auditor.md +83 -0
  56. package/src/hidden-skills/groundwork-bet/briefs/slice-worker.md +92 -26
  57. package/src/hidden-skills/groundwork-bet/instructions.md +4 -4
  58. package/src/hidden-skills/groundwork-bet/templates/bet-progress-test.md +16 -27
  59. package/src/hidden-skills/groundwork-bet/templates/change-proposal.md +1 -1
  60. package/src/hidden-skills/groundwork-bet/templates/decomposition/milestone-index.md +12 -16
  61. package/src/hidden-skills/groundwork-bet/templates/decomposition/slice.md +4 -8
  62. package/src/hidden-skills/groundwork-bet/templates/technical-design/03-api-design.md +1 -1
  63. package/src/hidden-skills/groundwork-bet/workflows/01-discovery.md +3 -1
  64. package/src/hidden-skills/groundwork-bet/workflows/02-design.md +11 -1
  65. package/src/hidden-skills/groundwork-bet/workflows/03-decomposition.md +60 -64
  66. package/src/hidden-skills/groundwork-bet/workflows/04-delivery.md +75 -42
  67. package/src/hidden-skills/groundwork-bet/workflows/05-validation.md +18 -7
  68. package/src/hidden-skills/groundwork-designer/sync-anchor.md +1 -1
  69. package/src/hidden-skills/groundwork-persona/instructions.md +11 -0
  70. package/src/hidden-skills/groundwork-review/checklists/implementation-readiness.md +1 -0
@@ -0,0 +1,48 @@
1
+ # Observability
2
+
3
+ A Next.js app has two telemetry surfaces, and they obey different rules. The **server side** — route handlers, Server Actions, `instrumentation.ts` — is genuinely backend-like: it emits OpenTelemetry spans the same way a service does. The **browser client** emits user-experience signal: Core Web Vitals and errors from a device you do not control. Instrument each for the questions you will actually ask in an incident; the signal that proves a path correct in test is the signal you debug with in production (`docs/principles/quality/observability.md`).
4
+
5
+ ## Server Side — OpenTelemetry Spans
6
+
7
+ `instrumentation.ts` is the registration point; from there, route handlers and Server Actions emit spans through the OTel SDK to a collector. This half follows the backend canon unchanged: vendor lock-in lives at the collector boundary, not in application code.
8
+
9
+ - **Trace-driven.** Sketch the span a server path should produce — name, attributes, parent — before writing the handler. The instrumentation design shapes the code.
10
+ - **Assert what you debug.** The in-memory span exporter that proves a route's trace in `references/testing.md` (Trace Assertions) reads the same span a dashboard or SLO does. A critical-path span a query depends on is part of the contract, not decoration — a missing span is a test failure.
11
+ - **Structured logs carry the trace.** Server logs are JSON and inject `trace_id`/`span_id` from the active context, so a log line pivots to the trace that produced it. Sample debug/info; never sample errors.
12
+
13
+ ## Client Side — Web Vitals and Error Reporting
14
+
15
+ The browser cannot run a collector. It reports field signal to a sink.
16
+
17
+ - **Core Web Vitals (RUM).** Report LCP, INP, CLS, and TTFB from real sessions via `useReportWebVitals` to a sink. These are the user's experience; a green Lighthouse lab score is not — it measures one synthetic load, not the field.
18
+
19
+ ```tsx
20
+ // app/_components/web-vitals.tsx — mounted once in the root layout
21
+ 'use client';
22
+ import { useReportWebVitals } from 'next/web-vitals';
23
+
24
+ export function WebVitals() {
25
+ useReportWebVitals(({ name, value, id }) => {
26
+ navigator.sendBeacon('/api/rum', JSON.stringify({ name, value, id }));
27
+ });
28
+ return null;
29
+ }
30
+ ```
31
+
32
+ - **Error reporting.** `error.tsx` and `global-error.tsx` catch render errors; a `window` `error`/`unhandledrejection` handler catches the rest. Both forward to the sink. An error boundary that renders a fallback but reports nothing is a silent failure — the user sees the broken state and you never do.
33
+ - **Structured events, not `console.log`.** `console` output in a production bundle is not telemetry; emit structured events the sink can query.
34
+ - **Connect the halves.** The browser `fetch` can inject a W3C `traceparent` so a client interaction links to the server trace it triggered — one causal thread across the boundary.
35
+
36
+ ## What to Capture vs PII
37
+
38
+ - **Capture** route, status, duration, the span attributes a dashboard queries, the web-vital name and value, error type and stack.
39
+ - **Never** put tokens, full request bodies, or emails/PII in span attributes, breadcrumbs, or client events. The sink is third-party — redact at the edge.
40
+ - **Cardinality is a design choice.** High cardinality on server traces where it is queryable; keep it off client metric dimensions, which multiply by every session.
41
+
42
+ ## Anti-Patterns
43
+
44
+ - **`console.log` as telemetry.** It is noise in the field, not a signal you can query.
45
+ - **Boundary without report.** A fallback UI that swallows the error instead of forwarding it.
46
+ - **Lab metrics as field RUM.** Lighthouse is a synthetic check, not what users experienced.
47
+ - **A collector in the bundle.** The client reports to a sink; it does not run backend OTel infrastructure.
48
+ - **Over-instrumenting.** A span or metric nobody will query during an incident is cost and clutter — instrument the questions, not the surface area.
@@ -0,0 +1,131 @@
1
+ # Security
2
+
3
+ ## Table of Contents
4
+ - [The Posture](#the-posture)
5
+ - [XSS: Trust React's Escaping](#xss-trust-reacts-escaping)
6
+ - [The Client Bundle Is Public](#the-client-bundle-is-public)
7
+ - [Server Action Input Validation](#server-action-input-validation)
8
+ - [Auth and Sessions: httpOnly Cookies](#auth-and-sessions-httponly-cookies)
9
+ - [CSRF on Mutations](#csrf-on-mutations)
10
+ - [SSRF on Server Fetches](#ssrf-on-server-fetches)
11
+ - [Content-Security-Policy](#content-security-policy)
12
+ - [Security Review Checklist](#security-review-checklist)
13
+
14
+ ---
15
+
16
+ ## The Posture
17
+
18
+ A Next.js app runs in two places at once, and the boundary between them is the whole security model. Server Components, Server Actions, and route handlers run on a trusted server; everything else ships to a browser the user controls. Code, props, and environment values that cross into the client are public — assume an attacker reads the bundle and replays every request. This file is the Next.js idiom of the framework security canon (`docs/principles/quality/security.md`); when this file and the canon disagree, the canon wins and this file is the one to fix.
19
+
20
+ The single discipline underneath every rule below: validate and authorize on the server, never trust the client. Client-side validation is UX; the server check is the security boundary (`references/mutations-and-forms.md` → Error Flow).
21
+
22
+ ## XSS: Trust React's Escaping
23
+
24
+ React escapes every value interpolated into JSX by default — `{userValue}` cannot break out of its text node. XSS re-enters only when you opt out of that escaping.
25
+
26
+ - `dangerouslySetInnerHTML` is the named opt-out. Render it only with HTML you produced or sanitised server-side (a vetted sanitiser such as DOMPurify); never with a value that originated from a user or an API.
27
+ - A `javascript:` or `data:` URL in an `href`/`src` is script. Validate that user-supplied URLs are `https:` before rendering them.
28
+ - Untrusted JSON parsed and injected as markup is the same hole by another route — keep untrusted data as text.
29
+
30
+ ```tsx
31
+ // Hostile — renders attacker markup into a privileged origin
32
+ <div dangerouslySetInnerHTML={{ __html: order.customerNote }} />
33
+
34
+ // Safe — React escapes it; the note is text, not markup
35
+ <div>{order.customerNote}</div>
36
+ ```
37
+
38
+ ## The Client Bundle Is Public
39
+
40
+ Every value reachable from client code is shipped to the browser. The `NEXT_PUBLIC_` prefix is the boundary: a variable with that prefix is inlined into the bundle, and a variable without it is unreadable from any `'use client'` module.
41
+
42
+ - Secrets — API keys, signing secrets, database URLs — never carry the `NEXT_PUBLIC_` prefix and are read only in server code (Server Components, Server Actions, route handlers, `lib/api.ts` on the server).
43
+ - A secret consumed by the client is a secret leaked. If a feature "needs" a key in the browser, the call belongs on the server: route it through a Server Action and keep the key server-side.
44
+ - The downward dependency graph (`references/architecture.md`) keeps this honest — schemas and the API client never import client-only code, so server secrets have no path into a client component by construction.
45
+
46
+ ```ts
47
+ const apiKey = process.env.UPSTREAM_API_KEY; // server-only — correct
48
+ const pub = process.env.NEXT_PUBLIC_ANALYTICS_ID; // inlined into the bundle — public by design
49
+ ```
50
+
51
+ ## Server Action Input Validation
52
+
53
+ A Server Action is a public POST endpoint — anyone can invoke it with any payload, regardless of what the form allows. Every Server Action re-validates its input with the same Zod schema the form uses, on the server, before any work (`references/type-system.md` → Zod as the Contract).
54
+
55
+ ```tsx
56
+ 'use server';
57
+
58
+ export async function updateOrderAction(
59
+ id: string,
60
+ formData: FormData,
61
+ ): Promise<ActionResult<Order>> {
62
+ const principal = await requirePrincipal(); // 1. authenticate the caller
63
+ const parsed = updateOrderSchema.safeParse({ // 2. validate every field
64
+ quantity: Number(formData.get('quantity')),
65
+ note: formData.get('note'),
66
+ });
67
+ if (!parsed.success) {
68
+ return { data: null, error: parsed.error.issues[0].message };
69
+ }
70
+ if (!(await canEditOrder(principal, id))) { // 3. authorize this action on this resource
71
+ return { data: null, error: 'Not found' }; // deny as not-found — do not confirm existence
72
+ }
73
+ // ... mutate, revalidatePath, return ActionResult
74
+ }
75
+ ```
76
+
77
+ The order is non-negotiable: authenticate, validate, authorize, then act. A Server Action that trusts the form's own validation is unauthenticated and unvalidated.
78
+
79
+ ## Auth and Sessions: httpOnly Cookies
80
+
81
+ The session token lives in an `httpOnly`, `Secure`, `SameSite` cookie set by the server — never in `localStorage` or a JS-readable cookie. A token JavaScript can read is a token an XSS payload can exfiltrate; `httpOnly` removes it from script's reach entirely.
82
+
83
+ ```ts
84
+ cookies().set('session', token, {
85
+ httpOnly: true,
86
+ secure: true,
87
+ sameSite: 'lax', // lax/strict is the baseline CSRF defence for cookie auth
88
+ path: '/',
89
+ });
90
+ ```
91
+
92
+ - Authentication runs through a proven provider (OIDC); the app does not hand-roll JWT verification or session crypto. Auth is boring technology — `docs/principles/system-design/identity-and-access.md`.
93
+ - The session is read and verified on the server (Server Component / Server Action / route handler), and the proxy (`proxy.ts`) gates protected segments. A client-side `isLoggedIn` flag is UX, never a gate.
94
+
95
+ ## CSRF on Mutations
96
+
97
+ Cookie-authenticated mutations need CSRF protection, because the browser attaches the session cookie to cross-site requests automatically. The first layer is `SameSite=lax`/`strict` on the session cookie, which blocks the classic cross-site form post.
98
+
99
+ - Server Actions carry framework-level protection: Next.js verifies an origin/action token, so a third-party page cannot replay one. Keep that — do not expose the same mutation as an unprotected route handler that bypasses it.
100
+ - A route handler that mutates state under cookie auth validates the `Origin` header against an allowlist (and uses a CSRF token if it cannot rely on `SameSite`). A `GET` never mutates.
101
+
102
+ ## SSRF on Server Fetches
103
+
104
+ Server-side `fetch` runs from inside your network, so a fetch aimed at an input-supplied URL is an SSRF vector — an attacker points it at cloud metadata endpoints or internal services.
105
+
106
+ - The API client (`lib/api.ts`) targets a configured base URL, never a host taken from the request. Keep outbound targets constant or allowlisted.
107
+ - A feature that must fetch a user-supplied URL (a webhook, an image proxy) validates the resolved host against an allowlist and rejects non-`https:` schemes and private address ranges before the call.
108
+
109
+ ## Content-Security-Policy
110
+
111
+ A strict CSP is the second line that contains an XSS that slips past escaping. Set it on responses via the proxy (`proxy.ts`) or `next.config.ts` headers.
112
+
113
+ ```
114
+ default-src 'self'; script-src 'self'; object-src 'none'; base-uri 'self'; frame-ancestors 'none'
115
+ ```
116
+
117
+ - `frame-ancestors 'none'` (or an allowlist) blocks clickjacking; pair it with `X-Content-Type-Options: nosniff`.
118
+ - Avoid `'unsafe-inline'` in `script-src`; use a nonce for any inline script Next.js requires. A CSP containing `*` in `script-src` is not a CSP.
119
+
120
+ ## Security Review Checklist
121
+
122
+ For any PR touching Server Actions, route handlers, auth, `proxy.ts`, or `dangerouslySetInnerHTML`:
123
+
124
+ - [ ] No `dangerouslySetInnerHTML` on unsanitised or user-origin HTML
125
+ - [ ] No secret without the `NEXT_PUBLIC_` decision being deliberate; no secret read in a `'use client'` module
126
+ - [ ] Every Server Action authenticates, Zod-validates, and authorizes before acting
127
+ - [ ] Session token in an `httpOnly` `Secure` cookie — never `localStorage`
128
+ - [ ] Mutating route handlers check `Origin`; `GET` never mutates
129
+ - [ ] Server fetches target a constant/allowlisted host — no input-supplied URL unchecked
130
+ - [ ] CSP present and not weakened to `*`/`unsafe-inline` script
131
+ - [ ] Authorization decided on the server; no client flag used as a gate
@@ -15,6 +15,10 @@
15
15
 
16
16
  ## Testing Philosophy
17
17
 
18
+ The frontend shape is the **testing trophy** (Kent Dodds): a thin static-analysis base, a few unit tests, a fat middle of integration tests that render real component trees against a mocked network, and a thin layer of end-to-end checks. It is the frontend idiom of the framework testing canon (`docs/principles/foundations/testing.md`) — the backends run the honeycomb, the frontend runs the trophy, and both put the weight on integration rather than isolated units. When this file and the canon disagree, the canon wins and this file is the one to fix.
19
+
20
+ Above the trophy sits the front-door proof: drive the real running app the way a user does, end to end against the real backend — Playwright against the running stack — because component and integration tests that each pass against an MSW-mocked network can still assemble into an app that does nothing on the real API. That is also the fake-needs-a-real-test rule: every MSW handler or fixture standing in for a real endpoint is a debt, and some real integration or e2e test against the actual network path must pay it. Seeded inputs are fine; what cannot stand is a mock with no real test behind it — that is a green light wired to nothing (`docs/principles/foundations/testing.md`).
21
+
18
22
  Tests in the Next.js application follow four rules:
19
23
 
20
24
  1. **Vitest + React Testing Library** for all component and hook tests
@@ -413,6 +417,59 @@ it('shows error message on server failure', async () => {
413
417
 
414
418
  ---
415
419
 
420
+ ## Trace Assertions
421
+
422
+ The app ships OpenTelemetry through `instrumentation.ts`, so server-side work — route handlers and Server Actions — emits spans. Where a slice adds a server path whose trace a dashboard or SLO depends on, assert on it with an **in-memory span exporter** rather than trusting the instrumentation silently. This is server-side only; component and hook tests assert on rendered behaviour, not traces.
423
+
424
+ ```ts
425
+ import {
426
+ BasicTracerProvider,
427
+ InMemorySpanExporter,
428
+ SimpleSpanProcessor,
429
+ } from '@opentelemetry/sdk-trace-base';
430
+
431
+ const exporter = new InMemorySpanExporter();
432
+ const provider = new BasicTracerProvider({ spanProcessors: [new SimpleSpanProcessor(exporter)] });
433
+ provider.register();
434
+
435
+ // invoke the route handler / server action, then:
436
+ const names = exporter.getFinishedSpans().map((s) => s.name);
437
+ expect(names).toContain('POST /v1/meetings'); // the entry span exists, trace connected
438
+ ```
439
+
440
+ Assert the spans that must exist and the attributes a query depends on; let the rest float — pinning the whole span tree couples the test to implementation.
441
+
442
+ ## Mutation Testing — the assertion-quality read-out
443
+
444
+ Coverage tells you a line ran; it does not tell you an assertion checked it — a 100% covered `lib/utils.ts` can still assert nothing. **StrykerJS** is the read-out that proves the assertions bite: it mutates the code and confirms a test fails. Treat it as a **signal, never a gate**, and run it incrementally on changed code (`stryker run --incremental`, which diffs against the cached `reports/stryker-incremental.json` — there is no `--since` flag). Point it at the dense pure logic first (`lib/utils.ts`, schema validators, formatters); a surviving mutant there is the missing assertion to add. This is also the antidote to AI-generated component tests, whose oracle is lifted from the current markup and so cement bugs as expected.
445
+
446
+ ## Generate the Inputs You Can't Enumerate
447
+
448
+ Example-based tests check the cases you thought of; the bugs live in the cases you didn't (canon principle 7). Two generative surfaces apply to a Next.js app:
449
+
450
+ - **Property-based tests with `fast-check`** for the dense pure logic — formatters, `lib/utils.ts`, Zod-adjacent transforms, anything with an invariant (a round-trip, a sort that must stay stable, a parse that must never throw). State the property; fast-check generates and shrinks counterexamples. This is the highest-leverage complement to example-based unit tests: one property covers an infinity of inputs.
451
+
452
+ ```ts
453
+ import fc from 'fast-check';
454
+
455
+ it('formatDuration never throws and always ends in m', () => {
456
+ fc.assert(
457
+ fc.property(fc.nat({ max: 100_000 }), (minutes) => {
458
+ const out = formatDuration(minutes);
459
+ expect(out).toMatch(/m$/);
460
+ }),
461
+ );
462
+ });
463
+ ```
464
+
465
+ - **Schemathesis at the API boundary.** Route handlers backed by an OpenAPI schema are the bridge between contract testing and property fuzzing: point Schemathesis at the spec and it derives a semantics-aware fuzzer that finds materially more defects than example-based API tests for the cost of pointing it at the schema. Run it against the app's route handlers (`/api/*`) in a dedicated lane, not on every component PR.
466
+
467
+ Reach for these where invariants are real. Presentational components have no invariant to state — test them with example-based RTL renders.
468
+
469
+ ## Naming Tests by Behaviour
470
+
471
+ A test name must let an on-call engineer form a hypothesis from the failure log alone, without opening the file. State the behaviour and the condition — `should [expected outcome] when [condition]` — not the implementation. `renders correctly` and `works` say nothing the dashboard doesn't already show; `shows the retry button when the meetings request fails` does. The format serves the goal; a name that states behaviour and condition in another shape is fine.
472
+
416
473
  ## Test Commands
417
474
 
418
475
  | Command | Purpose |
@@ -425,9 +482,10 @@ it('shows error message on server failure', async () => {
425
482
 
426
483
  ## Bet Slice Rollout — the permanent tests a slice owes
427
484
 
428
- When a bet slice's progress tests go green, the slice rolls out permanent coverage before it closes (bet workflow, Delivery step 5). The bet-progress tests prove the capability once and are archived; these stay.
485
+ When a bet slice's progress tests go green, the slice-worker rolls out permanent coverage as part of the same slice, before the driver reviews it (bet workflow, Delivery). The bet-progress tests prove the capability once and are archived; these stay.
429
486
 
430
487
  - **Interface test (always).** One Playwright test per user-observable behaviour the slice delivered, using the page objects under `tests/system/pages/` — selectors live in the page object, assertions in the test.
431
488
  - **Component tests (when state earned them).** Components the slice introduced with conditional rendering, optimistic updates, or error states get component-level tests; purely presentational markup does not.
432
489
  - **Accessibility coverage (when the slice added a surface).** A new screen or interactive flow extends the a11y smoke — axe scan clean and keyboard path exercised — because regressions here are invisible to every other test type.
433
490
  - **Server action / route tests (when the slice added them).** Server actions and route handlers the slice introduced get request-level tests with Zod schema failures exercised, not just the happy path.
491
+ - **Critical-path trace assertions (when the slice added an instrumented server path).** A route handler or Server Action whose trace a dashboard or SLO depends on pins it with an in-memory-exporter test: the entry span exists and the trace stays connected. A missing span is a test failure, not an instrumentation TODO.
@@ -150,55 +150,7 @@ Actions should reveal contextually — on hover, on selection, or on focus. Don'
150
150
 
151
151
  ## Accessibility
152
152
 
153
- Accessibility is not optional — it is a baseline requirement.
154
-
155
- ### Contrast
156
-
157
- - Body text must meet **WCAG AA** (4.5:1 contrast ratio)
158
- - Large text (18px+ or 14px+ bold) must meet **3:1**
159
- - Interactive elements must meet **3:1** against their background
160
- - Test in both dark and light themes
161
-
162
- ### Focus States
163
-
164
- - All interactive elements must have a visible focus indicator
165
- - Focus indicators must contrast with the background (not just browser default outline)
166
- - Tab order must follow visual layout order
167
-
168
- ```css
169
- :focus-visible {
170
- outline: 2px solid var(--color-accent);
171
- outline-offset: 2px;
172
- border-radius: var(--radius-sm);
173
- }
174
- ```
175
-
176
- ### Semantic HTML & ARIA
177
-
178
- - Use semantic elements: `<nav>`, `<main>`, `<article>`, `<section>`, `<aside>`, `<header>`, `<footer>`
179
- - Icon-only buttons must have `aria-label`:
180
- ```tsx
181
- <button onClick={handleClose} aria-label="Close dialog">
182
- <X size={20} />
183
- </button>
184
- ```
185
- - Use `role="alert"` for error messages that must be announced by screen readers
186
- - Never use `div` or `span` for clickable elements — use `button` or `a`
187
-
188
- ### Colour Independence
189
-
190
- - Never communicate information through colour alone
191
- - Pair colour indicators with icons or text labels:
192
- ```tsx
193
- // Bad — colour-only status
194
- <span className="text-success">●</span>
195
-
196
- // Good — colour + icon + text
197
- <span className="text-success flex items-center gap-1">
198
- <CheckCircle size={16} aria-hidden />
199
- Completed
200
- </span>
201
- ```
153
+ Accessibility is a merge gate, not optional polish semantic HTML, keyboard reachability, WCAG AA contrast, and labelled forms are a baseline requirement on every surface. See `references/accessibility.md` for the full reference.
202
154
 
203
155
  ---
204
156
 
@@ -1,9 +1,16 @@
1
1
  # Sync Anchor
2
2
 
3
- This file pins the principle files this skill embeds. When any listed file
4
- changes, this skill must be reviewed in the same commit. CI verifies the
5
- hashes match.
3
+ This file pins the principle files this skill embeds both the per-stack
4
+ TypeScript/frontend idiom doc 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. CI verifies the hashes match.
6
7
 
7
8
  | Principle file | SHA-256 | Last reviewed |
8
9
  |---|---|---|
9
10
  | src/generators/nextjs-app/docs/principles/stack/typescript/frontend.md | 98232d067ad03c08d6c1ca5f2caec30e7c3400da55c3afb7754482bc121d7554 | 2026-05-26 |
11
+ | src/docs/principles/foundations/testing.md | 205ac40d4c643e7b61cf1e4295df8a7b8b46dcd7c81b857aa8c642ea353f62ef | 2026-06-27 |
12
+ | src/docs/principles/quality/observability.md | 8aa60e213ba03e989c93263153e3a1ac10b2336f6d0360c394f473660d565a0b | 2026-06-26 |
13
+ | src/docs/principles/quality/security.md | 61157d97677142737ec537954dc5aaad7a04012cc8a3dcc855e2d324287fdc64 | 2026-06-26 |
14
+ | src/docs/principles/quality/performance.md | 18b6d3391c57d97342068f9f1da732b24de4221489d0459bb6ad8900fac0a02e | 2026-06-26 |
15
+ | src/docs/principles/quality/accessibility.md | f921e7bf6256bc105b127b841d0a30af8a70ad1ddd7632d492589f052e6501b2 | 2026-06-26 |
16
+ | src/docs/principles/foundations/documentation.md | 8b576072eaf4970f1251b560781e3e755c864a7920faa599b2834c921cbb8734 | 2026-06-26 |
@@ -21,10 +21,11 @@ Python backend execution router for service repositories. Durable engineering gu
21
21
  ## Operating Contract
22
22
 
23
23
  1. Load reference docs from `references/` for architectural and implementation guidance. Treat the current repository's code, specs, and generated contracts as the source of truth for naming, structure, and behavior.
24
- 2. Inspect the current repository before naming packages, commands, import paths, schemas, or generated files.
24
+ 2. Orient with the repo map and Serena before reading widely (see Required First Checks) — find the hubs, then navigate by symbol. Inspect the current repository before naming packages, commands, import paths, schemas, or generated files.
25
25
  3. Load the smallest reference set that explains the task. Add more context only when the task crosses a boundary.
26
26
  4. Preserve the service's dependency direction and public contracts. Code implements OpenAPI, database migrations, event schemas, and documented architecture — it does not invent them.
27
- 5. Coordinate with adjacent skills when another skill owns the primary decision surface.
27
+ 5. Treat observability as part of the contract, not an afterthought: a critical path emits an unbroken trace, and a missing span is a defect. Route durable engineering policy to the canonical docs (`docs/principles/stack/python/`, and the cross-cutting canon under `docs/principles/quality/` and `docs/principles/foundations/`) rather than restating it in code comments or this skill.
28
+ 6. Coordinate with adjacent skills when another skill owns the primary decision surface.
28
29
 
29
30
  ---
30
31
 
@@ -40,6 +41,7 @@ Before non-trivial Python implementation or review work:
40
41
 
41
42
  | Check | Why |
42
43
  |---|---|
44
+ | **Orient with the repo map + Serena** — refresh `npx groundwork-method repo-map`, read its `centrality` ranking to find the hubs, then navigate them with Serena (`get_symbols_overview` / `find_symbol` / `find_referencing_symbols`) | A blind file crawl misses the structure the map already computed; symbol navigation and reference-aware edits beat grep-and-read. Fall back to ordinary reads only when these are unavailable |
43
45
  | Service package layout and nearby examples for the touched layer | Prevents inventing structure that already has a convention |
44
46
  | `pyproject.toml` for Python version and dependencies | Avoids version-specific advice that contradicts the project |
45
47
  | OpenAPI spec (if HTTP behavior changes) | HTTP contracts are generated — code must match the spec |
@@ -67,6 +69,7 @@ Load only the rows relevant to the current task. Reference files are in the skil
67
69
  | Resilience — timeouts, retries, circuit breakers, health probes | `resilience.md` |
68
70
  | Graceful shutdown, degradation, lifespan management | `resilience.md`, `async-patterns.md` |
69
71
  | Observability — tracing, structured logging, metrics | `observability.md` |
72
+ | Security, auth, secrets, input validation, supply chain, SSRF | `security.md` |
70
73
  | Tests, quality gates, coverage strategy, fixture design | `testing.md` |
71
74
  | Code documentation, docstrings, Pydantic Field docs | `documentation-mcp.md` |
72
75
  | Error handling, exception hierarchy, domain errors | `implementation-patterns.md` |
@@ -0,0 +1,148 @@
1
+ # Security
2
+
3
+ This service is a trust boundary. Everything outside it — clients, webhooks, queue events, upstream APIs, model output — is hostile until validated. This file is the Python idiom of the framework security canon (`docs/principles/quality/security.md`); when this file and the canon disagree, the canon wins and this file is the one to fix.
4
+
5
+ The controls below are enforced at the FastAPI entrypoint and the adapter edge, not scattered through the Domain. The boundary is validated once and explicitly; inside it, the core trusts its own types.
6
+
7
+ ## 1. Input is hostile; validate at the boundary
8
+
9
+ Every inbound payload is a Pydantic model parsed at the route, not a `dict` read field-by-field. Pydantic v2 validation *is* the boundary check — a request that fails parsing never reaches a service.
10
+
11
+ ```python
12
+ from pydantic import BaseModel, Field, EmailStr
13
+
14
+ class CreateOrderRequest(BaseModel):
15
+ model_config = {"extra": "forbid"} # reject unknown fields, never silently drop
16
+
17
+ customer_email: EmailStr
18
+ quantity: int = Field(gt=0, le=1000)
19
+ note: str = Field(default="", max_length=2000)
20
+
21
+ @router.post("/orders")
22
+ async def create_order(body: CreateOrderRequest) -> OrderResponse:
23
+ # body is validated; the service receives a typed domain request, not raw input
24
+ ...
25
+ ```
26
+
27
+ - `extra="forbid"` turns mass-assignment and typo'd fields into a `422`, not a silent accept.
28
+ - Constrain at the type (`gt`, `le`, `max_length`, `EmailStr`, `Literal`), so the constraint travels with the field and cannot be forgotten by a caller.
29
+ - Do not re-validate between internal callers — the core trusts its own dataclasses (`references/implementation-patterns.md` → Strict Typing). One boundary, scrutinised; no defensive re-checks inside.
30
+
31
+ ## 2. Parameterised queries — never string-built SQL
32
+
33
+ SQL injection is closed by construction: the query text is constant and every value is a bound parameter. SQLAlchemy and the driver do the binding; an f-string carrying user input into SQL is a defect.
34
+
35
+ ```python
36
+ from sqlalchemy import select, text
37
+
38
+ # ORM — values are bound, never interpolated
39
+ stmt = select(OrderRow).where(OrderRow.customer_id == customer_id)
40
+
41
+ # Raw SQL when unavoidable — named bind parameters, never an f-string
42
+ await session.execute(
43
+ text("SELECT * FROM orders WHERE customer_id = :cid"),
44
+ {"cid": customer_id},
45
+ )
46
+ ```
47
+
48
+ The session lifecycle and the repository port live in `references/database.md`; security adds one rule on top — no user value reaches a query except as a bound parameter, and table/column names are never taken from input.
49
+
50
+ ## 3. Authorization at the dependency boundary
51
+
52
+ Authentication establishes *who*; authorization decides *what they may do*. Both are FastAPI dependencies on the route, enforced through one path, not re-implemented per handler.
53
+
54
+ ```python
55
+ from fastapi import Depends, HTTPException, status
56
+
57
+ async def require_order_access(
58
+ order_id: str,
59
+ principal: Principal = Depends(get_principal), # from the verified token
60
+ ) -> str:
61
+ if not await policy.can_access_order(principal, order_id):
62
+ raise HTTPException(status.HTTP_403_FORBIDDEN)
63
+ return order_id
64
+
65
+ @router.get("/orders/{order_id}")
66
+ async def get_order(order_id: str = Depends(require_order_access)) -> OrderResponse:
67
+ ...
68
+ ```
69
+
70
+ - The token is verified by a proven provider (OIDC); the service does not hand-roll JWT or session logic. Auth is boring technology — see `docs/principles/system-design/identity-and-access.md`.
71
+ - In a multi-tenant service the tenant is bound to the authenticated principal and enforced at the data boundary, never trusted from a path or query parameter.
72
+ - Least privilege: the database role and any cloud identity the service runs as start minimal and widen only on evidence.
73
+
74
+ ## 4. Secrets are managed, never in code
75
+
76
+ No secret lives in source, in a committed `.env`, or baked into an image layer. Configuration is validated once at boot with `pydantic-settings` (`references/implementation-patterns.md` → Configuration Validation); secret *values* are injected from the platform's secret manager at runtime.
77
+
78
+ ```python
79
+ from pydantic import Field
80
+ from pydantic_settings import BaseSettings
81
+
82
+ class Secrets(BaseSettings):
83
+ # Sourced from the secret manager / injected env at runtime — never a default here
84
+ database_url: str = Field(..., min_length=1)
85
+ upstream_api_key: str = Field(..., min_length=16)
86
+
87
+ # .env.example carries names with empty values; real values never enter the repo
88
+ ```
89
+
90
+ The hierarchy is eliminate, then shorten, then rotate: prefer workload identity or OIDC federation (no static credential at all), then short-lived minted secrets, and reserve scheduled rotation for static credentials that genuinely cannot be made ephemeral.
91
+
92
+ ## 5. Supply chain is part of the attack surface
93
+
94
+ Every third-party package is a potential exploit vector. `uv` pins the full dependency graph in `uv.lock`; CI installs from the lockfile (`uv sync --frozen`), never an unpinned resolve.
95
+
96
+ - A new dependency is a reviewed decision, not an intuition — check maintenance, ownership, and transitive weight before adding it.
97
+ - Generate an SBOM and run a vulnerability scan (`uv pip audit` / `pip-audit` or equivalent) on every build; a known-vulnerable transitive dependency fails the build.
98
+ - Emit build provenance for anything published, so the artifact's origin is verifiable, not just its contents.
99
+
100
+ ## 6. SSRF on outbound calls
101
+
102
+ A service that fetches a URL derived from input is an SSRF vector — an attacker aims it at internal metadata endpoints or private hosts. Outbound targets are allowlisted, not reflected from the request.
103
+
104
+ ```python
105
+ from urllib.parse import urlparse
106
+
107
+ ALLOWED_HOSTS = {"api.partner.example", "cdn.partner.example"}
108
+
109
+ def assert_allowed(url: str) -> str:
110
+ host = urlparse(url).hostname
111
+ if host not in ALLOWED_HOSTS:
112
+ raise PermanentInferenceError(f"outbound host not allowed: {host}")
113
+ return url
114
+ ```
115
+
116
+ - Validate the resolved host against an allowlist before the call; reject `file:`, `gopher:`, and non-HTTPS schemes.
117
+ - Set explicit connect/read timeouts on every outbound client so a hostile or slow upstream cannot exhaust the service (`references/resilience.md`).
118
+
119
+ ## 7. Error envelopes that do not leak internals
120
+
121
+ A client receives a stable, structured error; stack traces, SQL fragments, and upstream provider messages stay in the logs. The Domain raises typed exceptions (`references/implementation-patterns.md` → Error Handling); one exception handler maps them to a safe envelope.
122
+
123
+ ```python
124
+ from fastapi import Request
125
+ from fastapi.responses import JSONResponse
126
+
127
+ @app.exception_handler(AppError)
128
+ async def handle_app_error(request: Request, exc: AppError) -> JSONResponse:
129
+ logger.exception("request failed", extra={"trace_id": current_trace_id()})
130
+ # client sees a code and a correlation id — never exc internals
131
+ return JSONResponse(
132
+ status_code=422,
133
+ content={"error": exc.code, "trace_id": current_trace_id()},
134
+ )
135
+ ```
136
+
137
+ The `422` unification and CORS rules live in `references/api-standards.md`; security's addition is that the body never carries an internal detail and the correlation id is how support traces it without exposing it.
138
+
139
+ ## Anti-Patterns
140
+
141
+ - **Reading the raw request body as a `dict`.** Bypasses validation; parse a Pydantic model with `extra="forbid"`.
142
+ - **f-string SQL.** `f"WHERE id = {user_id}"` is injection. Bind every value.
143
+ - **Per-handler permission checks that drift.** Authorize through one dependency; model the policy once.
144
+ - **Secrets in `.env`, an image layer, or a default value.** Inject from the secret manager at runtime.
145
+ - **Unpinned installs in CI.** `uv sync --frozen` against `uv.lock`; scan and SBOM every build.
146
+ - **Fetching an input-supplied URL unchecked.** Allowlist the host; block internal addresses and non-HTTPS schemes.
147
+ - **Returning the exception string to the client.** Log the detail, return a code and a trace id.
148
+ - **"It is an internal service, skip auth."** Internal services are an attacker's favourite foothold — zero trust between services.
@@ -6,6 +6,10 @@ Testcontainers spins up real Postgres/Pub/Sub in seconds. The confidence it prov
6
6
 
7
7
  **Service tests are the default.** Unit tests are reserved for genuinely complex logic. System tests are minimal.
8
8
 
9
+ This is the stack idiom of the framework testing canon (`docs/principles/foundations/testing.md`); when this file and the canon disagree, the canon wins and this file is the one to fix.
10
+
11
+ Above the honeycomb sits the front-door proof: drive the real running service through the front door a consumer actually calls — its HTTP API — end to end on the real pipeline. Service tests that each pass behind a `dependency_overrides` mock or a stubbed adapter can still assemble into a product that does nothing on the real path, so one proof exercises that path with nothing in the middle faked. This makes a rule on every fake: a stub or fixture standing in for a real stage — an LLM response, a downstream service, a producer's output — is a debt that some test of the real producer must pay. Seeding the input is fine; faking the work in the middle is the violation, and an unpaid debt is a green light wired to nothing.
12
+
9
13
  ---
10
14
 
11
15
  ## Tier 1 — Service Tests (Default)
@@ -159,6 +163,40 @@ uv run pytest tests/integration -m live # Live API tests — requires re
159
163
  uv run pytest tests/system # Bootstrap + golden path
160
164
  ```
161
165
 
166
+ ## Trace Assertions
167
+
168
+ Observability is a test surface: a critical-path request must emit an unbroken trace, and a missing span is a test failure, not an instrumentation TODO. The mechanism is an **in-memory span exporter** from the OTel SDK — no external tooling, and the durable approach now that the dedicated trace-test tools have gone dormant.
169
+
170
+ ```python
171
+ from opentelemetry import trace
172
+ from opentelemetry.sdk.trace import TracerProvider
173
+ from opentelemetry.sdk.trace.export import SimpleSpanProcessor
174
+ from opentelemetry.sdk.trace.export.in_memory_span_exporter import InMemorySpanExporter
175
+
176
+ @pytest.fixture
177
+ def spans():
178
+ exporter = InMemorySpanExporter()
179
+ provider = TracerProvider()
180
+ provider.add_span_processor(SimpleSpanProcessor(exporter))
181
+ trace.set_tracer_provider(provider)
182
+ return exporter
183
+
184
+ async def test_process_emits_trace(client, spans):
185
+ await client.post("/process", json={"input": "test"})
186
+ names = {s.name for s in spans.get_finished_spans()}
187
+ assert "POST /process" in names # the entry span exists
188
+ assert "db.insert" in names # the work span exists
189
+ # assert the work span is a descendant of the entry span — the trace is connected
190
+ ```
191
+
192
+ Assert what the contract promises — the spans that must exist on the journey, that the trace stays connected across hops, and the attributes a dashboard or SLO query depends on. Pinning the exact span tree and every attribute couples the test to implementation and trains the team to delete it.
193
+
194
+ ## Mutation Testing — the assertion-quality read-out
195
+
196
+ A fat service test drives many branches through one HTTP call and can *execute* them all while only asserting on the response body. Mutation testing is the one instrument that proves the suite checks what it runs: inject a fault, confirm a test fails, and a surviving mutant is a line you cover but do not check. Treat it as a **signal, never a gate** — run it on the high-risk modules the risk matrix flags and on changed code only.
197
+
198
+ Use `mutmut` or `cosmic-ray`. A gotcha: `mutmut` 3 no longer mutates module-level code (only function bodies), so a module whose risk lives in top-level constants or table definitions needs `cosmic-ray` or `mutmut` 2. Scope a run to the dense package under change (`mutmut run --paths-to-mutate src/<pkg>/pricing`), read it as a read-out, and turn each surviving mutant on changed high-risk code into the missing assertion — the same read-out catches AI-generated tests whose oracle was lifted from the implementation.
199
+
162
200
  ## Anti-Patterns
163
201
 
164
202
  - **Mocking the interior.** Test what comes out, not which methods were called.
@@ -169,9 +207,10 @@ uv run pytest tests/system # Bootstrap + golden path
169
207
 
170
208
  ## Bet Slice Rollout — the permanent tests a slice owes
171
209
 
172
- When a bet slice's progress tests go green, the slice rolls out permanent coverage before it closes (bet workflow, Delivery step 5). The bet-progress tests prove the capability once and are archived; these stay.
210
+ When a bet slice's progress tests go green, the slice-worker rolls out permanent coverage as part of the same slice, before the driver reviews it (bet workflow, Delivery). The bet-progress tests prove the capability once and are archived; these stay.
173
211
 
174
212
  - **Service perimeter test (always).** One test per capability the slice delivered, through `httpx.AsyncClient` against the real app with a real database — the coverage that survives refactors.
175
213
  - **Unit tests (when logic earned them).** Pure-function tests for branching business logic the slice introduced — validation rules, transformations, state machines. Plumbing does not earn unit tests; the perimeter test already covers it.
176
214
  - **Property-based tests (when invariants exist).** A slice that introduced an invariant — round-trip serialization, idempotent consumers, order-independent merges — pins it with Hypothesis, because example-based tests sample invariants instead of stating them.
215
+ - **Critical-path trace assertions (when the slice added an observable path).** A slice that introduced an endpoint or worker whose trace a dashboard or SLO depends on pins it with an in-memory-exporter test: the entry and work spans exist and the trace stays connected. A missing span is a test failure, not an instrumentation TODO.
177
216
  - **Contract conformance (when the slice changed an API).** FastAPI's served `/openapi.json` must match the promoted spec in `docs/architecture/api/<service>/openapi.yaml`; the generated system suite checks this — the slice's job is to keep the spec promotion current.
@@ -1,13 +1,20 @@
1
1
  # Sync Anchor
2
2
 
3
- This file pins the principle files this skill embeds. When any listed file
4
- changes, this skill must be reviewed in the same commit. CI verifies the
5
- hashes match.
3
+ This file pins the principle files this skill embeds both the per-stack Python
4
+ idiom docs and the cross-cutting central canon this skill distils. When any
5
+ listed file changes, this skill must be reviewed in the same commit (and the
6
+ matching per-stack idiom doc reconciled to the canon). CI verifies the hashes
7
+ match.
6
8
 
7
9
  | Principle file | SHA-256 | Last reviewed |
8
10
  |---|---|---|
9
11
  | src/generators/python-microservice/docs/principles/stack/python/async.md | 6fdd399fb3052381020ff6e792a724d72bdabe674817794093853cbf24fa9f97 | 2026-05-26 |
10
12
  | src/generators/python-microservice/docs/principles/stack/python/resilience.md | d5a7b8f089acdb71d64c1bd4fc9ce80e6947504b01b0ace695ac5ee66554a1b1 | 2026-06-19 |
11
- | src/generators/python-microservice/docs/principles/stack/python/testing.md | b596a4281825349c627dca17e671052ef64a371ff66c50b66d11ceab5ee7b5f2 | 2026-06-19 |
13
+ | src/generators/python-microservice/docs/principles/stack/python/testing.md | f15e62c83b659788f8b3e39f560779e892080d6bce73768be093d919f3e6946c | 2026-06-26 |
12
14
  | src/generators/python-microservice/docs/principles/stack/python/documentation.md | ac58228ba22435bf9bad2ea5bf924bdf9e3674e9967515d5c82aaf3b7825214d | 2026-05-26 |
13
15
  | src/generators/python-microservice/docs/principles/stack/python/mcp.md | 1e6deab0b45c7271e0038e9b3d51bc30cb2917488f608e847d566739ac6caeba | 2026-06-19 |
16
+ | src/docs/principles/foundations/testing.md | 205ac40d4c643e7b61cf1e4295df8a7b8b46dcd7c81b857aa8c642ea353f62ef | 2026-06-27 |
17
+ | src/docs/principles/quality/observability.md | 8aa60e213ba03e989c93263153e3a1ac10b2336f6d0360c394f473660d565a0b | 2026-06-26 |
18
+ | src/docs/principles/quality/security.md | 61157d97677142737ec537954dc5aaad7a04012cc8a3dcc855e2d324287fdc64 | 2026-06-26 |
19
+ | src/docs/principles/quality/reliability.md | 9c9788504e0963458667d2727c3fc2359776108be593a2efc6603f6470002252 | 2026-06-26 |
20
+ | src/docs/principles/foundations/documentation.md | 8b576072eaf4970f1251b560781e3e755c864a7920faa599b2834c921cbb8734 | 2026-06-26 |
@@ -14,6 +14,8 @@ Electron is GroundWork's standard desktop surface. This set owns what the deskto
14
14
 
15
15
  [Surface Stack Selection](../../surface-stack-selection.md) picks Electron on the agent-closable-loop axis: Electron renders on bundled Chromium, so Playwright's `_electron` driver launches the real packaged app, drives its windows as ordinary `Page`s, and evaluates code in the main process — one deterministic engine on every OS, headless under Xvfb in CI. No other desktop option closes generate → boot → test → observe without a human in the loop. The renderer reusing the web stack and brand-token projection wholesale is the second axis win. Tauri is the recorded alternative when binary size and RAM dominate — its per-OS system webviews and WebDriver-only testing surrender the loop, so it is never the default.
16
16
 
17
+ That `_electron` smoke (`tests/smoke/app.spec.ts`) is the Electron side of the **native UI check contract** (`src/generators/system-test-runner/NATIVE-CHECK-CONTRACT.md`): the `system-test-runner` drives it as the surface's visual gate, so it carries the contract's dimensions on the real binary — it renders without a blank or crash frame, drives the **named async state** (a deterministically unreachable core renders its designed state, not a crash), and confirms the **design-system tokens landed** (the brand custom properties resolve and the heading paints the projected primary token, not an unstyled default). Navigation / no-dead-ends is exempt while the app is single-screen; a bet that adds screens drives between them and back here. Keep it thin — the milestone's front-door bet-progress proof is what drives the real pipeline end to end.
18
+
17
19
  ## What this set owns
18
20
 
19
21
  | File | Owns |