@pikku/skills 0.12.25 → 0.12.27

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.
@@ -0,0 +1,377 @@
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, `fabric.config.json`, 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. Set `projectId`, `production.branch` and the frontend entry in `fabric.config.json` per **pikku-fabric**.
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 semver` (**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, `fabric.config.json`, the validate loop, reading a deployed
376
+ stage — use **pikku-fabric**. For a single feature *after* the rebuild, and for the post-clone
377
+ cleanup in Stage 2, use **pikku-build**.