@pikku/skills 0.12.30 → 0.12.33

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,11 @@
1
1
  {
2
2
  "name": "@pikku/skills",
3
- "version": "0.12.30",
3
+ "version": "0.12.33",
4
+ "repository": {
5
+ "type": "git",
6
+ "url": "git+https://github.com/pikkujs/pikku.git",
7
+ "directory": "packages/skills"
8
+ },
4
9
  "description": "The Pikku agent skills — the instruction set coding agents read to build, wire and deploy Pikku projects",
5
10
  "author": "yasser.fadl@gmail.com",
6
11
  "license": "MIT",
@@ -10,13 +15,13 @@
10
15
  "type": "module",
11
16
  "scripts": {
12
17
  "embed": "node scripts/embed.mjs",
13
- "tsc": "yarn embed && tsc",
14
- "build": "yarn embed && tsc -b",
18
+ "tsc": "bun run embed && tsc",
19
+ "build": "bun run embed && tsc -b",
15
20
  "ncu": "npx npm-check-updates",
16
21
  "test": "bash run-tests.sh",
17
22
  "test:watch": "bash run-tests.sh --watch",
18
23
  "test:coverage": "bash run-tests.sh --coverage",
19
- "prepublishOnly": "yarn build"
24
+ "prepublishOnly": "bun run build"
20
25
  },
21
26
  "exports": {
22
27
  ".": "./dist/index.js"
@@ -27,10 +32,9 @@
27
32
  ],
28
33
  "devDependencies": {
29
34
  "@types/node": "^24.13.3",
30
- "typescript": "^6.0.3",
31
35
  "yaml": "^2.9.0"
32
36
  },
33
37
  "engines": {
34
38
  "node": ">=24"
35
39
  }
36
- }
40
+ }
@@ -324,6 +324,11 @@ wireAddon({ name: 'todos', package: '@my-org/addon-todos' })
324
324
 
325
325
  After registration, run `yarn pikku all` to generate types for the addon's functions.
326
326
 
327
+ Give each addon its own wiring file. A deployment unit imports a `wireAddon`
328
+ file only while at least one addon that file wires survives the unit's filter,
329
+ so wiring two addons from one file means a unit needing either one registers
330
+ both and bundles both packages' dependencies.
331
+
327
332
  If the addon ships tables, `pikku db generate` then writes one migration per
328
333
  addon — named after the package, carrying the addon's own SQL — after Better
329
334
  Auth's and the runtime's, so an addon table may reference `user` or a runtime
@@ -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
 
@@ -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
@@ -332,6 +338,10 @@ What a milestone is:
332
338
  persona. If you cannot write the gherkin, you cannot build it yet — that is a
333
339
  `questions/` note, not a milestone.
334
340
 
341
+ If §1's screen mock was made and approved, the milestones are read off it: every
342
+ screen on that page belongs to some milestone, and a screen no milestone builds
343
+ is a hole in this plan. Say which milestone covers which screen.
344
+
335
345
  How to order them:
336
346
 
337
347
  1. **The spine first.** The one object everything else hangs off, and the screen
@@ -415,16 +425,26 @@ over, and an uncovered function is a half-milestone whether or not the note says
415
425
  5. **UI.** Pages in `<app>/src/pages/`, one route file each in `<app>/src/routes/`,
416
426
  calling functions through the generated `usePikkuQuery` / `usePikkuMutation`
417
427
  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.
428
+ file. Compose the kit from `@/components/<Name>` rather than hand-rolling
429
+ controls — and **add to that kit**: the component that draws the thing this
430
+ product is actually about, and the furniture you would otherwise copy-paste
431
+ into eight pages. The kit is where you start, not where you stop; an app whose
432
+ every screen is `Card` + `Stack` + `Text` composed the inventory rather than a
433
+ design. Register the screen in `useNavItems()` — that one file feeds both the
434
+ desktop sidebar and the phone navigation.
421
435
  **Read `references/design.md` before you write the first screen.** You commit
422
436
  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`.
437
+ layouts, because the design is yours to make. What you build here is then
438
+ judged at step 7, at the end of this milestone rather than once at §8, where
439
+ the only affordable fix is a repaint of eight screens.
440
+ 6. **Scenario** (§7).
441
+ 7. **Look at it.** Screenshot every screen this milestone touched, at both
442
+ widths, with the seed in place, and look at the images. This is a gate, the
443
+ same as the scenario: a milestone whose screens nobody has seen is not built,
444
+ it is unproven at the one layer scenarios cannot reach. `references/design.md`
445
+ carries how to take the shot when no browser tool is wired up, and what to
446
+ look for. Then `status: built`, and say in the note what you looked at and
447
+ what it made you change.
428
448
 
429
449
  Rules that are not optional:
430
450
 
@@ -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.
@@ -23,6 +23,85 @@ how a worker deploy fails at runtime instead of at build.
23
23
  Always run `plan` before `apply`, and read it. It names what will be created,
24
24
  updated and deleted — the deletions are the reason to look.
25
25
 
26
+ ### How many workers you get
27
+
28
+ By default a unit holds every function that builds the same set of singleton
29
+ services. One unit per function is available too, but on a large app it means a
30
+ hundred workers whose bundles are mostly the same framework code repeated, and a
31
+ build and an upload for each. `deploy.grouping` in `pikku.config.json` sets the
32
+ shape:
33
+
34
+ ```json
35
+ "deploy": {
36
+ "grouping": {
37
+ "strategy": "single",
38
+ "rules": [
39
+ { "unit": "console", "addon": "console" },
40
+ { "unit": "pdf", "tags": ["pdf"] }
41
+ ]
42
+ }
43
+ }
44
+ ```
45
+
46
+ `strategy` is the fallback for a function no rule matches:
47
+
48
+ | strategy | fallback |
49
+ | --- | --- |
50
+ | `services` | one unit per distinct set of singleton services (the default) |
51
+ | `function` | one unit each |
52
+ | `single` | one shared unit |
53
+
54
+ Rules are ordered, first match wins, and match on `tags`, an `addon` namespace
55
+ or `routes` globs. A rule always beats the strategy, so under `function` a rule
56
+ merges and under `single` or `services` it carves out.
57
+
58
+ `services` is the default because it is the middle ground: `single` is too
59
+ coarse to reason about and `function` pays a cold start and a deploy step per
60
+ function. Functions are keyed
61
+ on the singleton services their bodies destructure, minus the ones every unit
62
+ builds anyway (`config`, `logger`, `variables`, `schema`, `secrets`, and the
63
+ per-request `rpc`/`mcp`/`channel`/`userSession`). Units are named for that set —
64
+ `svc-todo-store`, `svc-event-hub-todo-store`, `svc-base` for functions needing
65
+ nothing else — and each records it as `servicesKey` in the manifest. On the
66
+ `templates/functions` app it turns 44 units into 10.
67
+
68
+ The deploy target is part of the key, so a `server` unit is named `-server` and
69
+ can never merge with its serverless twin. That is what keeps `services` from
70
+ tripping the mixed-target refusal below: two functions can carry identical
71
+ services and still run in different places, because a function may name
72
+ `deploy: 'server'` itself without any service crossing it.
73
+
74
+ Read the shape before trusting it. The partition depends entirely on how varied
75
+ the app's service use is: an app where nearly every function reaches the same
76
+ database collapses to roughly `single` with a few carve-outs. Run `pikku deploy
77
+ plan` and look at the unit list; set `"strategy": "function"` if you want a unit
78
+ per function back.
79
+
80
+ Changing the strategy renames units, and a unit name is what a queue consumer, a
81
+ scheduled task and a `dependsOn` point at. The manifest rewrites all three, but
82
+ anything holding a unit name outside the manifest does not follow. Units dropped
83
+ from the manifest are not deleted by OSS `deploy()` either (pikkujs/pikku#543) —
84
+ fabric sweeps its dispatch namespace after each deploy, plain pikku leaves the
85
+ old workers in place.
86
+
87
+ `tags` matches the tags a function inherits from its wirings — `wireHTTP({ ...,
88
+ tags: ['pdf'] })` — as well as any on the function itself, which is where almost
89
+ every project writes them.
90
+
91
+ Two things that bite:
92
+
93
+ - **Grouping cannot change a deploy target.** Put a `serverlessIncompatible`
94
+ function in a group with serverless ones and the build fails naming both
95
+ sides. Give it its own rule — that refusal is the design, not a bug. To find
96
+ the culprit, read `deployment-manifest.json`: a unit carries `targetForcedBy`
97
+ naming the services that crossed it to `server`, and `groupedBy` naming the
98
+ rule that made it, so you can see which rule to carve the function out of.
99
+ - **Grouping widens secret scope.** Every function in a unit reads every secret
100
+ that unit is granted, so treat a merge as a security decision too.
101
+
102
+ Group by something that means something — a domain, a secret scope, a deploy
103
+ cadence. There is deliberately no automatic packing by bundle size.
104
+
26
105
  The frontends build independently (`bun run build` at the root builds every
27
106
  workspace). Serve each behind its own hostname, and put the API behind `/api` on
28
107
  **all of them**, mirroring the Vite proxy from the multi-app reference: `/api/auth/*` keeps its
@@ -53,6 +53,128 @@ Then **write the direction into `knowledge/decisions/design/`** — the words th
53
53
  user gave you, what you chose, and what it rules out. The JSON records what the
54
54
  theme is; only the note records why.
55
55
 
56
+ ## The colours the theme has no field for
57
+
58
+ `brand` is the product's accent. It is not the only colour a screen needs, and
59
+ the missing ones are why "don't hardcode colours per component" gets broken by
60
+ the same agent that wrote it down.
61
+
62
+ A screen has to say *covered* and *still open*, *fine* and *needs attention* —
63
+ and those are not the accent. Using the accent for them is worse than a stray
64
+ hex: the brand colour stops meaning "this product" and starts meaning "good", so
65
+ it means nothing. But there is no `brand.covered` field, so the value lands
66
+ inline as `#3f7d5c`, once per component, slightly different each time.
67
+
68
+ Give them a home. A small stylesheet of custom properties, imported once beside
69
+ the Mantine styles, is enough:
70
+
71
+ ```css
72
+ :root:root:root {
73
+ --app-covered: #3f7d5c; --app-covered-bg: #e6f1ea;
74
+ --app-open: #a8701a; --app-open-bg: #fbeedb;
75
+ --app-sunk: #fdf7f4; /* a recessed surface, for forms and asides */
76
+ --app-hairline: #ecdfd9; /* NOT var(--mantine-color-gray-2) — see below */
77
+ }
78
+ :root:root[data-mantine-color-scheme='dark'] {
79
+ --app-covered: #7fc09a; --app-covered-bg: #1e2f26;
80
+ --app-open: #e0ab5c; --app-open-bg: #33271a;
81
+ --app-sunk: #241b18;
82
+ --app-hairline: #392b26;
83
+ }
84
+ ```
85
+
86
+ Those values are one app's warm direction, not a palette to copy — derive your
87
+ own from yours.
88
+
89
+ ## Choose the neutrals; do not inherit them
90
+
91
+ The two details in that snippet that look like typos are the two things most
92
+ likely to make your app look like every other app.
93
+
94
+ **The hairline is a literal, not `var(--mantine-color-gray-2)`.** Mantine's grey
95
+ ramp is blue-biased — `#f8f9fa`, `#dee2e6`, `#868e96` are all cool — and it is
96
+ what draws card borders, dividers, the shell's edges and every disabled control.
97
+ The theme JSON has `brand` and `structure` and **no neutral field at all**, so
98
+ unless you choose otherwise, every app built from this skill runs its accent on
99
+ somebody else's greys.
100
+
101
+ If the accent is not itself blue, that mismatch lands on every screen at once:
102
+ warm content ruled off in cold lines, off everywhere and wrong nowhere in
103
+ particular, which is the hardest kind of wrong to find. It survives a careful
104
+ critique because no single screen is broken.
105
+
106
+ So bias the whole ramp toward the accent — not just the tokens with obvious
107
+ names. Keep Mantine's lightness steps so contrast behaviour and every component
108
+ that picks a step by number are unchanged; move only the hue:
109
+
110
+ ```css
111
+ /* The ramps: hue only. Mantine picks a step by number in either scheme, so
112
+ these are scheme-independent and belong in the unscoped block. */
113
+ :root:root:root {
114
+ --mantine-color-gray-0: #faf7f5; --mantine-color-gray-5: #b8a49d;
115
+ --mantine-color-gray-1: #f5efec; --mantine-color-gray-6: #93807a;
116
+ --mantine-color-gray-2: #efe6e2; --mantine-color-gray-7: #574a45;
117
+ --mantine-color-gray-3: #e6dad5; --mantine-color-gray-8: #3d332f;
118
+ --mantine-color-gray-4: #d8c8c2; --mantine-color-gray-9: #2a1f1b;
119
+ --mantine-color-dark-0: #f3e9e4; --mantine-color-dark-5: #4a3a34;
120
+ --mantine-color-dark-1: #cdbdb6; --mantine-color-dark-6: #392b26;
121
+ --mantine-color-dark-2: #a8938c; --mantine-color-dark-7: #241b18;
122
+ --mantine-color-dark-3: #7a655e; --mantine-color-dark-8: #1b1512;
123
+ --mantine-color-dark-4: #5c4a44; --mantine-color-dark-9: #120d0b;
124
+ }
125
+
126
+ /* Ground, text and border are a different colour in each scheme, so each one
127
+ is set in the scheme it belongs to. A scheme-dependent token left in the
128
+ block above is the classic unreadable-in-dark bug. */
129
+ :root:root:root[data-mantine-color-scheme='light'] {
130
+ --mantine-color-body: #fdfaf8;
131
+ --mantine-color-text: #2a1f1b;
132
+ --mantine-color-default-border: #ecdfd9;
133
+ --mantine-color-dimmed: #7a625c; /* keep AA: ~5.4:1 on the body above */
134
+ --mantine-color-placeholder: #826a64; /* ~4.8:1 */
135
+ }
136
+ :root:root:root[data-mantine-color-scheme='dark'] {
137
+ --mantine-color-body: #1b1512;
138
+ --mantine-color-text: #f3e9e4;
139
+ --mantine-color-default-border: #392b26;
140
+ --mantine-color-dimmed: #a8938c; /* ~5.6:1 on the body above */
141
+ --mantine-color-placeholder: #8c7a74; /* ~4.7:1 */
142
+ }
143
+ ```
144
+
145
+ Check the four text tokens against your own grounds rather than copying these —
146
+ `dimmed` is the most-used text colour in the app and the easiest to drop below
147
+ 4.5:1 while making it prettier, and it has to clear the bar on *both* grounds.
148
+
149
+ **The selector is tripled on purpose.** Mantine's `cssVariablesResolver` injects
150
+ its own `:root` block into `<head>` at runtime, which lands *after* your
151
+ stylesheet and wins on source order at equal specificity. A plain `:root` here is
152
+ silently reverted: the file reads correct, the app renders Mantine's defaults,
153
+ and nothing errors. Repeating the pseudo-class raises specificity without adding
154
+ an element to the selector.
155
+
156
+ This one costs a whole pass if you meet it without knowing: you diagnose the
157
+ colours correctly, write the right values, reload, and see no change — so you
158
+ assume the diagnosis was wrong. **The only thing that catches it is looking at a
159
+ screenshot and disbelieving the CSS.**
160
+
161
+ Name them for what they *mean* in this product, never for the colour — `covered`,
162
+ not `green`. The name is the whole value: it survives a change of palette, and it
163
+ is the thing that makes the second use agree with the first. Define both colour
164
+ schemes at once; a token defined only in light is the classic unreadable-in-dark
165
+ bug, and Mantine will happily render it.
166
+
167
+ The same file is where a couple of other things belong that the theme JSON has no
168
+ field for and every screen otherwise re-invents: the hairline that separates rows
169
+ in a list, the recessed surface a form sits on so it does not carry the same
170
+ weight as the content it adds to, and the one animation the product is allowed
171
+ (behind `prefers-reduced-motion`). Two or three rules, not a framework.
172
+
173
+ If the app ships template screens you did not write — the error and not-found
174
+ pages usually — read them before you call the palette done. They arrive with the
175
+ scaffold's colours hardcoded, and a stock blue accent on an app whose direction
176
+ says warm is the single loudest contradiction in the build.
177
+
56
178
  **Set the theme once, don't hardcode colours per component.** A screen full of
57
179
  inline `color="blue"` and one-off hex values is why apps look templated. Change
58
180
  the theme, not the components — and keep it theme-aware for light and dark.
@@ -284,6 +284,20 @@ pikku knowledge plan defer <milestone> <item> -r "<why>"
284
284
 
285
285
  `progress` reconciles the plan against pikku's generated meta — set membership, never anyone's status — and exits non-zero while the first pass is short, or while anything already built contradicts the plan. Unbuilt work in a later pass is reported, not blocked; a function that shipped wide open against a planned permission rule blocks from any pass, because that is a hole rather than a backlog. Writing a plan is its own seat: read `pikku-architect`. Building against one is `pikku-build`.
286
286
 
287
+ ### A finished milestone is a tombstone
288
+
289
+ **Once a milestone reaches `built`, its note and its plan are closed. Do not edit either.** Not to correct the wording, not to fold in what the build actually turned out to need, not to add the item everyone agrees should have been there. A finished milestone is the record of what was agreed and what was measured against it, and a record that can be revised afterwards measures nothing.
290
+
291
+ This is the rule the shape of the thing already implies. `progress` reconciles a plan against generated meta and fails when what shipped contradicts it — a check with no force at all if the losing side of the contradiction may simply be rewritten. `attempts:` brakes a note nothing can satisfy, and refunds that budget when the note's content really changes; a `built` note that keeps changing is that brake removed. Both only work while the plan stays still.
292
+
293
+ So when a `built` milestone turns out to be wrong or incomplete, **the answer is always a new note, never an edit to the old one**:
294
+
295
+ - It needed more than it said → a new milestone, which may name the old one.
296
+ - It was built differently than planned → that is what `progress` is for. Reconcile forward, or record a decision saying why the plan was not the right shape.
297
+ - It was simply wrong → a decision note that supersedes it. The wrong milestone stays where it is; a base whose history is edited cannot answer *why* anything is the way it is, which is most of what a base is for.
298
+
299
+ The exception, and it is narrow: bookkeeping the loop owns. `statusAt:` and `attempts:` are written by whatever moved the note, at any status, and are bookkeeping rather than content. Nothing else about a `built` note moves again.
300
+
287
301
  ## Profiles built on this one
288
302
 
289
303
  OKF permits frontmatter fields a reader does not know, and the parser ignores them rather than failing. That is the extension point: a tool layered on Pikku can add its own sections and fields on top of everything above without forking the format.
@@ -1,6 +1,5 @@
1
1
  # Pikku Services (Dependency Injection)
2
2
 
3
-
4
3
  ## Before You Start
5
4
 
6
5
  ```bash
@@ -195,6 +194,13 @@ const createSingletonServices = pikkuServices(async (config) => {
195
194
  })
196
195
  ```
197
196
 
197
+ A deployment split regenerates this manifest **per unit**, so the same
198
+ `services.ts` sees a different manifest in each bundle and each unit builds only
199
+ what its own functions, middleware and permissions reach. A `services.ts` that
200
+ constructs unconditionally gets none of that: every unit pays for every service,
201
+ and the split buys nothing but duplicated bytes. Branching on the manifest is
202
+ what makes the split real.
203
+
198
204
  ### Audit Wire Service
199
205
 
200
206
  `createInvocationAudit` + `createAuditedKysely` add per-request audit buffering that flushes on request close (no-op if `audit` is unconfigured). For the full pattern, no-op behavior, custom-event usage, and Fabric notes, read `references/audit-wire-service.md`.