@pikku/skills 0.12.41 → 0.12.43

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/skills",
3
- "version": "0.12.41",
3
+ "version": "0.12.43",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/pikkujs/pikku.git",
@@ -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. These are the ones it cannot:
263
-
264
- - **A plan for a different milestone.** The note is about `entries`; the plan builds `projects`.
265
- Every entity the note names must appear in a function or a table.
266
- - **A pass 1 that is a layer, not a slice.** "Pass 1: the data model. Pass 2: the API. Pass 3: the
267
- screens." That is three passes of nothing working.
268
- - **Scenarios that assert the code ran rather than that the person got what they came for.** A
269
- scenario proving `saveEntry` returns 200 proves the wire. The one worth planning is the one where
270
- a person writes something, comes back, and it is still there. A browser scenario that opens a page
271
- and asserts it is still on it proves the route loads and nothing else —
272
- `pikku knowledge plan progress` names it as a problem and refuses the milestone.
273
- - **A permission rule invented here.** If the notes do not say who may do a thing, the answer is
274
- `null` with the reason, not a rule you made up. A rule the user never agreed to is one they find
275
- out about by being locked out of their own app.
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.
@@ -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.** `yarn pikku meta context --json` returns
92
- functions, wires, middleware, permissions, workflows, `capabilities` and
93
- `layout` in one call. Fall back to targeted `meta` commands only for a full
94
- schema or a workflow's steps.
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
- ```typescript
641
- import { pikkuScenario } from '#pikku/scenarios'
642
-
643
- export const tenantReportsAFaultScenario = pikkuScenario<void, { id: string }>({
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
- - **`do` takes an RPC name; `given`/`when`/`then` take a declared step.** A step
673
- is a `pikkuScenarioStep` that says what a person is doing and holds one
674
- implementation per surface (server-side by default, plus a `browser` one that
675
- drives the page). Reaching for an RPC name in a `then` will not resolve.
676
- - **Every scenario must assert.** A ladder of `given`/`when` with no `then` is a
677
- PKU680 critical — it fails `pikku all`, so it stops codegen rather than a test.
678
- Coverage counts every step, so without that rule an assertion-free ladder of
679
- clicks would score a perfect run while checking nothing.
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 # server-side, the fast path
695
- bunx --bun pikku scenario run local --spawn --run browser # the same journeys, driven as a human
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
- `--spawn` starts and stops the server for the run; drop it if `bun run dev` is
700
- already up. The browser pass needs the environment's `appUrl` and a browser
701
- driver installed — without them the run fails fast rather than half-running.
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, and that gap is invisible without
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
- That writes `coverage/scenario-coverage.json` — which functions each journey
715
- exercised. **A function no scenario touches has never been run by anything but
716
- you, by hand, once.** It compiles, it typechecks, `pikku all` is happy, and
717
- nobody has proven it does what it says.
718
-
719
- Run it **as each milestone closes**, not once at the end. Coverage read per
720
- milestone is a short list you can act on — the milestone you just built either
721
- covered its own functions or it did not. Read for the first time after ten
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
- - `references/multi-app.md` — adding a second frontend (§4), at the milestone
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: `references/quick.md``. Doing more: `references/platform.md``.
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