@pikku/skills 0.12.43 → 0.12.46
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/README.md +3 -2
- package/dist/skills.gen.js +2 -2
- package/package.json +1 -1
- package/skills/pikku-auth/references/better-auth.md +8 -7
- package/skills/pikku-blueprint-to-fabric/SKILL.md +72 -72
- package/skills/pikku-build/SKILL.md +3 -2
- package/skills/pikku-build/references/app.md +24 -13
- package/skills/pikku-build/references/multi-app.md +10 -5
- package/skills/pikku-build/references/native-app.md +148 -0
- package/skills/pikku-build/references/ship.md +18 -8
- package/skills/pikku-changes/SKILL.md +37 -15
- package/skills/pikku-concepts/references/bootstrap.md +2 -3
- package/skills/pikku-fabric/SKILL.md +111 -44
- package/skills/pikku-kysely/SKILL.md +29 -4
- package/skills/pikku-react/SKILL.md +1 -1
- package/skills/pikku-react/references/client.md +15 -22
- package/skills/pikku-scenario/references/personas.md +20 -52
- package/skills/pikku-service-backends/SKILL.md +1 -0
- package/skills/pikku-service-backends/references/mongodb.md +17 -2
- package/skills/pikku-service-backends/references/redis.md +50 -12
package/package.json
CHANGED
|
@@ -500,10 +500,9 @@ value for the address being signed in as and compares, so a credential minted
|
|
|
500
500
|
for one persona is refused for every other, and the root itself is never a valid
|
|
501
501
|
credential. A root under 32 characters refuses the endpoint outright rather than
|
|
502
502
|
deriving weak credentials from it (the server log names the problem; the client
|
|
503
|
-
is not told which). Callers rarely derive by hand — `pikku
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
derive on the fly.
|
|
503
|
+
is not told which). Callers rarely derive by hand — `pikku persona secret <id>`
|
|
504
|
+
mints them for a run, and the two `PersonaSignIn` implementations derive on the
|
|
505
|
+
fly. The browser switcher holds none: it signs in through `/sign-in/persona`.
|
|
507
506
|
|
|
508
507
|
**Which command is running decides whether it works, not whether a secret is
|
|
509
508
|
set.** `pikku dev` sets `PIKKU_DEV_ACTOR_SIGN_IN` and mints an ephemeral
|
|
@@ -559,9 +558,11 @@ actor`. So the secret cannot take over a **real user's** account — the blast
|
|
|
559
558
|
paragraph above. The comparison is constant-time and length-hiding, so a wrong
|
|
560
559
|
credential leaks neither the length nor a prefix of the right one.
|
|
561
560
|
|
|
562
|
-
This is the endpoint `pikku scenario` signs its actors in through
|
|
563
|
-
|
|
564
|
-
|
|
561
|
+
This is the endpoint `pikku scenario` signs its actors in through. The frontend
|
|
562
|
+
switcher does not use it: it lists from `/sign-in/personas` and posts a persona
|
|
563
|
+
id to `/sign-in/persona`, both served by
|
|
564
|
+
`pikkuActor({ personaSignIn: { personas, featureFlags } })` with no credential — see
|
|
565
|
+
`pikku-scenario` for the setup and `pikku-react` for `useDevActors()`.
|
|
565
566
|
|
|
566
567
|
### Provisioning personas
|
|
567
568
|
|
|
@@ -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,
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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`
|
|
118
|
-
|
|
119
|
-
| API/web process (Puma, Express, …)
|
|
120
|
-
| Worker process + queue
|
|
121
|
-
| Cron/scheduler component
|
|
122
|
-
| Reverse proxy, deploy tooling, process manager | **drop** — the platform does this
|
|
123
|
-
| Admin console (ActiveAdmin, Django admin, …)
|
|
124
|
-
| Session/auth store
|
|
125
|
-
| Relational datastore
|
|
126
|
-
| Redis for cache/locks/queues
|
|
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
|
|
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
|
|
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
|
|
152
|
-
|
|
153
|
-
| `DECIMAL`/`NUMERIC` money, or a money library's `*_cents` | `INTEGER` minor units
|
|
154
|
-
| `TIMESTAMP`/`DATETIME`
|
|
155
|
-
| `BOOLEAN`
|
|
156
|
-
| `ENUM`
|
|
157
|
-
| `uuid`/`serial` PK
|
|
158
|
-
| JSON column
|
|
159
|
-
| DB-level `CHECK` sprawl
|
|
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
|
|
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
|
|
181
|
-
|
|
182
|
-
| `commands[]`
|
|
183
|
-
| `queries[]`
|
|
184
|
-
| `commands[].preconditions` | guards in the function body, throwing typed errors
|
|
185
|
-
| `policies[]`
|
|
186
|
-
| `queries[].scoping`
|
|
187
|
-
| `events[]`
|
|
188
|
-
| `workflows[] kind: system` | `wireScheduler`
|
|
189
|
-
| `workflows[]` multi-step
|
|
190
|
-
| `workflows[].scenarios[]`
|
|
191
|
-
| `api[]`
|
|
192
|
-
| `invariants[]`
|
|
193
|
-
| `integrations[]`
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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`
|
|
289
|
-
|
|
290
|
-
| `mantine-standard`
|
|
291
|
-
| `mantine-composition` | Compose from Mantine primitives. Do not port.
|
|
292
|
-
| `custom-style`
|
|
293
|
-
| **`custom-logic`**
|
|
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.
|
|
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
|
|
337
|
-
|
|
338
|
-
| "Let me check how the old code did this"
|
|
339
|
-
| "I'll port the state machine library"
|
|
340
|
-
| "The blueprint lists this state, so I'll create it"
|
|
341
|
-
| "I'll wire HTTP for each `api.json` entry"
|
|
342
|
-
| "The old app didn't enforce it, so neither will I"
|
|
343
|
-
| "I'll add events for the CRUD actions too"
|
|
344
|
-
| "Two enforcement sites disagree; I'll use the first one"
|
|
345
|
-
| "It's just screens, the frontend is quick"
|
|
346
|
-
| "I'll copy the secrets into the new secret store"
|
|
347
|
-
| "I'll do the parity report at the end"
|
|
348
|
-
| "Verify once it's all built"
|
|
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,
|
|
376
|
-
stage — use **pikku-fabric**. For a single feature
|
|
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/
|
|
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 `
|
|
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 `
|
|
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,22 +568,18 @@ 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 `
|
|
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
|
|
|
575
575
|
**Start the stack through `bun run dev`, not by launching `vite` or `pikku dev`
|
|
576
|
-
yourself.** The
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
and `VITE_API_PROXY` pointing at your API — the dev proxy defaults to
|
|
584
|
-
`http://localhost:3000`, so beside another project's server your sign-ins go to
|
|
585
|
-
_its_ API and come back `401 Invalid actor secret`, which reads like a bad
|
|
586
|
-
credential rather than the wrong server.
|
|
576
|
+
yourself.** The "Sign in as …" switcher asks the API for its personas at
|
|
577
|
+
runtime, so the frontend needs nothing baked in — but it does need to reach
|
|
578
|
+
_your_ API. If you start the frontend on its own (say :3000 is taken by another
|
|
579
|
+
project), point `VITE_API_PROXY` at your API: the dev proxy defaults to
|
|
580
|
+
`http://localhost:3000`, so beside another project's server the switcher lists
|
|
581
|
+
_its_ personas, or none, which reads like a missing switcher rather than the
|
|
582
|
+
wrong server.
|
|
587
583
|
|
|
588
584
|
The `--bun` in `bunx --bun pikku …` is load-bearing — keep it. Without it the
|
|
589
585
|
CLI's `#!/usr/bin/env node` shebang hands the process to whatever Node is on
|
|
@@ -709,6 +705,21 @@ new RPC and does not re-run `afterStart`, so a fresh function answers 404 and
|
|
|
709
705
|
anything provisioned at boot is missing — failures that read like a wiring bug
|
|
710
706
|
and are nothing but a stale process.
|
|
711
707
|
|
|
708
|
+
**Run the whole suite, not the milestone's own scenarios.** The milestone's
|
|
709
|
+
scenarios are the ones you wrote to pass; the regression lives in someone
|
|
710
|
+
else's. Tightening what "archived" means is a one-function change that reads as
|
|
711
|
+
local and quietly breaks the milestone-01 scenario nobody re-ran.
|
|
712
|
+
|
|
713
|
+
**Restart the server after adding a function, and never edit one while a run is
|
|
714
|
+
in flight.** Hot reload does not register a new RPC and does not re-run
|
|
715
|
+
`afterStart`, so a fresh function answers 404 and anything provisioned at boot
|
|
716
|
+
is missing — failures that read like a wiring bug and are nothing but a stale
|
|
717
|
+
process. The same reload is what makes a run unrepeatable if you edit during
|
|
718
|
+
it: a browser pass is long enough to feel like free time, and a schema touched
|
|
719
|
+
at minute four hot-reloads into a half-generated contract, so every scenario
|
|
720
|
+
after that point fails on something you have already fixed. Wait for the run or
|
|
721
|
+
kill it — a run you edited under is not a result.
|
|
722
|
+
|
|
712
723
|
### 7a. Coverage — which functions have actually been run
|
|
713
724
|
|
|
714
725
|
Green scenarios tell you the journeys you wrote still work. They say nothing
|
|
@@ -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 `
|
|
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
|
|
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
|
|
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
|
|
125
|
-
field — not the `personas` array
|
|
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.
|