@pikku/skills 0.12.32 → 0.12.34

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/skills",
3
- "version": "0.12.32",
3
+ "version": "0.12.34",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/pikkujs/pikku.git",
@@ -3,9 +3,9 @@ name: pikku-architect
3
3
  description: >-
4
4
  Use to turn one settled milestone note into the technical plan the build is measured against —
5
5
  the tables, functions, wires, roles, scopes, screens and scenarios it owes, split into passes and
6
- written through `pikku knowledge plan set`. This is a SEPARATE SEAT from the build: the plan is
7
- the denominator `pikku knowledge plan progress` divides by, so whoever writes it must not be the
8
- one grading themselves against it. TRIGGER when: a milestone note is settled and the next step is
6
+ written through `pikku knowledge plan set`. The plan is the denominator
7
+ `pikku knowledge plan progress` divides by, so it is written BEFORE any of the milestone's code
8
+ exists and never edited afterwards to match what got built. TRIGGER when: a milestone note is settled and the next step is
9
9
  planning it, the user asks to plan or architect a milestone, `pikku knowledge plan progress` says
10
10
  a milestone has no plan, or pikku-build's App mode reaches a milestone with nothing planned. DO
11
11
  NOT TRIGGER when: the milestone notes themselves are still being written (use pikku-knowledge),
@@ -17,18 +17,19 @@ installGroups: [core]
17
17
  # Plan one milestone
18
18
 
19
19
  A milestone note says what the app must DO and how it must feel for the person using it. It
20
- deliberately does not say how. You are the seat that decides how, once, in writing, before anyone
21
- builds it.
20
+ deliberately does not say how. This is where how gets decided, once, in writing, before any of
21
+ it is built.
22
22
 
23
- **Why this is a separate seat.** The build agent used to write its own plan. That makes one party
24
- both author and examiner: it can build a fraction, plan only that fraction, and certify itself
25
- complete — and `pikku knowledge plan progress` then divides by a denominator the builder chose
26
- after seeing its own answer. A plan written here, against the note, by someone who is not going to
27
- build it, is the denominator the builder does not own.
23
+ **Why the plan comes first and stays fixed.** A builder who plans after seeing its own work can
24
+ build a fraction, plan only that fraction, and certify itself complete — `pikku knowledge plan
25
+ progress` then divides by a denominator chosen after the answer. The defence is the ORDER: the plan
26
+ is written against the note, in its own turn, before a single migration for it exists, and is never
27
+ edited afterwards. An item that will not land is deferred with its reason through `plan defer`,
28
+ not rewritten out of the plan. The same agent plans and then builds; nothing hands off.
28
29
 
29
- **One milestone, one plan, then stop.** Do not build in this session. Do not plan the next
30
- milestone "while you are here" — the notes after this one are still allowed to change, and a plan
31
- written against a note that later moves is worse than no plan.
30
+ **One milestone, one plan, then build it.** Do not plan the next milestone "while you are here" —
31
+ the notes after this one are still allowed to change, and a plan written against a note that later
32
+ moves is worse than no plan.
32
33
 
33
34
  ---
34
35
 
@@ -38,6 +39,11 @@ The plan reaches disk through `pikku knowledge plan set <milestone> <file>` and
38
39
  validates first and names the field that is wrong if it refuses; a plan file written with an editor
39
40
  is a plan nothing checked, and the place that discovers that is a finished build.
40
41
 
42
+ It is also written ONCE. `plan set` refuses a milestone that already has a plan: the plan is the
43
+ order the build is measured against, and an order that can be rewritten measures nothing. The one
44
+ way down from a written plan is `pikku knowledge plan defer <milestone> <item> --reason <why>`,
45
+ which records what was left out and why.
46
+
41
47
  It is JSON rather than a note on purpose. Everything else under `knowledge/` is prose a human
42
48
  reads; this one is consumed field-by-field, and a markdown parser is one more place a misspelt
43
49
  heading silently passes. It cannot live INSIDE the milestone note either: that note is frozen once
@@ -260,6 +266,7 @@ cover it — so a role × resource cross product there costs the milestone nothi
260
266
 
261
267
  ## When you are done
262
268
 
263
- The accepted `plan set` is the end of the seat. Hand the milestone to `pikku-build`, which reads the
264
- plan with `plan show --for-build`, builds it, and closes the milestone only when
265
- `pikku knowledge plan progress` is clean. What you wrote is what it is measured against.
269
+ The accepted `plan set` is the end of planning. Go straight on to the build in `pikku-build` §6:
270
+ read the plan with `plan show --for-build`, build it, and close the milestone only when
271
+ `pikku knowledge plan progress` is clean. What you wrote is what you are measured against, so do
272
+ not touch it once the first migration is open.
@@ -19,13 +19,13 @@ installGroups: [core]
19
19
 
20
20
  ## Which mode
21
21
 
22
- | The situation | Read |
23
- | --- | --- |
24
- | A template was just cloned or scaffolded, and the tree still looks like one | `references/post-clone.md` first, then come back |
25
- | A real product, meant to be picked up by someone else | `references/app.md` — the default |
26
- | A spike, a throwaway demo, an idea nobody has committed to | `references/quick.md` |
27
- | A showcase meant to exercise every Pikku surface | `references/platform.md`, which is a delta on top of `references/app.md` |
28
- | A feature added to an app that already has its knowledge base and milestones | `references/feature.md` |
22
+ | The situation | Read |
23
+ | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
24
+ | A template was just cloned or scaffolded, and the tree still looks like one | `references/post-clone.md` first, then come back |
25
+ | A real product, meant to be picked up by someone else | `references/app.md` — the default |
26
+ | A spike, a throwaway demo, an idea nobody has committed to | `references/quick.md` |
27
+ | A showcase meant to exercise every Pikku surface | `references/platform.md`, which is a delta on top of `references/app.md` |
28
+ | A feature added to an app that already has its knowledge base and milestones | `references/feature.md` |
29
29
 
30
30
  **App is the default.** A small or toy-sounding app does not make it Quick;
31
31
  only an explicit signal of speed or throwaway-ness does. Platform is not "App
@@ -33,9 +33,10 @@ plus more effort" — it is App plus a deliberate surface checklist, so read the
33
33
  base first and follow it in full rather than blending the two into one plan.
34
34
 
35
35
  The supporting references belong to whichever mode sends you to them:
36
- `references/multi-app.md` (a second frontend), `references/design.md` (committing
37
- to a design direction and judging whether the screens realise it — read before
38
- the first screen is built, not after the last), `references/theming.md`
36
+ `references/multi-app.md` (a second frontend), `references/design.md` (offering
37
+ to mock the screens first, committing to a design direction, and judging whether
38
+ the screens realise it — read before the first screen is built, not after the
39
+ last), `references/theming.md`
39
40
  (authoring the theme),
40
41
  `references/ship.md` (deploying, and the Fabric-readiness contract).
41
42
 
@@ -52,9 +53,10 @@ while still planning. Those failures look alarming and are nothing but this.
52
53
 
53
54
  ## What holds in every mode
54
55
 
55
- - **The branch and the diff are the contract.** There is no plan JSON. A
56
- reviewer sees real, compiled, working code: apply is a merge, reject is a
57
- `git branch -D`.
56
+ - **The branch and the diff are the contract.** A reviewer sees real, compiled,
57
+ working code: apply is a merge, reject is a `git branch -D`. The milestone's
58
+ plan is your own denominator, measured by `pikku knowledge plan progress` —
59
+ never something a reviewer is handed instead of the code.
58
60
  - **Discover before editing.** `yarn pikku meta context --json` returns
59
61
  functions, wires, middleware, permissions, workflows, `capabilities` and
60
62
  `layout` in one call. Fall back to targeted `meta` commands only for a full
@@ -65,11 +67,13 @@ while still planning. Those failures look alarming and are nothing but this.
65
67
  lives in `messages/*.json`.
66
68
  - **`pikku all` is the gate.** Run it after touching functions, wirings or
67
69
  schemas, and treat its criticals as real.
68
- - **A milestone is planned by a different seat than the one that builds it.**
70
+ - **A milestone is planned before it is built, and the plan then stays fixed.**
69
71
  The plan — tables, functions, wires, roles, scopes, screens, scenarios, in
70
- passes — is written through `pikku knowledge plan set` by `pikku-architect`,
71
- and `pikku knowledge plan progress` measures the build against it from the
72
- generated meta. A builder who writes its own plan is grading itself.
72
+ passes — is written through `pikku knowledge plan set` (how: `pikku-architect`)
73
+ in its own turn before any of that milestone's code exists, and
74
+ `pikku knowledge plan progress` measures the build against it from the
75
+ generated meta. You plan it and you build it; what you never do is edit the
76
+ plan afterwards to match what you built — that is grading yourself.
73
77
 
74
78
  ## What NOT to do
75
79
 
@@ -61,6 +61,12 @@ in one message. Then stop; do not interview the user.
61
61
  Neutral (fine for an internal tool, but say so out loud); a direction in words;
62
62
  a reference (brand guide, screenshots, a site whose register they want); or
63
63
  their own design agent/prompt, whose output you take as the direction.
64
+ - **Do they want to see the screens before you build them?** Offer it here, in
65
+ this same round, as a question and not a gate: one HTML page mocking the main
66
+ screens, a few minutes, far cheaper to change than built screens. If they say
67
+ yes, `references/design.md` owns what to make and what it then binds — the
68
+ approved page becomes source of truth for the screens, and the theme is written
69
+ before it so what they approve is what ships. If they say no, build.
64
70
  - **What language should the app speak, and what language does the team work
65
71
  in?** Two answers, not one — see §1a, which is where they go. Ask only if the
66
72
  request is not obviously English; a brief written in English about an English
@@ -75,11 +81,11 @@ A brief saying "the entire UI is German" is about **one** of these. Getting this
75
81
  wrong has already shipped a project that can never add a second language, so
76
82
  settle all three explicitly before you write code.
77
83
 
78
- | Axis | What it covers | Where it goes |
79
- | --------------- | -------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- |
80
- | **Identifiers** | Function, component, type, variable and file names. Database tables and columns. Commit messages. | Nowhere — **always English**, no setting, not negotiable |
81
- | **Meta** | `description` on functions and steps, `name`/`title` on features and scenarios, step `template`, role and persona descriptions | `metaLocale` in `pikku.config.json`, default `en` |
82
- | **Product UI** | Every string the app shows a user | `messages/<locale>.json`, and `defaultLocale` for what a first-time visitor opens in |
84
+ | Axis | What it covers | Where it goes |
85
+ | --------------- | ------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------ |
86
+ | **Identifiers** | Function, component, type, variable and file names. Database tables and columns. Commit messages. | Nowhere — **always English**, no setting, not negotiable |
87
+ | **Meta** | `description` on functions and steps, `name`/`title` on features and scenarios, step `template`, role and persona descriptions | `metaLocale` in `pikku.config.json`, default `en` |
88
+ | **Product UI** | Every string the app shows a user | `messages/<locale>.json`, and `defaultLocale` for what a first-time visitor opens in |
83
89
 
84
90
  **Identifiers are English.** The product's market does not change this and
85
91
  neither does `metaLocale`. Identifiers are the surface the generated `#pikku/*`
@@ -212,7 +218,11 @@ defineSystemRole({
212
218
  })
213
219
 
214
220
  definePersonas({
215
- visitor: { name: 'Visitor', jobTitle: 'Synthetic health-check user', account: {} },
221
+ visitor: {
222
+ name: 'Visitor',
223
+ jobTitle: 'Synthetic health-check user',
224
+ account: {},
225
+ },
216
226
  amina: {
217
227
  name: 'Amina',
218
228
  jobTitle: 'Property owner',
@@ -223,7 +233,8 @@ definePersonas({
223
233
  bilal: {
224
234
  name: 'Bilal',
225
235
  jobTitle: 'Property owner',
226
- personality: 'A second owner — exists so "you see yours, not theirs" is testable',
236
+ personality:
237
+ 'A second owner — exists so "you see yours, not theirs" is testable',
227
238
  roles: ['owner'],
228
239
  account: {},
229
240
  },
@@ -332,6 +343,10 @@ What a milestone is:
332
343
  persona. If you cannot write the gherkin, you cannot build it yet — that is a
333
344
  `questions/` note, not a milestone.
334
345
 
346
+ If §1's screen mock was made and approved, the milestones are read off it: every
347
+ screen on that page belongs to some milestone, and a screen no milestone builds
348
+ is a hole in this plan. Say which milestone covers which screen.
349
+
335
350
  How to order them:
336
351
 
337
352
  1. **The spine first.** The one object everything else hangs off, and the screen
@@ -359,10 +374,10 @@ scenarios, split into passes. It is JSON, it lives beside the note, and
359
374
  `pikku knowledge plan progress` measures the finished build against it.
360
375
 
361
376
  **Read `pikku-architect` and follow it.** The plan is the denominator the
362
- completion check divides by, so a builder who writes their own plan can build a
363
- fraction, plan only that fraction, and certify itself complete. Fabric answers
364
- that by giving the plan its own seat; here the defence is the ORDER, and it only
365
- holds if you keep it: the plan is written against the note in its own turn,
377
+ completion check divides by, so a builder who plans after seeing their own work
378
+ can build a fraction, plan only that fraction, and certify itself complete. The
379
+ defence is the ORDER, and it only holds if you keep it: the plan is written
380
+ against the note in its own turn,
366
381
  before any of the code it measures exists, and is never edited afterwards to
367
382
  match what you ended up building. An item that will not land is deferred with
368
383
  its reason — `plan defer` — not quietly rewritten. Write it before you open a
@@ -386,8 +401,8 @@ no plan, and everything after the current milestone is still allowed to move.
386
401
 
387
402
  **Per milestone** — plan it (§5a), set its note to `status: dispatched`, do the
388
403
  six steps, close it out (§6a), set it to `built`. Do not start the next one
389
- until §6a passes, §7 is green for this one *and §7a shows its functions
390
- covered*. A stack of half-milestones cannot be reviewed and cannot be handed
404
+ until §6a passes, §7 is green for this one _and §7a shows its functions
405
+ covered_. A stack of half-milestones cannot be reviewed and cannot be handed
391
406
  over, and an uncovered function is a half-milestone whether or not the note says
392
407
  `built`.
393
408
 
@@ -406,7 +421,7 @@ over, and an uncovered function is a half-milestone whether or not the note says
406
421
  Do this generously and do it now: an empty app demos badly and critiques
407
422
  badly, and you cannot judge a screen's hierarchy, overflow, or truncation
408
423
  against zero rows. Seed rows each persona sees differently — with an ownership
409
- rule that means seeding rows for the *second* owner too.
424
+ rule that means seeding rows for the _second_ owner too.
410
425
  3. **Functions.** One `pikkuFunc` per `*.function.ts`. Mark it `expose: true` and
411
426
  Pikku generates the typed RPC client and the React Query hooks the UI calls;
412
427
  you do NOT write an HTTP route for it. Add `wireHTTP` only for a real REST
@@ -415,16 +430,28 @@ over, and an uncovered function is a half-milestone whether or not the note says
415
430
  5. **UI.** Pages in `<app>/src/pages/`, one route file each in `<app>/src/routes/`,
416
431
  calling functions through the generated `usePikkuQuery` / `usePikkuMutation`
417
432
  hooks from `@project/functions-sdk/pikku/api.gen`. One component per `.tsx`
418
- file. Compose the kit from `@/components/<Name>` rather than hand-rolling.
419
- Register the screen in `useNavItems()` — that one file feeds both the desktop
420
- sidebar and the phone navigation.
433
+ file. Compose the kit from `@/components/<Name>` rather than hand-rolling
434
+ controls — and **add to that kit**: the component that draws the thing this
435
+ product is actually about, and the furniture you would otherwise copy-paste
436
+ into eight pages. The kit is where you start, not where you stop; an app whose
437
+ every screen is `Card` + `Stack` + `Text` composed the inventory rather than a
438
+ design. Register the screen in `useNavItems()` — that one file feeds both the
439
+ desktop sidebar and the phone navigation.
421
440
  **Read `references/design.md` before you write the first screen.** You commit
422
441
  to a design direction there and are then accountable to it — it hands you no
423
- layouts, because the design is yours to make. Screenshot each screen at 390
424
- and 1440 with the seed in place at the END of every milestone, and look at the
425
- images — not once at §8, where the only affordable fix is a repaint of eight
426
- screens.
427
- 6. **Scenario** (§7), then `status: built`.
442
+ layouts, because the design is yours to make. What you build here is then
443
+ judged at step 7, at the end of this milestone rather than once at §8, where
444
+ the only affordable fix is a repaint of eight screens.
445
+ 6. **Scenario** (§7).
446
+ 7. **Look at it.** Screenshot every screen this milestone touched, at both
447
+ widths, with the seed in place, and look at the images. This is a gate, the
448
+ same as the scenario: a milestone whose screens nobody has seen is not built,
449
+ it is unproven at the one layer scenarios cannot reach. `references/design.md`
450
+ carries how to take the shot when no browser tool is wired up, and what to
451
+ look for. Then close it against its plan (§6a) — `pikku knowledge plan
452
+ progress` has to exit zero before anything is `built` — and only then set
453
+ `status: built`, saying in the note what you looked at and what it made you
454
+ change.
428
455
 
429
456
  Rules that are not optional:
430
457
 
@@ -444,8 +471,8 @@ Rules that are not optional:
444
471
  ```typescript
445
472
  export const classifications = {
446
473
  payment: {
447
- paid_at: { kind: 'date' }, // -> Date, not an ISO string
448
- metadata: { kind: 'json', tsType: 'PaymentMeta' }, // -> parsed object, not unknown
474
+ paid_at: { kind: 'date' }, // -> Date, not an ISO string
475
+ metadata: { kind: 'json', tsType: 'PaymentMeta' }, // -> parsed object, not unknown
449
476
  },
450
477
  }
451
478
  ```
@@ -454,6 +481,7 @@ Rules that are not optional:
454
481
  `JSON` column with no entry types as `unknown` (the CLI warns PKU481). Add the
455
482
  annotation rather than casting around the generated type. Once the file carries
456
483
  manual fields, `db migrate` stops overwriting it.
484
+
457
485
  - A `z.date()` on a function's **input** arrives over RPC as an ISO string, not a
458
486
  `Date`. Normalise before calling date methods on it (`new Date(value)`), or it
459
487
  throws `.getTime is not a function` at runtime — schema validation accepts the
@@ -522,6 +550,7 @@ Three things it says, and what each one asks of you:
522
550
  milestone is two milestones — say so to the user rather than deferring again.
523
551
  What you may never do is drop the item silently: the plan is what the next
524
552
  person reads to know what this milestone was for.
553
+
525
554
  - **PROBLEMS** — something exists but does not do what was planned. A function
526
555
  planned as restricted whose meta says `auth: false`; a `cascade` no migration
527
556
  declares; a browser scenario that opens a page and asserts it is still on it.
@@ -530,8 +559,9 @@ Three things it says, and what each one asks of you:
530
559
  visible, never blocking.
531
560
 
532
561
  **Do not set the note to `built` while this exits non-zero**, and do not edit the
533
- plan to match what you built — `plan set` is the architect's seat, and a builder
534
- rewriting its own denominator is exactly what the split exists to stop.
562
+ plan to match what you built — the plan was written before the code on purpose,
563
+ and rewriting your own denominator afterwards is exactly what that order exists
564
+ to stop.
535
565
 
536
566
  ## 7. Prove it — scenarios
537
567
 
@@ -546,26 +576,27 @@ import { pikkuScenario } from '#pikku/scenarios'
546
576
 
547
577
  export const tenantReportsAFaultScenario = pikkuScenario<void, { id: string }>({
548
578
  title: 'A tenant reports a fault and the owner sees it',
549
- description: 'The report lands on the owning landlord’s queue, and nobody else’s',
579
+ description:
580
+ 'The report lands on the owning landlord’s queue, and nobody else’s',
550
581
  tags: ['scenario', 'maintenance'],
551
582
  func: async (_services, _data, { scenario, actors }) => {
552
583
  const report = await scenario.do(
553
584
  'reports a broken boiler',
554
585
  'createMaintenanceReport',
555
586
  { summary: 'No hot water' },
556
- { actor: actors.chidi },
587
+ { actor: actors.chidi }
557
588
  )
558
589
  await scenario.then(
559
590
  'appears on the owner’s queue',
560
591
  'reportShowsOnQueue',
561
592
  { id: report.id },
562
- { actor: actors.amina },
593
+ { actor: actors.amina }
563
594
  )
564
595
  await scenario.then(
565
596
  'is invisible to the other owner',
566
597
  'reportIsNotVisible',
567
598
  { id: report.id },
568
- { actor: actors.bilal },
599
+ { actor: actors.bilal }
569
600
  )
570
601
  return { id: report.id }
571
602
  },
@@ -30,6 +30,117 @@ and so is the commitment.
30
30
  Be ambitious with it. A direction that could describe any SaaS app has not been
31
31
  chosen — it has been defaulted to in words instead of in components.
32
32
 
33
+ ### The looks you will default to
34
+
35
+ Being ambitious is easier against a list of specific things to not be. Generated
36
+ interfaces cluster hard, and these are the attractors — not because any is ugly,
37
+ but because arriving at one *by default* means no choice was made:
38
+
39
+ - **Stock Mantine.** The strongest pull, and the hardest to see: a component
40
+ library's untouched defaults do not look broken, they look finished. Blue
41
+ accent, `#dee2e6` borders, `md` radius on everything, `Card` + `Stack` + `Text`
42
+ down every page. An app can be entirely this and never trip a critique, because
43
+ nothing on any screen is *wrong*.
44
+ - Warm cream ground with a serif display face and a terracotta accent.
45
+ - Near-black with one acid-green or vermilion pop.
46
+ - A purple-to-blue gradient header on white.
47
+ - Inter, or Space Grotesk, as the "safe" typeface.
48
+ - Emoji as section markers; everything centre-aligned; one radius and one shadow
49
+ stamped on every block, which flattens hierarchy instead of creating it.
50
+
51
+ If the user asks for one of these, build it — their words win. What is not
52
+ allowed is landing on one because it was nearest to hand.
53
+
54
+ ## Offer to draw the screens before you build them
55
+
56
+ Before the first milestone, **ask** whether they want to see the screens first.
57
+ One question, in §1's round, not a gate of its own:
58
+
59
+ > Want me to mock the main screens as a page you can look at before I build
60
+ > anything? It takes a few minutes and it is much cheaper to change a picture
61
+ > than a built screen.
62
+
63
+ If they decline, build; the direction in words is enough to be accountable to.
64
+ If they accept, this is the cheapest decision in the project — a picture of eight
65
+ screens costs a fraction of eight built screens, and it is the only point where
66
+ "that is not what I meant" is free.
67
+
68
+ **Author the theme first, then draw the mock from it.** This order is the whole
69
+ point. A beautiful page in hand-rolled CSS sets a bar Mantine then misses, and
70
+ what the user approved is not what ships — they signed off on a picture and
71
+ received an approximation of it. So write `themes/<name>.json` first
72
+ (`references/theming.md`), and let the mock take its every value from that file:
73
+ the palette, `structure.radius`, the spacing scale, the fonts, the component
74
+ `defaultProps`. Approving the mock then approves the theme, and the built screens
75
+ inherit it rather than chase it.
76
+
77
+ **The mock has two halves, and only one of them is Mantine's.** This is the same
78
+ split the built screen lives under, applied a step earlier so the two agree by
79
+ construction. The PAGE — the shell, the regions, the columns, the rhythm, the
80
+ material behind the content, what overlaps what — is plain HTML and your own
81
+ CSS, arranged however the layout decision demands; that half is free, and it is
82
+ where the design actually happens. The COMPONENTS — anything a person would
83
+ point at and call a control, and that the app will adopt as itself: buttons,
84
+ inputs, selects, tables, badges, menus, modals — are drawn as *Mantine's*, at the
85
+ metrics Mantine actually uses: its control heights, its input shapes, its table
86
+ and menu behaviour. The test is the one the build will apply too: is this thing
87
+ the SHAPE OF THE PAGE, or a COMPONENT someone would point at?
88
+
89
+ Getting that second half wrong is what makes a mock a lie. A control the mock
90
+ invents is a promise the app cannot keep, and a beautiful hand-rolled input sets
91
+ a bar the real one misses on screen one. If the mock wants something Mantine
92
+ does not do, that is a real finding, and finding it here is the point: change the
93
+ theme so it does, or change the mock, and say which.
94
+
95
+ **What to make.** One self-contained HTML page holding every screen the app
96
+ needs — not a prototype, not a click-through. Static markup, real content from
97
+ their domain (never lorem), the empty and error states beside the happy path,
98
+ laid out so the whole app is legible by scrolling. Its CSS is custom properties
99
+ on `:root` carrying the theme JSON's values, so a change to either is a change
100
+ to one number in both. Mantine itself will not load here — it is a React library
101
+ and a page like this has no bundler, and on hosts that sandbox the page (a Claude
102
+ Artifact) external stylesheets are blocked outright — so do not try; the mock
103
+ reproduces the theme's values by hand, which is why they have to be written down
104
+ first. Whatever your host offers for showing a page is how you show it: an
105
+ Artifact, a file they open, a preview server. The page is the deliverable; how it
106
+ gets in front of them is not this file's business.
107
+
108
+ Write it to `knowledge/decisions/design/screens.html` and treat it as **source of
109
+ truth for the screens** once they approve it. That has consequences worth
110
+ stating:
111
+
112
+ - The milestones are read off it. A screen in the mock that no milestone builds
113
+ is a gap in the plan, not a spare drawing.
114
+ - A screen the build turns out to need that the mock does not have means the
115
+ mock was wrong. Update it, and say you did. Do not let the app and the mock
116
+ drift and then quietly prefer the app.
117
+ - The knowledge graph still owns the domain — objects, roles, rules. The mock
118
+ owns what the screens look like. When they disagree about a *fact*, knowledge
119
+ wins; when they disagree about a *layout*, the mock wins.
120
+
121
+ **Building it is then a transcription, not a translation.** Because the theme
122
+ already exists and the mock was drawn from it, the screen is Mantine components
123
+ arranged the way the mock arranges them — the look arrives with the theme. Two
124
+ rules keep it that way:
125
+
126
+ - **Layout is yours to write; components are Mantine's.** The page shape — the
127
+ regions, the columns, the rhythm, what sits beside what — is ordinary markup
128
+ and your own CSS. Anything a person would point at and call a control comes
129
+ from Mantine: buttons, inputs, selects, tables, badges, menus. Those carry
130
+ focus rings, keyboard behaviour and i18n, and hand-rolling one throws all of
131
+ it away.
132
+ - **A gap goes back to the theme, never into a component override.** If a screen
133
+ does not match the mock, the fix is a value in `themes/<name>.json`. A stack of
134
+ one-off `className`s and `!important` fighting Mantine's defaults looks like
135
+ progress on screen one and is unmaintainable by screen five, and the screens
136
+ drift apart because nothing central holds them together.
137
+
138
+ **Checking the built screen against the mock** is a structural comparison, not a
139
+ pixel one: the same regions in the same order, the same hierarchy, the same
140
+ states present, the same tokens used. Do not chase pixel equality — Mantine's
141
+ components have their own metrics and the mock was drawn without them. A built
142
+ screen that reads as the same screen has passed.
143
+
33
144
  ## One screen, designed properly, before the rest exist
34
145
 
35
146
  Design the first real screen as if it were the only one, and take it further than
@@ -48,6 +159,31 @@ the thing that made it is always yes. Use evidence.
48
159
  - **Screenshot every screen and look at the image**, at ~390px and at ~1440px.
49
160
  Judging your own UI from source is guessing, and the failures that matter —
50
161
  proportion, hierarchy, a wall of identical boxes — are invisible in JSX.
162
+ **Sort out how you will take that screenshot before you need it**, because an
163
+ instruction with no working mechanism behind it is one that gets skipped, and
164
+ this is the one that gets skipped. If a browser-driving tool is wired up, use
165
+ it. If it is not — or it fails to connect, which happens — the fallback is
166
+ short enough to write once and keep:
167
+
168
+ ```sh
169
+ CHROME=$(command -v google-chrome || command -v chromium || \
170
+ command -v chromium-browser || \
171
+ echo "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome")
172
+ "$CHROME" --headless=new --no-sandbox \
173
+ --remote-debugging-port=9333 --user-data-dir=/tmp/gc-shots &
174
+ ```
175
+
176
+ Resolve the binary rather than hardcoding the macOS path: the same gate has to
177
+ work on a Linux box and in CI, and a fallback that only starts on one host is
178
+ a gate that gets skipped everywhere else. If the host already exposes a CDP
179
+ endpoint, point the script at that instead and start nothing.
180
+
181
+ Then drive it over CDP from a script: `Page.navigate`,
182
+ `Emulation.setDeviceMetricsOverride` for the two widths,
183
+ `Page.captureScreenshot`, write the PNG, and open it. Sign in the way a person
184
+ does — click the dev actor switcher on the login page — rather than reaching
185
+ for the secret the client uses; the UI path is shorter and it also proves the
186
+ login screen works. Keep the script; you will run it at every milestone.
51
187
  - **Run `impeccable`** (`npx impeccable install`, Node 22.18+) and feed it the
52
188
  screenshots. It is external, it does not flatter, and it scores execution
53
189
  against interaction heuristics. But it audits how well you executed the design
@@ -69,6 +205,77 @@ questions are fixed:
69
205
  had?
70
206
  6. Would you show it to the user without apologising for it?
71
207
 
208
+ ## The kit is a floor, not a ceiling
209
+
210
+ The scaffold hands you a component kit, and the build instructions tell you to
211
+ compose from it rather than hand-roll. Both are right, and together they have a
212
+ failure mode worth naming: an app whose every screen is `Card` + `Stack` + `Text`
213
+ because those were the pieces in the box. That is not a composed design, it is an
214
+ inventory, and it produces the wall of identical boxes further down this file.
215
+
216
+ **Composing from the kit means using its primitives, not being limited to its
217
+ list.** A product has objects of its own, and the ones that carry its meaning are
218
+ the ones no generic kit ships:
219
+
220
+ - the thing the product is *about*, rendered as itself — a funding meter, a
221
+ streak, a seat map, a run's status over time. If a decision note says the
222
+ progress toward a goal is the primary object, then a component that draws that
223
+ progress has to exist, or the note is describing an app you did not build.
224
+ - the repeated furniture that is currently copy-pasted — the page header, the
225
+ section label, the empty state, the recessed panel a form sits in. Eight inline
226
+ copies of a heading block will not agree with each other; they will disagree by
227
+ a few pixels each, and the screens will read as unrelated for reasons nobody
228
+ can point at.
229
+
230
+ Both kinds are ordinary components built out of kit primitives and theme values.
231
+ Adding them is not hand-rolling, and they are the difference between an app that
232
+ uses a design system and an app that looks like one.
233
+
234
+ The tell that you skipped this: your `components/` directory maps one-to-one onto
235
+ your data model and contains nothing that names a *quality* of the product.
236
+
237
+ ## Design is a gate on the milestone, not a phase at the end
238
+
239
+ The loop that actually runs is plan, build, prove, close. Design advice that
240
+ lives outside that loop does not run — it gets read, agreed with, and skipped,
241
+ because nothing blocks on it. Milestones close on green scenarios, and scenarios
242
+ say nothing about how anything looks.
243
+
244
+ So put it in the loop. **A milestone is not built until its screens have been
245
+ looked at**, in the same sense that it is not built until its scenario passes:
246
+
247
+ - Screenshot every screen the milestone touched, at both widths, with the seed
248
+ in place.
249
+ - Look at the images. Not the JSX.
250
+ - Fix what they show, in this milestone, while it is one screen and not eight.
251
+ - Say in the milestone note what you looked at and what you changed.
252
+
253
+ A milestone closed without that is closed on a claim, not on evidence. The cost
254
+ of being honest about it now is minutes; the cost at §8 is a repaint of the whole
255
+ app, and by then the wrong register has been inherited by every screen so the
256
+ repaint is a rewrite.
257
+
258
+ ### The seed is part of the gate
259
+
260
+ A screenshot is only evidence if the screen has something on it. Before the gate
261
+ runs, the dev seed must populate what each screen *is for* — not one row, and not
262
+ an empty state.
263
+
264
+ This is a real and quiet failure: a list app whose seed has no list, a countdown
265
+ whose seed has no dates, judged for weeks against its own empty state while the
266
+ screen it was built for was never once looked at. The empty state is worth
267
+ designing and is not what the milestone is about.
268
+
269
+ Seed enough to be judged against: several rows, not three identical ones, and a
270
+ deliberate spread of the cases the screen has to hold — a long title that wraps,
271
+ a missing optional field, a picture and no picture, one item in each state the
272
+ screen can show. Anything derived from *today* — a countdown, "3 days ago", an
273
+ expiry — is seeded as an interval from `now`, never as a fixed date: fixed dates
274
+ are correct on the afternoon they are written and meaningless a month later.
275
+
276
+ Data left over from a scenario run is not a seed. If the screens are full of
277
+ `Filter coffee grinder mttqdvsx`, you are designing against test debris.
278
+
72
279
  ## Facts, not taste
73
280
 
74
281
  These are not design opinions and are not open to a different answer.
@@ -125,4 +332,15 @@ first one.
125
332
  - Every row carries the same buttons, and the buttons outweigh the content.
126
333
  - The palette's meaningful colours are also used decoratively, so they have
127
334
  stopped meaning anything.
335
+ - A decision note describes something the screens do not do — the note says
336
+ progress is the primary object and no screen draws progress, or it says warm
337
+ and not clinical and the error page is still template blue.
338
+ - The theme JSON is rich and the screens are bare. Tokens are the cheapest half
339
+ of design and the easiest to mistake for the whole of it: a considered palette
340
+ and a display font applied to a default layout is a well-dressed default.
341
+ - Every border, divider and disabled control is a cool blue-grey while the
342
+ accent is not — the surest sign the neutrals were inherited rather than
343
+ chosen. See `references/theming.md`.
128
344
  - It looks like the last app you built.
345
+ - It looks like Mantine. Not *built with* Mantine, which it is and should be —
346
+ but indistinguishable from a component gallery with the brand hue swapped in.