@pikku/skills 0.12.40 → 0.12.42
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/dist/skills.gen.js +2 -2
- package/package.json +1 -1
- package/skills/pikku-addon/SKILL.md +8 -0
- package/skills/pikku-architect/SKILL.md +31 -14
- package/skills/pikku-architect/references/plan-defects.md +157 -0
- package/skills/pikku-blueprint-to-fabric/SKILL.md +1 -1
- package/skills/pikku-build/SKILL.md +49 -6
- package/skills/pikku-build/references/app.md +79 -85
- package/skills/pikku-build/references/multi-app.md +42 -0
- package/skills/pikku-build/references/platform.md +2 -2
- package/skills/pikku-build/references/scenarios.md +292 -0
- package/skills/pikku-build/references/ship.md +1 -1
- package/skills/pikku-concepts/SKILL.md +1 -1
- package/skills/pikku-fabric/SKILL.md +21 -0
- package/skills/pikku-guide/SKILL.md +199 -169
- package/skills/pikku-kysely/SKILL.md +4 -1
- package/skills/pikku-meta/SKILL.md +5 -5
- package/skills/pikku-meta/references/versioning.md +74 -17
- package/skills/pikku-wiring/SKILL.md +4 -0
- package/skills/pikku-wiring/references/trigger.md +54 -1
package/package.json
CHANGED
|
@@ -340,6 +340,14 @@ wireAddon({ name: 'todos', package: '@my-org/addon-todos' })
|
|
|
340
340
|
|
|
341
341
|
After registration, run `yarn pikku all` to generate types for the addon's functions.
|
|
342
342
|
|
|
343
|
+
In a workspace, add the addon to the dependencies of the package whose file
|
|
344
|
+
calls `wireAddon`, not the repo root. Codegen resolves an addon from the
|
|
345
|
+
package that wires it first and the project root second, the way that
|
|
346
|
+
package's own imports resolve at runtime, and a remote addon's
|
|
347
|
+
`devDependencies` check reads that same package's manifest. If codegen reports
|
|
348
|
+
PKU340, the dependency is missing from the package its message names — do not
|
|
349
|
+
add it at the root to paper over it. `verifiers/addon-workspace` is that layout.
|
|
350
|
+
|
|
343
351
|
If the addon ships tables, `pikku db generate` then writes one migration per
|
|
344
352
|
addon — named after the package, carrying the addon's own SQL — after Better
|
|
345
353
|
Auth's and the runtime's, so an addon table may reference `user` or a runtime
|
|
@@ -200,6 +200,15 @@ callback, a public URL another system posts to), a queue job, a channel, a sched
|
|
|
200
200
|
workflow entry point. Those last two are not alternate URLs — they are what the milestone IS, and a
|
|
201
201
|
plan that omits them ships a `status` column nothing advances or a job nobody runs.
|
|
202
202
|
|
|
203
|
+
A wire is also a constraint on the function's SHAPE, not only an address for it. `wireScheduler`
|
|
204
|
+
takes a function of `void` to `void` — the clock passes nothing and reads nothing back — so a
|
|
205
|
+
function that answers with a report cannot be the one on the clock. One plan here put
|
|
206
|
+
`wire: scheduler` on a nightly collector whose output was the counts a member of staff needed to
|
|
207
|
+
see, and the build had to split it in two: a void shell for the clock, and the exposed collector it
|
|
208
|
+
calls. That was the right answer, but it was a design decision made mid-build because the plan had
|
|
209
|
+
not asked what the wire would accept. Before you write a wire, name what that wire hands the
|
|
210
|
+
function and what it does with the answer; where the two disagree, plan BOTH halves.
|
|
211
|
+
|
|
203
212
|
`permission` is a SENTENCE, not a role name — "only the person who wrote it can edit it". The roles
|
|
204
213
|
are the engineer's choice; the rule is the part that has to survive being implemented, in the
|
|
205
214
|
function's `permissions` field and never in its body. `null` means open to anyone signed in, and
|
|
@@ -259,20 +268,28 @@ cover it — so a role × resource cross product there costs the milestone nothi
|
|
|
259
268
|
|
|
260
269
|
## What makes a plan wrong
|
|
261
270
|
|
|
262
|
-
`plan set` catches the mechanical failures
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
271
|
+
`plan set` catches the mechanical failures — a missing slot, a bad hash, a pass 1 with no `ui` item.
|
|
272
|
+
Read your draft back against these six questions, which it cannot ask. Each has cost a real
|
|
273
|
+
milestone; **[references/plan-defects.md](references/plan-defects.md)** carries the case behind every
|
|
274
|
+
one, and is worth opening for any question you cannot answer with a flat yes.
|
|
275
|
+
|
|
276
|
+
1. **Is this a plan for THIS note?** Every entity the note names appears in a function or a table.
|
|
277
|
+
2. **Does pass 1 slice, and does the model fit inside it?** Not "pass 1: the data model, pass 2: the
|
|
278
|
+
API" — and `model` holds only the tables pass 1 or 2 actually migrates, because the model slot has
|
|
279
|
+
no passes and a later table is a PROBLEM from the first day.
|
|
280
|
+
3. **Can each scenario actually be performed?** Name the persona; assert what the person got rather
|
|
281
|
+
than that the code ran; write totals as deltas against a database nobody resets; check that every
|
|
282
|
+
input the prose describes is a field somebody planned.
|
|
283
|
+
4. **Does something produce every state and field the plan reads?** For each clause of a description,
|
|
284
|
+
each screen the opening paragraph names, each field you filter or badge on, and each state a
|
|
285
|
+
scenario waits in — name the function that gets the world there. A producer you are reusing is
|
|
286
|
+
checked against code that already exists; a producer this milestone is adding is checked against
|
|
287
|
+
the plan that adds it. What is never allowed is a state with no named producer at all.
|
|
288
|
+
5. **Can two sentences in the plan both be true?** Write a state machine out once as a table in
|
|
289
|
+
`model`, name who sets and reads every clock in it, and say whether saving a child collection
|
|
290
|
+
REPLACES it or ADDS to it.
|
|
291
|
+
6. **Did you invent anything?** If the notes do not say who may do a thing, that is `null` with a
|
|
292
|
+
reason, not a rule you made up.
|
|
276
293
|
|
|
277
294
|
---
|
|
278
295
|
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# What makes a plan wrong
|
|
2
|
+
|
|
3
|
+
`plan set` catches the mechanical failures — a missing slot, a bad hash, a pass 1 with no `ui`
|
|
4
|
+
item. These are the ones it cannot, each carried back from a milestone that shipped it. They are
|
|
5
|
+
grouped by the question that finds them, and the fastest way to use this file is to read the six
|
|
6
|
+
questions and only open the group that worries you.
|
|
7
|
+
|
|
8
|
+
1. [Is this a plan for THIS note?](#is-this-a-plan-for-this-note)
|
|
9
|
+
2. [Does pass 1 slice, and does the model fit inside it?](#does-pass-1-slice-and-does-the-model-fit-inside-it)
|
|
10
|
+
3. [Can each scenario actually be performed?](#can-each-scenario-actually-be-performed)
|
|
11
|
+
4. [Does something produce every state and field the plan reads?](#does-something-produce-every-state-and-field-the-plan-reads)
|
|
12
|
+
5. [Can two sentences in the plan both be true?](#can-two-sentences-in-the-plan-both-be-true)
|
|
13
|
+
6. [Did you invent anything?](#did-you-invent-anything)
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Is this a plan for THIS note?
|
|
18
|
+
|
|
19
|
+
**Every entity the note names must appear in a function or a table.** The note is about
|
|
20
|
+
`entries`; the plan builds `projects`. Nothing downstream compares the two.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## Does pass 1 slice, and does the model fit inside it?
|
|
25
|
+
|
|
26
|
+
**Pass 1 is a slice, not a layer.** "Pass 1: the data model. Pass 2: the API. Pass 3: the
|
|
27
|
+
screens" is three passes of nothing working.
|
|
28
|
+
|
|
29
|
+
**Put in `model` only the tables pass 1 — or at worst pass 2 — actually migrates.** The model
|
|
30
|
+
slot has no passes: every item is checked from the moment the milestone starts, so a table whose
|
|
31
|
+
migration belongs to pass 3 is a PROBLEM from pass 1, and `plan progress` refuses on a problem
|
|
32
|
+
rather than deferring it. A milestone that finishes pass 1 green then cannot be closed, and every
|
|
33
|
+
fix is outside the build's hands. When a later pass needs its own tables, that is the signal it is
|
|
34
|
+
a second milestone — say so in `covers`.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Can each scenario actually be performed?
|
|
39
|
+
|
|
40
|
+
**Ask which persona performs it. If the answer is "nobody", it belongs one level up or to no pass
|
|
41
|
+
at all.** The runner drives the app as the personas the project declares; there is no anonymous
|
|
42
|
+
RPC caller, so "a caller with no session is refused" is a sentence a backend or permission
|
|
43
|
+
scenario cannot perform. Signed-out is a BROWSER fact.
|
|
44
|
+
|
|
45
|
+
**Assert that the person got what they came for, not that the code ran.** A scenario proving
|
|
46
|
+
`saveEntry` returns 200 proves the wire. The one worth planning is the one where a person writes
|
|
47
|
+
something, comes back, and it is still there. A browser scenario that opens a page and asserts it
|
|
48
|
+
is still on it proves the route loads and nothing else — `plan progress` names it a problem and
|
|
49
|
+
refuses the milestone.
|
|
50
|
+
|
|
51
|
+
**Write summary and dashboard scenarios as deltas.** The suite runs against a live database nobody
|
|
52
|
+
resets, so "the payment-failed count is zero, then one" is a claim about every run that came
|
|
53
|
+
before. Read the figure, do the thing, assert what moved. And before planning a tile, name the
|
|
54
|
+
function in this plan that can move it: one counted uncaptured paid orders, which the checkout
|
|
55
|
+
function makes impossible to produce, so the scenario could only ever assert a zero.
|
|
56
|
+
|
|
57
|
+
**Every input the person is described as giving has to be a field somebody planned.** A plan wrote
|
|
58
|
+
"the desk lists what is due, and staff run the collection as of that date" while its screen only
|
|
59
|
+
ever asked about today — and deliveries fall on the first of a month, so the only thing that
|
|
60
|
+
scenario could assert was an empty desk. Read each scenario's prose against the `ui` items the
|
|
61
|
+
same way you read it against the functions.
|
|
62
|
+
|
|
63
|
+
**Write every refusal as "X, which is `<state>`, is refused because `<rule>`" — then check that
|
|
64
|
+
state against the rule you wrote in the same plan.** A refusal claims two things at once: that the
|
|
65
|
+
rule shuts, and that the described situation reaches the shut state. One plan asked for "a paused
|
|
66
|
+
licence is refused the academy" in the same milestone whose gate admitted `active_until_expired`,
|
|
67
|
+
which is exactly what pausing inside the paid period produces. The scenario could only ever have
|
|
68
|
+
asserted a bug.
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## Does something produce every state and field the plan reads?
|
|
73
|
+
|
|
74
|
+
This is one question asked of four different things. Each costs a build a mid-flight design
|
|
75
|
+
decision that nothing records.
|
|
76
|
+
|
|
77
|
+
**Every clause of a `feature` or `scenario` description has to be performed by a function** — one
|
|
78
|
+
in this plan, or one already in the meta. Write "when the distributor is removed its companies
|
|
79
|
+
fall back to the direct catalog" with no function that removes a distributor and `plan set`
|
|
80
|
+
accepts it, `plan progress` passes, and the milestone ships a sentence nothing proves. Read each
|
|
81
|
+
description back asking *which function does this*, and cut the half you cannot name.
|
|
82
|
+
|
|
83
|
+
**Where a scenario calls the same function twice, check it for a per-period guard first.** A
|
|
84
|
+
one-per-day, one-per-order or one-per-person rule makes the second call a refusal, so the journey
|
|
85
|
+
cannot be performed in a single run however correct the code is. One planned "she finishes the
|
|
86
|
+
second lesson and is nudged about the third" against a function that refuses a second completion
|
|
87
|
+
the same calendar day.
|
|
88
|
+
|
|
89
|
+
**Every screen the milestone's opening paragraph names is either a `ui` item or a sentence to
|
|
90
|
+
cut.** This trap is easier to miss in the summary prose than in the scenarios: a screen named
|
|
91
|
+
there and carried by no `ui` item is invisible to `plan progress`, so the milestone closes green
|
|
92
|
+
with a sentence of itself unbuilt. One promised "the interval control on the admin product form"
|
|
93
|
+
for a form that does not exist anywhere in the app.
|
|
94
|
+
|
|
95
|
+
**Every field the plan filters, orders or badges on needs the function that sets it** — or a line
|
|
96
|
+
saying it is seed data, and why. A catalog planned to hide products by country, with no way to
|
|
97
|
+
mark a product's countries, cannot be proven without amending the plan mid-build.
|
|
98
|
+
|
|
99
|
+
**Every state a scenario waits in needs the function that leaves the world like that — checked
|
|
100
|
+
against the code ALREADY built.** Three failure shapes, in rising order of cost:
|
|
101
|
+
|
|
102
|
+
- *Stale*: a staff queue of "paid orders with no invoice yet", where an earlier milestone had
|
|
103
|
+
moved invoicing to checkout. No order a customer could place was ever in that state; three
|
|
104
|
+
scenarios passed once against old rows, then failed.
|
|
105
|
+
- *Unreachable*: staff asked to "capture the payment on an authorised order" when no function in
|
|
106
|
+
the app or its addons could put an order in `authorized` — the addon only holds money when
|
|
107
|
+
checkout was started with a flag the plan's own checkout input did not carry.
|
|
108
|
+
- *Wrong person*: a distributor salon owner planned to buy from a catalogue that is scoped by
|
|
109
|
+
distributor and does not show her the product. Actors are not interchangeable.
|
|
110
|
+
|
|
111
|
+
**A `model` slot saying "this milestone adds no table" has to be true of the DATA the scenarios
|
|
112
|
+
read, not only of the entities they name.** One promised a checkout priced by delivery country — a
|
|
113
|
+
shipping rate per country, a VAT rate per country — against an `n/a` model, while the only shipping
|
|
114
|
+
table in the tree (an addon's) carries no country column at all. The builder then chooses between
|
|
115
|
+
altering someone else's table and amending the plan, with neither choice recorded. Wherever a
|
|
116
|
+
description prices, rates or tiers something BY a dimension, say in `model` where that lookup lives:
|
|
117
|
+
a table this milestone adds, a column on one that exists, or config-as-code — and if it is config,
|
|
118
|
+
say so and why, exactly as for seed data.
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Can two sentences in the plan both be true?
|
|
123
|
+
|
|
124
|
+
A plan is read one field at a time, so a contradiction between two descriptions survives every
|
|
125
|
+
check `plan set` makes and is discovered mid-build, with the code already written one of the two
|
|
126
|
+
ways.
|
|
127
|
+
|
|
128
|
+
**Where the milestone has a state machine — anything with more than two states and a clock — write
|
|
129
|
+
the transitions out once, in the model slot, as the table they are.** Every scenario description
|
|
130
|
+
then quotes that table instead of restating it from memory. One plan said a cancel on a paid-up
|
|
131
|
+
licence lands in `canceled` in one scenario and `active_until_expired` in the next.
|
|
132
|
+
|
|
133
|
+
**Then find every clock in that table: name who sets it, who reads it, and check that every rule
|
|
134
|
+
reading it agrees about when it starts.** The table itself is where the next contradiction hides.
|
|
135
|
+
The next plan wrote exactly this table and still shipped one — it re-stamped `paused_at` at the
|
|
136
|
+
moment a pause took effect, so "resume after four weeks paused" meant four weeks, but left
|
|
137
|
+
`canceled_at` at the moment the customer ASKED, as the legacy source does. A licence cancelled in
|
|
138
|
+
month one was, on the night its year ran out, already past both the 28-day chase and the 60-day
|
|
139
|
+
reactivation, and got both in the same sweep. One clock, two readings, in one table, on one screen.
|
|
140
|
+
When one clock in a family is deliberately diverged from its source, the sibling clocks are where
|
|
141
|
+
you look next: either the same reasoning applies to them, or the plan says why it does not.
|
|
142
|
+
|
|
143
|
+
**Wherever a function writes a set under a parent, say whether a save REPLACES that set or ADDS to
|
|
144
|
+
it.** A product's variants, an order's lines, a company's members: the two produce identical tables
|
|
145
|
+
and identical scenarios, and differ only on the second save. Left unsaid, one milestone shipped an
|
|
146
|
+
input schema that could not carry a variant's id, so every save re-added the variants it was given
|
|
147
|
+
to update — 1, 2, 4, 8, and by the nineteenth save 262,144 rows, with the suite green throughout.
|
|
148
|
+
Say it in the model slot in the same breath as `onDelete`: that field settles what happens when the
|
|
149
|
+
parent goes, this settles what happens when it stays.
|
|
150
|
+
|
|
151
|
+
---
|
|
152
|
+
|
|
153
|
+
## Did you invent anything?
|
|
154
|
+
|
|
155
|
+
**If the notes do not say who may do a thing, the answer is `null` with the reason, not a rule you
|
|
156
|
+
made up.** A rule the user never agreed to is one they find out about by being locked out of their
|
|
157
|
+
own app.
|
|
@@ -313,7 +313,7 @@ Then run the slice's scenarios. All four green — validate, codegen, `tsc`, sce
|
|
|
313
313
|
|
|
314
314
|
Never batch. A rebuild verified only at the end gives you an undifferentiated pile of failures with no bisect point, and the whole reason for slicing is that each slice is a checkpoint you can trust.
|
|
315
315
|
|
|
316
|
-
New functions with `expose: true` are versioned from the start — `pikku versions` / `pikku
|
|
316
|
+
New functions with `expose: true` are versioned from the start — `pikku versions` / `pikku release` (**pikku-meta**); you're establishing v1 contracts, not migrating them.
|
|
317
317
|
|
|
318
318
|
## Stage 9 — The parity report (the deliverable)
|
|
319
319
|
|
|
@@ -88,16 +88,47 @@ generated functions through `ref()`.
|
|
|
88
88
|
working code: apply is a merge, reject is a `git branch -D`. The milestone's
|
|
89
89
|
plan is your own denominator, measured by `pikku knowledge plan progress` —
|
|
90
90
|
never something a reviewer is handed instead of the code.
|
|
91
|
-
- **Discover before editing
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
schema or a workflow's
|
|
91
|
+
- **Discover before editing, and there are two questions, not one.** What THIS
|
|
92
|
+
PROJECT has wired: `pikku meta context --json` returns functions, wires,
|
|
93
|
+
middleware, permissions, workflows, `capabilities` and `layout` in one call
|
|
94
|
+
(fall back to targeted `meta` commands only for a full schema or a workflow's
|
|
95
|
+
steps). What PIKKU ITSELF offers: `pikku doc <door|export>` — the surface of
|
|
96
|
+
the pikku installed here, every option key with what it is for, and a worked
|
|
97
|
+
example. `pikku doc scheduler wireScheduler pikkuVoidFunc` answers in one call
|
|
98
|
+
what reading `node_modules/@pikku/core` answers slowly and wrongly. **Never
|
|
99
|
+
open node_modules to find a signature, and never write an import, an export
|
|
100
|
+
name or an option key you have not seen in `pikku doc`.**
|
|
101
|
+
- **`capabilities.<type>` reports what this app USES, not what pikku offers.**
|
|
102
|
+
A `false` there is "no wire of this type is declared yet", so the rule below
|
|
103
|
+
about not introducing one is about not widening an app's surface on a whim —
|
|
104
|
+
it is not a statement that the surface is unavailable. When a milestone's plan
|
|
105
|
+
calls for a scheduled task and `capabilities.scheduler` is `false`, check
|
|
106
|
+
`pikku doc` for the door before concluding you cannot build it.
|
|
95
107
|
- **`metaLocale` in `pikku.config.json` is the language of authored meta** —
|
|
96
108
|
every `description`, `title` and step `template` the console renders.
|
|
97
109
|
Identifiers stay English whatever it says, and the product's own language
|
|
98
110
|
lives in `messages/*.json`.
|
|
99
111
|
- **`pikku all` is the gate.** Run it after touching functions, wirings or
|
|
100
|
-
schemas, and treat its criticals as real.
|
|
112
|
+
schemas, and treat its criticals as real. A run that fails on a critical still
|
|
113
|
+
records the contract hashes it got to, so the next run adds a PKU861 drift on
|
|
114
|
+
top of the original error — and `pikku versions update` cannot clear it,
|
|
115
|
+
because codegen refuses before it gets there. Delete those contracts' entries
|
|
116
|
+
from `versions.pikku.json` and re-run; they are re-recorded. Fix the real
|
|
117
|
+
diagnostic first, or you will chase the echo instead. Deleting is right only
|
|
118
|
+
for a contract first recorded INSIDE the milestone you are building — nothing
|
|
119
|
+
has consumed it, so there is no version to keep. A contract that shipped and
|
|
120
|
+
then genuinely changed shape gets `version: N+1` on its `pikkuFunc({...})`
|
|
121
|
+
followed by `pikku versions update`; delete its entry and you erase a version
|
|
122
|
+
a client is holding.
|
|
123
|
+
- **A new migration reaches the database through `pikku db migrate` (or
|
|
124
|
+
`pikku db reset`, which wipes first).** `pikku dev` does not apply one — it will happily serve a
|
|
125
|
+
schema older than the file you just wrote, and the failure surfaces as a
|
|
126
|
+
function reading a column that is not there yet. Run `pikku db migrate`, then
|
|
127
|
+
restart the dev server. And never put an underscore before a digit in a column
|
|
128
|
+
name: `address_line_1` writes correctly from `addressLine_1` and reads back as
|
|
129
|
+
`addressLine1`, so a value saved through the query builder comes back missing
|
|
130
|
+
with no error anywhere. Follow the columns already in `db/sqlite/`
|
|
131
|
+
(`address_line1`), and check a new one round-trips before building on it.
|
|
101
132
|
- **A milestone is planned before it is built, and the plan then stays fixed.**
|
|
102
133
|
The plan — tables, functions, wires, roles, scopes, screens, scenarios, in
|
|
103
134
|
passes — is written through `pikku knowledge plan set` (how: `pikku-architect`)
|
|
@@ -183,7 +214,19 @@ listed here.
|
|
|
183
214
|
`knowledge/`, milestone planning, design direction and refusal scenarios — say
|
|
184
215
|
so out loud to the user when you finish, and point at the way out.
|
|
185
216
|
- **Do not introduce a wire of a type whose `capabilities.<type>` is `false`**
|
|
186
|
-
unless the user asked for it
|
|
217
|
+
unless the user asked for it — and an approved milestone plan counts as them
|
|
218
|
+
asking. A plan the user signed off on authorizes the wires it names, so build
|
|
219
|
+
them and flip the capability, rather than refusing planned work because the
|
|
220
|
+
flag still reads `false` from before the plan.
|
|
221
|
+
- **Do not assume an addon's mounted route works because it mounted.** An
|
|
222
|
+
addon's singleton services are narrowed to the handful its own factory returns
|
|
223
|
+
plus config, logger, schema, variables and secrets — the host's `queueService`,
|
|
224
|
+
`kysely` and the rest never reach it. So an addon route whose handler asks the
|
|
225
|
+
host for one of those answers 500 on every call, with codegen, typecheck and
|
|
226
|
+
the route table all clean. Send one real request at the surface a mounted
|
|
227
|
+
contract claims before you build scenarios on top of it; when it cannot work,
|
|
228
|
+
wire your own function on the same path (the host's services are yours) and
|
|
229
|
+
report the addon bug upstream rather than working around it in the caller.
|
|
187
230
|
- **Do not hand-edit generated files** — `.pikku/`, `*.gen.*` or the SDK. Fix the
|
|
188
231
|
source and regenerate.
|
|
189
232
|
- **Do not invent a role.** An invented role becomes invented screens; build only
|
|
@@ -11,6 +11,14 @@ project shaped so `pikku fabric init` later adopts it with zero rework.
|
|
|
11
11
|
3. Plan the milestones — the buildable pieces, in dependency order
|
|
12
12
|
4. Implement them one at a time — each proven by a scenario before the next starts
|
|
13
13
|
|
|
14
|
+
The sections below follow those phases in order, and are meant to be worked
|
|
15
|
+
through rather than searched: §0 bootstrap, §1-§1a the last questions and the
|
|
16
|
+
three languages, §2 the knowledge graph, §3-§4 personas and apps, §5-§5a the
|
|
17
|
+
milestones and each one's technical plan, §6-§6a building and closing one,
|
|
18
|
+
§7-§7a proving it, §8 design, §9 ship. Four of them hand off to their own file —
|
|
19
|
+
[scenarios.md](scenarios.md), [design.md](design.md),
|
|
20
|
+
[multi-app.md](multi-app.md) and [ship.md](ship.md) — at the point you need it.
|
|
21
|
+
|
|
14
22
|
## Agent Operating Procedure
|
|
15
23
|
|
|
16
24
|
1. Discover before editing. Run `pikku info functions --verbose --silent` and
|
|
@@ -430,7 +438,13 @@ over, and an uncovered function is a half-milestone whether or not the note says
|
|
|
430
438
|
|
|
431
439
|
1. **Migration.** SQL in `db/sqlite/` at the project root, numbered on from the
|
|
432
440
|
ones already there. Apply with `bunx --bun pikku db migrate`, which also
|
|
433
|
-
regenerates the Kysely types your functions import.
|
|
441
|
+
regenerates the Kysely types your functions import. **Neither `pikku all` nor
|
|
442
|
+
restarting `pikku dev` applies a migration** — so a new column reads as
|
|
443
|
+
`TS2353 … does not exist in type 'InsertExpression<DB, "…">'` on the function
|
|
444
|
+
that writes it. An unapplied migration is the usual cause and the cheapest to
|
|
445
|
+
rule out, so run `db migrate` first — but the same error is what a misspelt
|
|
446
|
+
column or a stale generated type looks like, so if it survives the migration,
|
|
447
|
+
go and read the SQL.
|
|
434
448
|
2. **Seed.** Demo rows in `db/sqlite-dev-seed.sql`. **There is no seed command** —
|
|
435
449
|
`bunx --bun pikku db reset` is the only thing that applies the file, and it
|
|
436
450
|
wipes, migrates and seeds in one go (`--no-seed` stops after the migration,
|
|
@@ -629,6 +643,29 @@ plan to match what you built — the plan was written before the code on purpose
|
|
|
629
643
|
and rewriting your own denominator afterwards is exactly what that order exists
|
|
630
644
|
to stop.
|
|
631
645
|
|
|
646
|
+
### 6b. Feed the milestone back into the seats
|
|
647
|
+
|
|
648
|
+
Before starting the next milestone, answer two questions out loud:
|
|
649
|
+
|
|
650
|
+
- **What did the plan fail to say?** A field nothing wrote, a promise no function
|
|
651
|
+
could keep, a pass 1 that turned out to be two. That is a `pikku-architect`
|
|
652
|
+
lesson.
|
|
653
|
+
- **What did the build learn the hard way?** Anything that cost a wasted run —
|
|
654
|
+
a stale process, a scenario that only passes once, a diagnostic that turned
|
|
655
|
+
out to be an echo of an earlier one. That is a `pikku-build` lesson.
|
|
656
|
+
|
|
657
|
+
Then edit the skill — **at most one change to each per milestone**, and only for
|
|
658
|
+
something that actually went wrong here. A rule with no incident behind it is a
|
|
659
|
+
guess, and these files are read in full every time: they earn their length by
|
|
660
|
+
naming failures a reader would otherwise repeat. Prefer sharpening an existing
|
|
661
|
+
line to appending a new one, and delete a rule the last few milestones have
|
|
662
|
+
shown to be noise.
|
|
663
|
+
|
|
664
|
+
The gates are the compounding part. A lesson written into a scenario the suite
|
|
665
|
+
runs, or into a check `plan progress` can see, is enforced; the same lesson
|
|
666
|
+
written as prose is a thing the next reader has to remember. Reach for prose
|
|
667
|
+
only when there is nothing to hang a check on.
|
|
668
|
+
|
|
632
669
|
## 7. Prove it — scenarios
|
|
633
670
|
|
|
634
671
|
A scenario is a user journey run as one of your personas, over the real
|
|
@@ -637,105 +674,59 @@ here, because a passing one proves the app works the way a signed-in person
|
|
|
637
674
|
experiences it. Three ship in `packages/functions/test/scenarios/` — keep them
|
|
638
675
|
green — and every milestone's gherkin block from §5 becomes one more.
|
|
639
676
|
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
title: 'A tenant reports a fault and the owner sees it',
|
|
645
|
-
description:
|
|
646
|
-
'The report lands on the owning landlord’s queue, and nobody else’s',
|
|
647
|
-
tags: ['scenario', 'maintenance'],
|
|
648
|
-
func: async (_services, _data, { scenario, actors }) => {
|
|
649
|
-
const report = await scenario.do(
|
|
650
|
-
'reports a broken boiler',
|
|
651
|
-
'createMaintenanceReport',
|
|
652
|
-
{ summary: 'No hot water' },
|
|
653
|
-
{ actor: actors.chidi }
|
|
654
|
-
)
|
|
655
|
-
await scenario.then(
|
|
656
|
-
'appears on the owner’s queue',
|
|
657
|
-
'reportShowsOnQueue',
|
|
658
|
-
{ id: report.id },
|
|
659
|
-
{ actor: actors.amina }
|
|
660
|
-
)
|
|
661
|
-
await scenario.then(
|
|
662
|
-
'is invisible to the other owner',
|
|
663
|
-
'reportIsNotVisible',
|
|
664
|
-
{ id: report.id },
|
|
665
|
-
{ actor: actors.bilal }
|
|
666
|
-
)
|
|
667
|
-
return { id: report.id }
|
|
668
|
-
},
|
|
669
|
-
})
|
|
670
|
-
```
|
|
677
|
+
**Read [scenarios.md](scenarios.md) before writing the milestone's scenarios,
|
|
678
|
+
and again whenever one of these describes what you are doing.** It is the file
|
|
679
|
+
where the expensive lessons live, and most of them produce a GREEN suite that
|
|
680
|
+
proves nothing:
|
|
671
681
|
|
|
672
|
-
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
- **Write the refusals.** The third step above is the whole point of §4: one
|
|
681
|
-
persona reaching for another's row has to be rejected, and that rejection is a
|
|
682
|
-
scenario. It is how you prove access control instead of asserting it.
|
|
683
|
-
- **`SCENARIO_ACTOR_SECRET` must be in `.env`** (§6, before the first run).
|
|
684
|
-
Without it `/api/auth/sign-in/actor` is disabled — every scenario then fails
|
|
685
|
-
at sign-in, before its first step, for a reason that reads like an auth bug.
|
|
686
|
-
`pikku scenario run` reads it from the environment, so source `.env` first
|
|
687
|
-
(`set -a && . ./.env && set +a`) when you run outside `bun run dev`.
|
|
688
|
-
- **There is no state reset.** A scenario runs against a live server: scope what
|
|
689
|
-
you create to your own rows and unique ids, and never assume a clean database.
|
|
682
|
+
- the shape of a scenario, and `do` vs `given`/`when`/`then`
|
|
683
|
+
- what survives DSL extraction — a `then` in `return` position asserts nothing,
|
|
684
|
+
and a shared setup helper credits its steps to the wrong actor
|
|
685
|
+
- what to assert — refusals must read the REASON, totals must be deltas, and the
|
|
686
|
+
assertion nobody writes is the row count
|
|
687
|
+
- living without a state reset: the suite must be green on its SECOND run
|
|
688
|
+
- how a shared step rots as later milestones add writers of the rows it selects
|
|
689
|
+
- browser specifics — the click/navigate race, testids, and `Outlet` nesting
|
|
690
690
|
|
|
691
691
|
Run them:
|
|
692
692
|
|
|
693
693
|
```sh
|
|
694
|
-
bunx --bun pikku scenario run local --spawn
|
|
695
|
-
bunx --bun pikku scenario run local --spawn --run browser
|
|
696
|
-
bunx --bun pikku scenario run local-admin --spawn --run browser # the second app
|
|
694
|
+
bunx --bun pikku scenario run local --spawn # server-side, the fast path
|
|
695
|
+
bunx --bun pikku scenario run local --spawn --run browser # the same journeys, driven as a human
|
|
697
696
|
```
|
|
698
697
|
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
698
|
+
In a multi-app project that one run covers both frontends: each persona carries
|
|
699
|
+
its own `app` and `@pikku/playwright` resolves the base url from the
|
|
700
|
+
environment's `appUrls` map, so there is no second environment to run.
|
|
701
|
+
|
|
702
|
+
**Run the whole suite, not the milestone's own scenarios.** The milestone's
|
|
703
|
+
scenarios are the ones you wrote to pass; the regression lives in someone
|
|
704
|
+
else's. Tightening what "archived" means is a one-function change that reads as
|
|
705
|
+
local and quietly breaks the milestone-01 scenario nobody re-ran.
|
|
706
|
+
|
|
707
|
+
**Restart the server after adding a function.** Hot reload does not register a
|
|
708
|
+
new RPC and does not re-run `afterStart`, so a fresh function answers 404 and
|
|
709
|
+
anything provisioned at boot is missing — failures that read like a wiring bug
|
|
710
|
+
and are nothing but a stale process.
|
|
702
711
|
|
|
703
712
|
### 7a. Coverage — which functions have actually been run
|
|
704
713
|
|
|
705
714
|
Green scenarios tell you the journeys you wrote still work. They say nothing
|
|
706
|
-
about the code you never wrote a journey for
|
|
707
|
-
measuring it:
|
|
715
|
+
about the code you never wrote a journey for:
|
|
708
716
|
|
|
709
717
|
```sh
|
|
710
718
|
bunx --bun pikku dev --coverage # server, instrumented
|
|
711
719
|
bunx --bun pikku scenario run local --coverage # against that server
|
|
712
720
|
```
|
|
713
721
|
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
milestones it is a wall of red that nobody triages, and the honest response to a
|
|
723
|
-
wall of red is to ignore it.
|
|
724
|
-
|
|
725
|
-
Every gap is one of three things, and naming which is the point of looking:
|
|
726
|
-
|
|
727
|
-
- **A missing scenario** — the function matters and no journey reaches it. Write
|
|
728
|
-
the journey. Refusal paths dominate this category, because it is the case you
|
|
729
|
-
are least likely to have clicked through by hand.
|
|
730
|
-
- **A function that should not exist** — nothing reaches it because nothing needs
|
|
731
|
-
it. Delete it. An unused exposed function is also reachable over
|
|
732
|
-
`POST /rpc/:rpcName`, so this is a security finding, not only dead weight.
|
|
733
|
-
- **Genuinely deferred** — real, not yet reachable from the UI. Say so in the
|
|
734
|
-
milestone note that will cover it, so the gap is a decision rather than a
|
|
735
|
-
hole.
|
|
736
|
-
|
|
737
|
-
Report the number when you hand the milestone over. A number nobody says out
|
|
738
|
-
loud is a number nobody acts on.
|
|
722
|
+
**A function no scenario touches has no scenario coverage** — the file knows
|
|
723
|
+
what the suite exercises and nothing else, so a unit test, a scheduled job, a
|
|
724
|
+
webhook or a hand call leaves no trace in it. Read
|
|
725
|
+
`coverage/scenario-coverage.json` **as each milestone closes** — per milestone it is a short list you can act on, whereas read for the
|
|
726
|
+
first time after ten milestones it is a wall of red nobody triages. Every gap is
|
|
727
|
+
a missing scenario, a function that should not exist, or a deferral worth
|
|
728
|
+
writing down; [scenarios.md](scenarios.md) says how to tell them apart. Report
|
|
729
|
+
the number when you hand the milestone over.
|
|
739
730
|
|
|
740
731
|
## 8. Make it look like someone designed it
|
|
741
732
|
|
|
@@ -870,8 +861,11 @@ cheaper to honour than to retrofit:
|
|
|
870
861
|
|
|
871
862
|
## Reference
|
|
872
863
|
|
|
873
|
-
|
|
864
|
+
Read these when the section that names them comes up, not up front:
|
|
865
|
+
|
|
866
|
+
- [multi-app.md](multi-app.md) — adding a second frontend (§4), at the milestone
|
|
874
867
|
that needs it
|
|
868
|
+
- [scenarios.md](scenarios.md) — writing journeys that stay proven (§7, §7a)
|
|
875
869
|
- `references/design.md` — committing to a design direction, and how to tell
|
|
876
870
|
whether the screens realise it. Read BEFORE the first screen (§6), not at §8
|
|
877
871
|
- `references/theming.md` — authoring the theme (§8a)
|
|
@@ -879,4 +873,4 @@ cheaper to honour than to retrofit:
|
|
|
879
873
|
- Sibling skills: `pikku-knowledge` (§2), `pikku-auth` (§3),
|
|
880
874
|
`pikku-scenario` (§7, §7a), `pikku-deploy` and `pikku-fabric` (§9)
|
|
881
875
|
- Project conventions written by the template: `AGENTS.md`
|
|
882
|
-
- Doing less than this:
|
|
876
|
+
- Doing less than this: [quick.md](quick.md). Doing more: [platform.md](platform.md).
|
|
@@ -119,6 +119,48 @@ It stops after `bun install`. Serving the new app — a reverse proxy, a
|
|
|
119
119
|
supervisor, a dev runner, a deploy target — belongs to whatever is hosting it.
|
|
120
120
|
On a plain checkout, `bun --filter @project/<slug> dev` is enough.
|
|
121
121
|
|
|
122
|
+
### Scenarios across the two apps
|
|
123
|
+
|
|
124
|
+
`pikku new app` stamps `app: '<slug>'` onto each persona it is given, and that
|
|
125
|
+
field — not the `personas` array in `pikkufabric.config.json` — is what
|
|
126
|
+
`@pikku/playwright` resolves a persona's base url from at sign-in. A persona
|
|
127
|
+
with no `app` lands on the fallback frontend, which is the other app's screens
|
|
128
|
+
with the right session on them.
|
|
129
|
+
|
|
130
|
+
One environment is enough. `pikku.config.json` takes an `appUrls` map beside the
|
|
131
|
+
single `appUrl` fallback, and the CLI takes `--app-url <app>=<url>,<app>=<url>`:
|
|
132
|
+
|
|
133
|
+
```json
|
|
134
|
+
"local": {
|
|
135
|
+
"apiUrl": "http://localhost:3000",
|
|
136
|
+
"appUrl": "http://localhost:7104",
|
|
137
|
+
"appUrls": { "app": "http://localhost:7104", "admin": "http://localhost:7105" }
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Three things follow, and each one has cost a run:
|
|
142
|
+
|
|
143
|
+
- **An actor is bound to ONE app for the whole run.** The resolution happens once,
|
|
144
|
+
at sign-in. A staff persona cannot be walked through the customer app to check
|
|
145
|
+
something is absent from it — drive that assertion with a persona who lives
|
|
146
|
+
there, and say in the scenario's docblock why the witness is who it is. This
|
|
147
|
+
binds a SIGNED-OUT scenario too, and that is where it bites: 'a visitor with no
|
|
148
|
+
session cannot open the admin desk' driven by a customer-app persona points the
|
|
149
|
+
browser at a router with no such route at all, so what the assertion measures is
|
|
150
|
+
a 404, not the guard — and on an app with a catch-all redirect to login it
|
|
151
|
+
passes while proving nothing. Pick the actor by which app owns the ROUTE, not by
|
|
152
|
+
who should be refused, and drop their session instead.
|
|
153
|
+
- **Every frontend needs its API proxy pointed at the API you are actually
|
|
154
|
+
running.** `vite.config.ts` reads a variable (`VITE_API_PROXY`) with a default
|
|
155
|
+
port in it; start the second app without it and every RPC from that app
|
|
156
|
+
answers 500 while the first app is green, which reads as an auth bug for as
|
|
157
|
+
long as you let it.
|
|
158
|
+
- **A route sweep must be scoped to one app.** `staticRoutes(repoRoot)` in
|
|
159
|
+
`@pikku/playwright` unions the route trees of every directory under `apps/`, so
|
|
160
|
+
a sweep driven by a customer opens the admin app's routes and reports 404s that
|
|
161
|
+
are correct behaviour. Give the sweep step an `app` input and hand it a root
|
|
162
|
+
containing only that app.
|
|
163
|
+
|
|
122
164
|
## Sessions across two origins
|
|
123
165
|
|
|
124
166
|
Better Auth lives once, at `/api/auth/*`, and every app proxies to it (see
|
|
@@ -167,8 +167,8 @@ bunx --bun pikku versions init
|
|
|
167
167
|
|
|
168
168
|
The CLI suggests this on every run of a project without it. Versioning a function
|
|
169
169
|
contract, then changing it, is a short milestone that shows something no
|
|
170
|
-
scaffold demonstrates on its own. `pikku
|
|
171
|
-
comparing this build's surface against
|
|
170
|
+
scaffold demonstrates on its own. `pikku release` derives the release version by
|
|
171
|
+
comparing this build's surface against the last release, then ships it.
|
|
172
172
|
|
|
173
173
|
### Addons — `pikku-addon`
|
|
174
174
|
|