@pikku/skills 0.12.10 → 0.12.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +819 -0
- package/dist/skills.gen.js +1 -1
- package/package.json +4 -4
- package/skills/pikku-addon/SKILL.md +17 -11
- package/skills/pikku-addon/references/addon-package-manifest.md +2 -2
- package/skills/pikku-agent/SKILL.md +4 -5
- package/skills/pikku-audit/SKILL.md +28 -13
- package/skills/pikku-aws/SKILL.md +2 -2
- package/skills/pikku-better-auth/SKILL.md +97 -17
- package/skills/pikku-build-app/SKILL.md +621 -0
- package/skills/pikku-build-app/references/multi-app.md +117 -0
- package/skills/pikku-build-app/references/ship.md +98 -0
- package/skills/pikku-build-app/references/theming.md +70 -0
- package/skills/pikku-build-platform/SKILL.md +239 -0
- package/skills/pikku-build-quick/SKILL.md +238 -0
- package/skills/pikku-cli/SKILL.md +7 -7
- package/skills/pikku-cli/references/complete-example.md +1 -1
- package/skills/pikku-concepts/SKILL.md +5 -2
- package/skills/pikku-concepts/references/concept-mapping.md +1 -1
- package/skills/pikku-config/SKILL.md +5 -3
- package/skills/pikku-deploy-azure/SKILL.md +5 -4
- package/skills/pikku-emails/SKILL.md +28 -7
- package/skills/pikku-fabric/SKILL.md +27 -3
- package/skills/pikku-fabric-debug/SKILL.md +1 -1
- package/skills/pikku-feature/SKILL.md +5 -4
- package/skills/pikku-http/SKILL.md +4 -4
- package/skills/pikku-http/references/http-options.md +13 -13
- package/skills/pikku-i18n/SKILL.md +2 -1
- package/skills/pikku-info/SKILL.md +1 -1
- package/skills/pikku-knowledge/SKILL.md +13 -13
- package/skills/pikku-mcp/SKILL.md +4 -4
- package/skills/pikku-middleware/SKILL.md +5 -5
- package/skills/pikku-middleware/references/middleware-patterns.md +14 -8
- package/skills/pikku-n8n-import/SKILL.md +12 -12
- package/skills/pikku-n8n-import/SPEC.md +3 -0
- package/skills/pikku-n8n-import/references/code-translation.md +26 -22
- package/skills/pikku-n8n-import/references/loops-and-control.md +7 -7
- package/skills/pikku-paraglide/SKILL.md +11 -6
- package/skills/pikku-permissions/SKILL.md +19 -15
- package/skills/pikku-product-second-opinion/README.md +3 -3
- package/skills/pikku-product-second-opinion/example/sample-report.md +15 -12
- package/skills/pikku-product-second-opinion/references/report-template.md +10 -7
- package/skills/pikku-queue/SKILL.md +1 -1
- package/skills/pikku-react/SKILL.md +53 -13
- package/skills/pikku-realtime/SKILL.md +52 -21
- package/skills/pikku-rpc/SKILL.md +4 -2
- package/skills/pikku-rtl/SKILL.md +1 -1
- package/skills/pikku-scenario/SKILL.md +131 -20
- package/skills/pikku-schedule/SKILL.md +6 -1
- package/skills/pikku-schema-ajv/SKILL.md +2 -2
- package/skills/pikku-schema-cfworker/SKILL.md +1 -1
- package/skills/pikku-security/SKILL.md +9 -5
- package/skills/pikku-services/SKILL.md +27 -18
- package/skills/pikku-services/references/audit-wire-service.md +14 -8
- package/skills/pikku-software-archaeology/scripts/validate.mjs +146 -69
- package/skills/pikku-tag-middleware/SKILL.md +1 -0
- package/skills/pikku-template-clone/SKILL.md +2 -1
- package/skills/pikku-trigger/SKILL.md +3 -3
- package/skills/pikku-websocket/SKILL.md +4 -3
- package/skills/pikku-workflow/SKILL.md +2 -2
- 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`.
|