@pikku/skills 0.12.52 → 0.12.55

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.
Files changed (112) hide show
  1. package/README.md +1 -1
  2. package/dist/skills.gen.js +2 -2
  3. package/dist/snippets.js +1 -1
  4. package/package.json +1 -1
  5. package/skills/pikku-a11y/SKILL.md +22 -15
  6. package/skills/pikku-addon/SKILL.md +5 -5
  7. package/skills/pikku-addon/references/addon-package-manifest.md +7 -0
  8. package/skills/pikku-addon/references/openapi.md +11 -0
  9. package/skills/pikku-agent/SKILL.md +6 -6
  10. package/skills/pikku-agent/references/agents.md +8 -1
  11. package/skills/pikku-agent/references/runner-vercel.md +2 -2
  12. package/skills/pikku-agent/references/voice.md +7 -1
  13. package/skills/pikku-architect/SKILL.md +2 -2
  14. package/skills/pikku-architect/references/plan-defects.md +9 -7
  15. package/skills/pikku-auth/SKILL.md +15 -36
  16. package/skills/pikku-auth/references/better-auth.md +106 -133
  17. package/skills/pikku-auth/references/jose.md +6 -0
  18. package/skills/pikku-auth/references/machine-auth.md +6 -0
  19. package/skills/pikku-auth/references/sessions.md +6 -0
  20. package/skills/pikku-build/SKILL.md +37 -34
  21. package/skills/pikku-build/references/app.md +113 -99
  22. package/skills/pikku-build/references/design.md +43 -31
  23. package/skills/pikku-build/references/feature.md +17 -3
  24. package/skills/pikku-build/references/mocks.md +29 -0
  25. package/skills/pikku-build/references/multi-app.md +25 -18
  26. package/skills/pikku-build/references/native-app.md +9 -0
  27. package/skills/pikku-build/references/openapi.md +41 -34
  28. package/skills/pikku-build/references/platform.md +23 -14
  29. package/skills/pikku-build/references/post-clone.md +1 -1
  30. package/skills/pikku-build/references/quick.md +76 -39
  31. package/skills/pikku-build/references/scenarios.md +28 -16
  32. package/skills/pikku-build/references/ship.md +19 -16
  33. package/skills/pikku-build/references/theming.md +76 -127
  34. package/skills/pikku-changes/SKILL.md +7 -10
  35. package/skills/pikku-concepts/SKILL.md +34 -44
  36. package/skills/pikku-concepts/references/concept-mapping.md +18 -0
  37. package/skills/pikku-concepts/references/language.md +6 -6
  38. package/skills/pikku-convert-screen/SKILL.md +45 -0
  39. package/skills/pikku-deploy/references/cloudflare.md +8 -0
  40. package/skills/pikku-deploy/references/lambda.md +8 -0
  41. package/skills/pikku-deploy/references/nextjs.md +1 -1
  42. package/skills/pikku-deploy/references/uws.md +1 -1
  43. package/skills/pikku-deploy/references/ws.md +3 -3
  44. package/skills/pikku-emails/SKILL.md +3 -9
  45. package/skills/pikku-guide/SKILL.md +28 -27
  46. package/skills/pikku-i18n/SKILL.md +7 -7
  47. package/skills/pikku-i18n/references/enum-labels.md +9 -0
  48. package/skills/pikku-i18n/references/messages.md +21 -9
  49. package/skills/pikku-i18n/references/rtl.md +18 -38
  50. package/skills/pikku-knowledge/SKILL.md +12 -13
  51. package/skills/pikku-kysely/SKILL.md +3 -21
  52. package/skills/pikku-list-query/SKILL.md +2 -2
  53. package/skills/pikku-meta/references/audit.md +10 -1
  54. package/skills/pikku-meta/references/meta.md +6 -0
  55. package/skills/pikku-meta/references/versioning.md +10 -0
  56. package/skills/pikku-middleware/SKILL.md +7 -16
  57. package/skills/pikku-n8n-import/SKILL.md +9 -1
  58. package/skills/pikku-n8n-import/references/addon-mapping.md +6 -0
  59. package/skills/pikku-n8n-import/references/code-translation.md +7 -0
  60. package/skills/pikku-permissions/SKILL.md +41 -10
  61. package/skills/pikku-permissions/references/scopes.md +59 -0
  62. package/skills/pikku-react/SKILL.md +20 -22
  63. package/skills/pikku-react/references/client.md +2 -70
  64. package/skills/pikku-react/references/react-query.md +11 -0
  65. package/skills/pikku-react/references/workflows.md +11 -0
  66. package/skills/pikku-realtime/SKILL.md +9 -3
  67. package/skills/pikku-report/SKILL.md +17 -16
  68. package/skills/pikku-scenario/SKILL.md +72 -81
  69. package/skills/pikku-scenario/references/browser.md +5 -5
  70. package/skills/pikku-scenario/references/coverage.md +4 -4
  71. package/skills/pikku-scenario/references/steps.md +31 -30
  72. package/skills/pikku-scenario/references/{persona-run.md → virtual-user-run.md} +47 -42
  73. package/skills/pikku-scenario/references/virtual-users.md +114 -0
  74. package/skills/pikku-seo/SKILL.md +12 -16
  75. package/skills/pikku-service-backends/SKILL.md +0 -10
  76. package/skills/pikku-service-backends/references/aws.md +6 -0
  77. package/skills/pikku-service-backends/references/mongodb.md +7 -0
  78. package/skills/pikku-service-backends/references/redis.md +6 -0
  79. package/skills/pikku-services/references/audit-wire-service.md +2 -2
  80. package/skills/pikku-services/references/audit.md +10 -1
  81. package/skills/pikku-services/references/config.md +6 -0
  82. package/skills/pikku-services/references/services.md +9 -2
  83. package/skills/pikku-software-archaeology/SKILL.md +21 -27
  84. package/skills/pikku-software-archaeology/references/blueprint.schema.json +16 -16
  85. package/skills/pikku-software-archaeology/references/pikku-mapping.md +3 -3
  86. package/skills/pikku-software-archaeology/references/second-opinion.md +13 -1
  87. package/skills/pikku-tailwind/SKILL.md +111 -0
  88. package/skills/pikku-theme/SKILL.md +187 -0
  89. package/skills/pikku-theme/references/from-a-reference.md +44 -0
  90. package/skills/pikku-theme/references/presets.md +40 -0
  91. package/skills/pikku-ui/SKILL.md +41 -0
  92. package/skills/pikku-wiring/SKILL.md +13 -12
  93. package/skills/pikku-wiring/references/channel.md +7 -1
  94. package/skills/pikku-wiring/references/cli.md +7 -1
  95. package/skills/pikku-wiring/references/gateway-slack.md +6 -0
  96. package/skills/pikku-wiring/references/http.md +10 -3
  97. package/skills/pikku-wiring/references/mcp.md +8 -0
  98. package/skills/pikku-wiring/references/queue.md +6 -0
  99. package/skills/pikku-wiring/references/realtime.md +34 -4
  100. package/skills/pikku-wiring/references/rpc.md +6 -0
  101. package/skills/pikku-wiring/references/scheduler.md +6 -0
  102. package/skills/pikku-wiring/references/trigger.md +8 -0
  103. package/skills/pikku-workflow/SKILL.md +9 -7
  104. package/skills/pikku-admin-to-fabric/SKILL.md +0 -212
  105. package/skills/pikku-auth/references/permissions.md +0 -261
  106. package/skills/pikku-blueprint-to-fabric/SKILL.md +0 -377
  107. package/skills/pikku-blueprint-to-fabric/scripts/inventory.mjs +0 -367
  108. package/skills/pikku-fabric/SKILL.md +0 -619
  109. package/skills/pikku-fabric/references/debugging.md +0 -110
  110. package/skills/pikku-mantine/SKILL.md +0 -80
  111. package/skills/pikku-scenario/references/personas.md +0 -117
  112. package/skills/pikku-wiring/references/realtime-other-routes.md +0 -23
@@ -1,261 +0,0 @@
1
- # Pikku Permissions
2
-
3
- ## ⛔ FIRST: is the caller a machine with a token? ⛔
4
-
5
- **Then this is NOT a permissions problem.** Resolve the token in `addHTTPMiddleware('*')` middleware that calls `setSession`, make the function a `pikkuFunc`, and gate it with `scopes`. A `permissions` check that verifies a bearer token and returns `true` is authentication wearing an authorization hat — and it leaves the function sessionless, so every body still has to work out who called it. See `references/machine-auth.md`. The only exception is a bootstrap endpoint whose caller has no identity yet (a shared-secret registration, a login): that one is sessionless and declares its gate here.
6
-
7
- ## The Rule
8
-
9
- **ALWAYS put authorization checks in the `permissions` field of `pikkuFunc` or `pikkuSessionlessFunc` — NEVER inside the `func` body.**
10
-
11
- This includes: org access checks, repo access checks, role checks, resource ownership, and any other authorization logic. The `permissions` field runs before `func` and is visible to the inspector, so the gate is declared rather than buried — which is what lets `pikku info permissions` and an audit see it at all. Alongside it sits `scopes` (see below) for grant-based gating; between them they are where Pikku enforces authorization. The one sanctioned exception is `permissionsInBody`, covered at the end.
12
-
13
- ```typescript
14
- // CORRECT
15
- export const deleteBook = pikkuFunc({
16
- func: async ({ db }, { bookId }) => {
17
- await db.deleteBook(bookId)
18
- },
19
- permissions: {
20
- owner: isBookOwner, // ← authorization here
21
- },
22
- })
23
-
24
- // WRONG — permission check inside func body
25
- export const deleteBook = pikkuFunc({
26
- func: async ({ db }, { bookId }, { session }) => {
27
- if (!session) throw new UnauthorizedError() // ← never do this
28
- await db.deleteBook(bookId)
29
- },
30
- })
31
- ```
32
-
33
-
34
- ## Permission Factories
35
-
36
- ### `pikkuAuth(fn)` — Session-Only Checks
37
-
38
- Use for checks that read the session but need no request data — and that assert
39
- something **beyond** merely having a session (a flag, a tier, a claim).
40
-
41
- ```typescript
42
- import { pikkuAuth } from '#pikku/auth'
43
-
44
- // Good: a real gate on the session's contents, not just its existence.
45
- export const isVerified = pikkuAuth(
46
- async (_services, session) => !!session?.emailVerified
47
- )
48
- ```
49
-
50
- **Do NOT write an "is signed in" permission.** A checker that just returns
51
- `!!session` is not authorization — it re-checks authentication, which the
52
- function already enforces. A function that needs a signed-in user sets
53
- `auth: true` (the default for `pikkuFunc`); it does not also carry a
54
- `permissions: { signedIn }`.
55
-
56
- ```typescript
57
- // WRONG — redundant with auth: true; adds a permission that gates nothing.
58
- export const isSignedIn = pikkuAuth(async (_s, session) => !!session)
59
- pikkuFunc({ auth: true, permissions: { signedIn: isSignedIn } /* ... */ })
60
-
61
- // RIGHT — auth: true already requires the session; permissions are for capability.
62
- pikkuFunc({ auth: true /* ... */ })
63
- ```
64
-
65
- A permission answers "_may this user do this?_" (role, ownership, tier) — never
66
- "_is there a session?_".
67
-
68
- ### `pikkuPermission(fn)` — Data-Aware Checks
69
-
70
- Use when authorization depends on the actual request data (e.g., resource ownership).
71
-
72
- ```typescript
73
- import { pikkuPermission } from '#pikku/auth'
74
-
75
- export const isBookOwner = pikkuPermission(
76
- async ({ db }, { bookId }, { session }) => {
77
- const book = await db.getBook(bookId)
78
- return book?.authorId === session?.userId
79
- }
80
- )
81
-
82
- export const hasBookAccess = pikkuPermission(
83
- async ({ db }, { bookId }, { session }) => {
84
- return await db.hasAccess(session?.userId, bookId)
85
- }
86
- )
87
- ```
88
-
89
- ## OR / AND Logic
90
-
91
- ```typescript
92
- permissions: {
93
- verified: isVerified, // OR: verified users can access
94
- owner: isBookOwner, // OR: owners can access
95
- reviewer: [isVerified, hasBookAccess], // AND: both must pass
96
- }
97
- // Logic: verified OR owner OR (isVerified AND hasBookAccess)
98
- ```
99
-
100
- Groups are OR'd. Entries within a group array are AND'd.
101
-
102
- ## Where to Apply Permissions
103
-
104
- ### Per-Function (preferred)
105
-
106
- ```typescript
107
- export const deleteBook = pikkuFunc({
108
- func: async ({ db }, { bookId }) => {
109
- await db.deleteBook(bookId)
110
- },
111
- permissions: {
112
- verified: isVerified,
113
- owner: isBookOwner,
114
- },
115
- })
116
- ```
117
-
118
- ### Global (`addGlobalPermission`) — App-Wide AND Gate
119
-
120
- A global permission is an app-wide baseline that **every** function must additionally pass. It is an independent AND gate: it can only ever _narrow_ access — it never grants access a function's own `permissions` would deny.
121
-
122
- ```typescript
123
- import { addGlobalPermission } from '#pikku/auth'
124
-
125
- addGlobalPermission([isEmployee]) // every function now also requires an employee session
126
- ```
127
-
128
- Multiple `addGlobalPermission` calls accumulate and are AND'd together.
129
-
130
- > Wire-, tag-, and HTTP-route-level permissions (`addHTTPPermission`, `addTagPermission`, and a `permissions` field on HTTP/channel/MCP wirings) were **removed in #972**. Permissions now live only on the function definition, plus the optional global gate. Tags are organizational only — use tag/HTTP _middleware_ (`addTagMiddleware`, `addHTTPMiddleware`) for cross-cutting request handling, not authorization.
131
-
132
- ## Scopes — the AND Gate Above Permissions
133
-
134
- Scopes answer "what was this session granted?" before permissions ask "may this
135
- user do this to this resource?". They are AND-ed: every scope listed must be
136
- held. Because they are checked first and fail closed, a scope can only ever
137
- _narrow_ access — it never grants what `permissions` would deny.
138
-
139
- Declare the scope tree once with `defineScope`. The body is a no-op that
140
- tree-shakes away; the CLI reads the call by AST and generates a `ScopeId` union,
141
- so a function naming an undeclared scope fails the build rather than silently
142
- gating on nothing.
143
-
144
- ```typescript
145
- // src/scopes.ts
146
- import { defineScope } from '#pikku/scopes'
147
-
148
- defineScope({
149
- admin: {
150
- displayName: 'Administration',
151
- description: 'Administrative access',
152
- scopes: {
153
- invoices: {
154
- description: 'Invoice management',
155
- scopes: {
156
- create: { description: 'Create invoices' },
157
- void: { description: 'Void invoices' },
158
- },
159
- },
160
- },
161
- },
162
- billing: {},
163
- })
164
- ```
165
-
166
- Every node is grantable, keyed by segment: the above yields `admin`,
167
- `admin:invoices`, `admin:invoices:create`, `admin:invoices:void` and `billing`.
168
- Scopes may be declared across more than one file — the declarations merge.
169
-
170
- ```typescript
171
- export const voidInvoice = pikkuFunc({
172
- scopes: ['admin:invoices:void'],
173
- permissions: { owner: isInvoiceOwner },
174
- func: async ({ db }, { invoiceId }) => { ... },
175
- })
176
- ```
177
-
178
- A grant satisfies a required scope if it is the scope itself, an ancestor of it,
179
- or a wildcard at any level — so a session holding `admin` satisfies
180
- `admin:invoices:void`, and `admin:*` does too. A missing scope throws
181
- `MissingScopeError` naming the first one that failed.
182
-
183
- `scopes` requires a session and so is unavailable on `pikkuSessionlessFunc`:
184
- scopes fail closed, an anonymous caller holds none, and a sessionless function
185
- with scopes would reject every caller it exists to serve. Gate those with
186
- `permissions`, which receive the optional session and may pass anonymous.
187
-
188
- ## The Three Gates
189
-
190
- Authorization is three independent gates, evaluated in this order, all of which must pass:
191
-
192
- 1. **Scopes** (`scopes`) — AND'd, checked before input validation. Fails closed.
193
- 2. **Global permissions** (`addGlobalPermission`) — AND'd together. A broad baseline that can only narrow access.
194
- 3. **The function's own `permissions`** — OR'd groups (OR-of-ANDs), as above.
195
-
196
- The gates are independent: a broad global (e.g. `isEmployee`) can **never** satisfy an admin-only function's own requirement. Each function still enforces its own `scopes` and `permissions` in full.
197
-
198
- ## The Sanctioned Exception: `permissionsInBody`
199
-
200
- A few checks genuinely cannot be expressed as a permission — verifying a webhook
201
- signature, a signed token, or an invite code, where the "identity" arrives in the
202
- payload and there is no session to check. For those, declare
203
- `permissionsInBody: true` on the function and keep the check in the body.
204
-
205
- ```typescript
206
- export const handleStripeWebhook = pikkuSessionlessFunc({
207
- permissionsInBody: true,
208
- auth: false,
209
- func: async ({ stripe }, data, { http }) => {
210
- stripe.webhooks.constructEvent(
211
- data.raw,
212
- http.request.header('stripe-signature'),
213
- secret
214
- )
215
- // ...
216
- },
217
- })
218
- ```
219
-
220
- This is a last resort, and it is purely declarative — it grants nothing and
221
- enforces nothing. Its only job is to tell the auditor that this function's
222
- apparent openness is deliberate, so asserting it falsely disables the very check
223
- that would have caught the mistake. It requires `"allow": { "permissionsInBody": true }`
224
- in `pikku.config.json`, which keeps the decision visible at the project level.
225
- Prefer `permissions` whenever the check can be expressed as one — they are
226
- declared, inspectable, and reusable.
227
-
228
- ## Complete Example
229
-
230
- ```typescript
231
- // src/permissions.ts
232
- import { pikkuAuth, pikkuPermission } from '#pikku/auth'
233
-
234
- export const isVerified = pikkuAuth(
235
- async (_services, session) => !!session?.emailVerified
236
- )
237
-
238
- export const isOrgMember = pikkuPermission(
239
- async ({ db }, { orgId }, { session }) => {
240
- return await db.isMember(session?.userId, orgId)
241
- }
242
- )
243
-
244
- // src/functions/org.function.ts
245
- export const deleteOrg = pikkuFunc({
246
- func: async ({ db }, { orgId }) => {
247
- await db.deleteOrg(orgId)
248
- },
249
- permissions: {
250
- verified: isVerified,
251
- owner: [isVerified, isOrgMember],
252
- },
253
- })
254
- ```
255
-
256
- ## After Changes
257
-
258
- ```bash
259
- pikku all # regenerate if wirings changed
260
- pikku all --tsc # regenerate, then verify permission checker types (fails on type errors)
261
- ```
@@ -1,377 +0,0 @@
1
- ---
2
- name: pikku-blueprint-to-fabric
3
- description: 'Rebuild a legacy app as a Pikku Fabric app from a `.knowledge/` Product Blueprint (produced by pikku-software-archaeology). Covers the blueprint→Fabric mapping (domains→slices, commands/queries→pikkuFuncs, entities→SQLite migrations, policies→permissions, invariants→DB constraints, workflows→schedulers, frontend-routes→TanStack+Mantine), the decisions gate, and the parity report. TRIGGER when: a `.knowledge/` blueprint exists and the user wants to rebuild/port/recreate that app in Pikku or Fabric, or says "rebuild this from the blueprint". DO NOT TRIGGER when: no blueprint exists (run pikku-software-archaeology first), or the user wants a single new feature in an existing app (use pikku-build).'
4
- installGroups: [fabric]
5
- argument-hint: '<path to .knowledge/> [domain to slice next]'
6
- ---
7
-
8
- # Blueprint → Fabric
9
-
10
- ## Agent Operating Procedure
11
-
12
- Use this skill as an execution checklist, not reference material.
13
-
14
- 1. **Validate the blueprint before you trust it.** Run the archaeology validator; `0 error(s)` or stop.
15
- 2. **Clear the decisions gate.** Unresolved `decisionsNeeded` block the domains they touch. Ask; do not invent.
16
- 3. **Build the schema first**, from `entities.json` — everything else hangs off it.
17
- 4. **Then one vertical slice per domain, in dependency order.** Each slice ships functions + permissions + migrations + scenarios and verifies green before the next starts.
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
- 6. **Write the parity report as you go**, not at the end — it is the deliverable that proves the rebuild is complete.
20
-
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
-
23
- ## The one idea
24
-
25
- **The blueprint is a plan, not a transcript.**
26
-
27
- A faithful port reproduces the legacy app's bugs, dead code, and drifted rules — and you will have spent months to arrive back where you started. The blueprint already separates the **product** (what the business meant) from the **accident** (what the code happened to do): that separation is `gaps.json`, `migration.json.dropped`, `invariants.enforcedBy: "nothing"`, and the `confidence` field. Using that separation is the entire reason the blueprint exists.
28
-
29
- So:
30
-
31
- - `commands.json` / `queries.json` / `entities.json` / `policies.json` → **build these**.
32
- - `gaps.json` (`kind: bug` / `dead-code`) and `migration.json.dropped` → **do not build these.** They are the accident.
33
- - `invariants.json` with `enforcedBy: "nothing"` → **build these properly for the first time.** This is where a rebuild actually earns its cost.
34
- - `decisionsNeeded` → **ask.** These are the questions the legacy code never answered, and neither can you.
35
-
36
- If you find yourself opening the legacy source to "check how it did X", stop. Either the blueprint says X (build that) or it doesn't (it's a decision — ask). Reading the old code is how its accidents get back in.
37
-
38
- ## Stage 0 — Preflight
39
-
40
- ```bash
41
- node <archaeology-skill-dir>/scripts/validate.mjs <repo>/.knowledge # must print 0 error(s)
42
- ```
43
-
44
- A blueprint with validation errors has dangling concept names, and concept names are the IDs this whole skill maps on. Fix it there, not here.
45
-
46
- Then read, in this order — **whole files, once**: `product.json` (what it is, and the terminology traps), `domains.json` (the slice list and the roll-ups), `migration.json` (what survives, what drops, what's undecided). These three tell you the shape of the job. Read the rest per-slice, not up front — `commands.json` at 180+ entries will drown you if you read it whole before you need it.
47
-
48
- ### The terminology trap — do this before you name anything
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`).
51
-
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
-
54
- ## Stage 1 — The decisions gate (blocking)
55
-
56
- Collect every:
57
-
58
- - `migration.json.decisionsNeeded[]`
59
- - `gaps.json[]` where `kind: "open-product-decision"`
60
- - any concept with `confidence: "low"`
61
-
62
- **These block the domains they touch. They do not block the whole rebuild** — take them to the user grouped by domain, so unaffected slices proceed while decisions are pending.
63
-
64
- Present each as a real question with the options the code implies and what each costs — not "what should happen when a renewal fails?" but "the `unpaid` state exists and nothing can reach it; when a renewal payment fails, do we (a) lapse immediately, (b) grace period of N days, (c) suspend the public listing but keep the account? (c) is what the listing gate implies but nothing implements it."
65
-
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
-
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
-
70
- ## Stage 1.5 — Emit the implementation inventory (do this before any code)
71
-
72
- ```bash
73
- node <skill-dir>/scripts/inventory.mjs <repo>/.knowledge --resolved 1,2 > <repo>/.knowledge/implementation-inventory.md
74
- ```
75
-
76
- This projects the blueprint into **Pikku terms** — every `pikkuFunc`,
77
- `pikkuPermission`, `wireScheduler`, `wireQueueWorker`, webhook ingress, event
78
- channel, table and scenario that will exist, per domain, plus what is **blocked**
79
- by an open decision and what is **deliberately not built**. `--resolved` takes the
80
- 1-based indices of `decisionsNeeded` already answered at the gate.
81
-
82
- Do this **before** scaffolding, and show it to the user. Three reasons:
83
-
84
- 1. **It makes the size real.** "Rebuild the app" is not a plan; "187 functions, 61
85
- permissions, 12 scheduled tasks, 268 scenarios, 33 custom-logic components, and
86
- 14 concepts blocked behind 4 questions" is one. Nobody can consent to the work
87
- until they can see it.
88
- 2. **It is derived, so it cannot flatter you.** Every number is a projection of the
89
- blueprint. If it says 12 scheduled tasks, the blueprint found 12 cron jobs.
90
- 3. **It exposes the ratio that matters.** A 223-surface `api.json` typically yields
91
- ~20 `wireHTTP` wirings; the rest is RPC. If your inventory says otherwise, you
92
- are about to transcribe the legacy router.
93
-
94
- The classifier is a heuristic over `workflows.json` triggers and is **advisory** —
95
- check its calls. The distinctions it encodes are the ones that matter:
96
-
97
- - **system + cron → `wireScheduler`**; **system + webhook → ingress**;
98
- **system + queue → `wireQueueWorker`**.
99
- - **system + `after_commit`/callback → an event and its consumers**, NOT a
100
- workflow. That trigger is the legacy shape of an event: a handler doing five
101
- unrelated things because there was no bus. Splitting it is the upgrade.
102
- - **user/admin journeys → scenarios, NOT `pikkuWorkflowFunc`s.** A blueprint
103
- "workflow" is a _journey_ — a sequence a person drives through the UI. A
104
- `pikkuWorkflowFunc` is durable multi-step orchestration. Conflating them produces
105
- a workflow engine driving form submissions, which is the most common way this
106
- mapping goes wrong.
107
-
108
- Re-run it per slice: the blocked count falls as decisions land, and it is the
109
- cheapest progress report you have.
110
-
111
- ## Stage 2 — Scaffold
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. 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
-
115
- Map `architecture.json` onto Fabric honestly, and expect it to shrink:
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 |
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.
129
-
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
-
132
- ## Stage 3 — Schema first
133
-
134
- Build the whole schema from `entities.json` before writing functions: plain numbered `.sql` in `db/sqlite/`, applied with `pikku db migrate`, which regenerates the Kysely types. **Never hand-edit the generated `schema.gen.ts`.**
135
-
136
- Check the numbering against what is already there — the starter template ships
137
- migrations of its own (Better Auth's schema, the audit table, the auth plugins),
138
- and a colliding number applies in an order you did not intend.
139
-
140
- **Use semantic column types.** `BOOLEAN` types as a real `boolean`, `DATETIME`/`DATE`
141
- as a `Date`, `JSON` as a parsed object — the generated types and coercion follow from
142
- the SQL. Writing `INTEGER` 0/1 flags or Unix-ms timestamps throws that away and you
143
- hand-coerce forever. `TEXT` + `CHECK` for a closed set is the highest-leverage choice
144
- available: the constraint compiles into a **TypeScript union type**, so an invalid
145
- state is a compile error rather than a runtime one.
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.
148
-
149
- ### Legacy SQL → SQLite traps
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. |
160
-
161
- ### States and transitions
162
-
163
- `entities[].states` + `transitions` is a state machine the legacy app ran through a library (aasm, state_machine, …). **Do not port the library.** Store the state as `TEXT` + `CHECK`, and let each transition be the `pikkuFunc` that `commands.json` already names (`CancelMembership`, `RefundInvoice`).
164
-
165
- Two traps the blueprint hands you for free:
166
-
167
- - **Unreachable states.** A state in `entities[].states` that no transition targets is either a dead declaration (drop it) or a `decisionsNeeded` (ask). Do not create an unreachable state in the new app just because the old one had it.
168
- - **Dead transitions.** `migration.json.dropped` may list a transition dropped for a typo (a real case: `partial_refunded` vs `partially_refunded`, which made multi-step partial refunds raise). Build the transition the product **means**, which is the one the state list supports — not the typo.
169
-
170
- ## Stage 4 — Slices, in dependency order
171
-
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
-
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
-
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
-
178
- ### What one slice contains
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** |
194
-
195
- ### Names are the contract
196
-
197
- `commands.json`/`queries.json` names are already imperative domain-language `VerbNoun` — which is exactly `pikkuFunc` naming. **Carry them verbatim.** They are the IDs that tie the blueprint, the parity report, `frontend-routes.json.dataFrom`, and the generated RPC client together. Renaming `AssignMembershipToUser` to `assignMembership` because it reads better costs you the whole cross-reference and buys nothing.
198
-
199
- Two exceptions: apply resolved false-friend renames (Stage 0), and apply any rename the user decided at the gate. Record both in the parity glossary.
200
-
201
- Every function needs a real `description` — take it from the concept's `description`/`purpose`, which is already written in product language. Per **pikku-fabric**, `missing description` means the work isn't finished.
202
-
203
- ### The API is not the contract
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`).
206
-
207
- Add `wireHTTP` only where the URL shape is a real external contract:
208
-
209
- - `auth: "none"` public pages that must keep their paths (SEO, printed links, QR codes)
210
- - **inbound webhooks** — the sender's URL is fixed (**pikku-wiring**)
211
- - surfaces `interfaces.json` marks as a genuine `openapi-rest` channel with external consumers
212
-
213
- A 200-surface `api.json` typically yields a handful of `wireHTTP` calls. If you're wiring HTTP for most of it, you're transcribing the legacy router.
214
-
215
- `api.json.auth` is still load-bearing: it's the per-surface answer that fills each function's `permissions:`. Where `auth` and `policies.json` disagree, the disagreement is a finding — check `gaps.json`, and if it's not there, raise it.
216
-
217
- ### Invariants — where the rebuild earns its cost
218
-
219
- For each `invariants[]` entry, look at `enforcedBy`:
220
-
221
- - `"nothing"` → **build it properly now.** This is the highest-value work in the whole rebuild: a rule the business believes it has and does not. It's typically a `UNIQUE`, a `CHECK`, a foreign key, or a transaction — cheap here, and the legacy app couldn't get to it because the enforcement had drifted somewhere unreachable.
222
- - `"convention"` or `"code-guard"` with an `atRiskBecause` → **move it down to the database** if it's expressible there. `atRiskBecause` usually describes a race the constraint eliminates outright.
223
- - `"db-constraint"` → carry it across. It already works.
224
-
225
- Two patterns worth naming, because they recur:
226
-
227
- - **Read-then-write idempotency** (check `find_by(external_id:)`, then insert) on a **nullable, non-unique** column. The fix is a `UNIQUE` index and an upsert — not a port of the check.
228
- - **Application-held sequence numbers** (an invoice counter behind a distributed lock). Use a DB-level guarantee. If a legacy test for this is commented out (the blueprint flags this under `gaps.json`), write it for real in the new app — that's a scenario, and it's the one that would have caught it.
229
-
230
- ### Events — make the implicit explicit
231
-
232
- Most `events[]` in a legacy blueprint carry `explicit: false`: there was no event bus, and the archaeologist reconstructed the event from a side-effect cluster (an email + a status flip + a counter bump in one handler). The `consumers` field lists what reacted.
233
-
234
- In the rebuild these become real: publish the event (**pikku-realtime**) or enqueue it (**pikku-wiring**), and make each listed consumer its own subscriber. That is the structural upgrade — the handler stops doing five unrelated things, and adding a sixth consumer stops meaning editing the handler.
235
-
236
- Two disciplines:
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.
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
-
241
- ### Policies — collapse the drift
242
-
243
- `policies[].enforcedAt` lists **every** legacy site enforcing the rule. Two or more entries usually means it drifted — same rule, subtly different versions. The blueprint often pairs it with a `gaps.json` `duplication` entry naming the drift.
244
-
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
-
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
-
249
- ## Stage 5 — Scenarios from the blueprint's tests
250
-
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
-
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
-
255
- Two rules:
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.
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
-
260
- ## Stage 6 — Integrations
261
-
262
- `integrations[]` gives `direction`, `dataExchanged`, `importance`, `replacementDifficulty`, `envVars`.
263
-
264
- - `replacementDifficulty: "hard"` + `importance: "critical"` → **keep**, and put it behind a service (**pikku-services**). These are the load-bearing vendors; a rebuild is not the time to also swap them.
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
- - `envVars` → `defineVariable` / `defineSecret` (**pikku-services**). Per **pikku-fabric**: no `process.env`, ever.
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.
269
-
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
-
272
- ## Stage 7 — Frontend
273
-
274
- Only when `frontend*.json` is present. Target: TanStack Start + Mantine.
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.
277
-
278
- ### Routes
279
-
280
- `frontend-routes[]` → TanStack routes. `path` and `purpose` carry over; `auth` becomes the route guard.
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.
283
-
284
- ### Components — the honest cost
285
-
286
- `frontend-components[].rebuild` is the only field that matters for planning:
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`. |
294
-
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
-
297
- Two things to watch:
298
-
299
- - **Scope `custom-logic` explicitly, per component, before starting the frontend.** If a single component is thousands of lines (a map/finder surface is the classic), it is a project of its own and must be planned as one. "It's just screens" is how frontend rebuilds overrun.
300
- - **Forked twins.** `designFindings` often shows the same custom-logic surface duplicated (a finder and its near-identical sibling). Build it **once**, parameterized. That's a rebuild dividend — say so in the parity report.
301
-
302
- ## Stage 8 — Verify
303
-
304
- Per slice, narrowest first:
305
-
306
- ```bash
307
- pikku fabric validate --json # structural: fix every error and warn
308
- yarn pikku all # codegen + version compliance
309
- yarn tsc --noEmit
310
- ```
311
-
312
- Then run the slice's scenarios. All four green — validate, codegen, `tsc`, scenarios — is what "slice done" means; three of four is a slice you have not finished. **pikku-fabric** owns the loop and what each finding means.
313
-
314
- Never batch. A rebuild verified only at the end gives you an undifferentiated pile of failures with no bisect point, and the whole reason for slicing is that each slice is a checkpoint you can trust.
315
-
316
- New functions with `expose: true` are versioned from the start — `pikku versions` / `pikku release` (**pikku-meta**); you're establishing v1 contracts, not migrating them.
317
-
318
- ## Stage 9 — The parity report (the deliverable)
319
-
320
- Write `<repo>/.knowledge/parity-<domain>.md` per slice, as you finish it. This is a real output, not
321
- bookkeeping: it is the one place a human can answer "is the rebuild done?" without reading the
322
- diff, because it is the only document that holds the blueprint and the new code side by side.
323
-
324
- Per domain:
325
-
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.
328
- - **Decisions taken** — each gate answer, who decided, when. Behaviour that deliberately differs from the legacy app.
329
- - **Now enforced** — invariants that were `enforcedBy: "nothing"` and now have a constraint. The rebuild's actual dividend, in one list.
330
- - **Reconstructed** — anything from `confidence: low`/`medium` or `explicit: false` events. Flag for confirmation against production behaviour.
331
- - **Not verifiable from the blueprint** — what needs real data or a human (volume-dependent races, whether a legacy bug ever fired).
332
- - **Glossary** — legacy name → new name, for every rename including the false friends.
333
-
334
- ## Red flags
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. |
350
-
351
- ## Quick reference
352
-
353
- ```bash
354
- node <archaeology-skill>/scripts/validate.mjs <repo>/.knowledge # Stage 0 — must be 0 errors
355
- # Stage 1 — decisions gate: ask, don't invent
356
- # Stage 2 — clone starter template, then the post-clone cleanup (pikku-build)
357
- # Stage 3 — entities.json -> db/sqlite/NNNN-*.sql ; pikku db migrate
358
- # Stage 4..7 — one domain slice at a time, dependency order
359
- pikku fabric validate --json
360
- yarn pikku all && yarn tsc --noEmit
361
- # Stage 9 — .knowledge/parity-<domain>.md per slice
362
- ```
363
-
364
- ## Relationship to the other skills
365
-
366
- ```
367
- legacy repo → pikku-software-archaeology → .knowledge/ blueprint
368
- └→ pikku-blueprint-to-fabric → Fabric app + parity-*.md
369
- ```
370
-
371
- **pikku-software-archaeology** extracts the facts and validates them. **This skill** builds the
372
- thing, and emits `parity-*.md` so the rebuild can be reviewed against the blueprint rather than
373
- against the legacy code.
374
-
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
- cleanup in Stage 2, use **pikku-build**.