@pikku/skills 0.12.44 → 0.12.47

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.44",
3
+ "version": "0.12.47",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/pikkujs/pikku.git",
@@ -18,7 +18,7 @@ Use this skill as an execution checklist, not reference material.
18
18
  5. **Verify with `pikku fabric validate --json`, then codegen and `tsc`** after every slice (Stage 8 has the exact commands). Never batch a whole app and verify at the end.
19
19
  6. **Write the parity report as you go**, not at the end — it is the deliverable that proves the rebuild is complete.
20
20
 
21
- This skill is the **translation layer** only. For how Fabric itself works (SQLite/libSQL, `fabric.config.json`, deploy provider, project layout) read **pikku-fabric**. For everything else, delegate to the sibling skill named at each step.
21
+ This skill is the **translation layer** only. For how Fabric itself works (SQLite/libSQL, project linking, deploy provider, project layout) read **pikku-fabric**. For everything else, delegate to the sibling skill named at each step.
22
22
 
23
23
  ## The one idea
24
24
 
@@ -47,7 +47,7 @@ Then read, in this order — **whole files, once**: `product.json` (what it is,
47
47
 
48
48
  ### The terminology trap — do this before you name anything
49
49
 
50
- `product.json.terminology` and `migration.json.decisionsNeeded` frequently carry a **false friend**: a word the legacy code used that means something else to everyone else (a real case: `tenant` meaning *the seller's own market/legal entity*, not a customer org — where the customer org was `Company`).
50
+ `product.json.terminology` and `migration.json.decisionsNeeded` frequently carry a **false friend**: a word the legacy code used that means something else to everyone else (a real case: `tenant` meaning _the seller's own market/legal entity_, not a customer org — where the customer org was `Company`).
51
51
 
52
52
  Carrying a false friend into the rebuild wastes the single cheapest opportunity you will ever have to fix it. Resolve the rename **before the first migration is written**, then apply the new name in table names, function names, and types. Add it to the parity report's glossary so the mapping stays legible to whoever compares old and new.
53
53
 
@@ -65,7 +65,7 @@ Present each as a real question with the options the code implies and what each
65
65
 
66
66
  **Never resolve one by reading the legacy code.** If the code answered it, the archaeologist would not have raised it. Silence in the legacy code is the finding.
67
67
 
68
- Record each answer in the parity report under **Decisions taken** with the date and who decided. This is the audit trail for behaviour that is *deliberately* not a port.
68
+ Record each answer in the parity report under **Decisions taken** with the date and who decided. This is the audit trail for behaviour that is _deliberately_ not a port.
69
69
 
70
70
  ## Stage 1.5 — Emit the implementation inventory (do this before any code)
71
71
 
@@ -100,7 +100,7 @@ check its calls. The distinctions it encodes are the ones that matter:
100
100
  workflow. That trigger is the legacy shape of an event: a handler doing five
101
101
  unrelated things because there was no bus. Splitting it is the upgrade.
102
102
  - **user/admin journeys → scenarios, NOT `pikkuWorkflowFunc`s.** A blueprint
103
- "workflow" is a *journey* — a sequence a person drives through the UI. A
103
+ "workflow" is a _journey_ — a sequence a person drives through the UI. A
104
104
  `pikkuWorkflowFunc` is durable multi-step orchestration. Conflating them produces
105
105
  a workflow engine driving form submissions, which is the most common way this
106
106
  mapping goes wrong.
@@ -110,22 +110,22 @@ cheapest progress report you have.
110
110
 
111
111
  ## Stage 2 — Scaffold
112
112
 
113
- Clone the Fabric starter template, then do the post-clone cleanup **pikku-build** covers (README, package name, lockfile, leftover template artifacts) — a rebuild that ships the template's own name and readme is the first thing a reviewer notices. Set `projectId`, `production.branch` and the frontend entry in `fabric.config.json` per **pikku-fabric**.
113
+ Clone the Fabric starter template, then do the post-clone cleanup **pikku-build** covers (README, package name, lockfile, leftover template artifacts) — a rebuild that ships the template's own name and readme is the first thing a reviewer notices. Declare the frontend in `frontends` in `pikku.config.json` and link with `pikku fabric link` per **pikku-fabric** — there is no Fabric config file.
114
114
 
115
115
  Map `architecture.json` onto Fabric honestly, and expect it to shrink:
116
116
 
117
- | Blueprint `architecture.json` | Fabric |
118
- |---|---|
119
- | API/web process (Puma, Express, …) | the Fabric worker — no component to build |
120
- | Worker process + queue | `wireQueueWorker` (**pikku-wiring**) |
121
- | Cron/scheduler component | `wireScheduler` (**pikku-wiring**) |
122
- | Reverse proxy, deploy tooling, process manager | **drop** — the platform does this |
123
- | Admin console (ActiveAdmin, Django admin, …) | `scaffold.console: true` first; only build screens for what it genuinely can't express |
124
- | Session/auth store | Better Auth (**pikku-auth**) |
125
- | Relational datastore | SQLite via libSQL/Kysely (**pikku-fabric**, **pikku-kysely**) |
126
- | Redis for cache/locks/queues | usually **nothing** — see the trap below |
117
+ | Blueprint `architecture.json` | Fabric |
118
+ | ---------------------------------------------- | -------------------------------------------------------------------------------------- |
119
+ | API/web process (Puma, Express, …) | the Fabric worker — no component to build |
120
+ | Worker process + queue | `wireQueueWorker` (**pikku-wiring**) |
121
+ | Cron/scheduler component | `wireScheduler` (**pikku-wiring**) |
122
+ | Reverse proxy, deploy tooling, process manager | **drop** — the platform does this |
123
+ | Admin console (ActiveAdmin, Django admin, …) | `scaffold.console: true` first; only build screens for what it genuinely can't express |
124
+ | Session/auth store | Better Auth (**pikku-auth**) |
125
+ | Relational datastore | SQLite via libSQL/Kysely (**pikku-fabric**, **pikku-kysely**) |
126
+ | Redis for cache/locks/queues | usually **nothing** — see the trap below |
127
127
 
128
- **The Redis trap.** Legacy apps use Redis for four unrelated jobs: queue backend (→ Fabric's queue), cache (→ usually delete; measure first), pub/sub (→ **pikku-realtime**), and **distributed locks**. That last one is the trap: a lock is nearly always a workaround for a missing database constraint (`invariants.json` will show the same rule with `enforcedBy: "code-guard"` and an `atRiskBecause`). Port the *invariant* to a constraint; do not port the lock. Re-implementing legacy locking on a new stack is how you carry a race condition across a rewrite.
128
+ **The Redis trap.** Legacy apps use Redis for four unrelated jobs: queue backend (→ Fabric's queue), cache (→ usually delete; measure first), pub/sub (→ **pikku-realtime**), and **distributed locks**. That last one is the trap: a lock is nearly always a workaround for a missing database constraint (`invariants.json` will show the same rule with `enforcedBy: "code-guard"` and an `atRiskBecause`). Port the _invariant_ to a constraint; do not port the lock. Re-implementing legacy locking on a new stack is how you carry a race condition across a rewrite.
129
129
 
130
130
  `architecture.json.deploymentConstraints` is the exception to "drop the infrastructure": entries there are constraints that must **survive** (raw-body ordering for webhook signatures, retry semantics an external caller depends on). Read them; they are cheap to lose and expensive to rediscover.
131
131
 
@@ -144,19 +144,19 @@ hand-coerce forever. `TEXT` + `CHECK` for a closed set is the highest-leverage c
144
144
  available: the constraint compiles into a **TypeScript union type**, so an invalid
145
145
  state is a compile error rather than a runtime one.
146
146
 
147
- The blueprint gives you `attributes`, `relationships`, `states`, `transitions`, `ownership`, `constraints`. It is a *domain* model, not the legacy DDL — you are not required to reproduce the old column layout, and usually shouldn't.
147
+ The blueprint gives you `attributes`, `relationships`, `states`, `transitions`, `ownership`, `constraints`. It is a _domain_ model, not the legacy DDL — you are not required to reproduce the old column layout, and usually shouldn't.
148
148
 
149
149
  ### Legacy SQL → SQLite traps
150
150
 
151
- | Legacy | SQLite / Fabric | Why |
152
- |---|---|---|
153
- | `DECIMAL`/`NUMERIC` money, or a money library's `*_cents` | `INTEGER` minor units | SQLite has no exact decimal. Minor units are what the legacy money library stored anyway — keep the currency in its own column. **Never `REAL` for money.** |
154
- | `TIMESTAMP`/`DATETIME` | `DATETIME` | Types as a real `Date`. Do NOT store Unix ms in an `INTEGER` — you lose the typing and coerce by hand forever. |
155
- | `BOOLEAN` | `BOOLEAN` | Types as a real `boolean`. Do NOT store 0/1 in an `INTEGER`. |
156
- | `ENUM` | `TEXT` + `CHECK` | The one place to spend a `CHECK` — it is a closed set, and a typo'd state is exactly the bug class the blueprint keeps finding. |
157
- | `uuid`/`serial` PK | `INTEGER PRIMARY KEY AUTOINCREMENT`, or `TEXT` for a public id | **Look at `queries.json.scoping` first.** If the legacy app used a random public id (`puid`, slug) precisely so ids aren't enumerable, that is a *security property* — keep it. Silently switching to sequential integers un-fixes a fix. |
158
- | JSON column | `TEXT` + a typed parse | Fine, but if `entities.json` gives it real attributes, it wants columns. |
159
- | DB-level `CHECK` sprawl | app-level validation | Per **pikku-fabric** — *except* the invariants below. |
151
+ | Legacy | SQLite / Fabric | Why |
152
+ | --------------------------------------------------------- | -------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
153
+ | `DECIMAL`/`NUMERIC` money, or a money library's `*_cents` | `INTEGER` minor units | SQLite has no exact decimal. Minor units are what the legacy money library stored anyway — keep the currency in its own column. **Never `REAL` for money.** |
154
+ | `TIMESTAMP`/`DATETIME` | `DATETIME` | Types as a real `Date`. Do NOT store Unix ms in an `INTEGER` — you lose the typing and coerce by hand forever. |
155
+ | `BOOLEAN` | `BOOLEAN` | Types as a real `boolean`. Do NOT store 0/1 in an `INTEGER`. |
156
+ | `ENUM` | `TEXT` + `CHECK` | The one place to spend a `CHECK` — it is a closed set, and a typo'd state is exactly the bug class the blueprint keeps finding. |
157
+ | `uuid`/`serial` PK | `INTEGER PRIMARY KEY AUTOINCREMENT`, or `TEXT` for a public id | **Look at `queries.json.scoping` first.** If the legacy app used a random public id (`puid`, slug) precisely so ids aren't enumerable, that is a _security property_ — keep it. Silently switching to sequential integers un-fixes a fix. |
158
+ | JSON column | `TEXT` + a typed parse | Fine, but if `entities.json` gives it real attributes, it wants columns. |
159
+ | DB-level `CHECK` sprawl | app-level validation | Per **pikku-fabric** — _except_ the invariants below. |
160
160
 
161
161
  ### States and transitions
162
162
 
@@ -171,26 +171,26 @@ Two traps the blueprint hands you for free:
171
171
 
172
172
  One vertical slice per domain. **Order by inbound reference count, not by importance**: the domains everything else points at go first. Identity and the customer/account domain are almost always the base — every other domain's `ownership` and `scoping` mentions them.
173
173
 
174
- Derive the order mechanically: for each domain, count how many *other* domains' entities have a relationship into it. Build the most-referenced first. Then, among the rest, take the ones that carry the most invariants and events (`domains.json` roll-ups tell you) — that's where the product is, and you want it under test early.
174
+ Derive the order mechanically: for each domain, count how many _other_ domains' entities have a relationship into it. Build the most-referenced first. Then, among the rest, take the ones that carry the most invariants and events (`domains.json` roll-ups tell you) — that's where the product is, and you want it under test early.
175
175
 
176
176
  Leave for last: thin CRUD domains with no events and no invariants (content, downloads, media libraries). They're mechanical, and `migration.json` may well say the honest thing — that some shouldn't be rebuilt at all.
177
177
 
178
178
  ### What one slice contains
179
179
 
180
- | Blueprint | Fabric artifact | Skill |
181
- |---|---|---|
182
- | `commands[]` | one `pikkuFunc` per command, one per file, `expose: true` | **pikku-concepts** |
183
- | `queries[]` | one `pikkuSessionlessFunc`, `readonly: true`, `expose: true` | **pikku-concepts** |
184
- | `commands[].preconditions` | guards in the function body, throwing typed errors | **pikku-fabric** hard rules |
185
- | `policies[]` | `pikkuPermission` on the function's `permissions:` field | **pikku-permissions** |
186
- | `queries[].scoping` | the permission + the query's `where` | **pikku-permissions**, **pikku-kysely** |
187
- | `events[]` | realtime topic / queue message | **pikku-realtime**, **pikku-wiring** |
188
- | `workflows[] kind: system` | `wireScheduler` | **pikku-wiring** |
189
- | `workflows[]` multi-step | `pikkuWorkflowFunc` + `*.steps.ts` | **pikku-workflow** |
190
- | `workflows[].scenarios[]` | scenario tests | **pikku-scenario** |
191
- | `api[]` | mostly **nothing** — see below | **pikku-wiring** |
192
- | `invariants[]` | DB constraints, in the migration | **pikku-fabric** |
193
- | `integrations[]` | services | **pikku-services** |
180
+ | Blueprint | Fabric artifact | Skill |
181
+ | -------------------------- | ------------------------------------------------------------ | --------------------------------------- |
182
+ | `commands[]` | one `pikkuFunc` per command, one per file, `expose: true` | **pikku-concepts** |
183
+ | `queries[]` | one `pikkuSessionlessFunc`, `readonly: true`, `expose: true` | **pikku-concepts** |
184
+ | `commands[].preconditions` | guards in the function body, throwing typed errors | **pikku-fabric** hard rules |
185
+ | `policies[]` | `pikkuPermission` on the function's `permissions:` field | **pikku-permissions** |
186
+ | `queries[].scoping` | the permission + the query's `where` | **pikku-permissions**, **pikku-kysely** |
187
+ | `events[]` | realtime topic / queue message | **pikku-realtime**, **pikku-wiring** |
188
+ | `workflows[] kind: system` | `wireScheduler` | **pikku-wiring** |
189
+ | `workflows[]` multi-step | `pikkuWorkflowFunc` + `*.steps.ts` | **pikku-workflow** |
190
+ | `workflows[].scenarios[]` | scenario tests | **pikku-scenario** |
191
+ | `api[]` | mostly **nothing** — see below | **pikku-wiring** |
192
+ | `invariants[]` | DB constraints, in the migration | **pikku-fabric** |
193
+ | `integrations[]` | services | **pikku-services** |
194
194
 
195
195
  ### Names are the contract
196
196
 
@@ -202,7 +202,7 @@ Every function needs a real `description` — take it from the concept's `descri
202
202
 
203
203
  ### The API is not the contract
204
204
 
205
- `api.json` has one entry per legacy surface, and it is **evidence, not a spec**. Each entry's `mapsTo` names the command or query — build *that*, and let RPC be the transport (**pikku-fabric**: RPC first, `expose: true`).
205
+ `api.json` has one entry per legacy surface, and it is **evidence, not a spec**. Each entry's `mapsTo` names the command or query — build _that_, and let RPC be the transport (**pikku-fabric**: RPC first, `expose: true`).
206
206
 
207
207
  Add `wireHTTP` only where the URL shape is a real external contract:
208
208
 
@@ -235,7 +235,7 @@ In the rebuild these become real: publish the event (**pikku-realtime**) or enqu
235
235
 
236
236
  Two disciplines:
237
237
 
238
- - **Do not invent events.** The archaeologist applied a threshold (≥1 real consumer beyond the row write). If a CRUD fact isn't in `events.json`, it didn't earn an event; a state row that is only *read* later is state, not an event.
238
+ - **Do not invent events.** The archaeologist applied a threshold (≥1 real consumer beyond the row write). If a CRUD fact isn't in `events.json`, it didn't earn an event; a state row that is only _read_ later is state, not an event.
239
239
  - **`explicit: false` is a confidence marker.** These are reconstructions of intent. When one drives money or an external side effect, the parity report says it was reconstructed — the reviewer should confirm the consumer list is complete.
240
240
 
241
241
  ### Policies — collapse the drift
@@ -244,17 +244,17 @@ Two disciplines:
244
244
 
245
245
  The whole point is **one `pikkuPermission` per rule**, referenced from every function that needs it (**pikku-permissions**). When the `enforcedAt` versions genuinely disagree, that's a decision, not a merge — ask which is correct. Picking the one you read first silently ships a behaviour change.
246
246
 
247
- Per **pikku-fabric**: no auth checks in function bodies. If a policy resists expression as a permission, that's a signal it's a *business rule* (a precondition) rather than authorization — those live in the function body and throw typed errors.
247
+ Per **pikku-fabric**: no auth checks in function bodies. If a policy resists expression as a permission, that's a signal it's a _business rule_ (a precondition) rather than authorization — those live in the function body and throw typed errors.
248
248
 
249
249
  ## Stage 5 — Scenarios from the blueprint's tests
250
250
 
251
251
  `workflows[].scenarios[]` entries carry `fromTest` — they were excavated from the legacy suite, which means **they are the legacy app's executable spec**, already in given/when/outcome shape. They map directly onto **pikku-scenario** actors and flows, and `product.json.actors` gives you the actor list.
252
252
 
253
- This is the highest-leverage stage in the rebuild and the easiest to skip. A scenario ported from a legacy test is the only artifact that can tell you the new app *behaves* like the old one — parity of function names proves nothing.
253
+ This is the highest-leverage stage in the rebuild and the easiest to skip. A scenario ported from a legacy test is the only artifact that can tell you the new app _behaves_ like the old one — parity of function names proves nothing.
254
254
 
255
255
  Two rules:
256
256
 
257
- - A scenario whose legacy test was **commented out or broken** (`gaps.json` flags these) still gets written — it just isn't parity, it's new coverage. Note which in the parity report; often the disabled test is disabled *because* the behaviour was broken.
257
+ - A scenario whose legacy test was **commented out or broken** (`gaps.json` flags these) still gets written — it just isn't parity, it's new coverage. Note which in the parity report; often the disabled test is disabled _because_ the behaviour was broken.
258
258
  - A workflow with **no scenarios** is a gap in the blueprint, not permission to skip testing. Flag it rather than inventing behaviour to test.
259
259
 
260
260
  ## Stage 6 — Integrations
@@ -265,7 +265,7 @@ Two rules:
265
265
  - `"trivial"` → candidates for a platform-native equivalent, but only if the user wants it. Swapping a vendor mid-rebuild makes every failure ambiguous.
266
266
  - `envVars` → `defineVariable` / `defineSecret` (**pikku-services**). Per **pikku-fabric**: no `process.env`, ever.
267
267
 
268
- **Secrets in the blueprint are live secrets.** `gaps.json` security entries routinely name credentials hardcoded in the legacy source *and its committed history*. They must be **rotated**, not copied into the new app's secret store — and rotation is the legacy app's problem, today, independent of the rebuild. Say so; don't let the rebuild timeline become the remediation timeline.
268
+ **Secrets in the blueprint are live secrets.** `gaps.json` security entries routinely name credentials hardcoded in the legacy source _and its committed history_. They must be **rotated**, not copied into the new app's secret store — and rotation is the legacy app's problem, today, independent of the rebuild. Say so; don't let the rebuild timeline become the remediation timeline.
269
269
 
270
270
  **Inbound webhooks deserve a real look.** They're the surfaces most likely to be carrying a `gaps.json` security entry (unverified signatures, disabled checks). Rebuild the verification properly (**pikku-wiring**), and if the reason it was disabled was a vendor that doesn't reliably sign, that's a `decisionsNeeded` — not something to replicate.
271
271
 
@@ -273,24 +273,24 @@ Two rules:
273
273
 
274
274
  Only when `frontend*.json` is present. Target: TanStack Start + Mantine.
275
275
 
276
- `frontend.json` records the legacy stack as facts. **It is context, not a port target** — a bespoke Sass system, a server-rendered template stack, or a different component library all land on the same target. Read `designSystemConsistency` and `designFindings` to know what *not* to carry: findings are the drift (hardcoded colors, forked-per-locale pages, duplicated components), and the rebuild is the moment they cost nothing to drop.
276
+ `frontend.json` records the legacy stack as facts. **It is context, not a port target** — a bespoke Sass system, a server-rendered template stack, or a different component library all land on the same target. Read `designSystemConsistency` and `designFindings` to know what _not_ to carry: findings are the drift (hardcoded colors, forked-per-locale pages, duplicated components), and the rebuild is the moment they cost nothing to drop.
277
277
 
278
278
  ### Routes
279
279
 
280
280
  `frontend-routes[]` → TanStack routes. `path` and `purpose` carry over; `auth` becomes the route guard.
281
281
 
282
- **`dataFrom` is the payoff.** It lists query/command names — the *same* names as `queries.json`/`commands.json`, which are the same names as your `pikkuFunc`s, which are the same names in the generated client. So a route's data layer is mechanical: each `dataFrom` entry is a generated hook (**pikku-react**). If `dataFrom` contains a name that isn't a real function, the blueprint wasn't reconciled — go fix it there.
282
+ **`dataFrom` is the payoff.** It lists query/command names — the _same_ names as `queries.json`/`commands.json`, which are the same names as your `pikkuFunc`s, which are the same names in the generated client. So a route's data layer is mechanical: each `dataFrom` entry is a generated hook (**pikku-react**). If `dataFrom` contains a name that isn't a real function, the blueprint wasn't reconciled — go fix it there.
283
283
 
284
284
  ### Components — the honest cost
285
285
 
286
286
  `frontend-components[].rebuild` is the only field that matters for planning:
287
287
 
288
- | `rebuild` | What to do |
289
- |---|---|
290
- | `mantine-standard` | Use the Mantine component. Do not port. |
291
- | `mantine-composition` | Compose from Mantine primitives. Do not port. |
292
- | `custom-style` | Normalize to Mantine + theme tokens. The divergence is the thing to drop. |
293
- | **`custom-logic`** | **Port the behaviour.** Read `customLogic` and `dependencies`. |
288
+ | `rebuild` | What to do |
289
+ | --------------------- | ------------------------------------------------------------------------- |
290
+ | `mantine-standard` | Use the Mantine component. Do not port. |
291
+ | `mantine-composition` | Compose from Mantine primitives. Do not port. |
292
+ | `custom-style` | Normalize to Mantine + theme tokens. The divergence is the thing to drop. |
293
+ | **`custom-logic`** | **Port the behaviour.** Read `customLogic` and `dependencies`. |
294
294
 
295
295
  The first three are the bulk and they're cheap — they're a re-expression, not a migration. **`custom-logic` is the actual project**: the bespoke chart, the virtualized table, the map surface, the rich editor, the drag interaction. Each has real behaviour that must survive, and `customLogic` says what it is.
296
296
 
@@ -324,7 +324,7 @@ diff, because it is the only document that holds the blueprint and the new code
324
324
  Per domain:
325
325
 
326
326
  - **Built** — each command/query/event/policy → its function/file. The concept-name-as-ID makes this a table, not prose.
327
- - **Deliberately not built** — from `migration.json.dropped` and `gaps.json`, with the reason. *The most important section.* Without it, every dropped bug and orphan reads as a regression to whoever reviews.
327
+ - **Deliberately not built** — from `migration.json.dropped` and `gaps.json`, with the reason. _The most important section._ Without it, every dropped bug and orphan reads as a regression to whoever reviews.
328
328
  - **Decisions taken** — each gate answer, who decided, when. Behaviour that deliberately differs from the legacy app.
329
329
  - **Now enforced** — invariants that were `enforcedBy: "nothing"` and now have a constraint. The rebuild's actual dividend, in one list.
330
330
  - **Reconstructed** — anything from `confidence: low`/`medium` or `explicit: false` events. Flag for confirmation against production behaviour.
@@ -333,20 +333,20 @@ Per domain:
333
333
 
334
334
  ## Red flags
335
335
 
336
- | Thought | Reality |
337
- |---|---|
338
- | "Let me check how the old code did this" | The blueprint says what it does. If it doesn't, it's a decision — ask. Reading legacy source is how its accidents get re-imported. |
339
- | "I'll port the state machine library" | Port the *states and transitions*. The library is implementation; `commands.json` already names every transition. |
340
- | "The blueprint lists this state, so I'll create it" | Check it's reachable. Unreachable states are a finding, not a spec. |
341
- | "I'll wire HTTP for each `api.json` entry" | You're transcribing the legacy router. RPC first; `wireHTTP` only for genuinely fixed external URLs. |
342
- | "The old app didn't enforce it, so neither will I" | `enforcedBy: "nothing"` is the highest-value work in the rebuild — the reason it's worth doing at all. |
343
- | "I'll add events for the CRUD actions too" | The archaeologist applied a consumer threshold. Not in `events.json` = didn't earn one. |
344
- | "Two enforcement sites disagree; I'll use the first one" | That's a silent behaviour change. Drift is a decision — ask which is correct. |
345
- | "It's just screens, the frontend is quick" | The `custom-logic` components are the project. Scope them individually before starting. |
346
- | "I'll copy the secrets into the new secret store" | Blueprint-exposed secrets are burned. Rotate. And the legacy app needs that today, regardless of the rebuild. |
347
- | "I'll do the parity report at the end" | You will not remember why you dropped things, and dropped-on-purpose will read as regression. |
348
- | "Verify once it's all built" | No bisect point. Verify per slice; that's what slices are for. |
349
- | "The blueprint has a `low`-confidence entry, I'll build my best guess" | That's inventing product. It's a gate question. |
336
+ | Thought | Reality |
337
+ | ---------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
338
+ | "Let me check how the old code did this" | The blueprint says what it does. If it doesn't, it's a decision — ask. Reading legacy source is how its accidents get re-imported. |
339
+ | "I'll port the state machine library" | Port the _states and transitions_. The library is implementation; `commands.json` already names every transition. |
340
+ | "The blueprint lists this state, so I'll create it" | Check it's reachable. Unreachable states are a finding, not a spec. |
341
+ | "I'll wire HTTP for each `api.json` entry" | You're transcribing the legacy router. RPC first; `wireHTTP` only for genuinely fixed external URLs. |
342
+ | "The old app didn't enforce it, so neither will I" | `enforcedBy: "nothing"` is the highest-value work in the rebuild — the reason it's worth doing at all. |
343
+ | "I'll add events for the CRUD actions too" | The archaeologist applied a consumer threshold. Not in `events.json` = didn't earn one. |
344
+ | "Two enforcement sites disagree; I'll use the first one" | That's a silent behaviour change. Drift is a decision — ask which is correct. |
345
+ | "It's just screens, the frontend is quick" | The `custom-logic` components are the project. Scope them individually before starting. |
346
+ | "I'll copy the secrets into the new secret store" | Blueprint-exposed secrets are burned. Rotate. And the legacy app needs that today, regardless of the rebuild. |
347
+ | "I'll do the parity report at the end" | You will not remember why you dropped things, and dropped-on-purpose will read as regression. |
348
+ | "Verify once it's all built" | No bisect point. Verify per slice; that's what slices are for. |
349
+ | "The blueprint has a `low`-confidence entry, I'll build my best guess" | That's inventing product. It's a gate question. |
350
350
 
351
351
  ## Quick reference
352
352
 
@@ -372,6 +372,6 @@ legacy repo → pikku-software-archaeology → .knowledge/ blueprint
372
372
  thing, and emits `parity-*.md` so the rebuild can be reviewed against the blueprint rather than
373
373
  against the legacy code.
374
374
 
375
- For Fabric mechanics — project layout, `fabric.config.json`, the validate loop, reading a deployed
376
- stage — use **pikku-fabric**. For a single feature *after* the rebuild, and for the post-clone
375
+ For Fabric mechanics — project layout, how a checkout is linked, the validate loop, reading a deployed
376
+ stage — use **pikku-fabric**. For a single feature _after_ the rebuild, and for the post-clone
377
377
  cleanup in Stage 2, use **pikku-build**.
@@ -44,7 +44,8 @@ plus more effort" — it is App plus a deliberate surface checklist, so read the
44
44
  base first and follow it in full rather than blending the two into one plan.
45
45
 
46
46
  The supporting references belong to whichever mode sends you to them:
47
- `references/multi-app.md` (a second frontend), `references/design.md` (showing
47
+ `references/multi-app.md` (a second frontend), `references/native-app.md`
48
+ (shipping an app as a desktop or Android app), `references/design.md` (showing
48
49
  a picture of the screens first, committing to a design direction, and judging
49
50
  whether the screens realise it — read before the first screen is built, not
50
51
  after the last), `references/theming.md`
@@ -139,7 +140,7 @@ generated functions through `ref()`.
139
140
  - **Print the links whenever the stack comes up, and in every hand-over.** Full,
140
141
  clickable URLs, with the ports taken from what `bun run dev` actually printed:
141
142
  - **App** — the frontend's URL (`http://localhost:7104` in the template; each
142
- frontend in `pikkufabric.config.json` has its own port)
143
+ frontend in `frontends` in `pikku.config.json` has its own port)
143
144
  - **API** — `http://localhost:3000`
144
145
  - **Console** — `http://localhost:3000/console`, plus a deep link to each
145
146
  page that shows what this turn produced (the paths are below)
@@ -335,7 +335,7 @@ Cloning `apps/app` materialises a folder of copied screens, so it belongs to the
335
335
  milestone that first needs the second app, not to planning.
336
336
 
337
337
  When you get there, read `references/multi-app.md`. It carries the clone, the
338
- `package.json` edits, the `pikkufabric.config.json` frontends map, the dev-runner
338
+ `package.json` edits, the `frontends` map in `pikku.config.json`, the dev-runner
339
339
  change that otherwise silently never starts your second app, the per-frontend
340
340
  scenario environments, and how sessions behave across two origins.
341
341
 
@@ -568,7 +568,7 @@ Then run it:
568
568
  bun run prebuild && bun run dev
569
569
  ```
570
570
 
571
- That starts the API on :3000 and every frontend in `pikkufabric.config.json`. A
571
+ That starts the API on :3000 and every frontend in `frontends`. A
572
572
  frontend running against a dead API looks exactly like an app bug, so if every
573
573
  request fails, check that both halves came up.
574
574
 
@@ -3,7 +3,7 @@
3
3
  Read this when the split you recorded in Phase 2 is "separate apps" and you have
4
4
  reached the milestone that needs the second one. **Not before.** Cloning
5
5
  `apps/app` materialises a directory of copied screens; doing it during planning
6
- leaves `pikkufabric.config.json` pointing at an app nobody has designed yet.
6
+ leaves `frontends` in `pikku.config.json` pointing at an app nobody has designed yet.
7
7
 
8
8
  If the split is "one app with paths", you never need this file — add route
9
9
  segments under `/app` and give each audience its own entries in `useNavItems()`.
@@ -66,14 +66,15 @@ second is the one that catches a `permissions` field nobody wired.
66
66
  ## The second app
67
67
 
68
68
  ```bash
69
- pikku new app admin --serves staff --personas manager,mechanic
69
+ pikku app new admin --serves staff --personas manager,mechanic
70
70
  ```
71
71
 
72
72
  One command does every step this section used to list by hand: it fetches
73
73
  `pikkujs/starter-template`'s `apps/app`, re-points its `package.json` at the
74
74
  new name, its own dev/preview port and its own `--tsBuildInfoFile`, stamps
75
75
  `app: '<slug>'` onto each named persona in `definePersonas({…})`, adds the
76
- `frontends` entry, and re-runs `bun install`.
76
+ `frontends` entry to `pikku.config.json`, and re-runs `bun install`. `pikku app
77
+ list` shows every app afterwards.
77
78
 
78
79
  **It scaffolds from the starter template, not from the app you already have.**
79
80
  Copying the working app drags its screens, routes and nav into an audience that
@@ -119,10 +120,14 @@ It stops after `bun install`. Serving the new app — a reverse proxy, a
119
120
  supervisor, a dev runner, a deploy target — belongs to whatever is hosting it.
120
121
  On a plain checkout, `bun --filter @project/<slug> dev` is enough.
121
122
 
123
+ Packaging it as a desktop or Android app is a separate step, on the same
124
+ `frontends` entry: `pikku app native init <slug>`. See
125
+ `references/native-app.md`.
126
+
122
127
  ### Scenarios across the two apps
123
128
 
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
129
+ `pikku app new` stamps `app: '<slug>'` onto each persona it is given, and that
130
+ field — not the `personas` array on the `frontends` entry — is what
126
131
  `@pikku/playwright` resolves a persona's base url from at sign-in. A persona
127
132
  with no `app` lands on the fallback frontend, which is the other app's screens
128
133
  with the right session on them.
@@ -0,0 +1,148 @@
1
+ # Shipping an app as a desktop or Android app
2
+
3
+ Read this when an app in `frontends` has to be installed rather than visited: a
4
+ desktop app, an Android APK, later an iPhone app. The app is still the same
5
+ frontend. A native app is that frontend packaged in a Tauri shell, configured on
6
+ its `frontends` entry, with the Tauri project committed beside it.
7
+
8
+ ```
9
+ apps/customer/ ← frontends.customer.cwd
10
+ package.json ← gains @tauri-apps/cli, @tauri-apps/api, one package per plugin
11
+ dist/ ← frontends.customer.dist, which the shell packages
12
+ src-tauri/ ← the native project, committed
13
+ ```
14
+
15
+ ## The commands
16
+
17
+ Every app command is under `pikku app` — native included, as `pikku app native`.
18
+ There is no `pikku new app`, and no app flag on `pikku deploy`.
19
+
20
+ ```bash
21
+ pikku app new <name> # a second frontend, from the starter template
22
+ pikku app list # every app, where it lives, what it ships as
23
+ pikku app native init <name> # write the native project (or re-apply config to it)
24
+ pikku app native add <name> store biometric # add plugins
25
+ pikku app native upgrade <name> # rewrite pikku's half from the config as it stands
26
+ pikku app native check [name] # compare projects to the config, and apps to each other
27
+ ```
28
+
29
+ `init` flags, all optional. Each one is saved into `frontends.<name>.native`
30
+ first, and every later `add`, `upgrade` and `check` reads the config, not the
31
+ flags:
32
+
33
+ | Flag | Meaning |
34
+ | --- | --- |
35
+ | `--desktop` `--android` `--ios` | Platforms. None given: desktop, Android and iOS, or desktop alone with `--bundle-server` |
36
+ | `--identifier com.acme.customer` | Bundle id and Android package name. Default `com.<npm scope>.<app name>` |
37
+ | `--product-name "Acme"` | The name people see. Default: the app name |
38
+ | `--url https://app.example.com` | Open a deployed origin instead of bundling `dist` |
39
+ | `--bundle-server` | Ship the compiled pikku server inside the app (desktop only) |
40
+ | `--plugins store,dialog` | Native plugins, comma-separated |
41
+
42
+ The resulting config:
43
+
44
+ ```json
45
+ "frontends": {
46
+ "customer": {
47
+ "cwd": "apps/customer",
48
+ "kind": "spa",
49
+ "native": {
50
+ "identifier": "com.acme.customer",
51
+ "platforms": ["desktop", "android"],
52
+ "plugins": ["store", "biometric"]
53
+ }
54
+ }
55
+ }
56
+ ```
57
+
58
+ ## Pick the mode first
59
+
60
+ | Config | The window shows | Platforms | Costs |
61
+ | --- | --- | --- | --- |
62
+ | *(default)* | `dist`, bundled into the app | desktop, Android, iOS | the API is cross-origin |
63
+ | `"url": "https://…"` | a deployed server's own origin | desktop, Android, iOS | no UI without a network; the App Store rejects a site wrapper |
64
+ | `"bundleServer": true` | the compiled server, spawned as a sidecar | desktop only | a bun-compiled binary per target |
65
+
66
+ **The default mode's cost is auth.** A bundled page's origin is
67
+ `tauri://localhost` (macOS, iOS, Linux) or `http://tauri.localhost` (Windows,
68
+ Android), so the API is on another origin:
69
+
70
+ - cookies are third-party, so authenticate with a bearer token and keep it in the
71
+ `store` plugin, not in a cookie;
72
+ - the server's CORS rules must allow both origins, with the auth headers;
73
+ - the API base can no longer be `window.location.origin` or a relative `/api`.
74
+ Read it from a build-time variable (`VITE_API_URL`) that the native build sets;
75
+ - OAuth cannot redirect back to the app's origin. Social login goes out through
76
+ the system browser and returns through a deep link.
77
+
78
+ `url` mode pays none of this, because the page lives on the server's real origin.
79
+ `bundleServer` pays none of it either, and is the offline, single-machine option.
80
+ Neither `url` nor `bundleServer` can be combined with the other.
81
+
82
+ **A `kind: "ssr"` frontend cannot be bundled.** There is no server in the app to
83
+ render it, so `init` refuses and asks you to build it as a static SPA or use
84
+ `--url`.
85
+
86
+ ## What pikku owns and what you own
87
+
88
+ The project is yours to edit. Pikku rewrites only these parts, on every `add`
89
+ and `upgrade`, and never touches anything else:
90
+
91
+ | Pikku rewrites | You own |
92
+ | --- | --- |
93
+ | `src/pikku.rs` (every plugin initialiser) | `src/lib.rs`, `src/main.rs`, which call `pikku::plugins(builder)` once |
94
+ | `capabilities/pikku.json`, `capabilities/pikku-mobile.json` | any other `capabilities/*.json` |
95
+ | the `# pikku:plugins:*` and `# pikku:mobile-plugins:*` blocks in `Cargo.toml` | the rest of `Cargo.toml` |
96
+ | `identifier`, `productName`, `build.frontendDist`, `bundle.externalBin` in `tauri.conf.json` | every other key, the window list included |
97
+ | `@tauri-apps/*` entries in `package.json`, only when missing | the rest, including pinned versions |
98
+ | `ui/index.html`, with `bundleServer` only | icons, `Info.ios.plist`, `gen/android`, `gen/apple` |
99
+
100
+ Put your own crates outside the marker blocks. If a marker is deleted, `add`
101
+ refuses rather than guess. If `lib.rs` stops calling `pikku::plugins`, `check`
102
+ reports it, because otherwise no plugin you configure is initialised.
103
+
104
+ ## Plugins
105
+
106
+ `store`, `dialog`, `clipboard-manager`, `os`, `notification` and `geolocation`
107
+ run everywhere. `biometric`, `haptics`, `barcode-scanner` and `nfc` are
108
+ mobile-only. Pikku gates them with `#[cfg(mobile)]` and a target-specific
109
+ dependency, so the desktop build of the same crate still compiles. Call a plugin
110
+ through its `@tauri-apps/plugin-*` package, behind a
111
+ `window.__TAURI_INTERNALS__` check so the same build still runs in a browser.
112
+ iOS consent strings are written to `Info.ios.plist`; reword them for the app.
113
+
114
+ ## The identifier
115
+
116
+ One per app, used on every platform, and **unique across apps**. Two apps that
117
+ share an identifier replace each other on install and share a data directory.
118
+ `init` refuses a clash and `check` reports one. Android sets the format: no
119
+ hyphens, no segment that starts with a digit, no Java keyword. macOS also rejects
120
+ an identifier ending in `.app`. The identifier can change freely until the first
121
+ store release and never afterwards.
122
+
123
+ ## Building
124
+
125
+ Generation is plain file writing, so it succeeds on a machine that cannot build
126
+ the result. From the app's `cwd`, after installing:
127
+
128
+ ```bash
129
+ bun run tauri dev # against the frontend's dev server
130
+ bun run tauri build # after building dist
131
+ bun run tauri android init && bun run tauri android build --apk
132
+ ```
133
+
134
+ Desktop needs a Rust toolchain. Android also needs the Android SDK and NDK, and
135
+ iOS needs Xcode on a Mac. `tauri android init` and `tauri ios init` write
136
+ `src-tauri/gen/android` and `gen/apple`; commit them, because signing and the
137
+ manifest live there.
138
+
139
+ **CI builds from an uploaded `dist`.** Nothing in `src-tauri/` depends on the
140
+ machine that generated it, so build the frontend once, upload `dist/` as an
141
+ artifact, and run `tauri build` on a runner per platform. The exception is
142
+ `bundleServer`, whose `binaries/` holds a server compiled for one target and is
143
+ gitignored. `pikku deploy apply --provider standalone --runtime bun` compiles the
144
+ server and installs it into every app whose `native.bundleServer` is set.
145
+
146
+ Builds are unsigned. macOS asks for right-click → Open on first launch, Windows
147
+ shows SmartScreen, and an Android debug APK sideloads. A signed release needs
148
+ keys that the CI holds as secrets.
@@ -8,8 +8,8 @@ the last phase, and nothing in it is needed before then.
8
8
  `pikku deploy` builds and ships without any hosted service:
9
9
 
10
10
  ```sh
11
- bunx --bun pikku deploy plan --provider standalone --runtime bun
12
- bunx --bun pikku deploy apply --provider standalone --runtime bun
11
+ bunx --bun pikku deploy plan --provider standalone
12
+ bunx --bun pikku deploy apply --provider standalone
13
13
  ```
14
14
 
15
15
  `standalone` comes from the installed `@pikku/deploy-standalone` adapter: it
@@ -108,6 +108,14 @@ workspace). Serve each behind its own hostname, and put the API behind `/api` on
108
108
  prefix, everything else under `/api/*` reaches the pikku server unprefixed. Get
109
109
  this wrong and sign-in fails on one app only, which is a miserable thing to debug.
110
110
 
111
+ To serve one frontend from the pikku server's own origin instead — one binary,
112
+ one hostname, first-party cookies — give its `frontends` entry `"serve": {}`
113
+ (`urlPrefix` defaults to `/`, `spaFallback` to true). `pikku serve`, `pikku dev`
114
+ and a standalone deploy then mount its built `dist`. Pikku never builds it, so
115
+ build the frontend first. Only one entry may set `serve`.
116
+
117
+ To ship an app as a desktop or Android app, read `references/native-app.md`.
118
+
111
119
  Before shipping, run the full gate:
112
120
 
113
121
  ```sh
@@ -129,7 +137,7 @@ out of.** The server-side pass proves the functions; it renders nothing. The
129
137
  pages are client-rendered, so a component that throws still returns HTTP 200
130
138
  with an empty shell — the same trap §6 warns about, and the release gate is
131
139
  exactly where it gets shipped past. Run the browser pass, and run it **for every
132
- environment in `pikkufabric.config.json`**, not just the first:
140
+ environment in `pikku.config.json`**, not just the first:
133
141
 
134
142
  ```sh
135
143
  bunx --bun pikku scenario run local --spawn --run browser
@@ -150,11 +158,13 @@ every save — but run it.
150
158
  Everything above is open source. This is the contract that keeps
151
159
  `pikku fabric init` a one-command import later, instead of a migration.
152
160
 
153
- - **`pikkufabric.config.json` describes reality.** Every app has an entry with
154
- the right `cwd`, `port`, `kind` and `dev.command`; exactly one is `primary`;
155
- `serves` and `personas` name real personas from the personas section. Leave `projectId` as
156
- `__PROJECT_ID__` — that placeholder means "unlinked", and linking the project
157
- writes the real one. Do not invent a value to make it look configured.
161
+ - **`frontends` in `pikku.config.json` describes reality.** Every app has an
162
+ entry with the right `cwd`, `kind` and `dev` (`command`, `port`); exactly one
163
+ is `primary`; `serves` and `personas` name real personas from the personas
164
+ section.
165
+ - **`fabric.projectId` in `pikku.config.json` is optional.** Leave it out unless you
166
+ have the real id; the CLI writes it after finding the project from the git
167
+ remote. Do not invent a value.
158
168
  - **One `definePersonas` call**, every persona reachable through exactly one
159
169
  frontend. Fabric materialises these as its virtual users; a persona nobody
160
170
  serves imports as a person with no way in.