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,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
|
|
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 —
|
|
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
|
|
4
|
-
|
|
5
|
-
|
|
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.
|
|
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
|
|
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
|
|
4
|
-
|
|
5
|
-
|
|
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 |
|
|
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 |
|