@pikku/skills 0.12.10 → 0.12.11

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 (60) hide show
  1. package/CHANGELOG.md +768 -0
  2. package/dist/skills.gen.js +1 -1
  3. package/package.json +4 -4
  4. package/skills/pikku-addon/SKILL.md +17 -11
  5. package/skills/pikku-addon/references/addon-package-manifest.md +2 -2
  6. package/skills/pikku-agent/SKILL.md +3 -3
  7. package/skills/pikku-audit/SKILL.md +28 -13
  8. package/skills/pikku-aws/SKILL.md +2 -2
  9. package/skills/pikku-better-auth/SKILL.md +97 -17
  10. package/skills/pikku-build-app/SKILL.md +621 -0
  11. package/skills/pikku-build-app/references/multi-app.md +117 -0
  12. package/skills/pikku-build-app/references/ship.md +98 -0
  13. package/skills/pikku-build-app/references/theming.md +70 -0
  14. package/skills/pikku-build-platform/SKILL.md +239 -0
  15. package/skills/pikku-build-quick/SKILL.md +238 -0
  16. package/skills/pikku-cli/SKILL.md +7 -7
  17. package/skills/pikku-cli/references/complete-example.md +1 -1
  18. package/skills/pikku-concepts/SKILL.md +5 -2
  19. package/skills/pikku-concepts/references/concept-mapping.md +1 -1
  20. package/skills/pikku-config/SKILL.md +5 -3
  21. package/skills/pikku-deploy-azure/SKILL.md +5 -4
  22. package/skills/pikku-emails/SKILL.md +5 -5
  23. package/skills/pikku-fabric/SKILL.md +27 -3
  24. package/skills/pikku-fabric-debug/SKILL.md +1 -1
  25. package/skills/pikku-feature/SKILL.md +5 -4
  26. package/skills/pikku-http/SKILL.md +4 -4
  27. package/skills/pikku-http/references/http-options.md +13 -13
  28. package/skills/pikku-i18n/SKILL.md +2 -1
  29. package/skills/pikku-info/SKILL.md +1 -1
  30. package/skills/pikku-knowledge/SKILL.md +13 -13
  31. package/skills/pikku-mcp/SKILL.md +4 -4
  32. package/skills/pikku-middleware/SKILL.md +5 -5
  33. package/skills/pikku-middleware/references/middleware-patterns.md +14 -8
  34. package/skills/pikku-n8n-import/SKILL.md +12 -12
  35. package/skills/pikku-n8n-import/SPEC.md +3 -0
  36. package/skills/pikku-n8n-import/references/code-translation.md +26 -22
  37. package/skills/pikku-n8n-import/references/loops-and-control.md +7 -7
  38. package/skills/pikku-paraglide/SKILL.md +11 -6
  39. package/skills/pikku-permissions/SKILL.md +19 -15
  40. package/skills/pikku-product-second-opinion/README.md +3 -3
  41. package/skills/pikku-product-second-opinion/example/sample-report.md +15 -12
  42. package/skills/pikku-product-second-opinion/references/report-template.md +10 -7
  43. package/skills/pikku-queue/SKILL.md +1 -1
  44. package/skills/pikku-react/SKILL.md +53 -13
  45. package/skills/pikku-realtime/SKILL.md +51 -19
  46. package/skills/pikku-rtl/SKILL.md +1 -1
  47. package/skills/pikku-scenario/SKILL.md +126 -14
  48. package/skills/pikku-schedule/SKILL.md +6 -1
  49. package/skills/pikku-schema-ajv/SKILL.md +2 -2
  50. package/skills/pikku-schema-cfworker/SKILL.md +1 -1
  51. package/skills/pikku-security/SKILL.md +9 -5
  52. package/skills/pikku-services/SKILL.md +27 -18
  53. package/skills/pikku-services/references/audit-wire-service.md +14 -8
  54. package/skills/pikku-software-archaeology/scripts/validate.mjs +146 -69
  55. package/skills/pikku-tag-middleware/SKILL.md +1 -0
  56. package/skills/pikku-template-clone/SKILL.md +2 -1
  57. package/skills/pikku-trigger/SKILL.md +3 -3
  58. package/skills/pikku-websocket/SKILL.md +4 -3
  59. package/skills/pikku-workflow/SKILL.md +2 -2
  60. package/skills/pikku-workflow/references/workflow-reference.md +24 -10
@@ -0,0 +1,621 @@
1
+ ---
2
+ name: pikku-build-app
3
+ description: >-
4
+ Build a real product on open-source Pikku using the full Fabric workflow, run locally — knowledge
5
+ base first, personas and roles, milestones planned then built one at a time, each proven by a
6
+ scenario, with a design pass. The default build mode, and the one that stays importable into
7
+ Fabric later. TRIGGER when: the user asked for an app to be built on Pikku and picked "App" (or
8
+ did not pick), a freshly scaffolded pikku project needs turning into a product, or the user says
9
+ "build this properly / so someone can pick it up". DO NOT TRIGGER when: the user asked for
10
+ something quick or throwaway (use pikku-build-quick), wants every platform surface demonstrated
11
+ (use pikku-build-platform), or is adding one feature to an app that already has its knowledge
12
+ base and milestones (use pikku-feature).
13
+ installGroups: [core]
14
+ ---
15
+
16
+ # Build a product on open-source Pikku
17
+
18
+ You have a scaffolded project with skills installed. This skill owns everything
19
+ from here: no Fabric account, no card, no hosted build — while keeping the
20
+ project shaped so `pikku fabric init` later adopts it with zero rework.
21
+
22
+ **Four phases, and you do not skip ahead:**
23
+
24
+ 1. Write the knowledge graph — what the app IS, before any code
25
+ 2. Declare the people and the apps — personas, roles, frontends
26
+ 3. Plan the milestones — the buildable pieces, in dependency order
27
+ 4. Implement them one at a time — each proven by a scenario before the next starts
28
+
29
+ ## Agent Operating Procedure
30
+
31
+ 1. Discover before editing. Run `pikku info functions --verbose --silent` and
32
+ read `AGENTS.md` before your first change.
33
+ 2. Make the smallest source change that satisfies the task. Keep generated files
34
+ generated — never hand-edit `.pikku/`, `*.gen.*`, or the SDK.
35
+ 3. Validate with the narrowest relevant command, then `pikku all` when functions,
36
+ wirings, schemas or generated clients may have changed.
37
+ 4. If validation fails, fix the source cause and rerun. Do not paper over
38
+ generated errors by editing generated files.
39
+
40
+ ## 0. Bootstrap, before anything else
41
+
42
+ ```sh
43
+ bunx --bun pikku bootstrap
44
+ ```
45
+
46
+ One command, run once, now — not later when you start building. It wires the
47
+ `#pikku` import alias the generated code depends on. On a fresh scaffold `.pikku/`
48
+ is empty, so **every command that touches codegen fails until this has run** —
49
+ including ones you would reasonably reach for while still planning, like
50
+ `pikku persona list`. Those failures look alarming (`Cannot find module
51
+ '#pikku/workflow/pikku-workflow-types.gen.js'`, `Schema generation failed for 16
52
+ schemas`) and they are nothing but this missing step.
53
+
54
+ `pikku knowledge index` and `knowledge validate` work without it — which is why
55
+ the planning phases below are safe either way.
56
+
57
+ ## 1. One more round of questions — then stop asking
58
+
59
+ Ask **three to five** questions that actually change the schema or the screens,
60
+ in one message. Then stop; do not interview the user.
61
+
62
+ - **Who uses it — who are the distinct kinds of people?** Ask this however small
63
+ the app is. The answer becomes §3's personas and roles, and you build only the
64
+ roles they name.
65
+ - **What are the two or three core objects?**
66
+ - **What is the main thing someone does on their first visit?** This answer
67
+ becomes the second milestone, not the tenth.
68
+ - **One app or several?** Separate apps on separate hosts, or one app with paths.
69
+ Cheap to answer now, expensive after the routes exist.
70
+ - **What should it look like?** The template ships one theme — "Neutral", a
71
+ deliberately unopinionated monochrome scaffold — and **nothing in the
72
+ open-source toolchain will ever replace it for you.** Accept any of: keep
73
+ Neutral (fine for an internal tool, but say so out loud); a direction in words;
74
+ a reference (brand guide, screenshots, a site whose register they want); or
75
+ their own design agent/prompt, whose output you take as the direction.
76
+
77
+ Skip anything you can decide yourself. If nobody answers, assume one app with
78
+ paths, the roles implied by the request, Neutral, English — and say so.
79
+
80
+ ---
81
+
82
+ ## PHASE 1 — What the app is
83
+
84
+ ## 2. Write the knowledge graph — before any code
85
+
86
+ `knowledge/` is not documentation you write at the end. It is the record of what
87
+ the app IS, in the words its users use, and it is the one part of the project
88
+ another agent picks up and continues from. **Nothing gets built until there is a
89
+ milestone note to build.**
90
+
91
+ Read `knowledge/index.md` and the `pikku-knowledge` skill, then write the notes
92
+ for what the user just told you. A project whose `knowledge/` is still only the
93
+ shipped index is a project nobody can resume.
94
+
95
+ Five sections, each answering exactly one question:
96
+
97
+ - `milestones/` — what is one buildable piece, and what proves it works
98
+ (some scaffolds call these `slices/`; follow the name `knowledge/index.md`
99
+ uses — `knowledge validate` accepts either)
100
+ - `entities/` — what a thing IS, in the words users use for it
101
+ - `decisions/` — what was chosen and what that rules out.
102
+ `decisions/security/` for who may reach what, `decisions/design/` for how it
103
+ looks and behaves
104
+ - `questions/` — what you asked and never got an answer to
105
+ - `wishlist/` — what someone wants that nobody has asked you to build
106
+
107
+ Rules that make it a graph rather than a pile of files:
108
+
109
+ - **A note's path is its identity.** Markdown, YAML frontmatter, `type` required.
110
+ Cross-link notes with plain markdown links — that is what makes it a graph.
111
+ - **Create a section the turn you have a note for it**, with its own `index.md`
112
+ written in the same turn. Never scaffold empty directories, and never leave
113
+ notes flat at the root: a `product.md` and a `glossary.md` at `knowledge/` is
114
+ not a knowledge base, and it leaves the project unbuildable.
115
+ - **A milestone note carries `status`** (`proposed` → `dispatched` → `built`,
116
+ nothing else), **at most three `entities`** (past three it is not one piece —
117
+ split it), and **its scenario as a fenced ` ```gherkin ` block in the third
118
+ person** — `Given 'owner' has no entry`, never `Given I …`. A quoted word
119
+ MEANS a persona, so quote only personas you declare in §3 and write domain
120
+ values bare. That block becomes a real scenario in §7.
121
+ - **Record only what pikku cannot tell you.** Tables, columns, function
122
+ signatures, routes, wirings, permissions and roles are all discoverable with
123
+ `pikku info` / `pikku meta`. Copying them into a note gives you a second copy
124
+ that goes stale. Knowledge is the why: decisions, constraints, what a thing
125
+ means.
126
+
127
+ Three decisions belong here on day one, because every later choice leans on them
128
+ and none is discoverable from code:
129
+
130
+ - **How the product is split into apps**, and why (§4) — `decisions/`
131
+ - **What each kind of person may reach**, in domain language — `decisions/security/`
132
+ - **What the app should look like** — the direction from §1 (§8) — `decisions/design/`
133
+
134
+ Then keep it honest — both must pass, and `validate` is a real gate:
135
+
136
+ ```sh
137
+ bunx --bun pikku knowledge index
138
+ bunx --bun pikku knowledge validate
139
+ ```
140
+
141
+ ---
142
+
143
+ ## PHASE 2 — Who it is for, and what it is made of
144
+
145
+ ## 3. Declare the people — personas and roles
146
+
147
+ The answer to "who uses it" becomes code, in one file:
148
+ `packages/functions/src/personas.ts`. It ships with a single `visitor`; add the
149
+ people the user named, and the roles they imply.
150
+
151
+ ```typescript
152
+ import { definePersonas } from '#pikku/scopes/pikku-personas.gen.js'
153
+ import { defineSystemRole } from '#pikku'
154
+
155
+ defineSystemRole({
156
+ owner: {
157
+ displayName: 'Owner',
158
+ description: 'Runs their own properties — sees only what they own',
159
+ scopes: [],
160
+ },
161
+ tenant: {
162
+ displayName: 'Tenant',
163
+ description: 'Lives in one unit — sees only their own tenancy',
164
+ scopes: [],
165
+ },
166
+ })
167
+
168
+ definePersonas({
169
+ visitor: { name: 'Visitor', jobTitle: 'Synthetic health-check user', account: {} },
170
+ amina: {
171
+ name: 'Amina',
172
+ jobTitle: 'Property owner',
173
+ personality: 'Checks arrears first, every single time',
174
+ roles: ['owner'],
175
+ account: {},
176
+ },
177
+ bilal: {
178
+ name: 'Bilal',
179
+ jobTitle: 'Property owner',
180
+ personality: 'A second owner — exists so "you see yours, not theirs" is testable',
181
+ roles: ['owner'],
182
+ account: {},
183
+ },
184
+ chidi: {
185
+ name: 'Chidi',
186
+ jobTitle: 'Tenant',
187
+ personality: 'Reports the boiler, wants to know it was seen',
188
+ roles: ['tenant'],
189
+ account: {},
190
+ },
191
+ })
192
+ ```
193
+
194
+ - **Keep `visitor`.** The shipped scenarios name `actors.visitor`, and PKU677
195
+ requires a browser step's actor to be a literal `actors.<name>`. Removing it
196
+ stops `actors.visitor` type-checking, which fails `pikku all`, and nothing you
197
+ write after that registers.
198
+ - **One `definePersonas` call for the whole project.** Codegen builds the
199
+ `PersonaId` union from it, materialises one scenario actor per person, and
200
+ seeds a user row each. A second call site is a second answer to "who uses this
201
+ app".
202
+ - **`roles` is typechecked against `defineSystemRole`.** An undeclared role is a
203
+ build error rather than a runtime surprise.
204
+ - **Never write an email address.** Each is derived from the persona id and
205
+ `scenarios.emailDomain` in `pikku.config.json` — `visitor@actors.local`.
206
+ Hand-writing one is how a run signs in as somebody who was never created.
207
+ - **Declare a second person of the same kind** whenever the rule is ownership
208
+ (`bilal` above). "You see yours, not theirs" is not testable with one owner,
209
+ and §7 is where it gets caught.
210
+ - **Build the roles the user's answer produces, no more.** An invented role
211
+ becomes invented screens and invented rules, and it is the user who has to
212
+ live with them.
213
+ - **Roles are what a permission check reads, not where it lives.** The check goes
214
+ in the function's `permissions` field (§6), never in the body. Read the
215
+ `pikku-permissions` skill.
216
+
217
+ `pikku persona list` shows who is declared and `pikku roles audit` reports roles
218
+ the database still holds that code no longer declares — both need §0's bootstrap
219
+ to have run, and both are worth a look once it has.
220
+
221
+ **One warning about the scaffold's own notes:** `knowledge/index.md` may claim
222
+ the people live in `pikku.config.json`, put there by a `fabric persona` command.
223
+ That is stale. In this template they live in `personas.ts` as above, and
224
+ `pikku.config.json` carries no personas key at all — only `scenarios.emailDomain`.
225
+ Trust the file you can read over the note describing it.
226
+
227
+ ## 4. Declare the apps — one API, several frontends
228
+
229
+ ### The rule that makes it cheap
230
+
231
+ **One backend, many frontends. Never fork `packages/functions`.**
232
+
233
+ Every app imports the same generated SDK (`@project/functions-sdk`) and calls the
234
+ same RPCs. What differs is which screens exist and what the shell looks like.
235
+ What must NOT differ is the data layer: two copies of a `listProperties` function
236
+ is two places for the permission check to be wrong.
237
+
238
+ **The app split is presentation, not security.** A tenant app that simply never
239
+ renders the arrears screen is not access control — it is a hidden button.
240
+ Security is the `permissions` field on the function, and it holds whether the
241
+ caller arrived from the admin origin, the tenant origin, curl, or the generated
242
+ client. Build the split for clarity, and prove the boundary with a refusal
243
+ scenario in §7.
244
+
245
+ ### Choosing the split
246
+
247
+ - **Separate apps on separate hosts** (`admin.example.com`, `portal.example.com`)
248
+ — when the two audiences share almost no screens, when they should not see each
249
+ other's brand register, or when one may later ship independently.
250
+ - **One app with paths** (`/app/admin/*`, `/app/portal/*`) — when they share most
251
+ of the shell and the difference is a handful of screens. Cheaper, and honest:
252
+ two nav trees in one app is still two apps to a user.
253
+
254
+ Either way, write the choice and its reason into `knowledge/decisions/`.
255
+
256
+ ### Adding a second frontend — later, not now
257
+
258
+ Recording the decision is Phase 2 work. **Creating the directory is not.**
259
+ Cloning `apps/app` materialises a folder of copied screens, so it belongs to the
260
+ milestone that first needs the second app, not to planning.
261
+
262
+ When you get there, read `references/multi-app.md`. It carries the clone, the
263
+ `package.json` edits, the `pikkufabric.config.json` frontends map, the dev-runner
264
+ change that otherwise silently never starts your second app, the per-frontend
265
+ scenario environments, and how sessions behave across two origins.
266
+
267
+ What Phase 2 owes you now is only this: the split, its reason, and who each app
268
+ serves, written into `knowledge/decisions/`.
269
+
270
+ ---
271
+
272
+ ## PHASE 3 — The plan
273
+
274
+ ## 5. Plan the milestones
275
+
276
+ Turn the app into an ordered list of buildable pieces, each a note in
277
+ `knowledge/milestones/`, each `status: proposed` with a gherkin block.
278
+
279
+ What a milestone is:
280
+
281
+ - **One buildable piece, at most three entities.** Past three it is not one piece.
282
+ - **Vertical, not layered.** "The owner sees this month's arrears" is a
283
+ milestone — migration, function, screen, scenario. "Add the database schema" is
284
+ not; it is a step inside one.
285
+ - **It ends in something a person can do**, in a browser, signed in as a named
286
+ persona. If you cannot write the gherkin, you cannot build it yet — that is a
287
+ `questions/` note, not a milestone.
288
+
289
+ How to order them:
290
+
291
+ 1. **The spine first.** The one object everything else hangs off, and the screen
292
+ that proves the app exists at all.
293
+ 2. **Then the loop the user named as "the main thing someone does on their first
294
+ visit."** That answer from §1 is the second milestone, not the tenth.
295
+ 3. **Then each audience's own surface**, one at a time. With two apps, finish one
296
+ app's spine before starting the other's — a half-built app in each is worse
297
+ than one working app.
298
+ 4. **Refusals ride along with the milestone that creates the thing being
299
+ refused**, never as a "permissions" milestone at the end. A milestone that
300
+ creates a row and does not say who may not see it is not finished.
301
+
302
+ Number the files (`01-…`, `02-…`) so the order is visible in the tree. Then
303
+ `knowledge index && knowledge validate` before you write a line of code.
304
+
305
+ **Show the user the list before building.** This is the last cheap moment to
306
+ reorder — after §6 the migrations are numbered and the order is concrete.
307
+
308
+ ---
309
+
310
+ ## PHASE 4 — Build
311
+
312
+ ## 6. Implement milestones, one at a time
313
+
314
+ **Per milestone** — set its note to `status: dispatched`, do the six steps,
315
+ set it to `built`. Do not start the next one until §7 is green for this one *and
316
+ §7a shows its functions covered*. A stack of half-milestones cannot be reviewed
317
+ and cannot be handed over, and an uncovered function is a half-milestone whether
318
+ or not the note says `built`.
319
+
320
+ 1. **Migration.** SQL in `db/sqlite/` at the project root, numbered on from the
321
+ ones already there. Apply with `bunx --bun pikku db migrate`, which also
322
+ regenerates the Kysely types your functions import.
323
+ 2. **Seed.** Demo rows in `db/sqlite-dev-seed.sql`. **There is no seed command** —
324
+ `bunx --bun pikku db reset` is the only thing that applies the file, and it
325
+ wipes, migrates and seeds in one go (`--no-seed` stops after the migration,
326
+ for working on an empty state the test data would hide). Because reset always
327
+ arrives at a database it just wiped, **the seed file is plain `INSERT`s** — no
328
+ `ON CONFLICT DO NOTHING`, no `INSERT OR IGNORE`. Nothing ever applies it
329
+ twice, so it never has to defend itself.
330
+ Do this generously and do it now: an empty app demos badly and critiques
331
+ badly, and you cannot judge a screen's hierarchy, overflow, or truncation
332
+ against zero rows. Seed rows each persona sees differently — with an ownership
333
+ rule that means seeding rows for the *second* owner too.
334
+ 3. **Functions.** One `pikkuFunc` per `*.function.ts`. Mark it `expose: true` and
335
+ Pikku generates the typed RPC client and the React Query hooks the UI calls;
336
+ you do NOT write an HTTP route for it. Add `wireHTTP` only for a real REST
337
+ shape (a third-party webhook).
338
+ 4. **Regenerate:** `bunx --bun pikku all`
339
+ 5. **UI.** Pages in `<app>/src/pages/`, one route file each in `<app>/src/routes/`,
340
+ calling functions through the generated `usePikkuQuery` / `usePikkuMutation`
341
+ hooks from `@project/functions-sdk/pikku/api.gen`. One component per `.tsx`
342
+ file. Compose the kit from `@/components/<Name>` rather than hand-rolling.
343
+ Register the screen in `useNavItems()` — that one file feeds both the desktop
344
+ sidebar and the phone navigation.
345
+ 6. **Scenario** (§7), then `status: built`.
346
+
347
+ Rules that are not optional:
348
+
349
+ - A function's input and output types come from its `input:`/`output:` zod
350
+ schemas. Never pass generic type params, never annotate the return type inline.
351
+ The schema is the type. (Generics XOR schemas — never both.)
352
+ - Auth and permission checks go in the `permissions` field, never in the function
353
+ body. This is what makes §4's app split safe.
354
+ - No `process.env` inside a function. Read config through the injected
355
+ `variables` / `secrets` services; `process.env` belongs only in bootstrap. Every
356
+ secret a function reads needs a matching `defineSecret`, or `pikku all` reports
357
+ PKU951 and nobody knows what to provision at deploy.
358
+ - Let the database type your values, via `db/annotations.ts`. A `BOOLEAN` column
359
+ is derived for you. On SQLite the other two are **not** — add them by hand,
360
+ once, and they are typed AND coerced end-to-end:
361
+
362
+ ```typescript
363
+ export const classifications = {
364
+ payment: {
365
+ paid_at: { kind: 'date' }, // -> Date, not an ISO string
366
+ metadata: { kind: 'json', tsType: 'PaymentMeta' }, // -> parsed object, not unknown
367
+ },
368
+ }
369
+ ```
370
+
371
+ A `TIMESTAMP`/`DATETIME`/`DATE` column with no entry types as `string`, and a
372
+ `JSON` column with no entry types as `unknown` (the CLI warns PKU481). Add the
373
+ annotation rather than casting around the generated type. Once the file carries
374
+ manual fields, `db migrate` stops overwriting it.
375
+ - A `z.date()` on a function's **input** arrives over RPC as an ISO string, not a
376
+ `Date`. Normalise before calling date methods on it (`new Date(value)`), or it
377
+ throws `.getTime is not a function` at runtime — schema validation accepts the
378
+ string without converting it.
379
+ - Every user-facing string is a translation key. Never a hardcoded literal. With
380
+ two apps that means two `messages/` directories; a string used by both belongs
381
+ to whichever app renders it, and duplication beats a shared bundle that couples
382
+ the apps together.
383
+ - Surface errors. No empty catch, no swallowed promise. If a mutation can fail,
384
+ render the failure inline next to the control that triggered it — not a toast.
385
+ - An exposed function with no session and no permission is reachable by anyone
386
+ over `POST /rpc/:rpcName` (PKU574). Either gate it or drop `expose: true`.
387
+
388
+ Then run it:
389
+
390
+ ```sh
391
+ bun run prebuild && bun run dev
392
+ ```
393
+
394
+ That starts the API on :3000 and every frontend in `pikkufabric.config.json`. A
395
+ frontend running against a dead API looks exactly like an app bug, so if every
396
+ request fails, check that both halves came up.
397
+
398
+ The `--bun` in `bunx --bun pikku …` is load-bearing — keep it. Without it the
399
+ CLI's `#!/usr/bin/env node` shebang hands the process to whatever Node is on
400
+ PATH, which fails below Node 24 with `ERR_UNKNOWN_BUILTIN_MODULE: No such
401
+ built-in module: node:sqlite`.
402
+
403
+ Open it, sign up, and click through what you built. **HTTP 200 is not evidence.**
404
+ The pages are client-rendered: the server returns 200 with an empty shell, so a
405
+ page whose component throws still looks fine to `curl`. Open it in a browser, or
406
+ drive it headlessly and assert on the text that actually rendered.
407
+
408
+ ## 7. Prove it — scenarios
409
+
410
+ A scenario is a user journey run as one of your personas, over the real
411
+ transport, with that persona's session. It is the only kind of test worth writing
412
+ here, because a passing one proves the app works the way a signed-in person
413
+ experiences it. Three ship in `packages/functions/test/scenarios/` — keep them
414
+ green — and every milestone's gherkin block from §5 becomes one more.
415
+
416
+ ```typescript
417
+ import { pikkuScenario } from '#pikku/workflow/pikku-workflow-types.gen.js'
418
+
419
+ export const tenantReportsAFaultScenario = pikkuScenario<void, { id: string }>({
420
+ title: 'A tenant reports a fault and the owner sees it',
421
+ description: 'The report lands on the owning landlord’s queue, and nobody else’s',
422
+ tags: ['scenario', 'maintenance'],
423
+ func: async (_services, _data, { scenario, actors }) => {
424
+ const report = await scenario.do(
425
+ 'reports a broken boiler',
426
+ 'createMaintenanceReport',
427
+ { summary: 'No hot water' },
428
+ { actor: actors.chidi },
429
+ )
430
+ await scenario.then(
431
+ 'appears on the owner’s queue',
432
+ 'reportShowsOnQueue',
433
+ { id: report.id },
434
+ { actor: actors.amina },
435
+ )
436
+ await scenario.then(
437
+ 'is invisible to the other owner',
438
+ 'reportIsNotVisible',
439
+ { id: report.id },
440
+ { actor: actors.bilal },
441
+ )
442
+ return { id: report.id }
443
+ },
444
+ })
445
+ ```
446
+
447
+ - **`do` takes an RPC name; `given`/`when`/`then` take a declared step.** A step
448
+ is a `pikkuScenarioStep` that says what a person is doing and holds one
449
+ implementation per surface (server-side by default, plus a `browser` one that
450
+ drives the page). Reaching for an RPC name in a `then` will not resolve.
451
+ - **Every scenario must assert.** A ladder of `given`/`when` with no `then` is a
452
+ PKU680 critical — it fails `pikku all`, so it stops codegen rather than a test.
453
+ Coverage counts every step, so without that rule an assertion-free ladder of
454
+ clicks would score a perfect run while checking nothing.
455
+ - **Write the refusals.** The third step above is the whole point of §4: one
456
+ persona reaching for another's row has to be rejected, and that rejection is a
457
+ scenario. It is how you prove access control instead of asserting it.
458
+ - **Add `SCENARIO_ACTOR_SECRET` to `.env`.** `bun run dev` generates that file
459
+ with a `BETTER_AUTH_SECRET` and nothing else, and without the actor secret
460
+ `/api/auth/sign-in/actor` is disabled — every scenario then fails at sign-in,
461
+ before its first step, for a reason that reads like an auth bug.
462
+ - **There is no state reset.** A scenario runs against a live server: scope what
463
+ you create to your own rows and unique ids, and never assume a clean database.
464
+
465
+ Run them:
466
+
467
+ ```sh
468
+ bunx --bun pikku scenario run local --spawn # server-side, the fast path
469
+ bunx --bun pikku scenario run local --spawn --run browser # the same journeys, driven as a human
470
+ bunx --bun pikku scenario run local-admin --spawn --run browser # the second app
471
+ ```
472
+
473
+ `--spawn` starts and stops the server for the run; drop it if `bun run dev` is
474
+ already up. The browser pass needs the environment's `appUrl` and a browser
475
+ driver installed — without them the run fails fast rather than half-running.
476
+
477
+ ### 7a. Coverage — which functions have actually been run
478
+
479
+ Green scenarios tell you the journeys you wrote still work. They say nothing
480
+ about the code you never wrote a journey for, and that gap is invisible without
481
+ measuring it:
482
+
483
+ ```sh
484
+ bunx --bun pikku dev --coverage # server, instrumented
485
+ bunx --bun pikku scenario run local --coverage # against that server
486
+ ```
487
+
488
+ That writes `coverage/scenario-coverage.json` — which functions each journey
489
+ exercised. **A function no scenario touches has never been run by anything but
490
+ you, by hand, once.** It compiles, it typechecks, `pikku all` is happy, and
491
+ nobody has proven it does what it says.
492
+
493
+ Run it **as each milestone closes**, not once at the end. Coverage read per
494
+ milestone is a short list you can act on — the milestone you just built either
495
+ covered its own functions or it did not. Read for the first time after ten
496
+ milestones it is a wall of red that nobody triages, and the honest response to a
497
+ wall of red is to ignore it.
498
+
499
+ Every gap is one of three things, and naming which is the point of looking:
500
+
501
+ - **A missing scenario** — the function matters and no journey reaches it. Write
502
+ the journey. Refusal paths dominate this category, because it is the case you
503
+ are least likely to have clicked through by hand.
504
+ - **A function that should not exist** — nothing reaches it because nothing needs
505
+ it. Delete it. An unused exposed function is also reachable over
506
+ `POST /rpc/:rpcName`, so this is a security finding, not only dead weight.
507
+ - **Genuinely deferred** — real, not yet reachable from the UI. Say so in the
508
+ milestone note that will cover it, so the gap is a decision rather than a
509
+ hole.
510
+
511
+ Report the number when you hand the milestone over. A number nobody says out
512
+ loud is a number nobody acts on.
513
+
514
+ ## 8. Make it look like someone designed it
515
+
516
+ Two separate jobs, and conflating them is why open-source builds come out looking
517
+ like the template:
518
+
519
+ - **8a. Direction** — deciding what it should look like. **No open-source tool
520
+ does this.** Fabric has `fabric-theme`; you have §1's answer and this section.
521
+ - **8b. Critique** — judging how well the built screens execute that direction.
522
+ `impeccable` does this well, and it is free.
523
+
524
+ Impeccable audits the design you chose. It will never tell you the app should
525
+ have looked like something else — it will happily award a clean bill of health to
526
+ a perfectly-executed default. Skip 8a and you ship Neutral with good spacing.
527
+
528
+ ### 8a. Author the theme — the step nothing does for you
529
+
530
+ The look lives in `packages/mantine-theme`, and it is data, not code:
531
+
532
+ The look lives in `packages/mantine-theme`, and it is data, not code — one JSON
533
+ per theme, `active.json` naming the live one. **Read `references/theming.md`** for
534
+ the file layout, what each field changes, and how to turn a direction in words
535
+ into a theme.
536
+
537
+ Two things that belong here rather than in the reference, because they govern
538
+ every screen you then build:
539
+
540
+ **Set the theme once, don't hardcode colours per component.** A screen full of
541
+ inline `color="blue"` and one-off hex values is why apps look templated. Change
542
+ the theme, not the components — and keep it theme-aware for light and dark.
543
+
544
+ With two apps, **share the theme package and vary the register, not the
545
+ palette.** A back-office can be denser and more tabular; a customer-facing app
546
+ can be roomier and warmer — that is `structure` and layout, not a second `brand`.
547
+ Two unrelated colour schemes read as two products from two companies.
548
+
549
+ Then **write the direction into `knowledge/decisions/design/`** — the words the
550
+ user gave you, what you chose, and what it rules out. The JSON records what the
551
+ theme is; only the note records why.
552
+
553
+ ### 8b. Compose real components, then critique
554
+
555
+ **Compose with Mantine's rich components — not tables and text everywhere:**
556
+
557
+ - **`@mantine/charts`** (Recharts underneath) for overviews — `AreaChart`,
558
+ `BarChart`, `LineChart`, `DonutChart`, `Sparkline`. A metric worth showing is
559
+ worth a chart, not a number in a `Text`.
560
+ - **`@mantine/dates`** for anything time-based — `DatePicker`, `Calendar`,
561
+ `DateTimePicker`, range inputs. Never hand-roll a date field.
562
+ - Composed layouts over flat lists — `Timeline` for history, `Stepper` for
563
+ multi-step progress, `Card` + `SimpleGrid` for a gallery, `RingProgress` for
564
+ completion, `Badge`/`ThemeIcon` for status.
565
+
566
+ Both ship in the template's app dependencies. Look each one up in the Mantine
567
+ llms.txt and use the real component.
568
+
569
+ Then critique it. Free, and works across coding agents:
570
+
571
+ ```sh
572
+ npx impeccable install # current releases need Node 22.18+
573
+ ```
574
+
575
+ Impeccable scores a screen against interaction heuristics and names what is
576
+ wrong: hierarchy, spacing, type registers, states you forgot. Run it on **every**
577
+ screen in **every** app, fix what it finds, and re-run the ones you changed.
578
+
579
+ Screenshot each page and feed it the images. Without them its findings drop to
580
+ inference from source, and it misses real misalignment, contrast, and overflow.
581
+ Judging your own UI from source code is guessing.
582
+
583
+ **Screenshot at a phone width too (≈390px), not just desktop, and critique
584
+ those.** A layout that is fine at 1440px routinely breaks at 390 — a table that
585
+ overflows, a row of buttons that wraps into a pile, text jammed against the edge,
586
+ a modal taller than the viewport. Mantine gives you the tools (responsive `Grid`,
587
+ `visibleFrom` / `hiddenFrom`, `Stack` instead of `Group` at small sizes); use
588
+ them. The template already mounts a phone navigation per `AGENTS.md` — pick
589
+ `MobileTabBar` or `MobileNavDrawer` deliberately per app, never both.
590
+
591
+ The gate: **no P0 findings left on any screen, in any app, at either width.**
592
+ Don't silence a finding by deleting the feature it is about.
593
+
594
+ ## 9. Ship it, and stay Fabric-ready
595
+
596
+ When every milestone is `built` and the scenarios are green, read
597
+ `references/ship.md`. It carries the open-source deploy paths (`--provider
598
+ standalone`, `cloudflare`, `aws`), how to serve several frontends behind one
599
+ API, the pre-release gate to run, and the contract that keeps `pikku fabric init`
600
+ a one-command import later rather than a migration.
601
+
602
+ Two things from it are worth knowing before you get there, because they are
603
+ cheaper to honour than to retrofit:
604
+
605
+ - **Nothing hardcodes a host, a port, or a `process.env` read inside a
606
+ function.** Secrets go through `defineSecret` and the injected `secrets`
607
+ service. This is the most common reason a working local project fails its
608
+ first deploy, on any platform.
609
+ - **Generated files stay generated.** No hand edits to `.pikku/`, `*.gen.*`, or
610
+ the SDK.
611
+
612
+ ## Reference
613
+
614
+ - `references/multi-app.md` — adding a second frontend (§4), at the milestone
615
+ that needs it
616
+ - `references/theming.md` — authoring the theme (§8a)
617
+ - `references/ship.md` — deploying, and the Fabric-readiness contract (§9)
618
+ - Sibling skills: `pikku-knowledge` (§2), `pikku-permissions` (§3),
619
+ `pikku-scenario` (§7, §7a), `pikku-deploy-cloudflare` and `pikku-fabric` (§9)
620
+ - Project conventions written by the template: `AGENTS.md`
621
+ - Doing less than this: `pikku-build-quick`. Doing more: `pikku-build-platform`.