@pikku/skills 0.12.26 → 0.12.28
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/CHANGELOG.md +41 -0
- package/dist/skills.gen.js +1 -1
- package/package.json +1 -1
- package/skills/pikku-admin-to-fabric/SKILL.md +212 -0
- package/skills/pikku-blueprint-to-fabric/SKILL.md +377 -0
- package/skills/pikku-blueprint-to-fabric/scripts/inventory.mjs +367 -0
- package/skills/pikku-knowledge/SKILL.md +28 -1
- package/skills/pikku-kysely/SKILL.md +107 -3
package/package.json
CHANGED
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: pikku-admin-to-fabric
|
|
3
|
+
description: 'Port a legacy back-office admin (ActiveAdmin, Django admin, Rails Admin, Laravel Nova, Filament) to Fabric admin screens, driven by a `.knowledge/` Product Blueprint. Covers the admin-DSL→Fabric mapping (resources→screens, index/column→tables, filter→query params, scope→query variants, member_action/collection_action→pikkuFuncs, permit_params→input schemas), the "the admin is half your app" audit, and admin-specific permissions. TRIGGER when: porting/rebuilding a legacy app that has a generated/DSL-driven admin, or the user says "port the admin screens" / "implement the admin". DO NOT TRIGGER when: no legacy admin exists (use pikku-fabric to build screens fresh), or the app is being extended rather than ported (use pikku-build).'
|
|
4
|
+
installGroups: [fabric]
|
|
5
|
+
argument-hint: '<path to .knowledge/> [resource to port next]'
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
# Legacy admin → Fabric admin screens
|
|
9
|
+
|
|
10
|
+
## Agent Operating Procedure
|
|
11
|
+
|
|
12
|
+
1. **Count first.** How many commands cite the admin? That number decides whether this is a chore or a third of the project.
|
|
13
|
+
2. **The blueprint already has the commands.** Do not re-derive them from the DSL. Map to them.
|
|
14
|
+
3. **Port the actions before the screens.** A screen with no action behind it is a table; the actions are the product.
|
|
15
|
+
4. **One resource per slice**, same as `pikku-blueprint-to-fabric`. Verify green before the next.
|
|
16
|
+
5. **An admin permission is not a checkbox.** Legacy admins routinely authenticate and do not authorize. Do not port that.
|
|
17
|
+
6. **Record what you did NOT port**, per resource, in the parity report.
|
|
18
|
+
|
|
19
|
+
## The mistake this skill exists to prevent
|
|
20
|
+
|
|
21
|
+
> "It's just the admin — CRUD screens over the same tables. We'll scaffold it at the end."
|
|
22
|
+
|
|
23
|
+
This is wrong in a specific, measurable way, and you can check it in one command
|
|
24
|
+
before you believe anything else in this file:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
node -e "
|
|
28
|
+
const c = require('./.knowledge/commands.json').commands
|
|
29
|
+
const admin = c.filter(x => (x.evidence||[]).some(e => (e.file||'').match(/admin/)))
|
|
30
|
+
console.log(admin.length + ' of ' + c.length + ' commands live in the admin')
|
|
31
|
+
"
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
On a real Rails app (Applause, 39 ActiveAdmin resources, 5,545 lines of DSL) the
|
|
35
|
+
answer was **82 of 187 — 44%**, plus 71 of 223 API surfaces. The admin was not a
|
|
36
|
+
side panel over the customer-facing app. It was nearly **half the application's
|
|
37
|
+
write surface**, and a large share of those commands existed *nowhere else*:
|
|
38
|
+
issue a refund, retrigger a payment, queue a sync, impersonate a user, mark a
|
|
39
|
+
blade returned, reassign a company. There is no customer screen for any of them.
|
|
40
|
+
|
|
41
|
+
So: the admin is not the last 10% of the port. Budget it as what the count says.
|
|
42
|
+
|
|
43
|
+
**Corollary — the admin is where the unguarded capabilities live.** A
|
|
44
|
+
`member_action :impersonate` or `:create_stripe_refund` is a command that moves
|
|
45
|
+
money or identity, defended in legacy by nothing more than "you reached an
|
|
46
|
+
`/admin` URL". Every one of these needs a real `pikkuPermission` in the rebuild,
|
|
47
|
+
and writing them is the point of the port, not overhead on top of it.
|
|
48
|
+
|
|
49
|
+
## Stage 0 — Preflight
|
|
50
|
+
|
|
51
|
+
- The `.knowledge/` blueprint must exist and validate (`0 error(s)`). If not, run
|
|
52
|
+
**pikku-software-archaeology** first. This skill maps to the blueprint; it does
|
|
53
|
+
not parse Ruby.
|
|
54
|
+
- Read `parity-*.md` for the domains you are about to touch. Renames decided in an
|
|
55
|
+
earlier slice (a `tenant` that became a `Market`, a `membership_level` that
|
|
56
|
+
became a `certification_level`) are binding here. An admin screen that reintroduces
|
|
57
|
+
the old word undoes the decision.
|
|
58
|
+
- Run the count above and say the number out loud in your plan.
|
|
59
|
+
|
|
60
|
+
## Stage 1 — Inventory the DSL
|
|
61
|
+
|
|
62
|
+
Every generated admin is the same six ideas under different syntax. Inventory
|
|
63
|
+
them, do not read them line by line:
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
# ActiveAdmin
|
|
67
|
+
grep -rhoE "^\s{0,4}(index|show|form|filter|scope|action_item|member_action|collection_action|batch_action|permit_params|csv|sidebar|panel|actions)\b" app/admin/*.rb | sort | uniq -c | sort -rn
|
|
68
|
+
grep -rhoE "(member_action|collection_action) :[a-z_]+" app/admin/*.rb | sort -u
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
| Legacy | Also called | Becomes in Fabric |
|
|
72
|
+
|---|---|---|
|
|
73
|
+
| `ActiveAdmin.register X` / `class XAdmin` | resource, ModelAdmin, Nova Resource | one TanStack route + one screen |
|
|
74
|
+
| `index do … column :x` | `list_display`, `columns()` | a Mantine `Table`/`DataTable`, columns from the query's return type |
|
|
75
|
+
| `filter :x` | `list_filter`, `searchable` | typed input fields on the list query |
|
|
76
|
+
| `scope :active` | `get_queryset` variants, `Nova::Filters` | a named variant of the list query — NOT a new function per scope |
|
|
77
|
+
| `form do f.input …` | `fields()`, `fieldsets` | a Mantine form; inputs from the command's zod input schema |
|
|
78
|
+
| `permit_params` | `fields`, `$fillable` | you already have this: it is the command's input schema. Cross-check, don't re-derive. |
|
|
79
|
+
| `member_action :foo` | custom action, `Nova::Actions` | **a `pikkuFunc`** — almost always already in `commands.json` |
|
|
80
|
+
| `collection_action :foo` | bulk action | a `pikkuFunc` taking a set |
|
|
81
|
+
| `batch_action` | admin action | a `pikkuFunc` taking ids[] |
|
|
82
|
+
| `csv do … end` | export | a readonly func returning rows; render client-side |
|
|
83
|
+
| `panel`/`sidebar` | inlines, `relations` | a section on the show screen, fed by a related query |
|
|
84
|
+
| `action_item` | — | a button. It is not a capability; find the action it calls. |
|
|
85
|
+
|
|
86
|
+
**`action_item` vs `member_action` is the distinction that matters.** An
|
|
87
|
+
`action_item` is a *button*; a `member_action` is a *capability*. Legacy files
|
|
88
|
+
pair them, and a fast reader counts the buttons. Count the capabilities.
|
|
89
|
+
|
|
90
|
+
## Stage 2 — Map actions to blueprint commands (do this before any UI)
|
|
91
|
+
|
|
92
|
+
For each `member_action`/`collection_action`, find its command in
|
|
93
|
+
`commands.json`. Three outcomes, and the third is the valuable one:
|
|
94
|
+
|
|
95
|
+
1. **Found** — the archaeology already lifted it (`ArchiveProduct`,
|
|
96
|
+
`CancelInvoice`). Wire the screen to it. Nothing to build.
|
|
97
|
+
2. **Found under a different name** — the blueprint names concepts in domain
|
|
98
|
+
language, the DSL names them after routes. `member_action :rerun` may be
|
|
99
|
+
`RetryWebhookDelivery`. Match on behaviour, not spelling. **Use the blueprint's
|
|
100
|
+
name.**
|
|
101
|
+
3. **Not found** — stop. Either the archaeology missed a command (fix
|
|
102
|
+
`.knowledge/`, do not paper over it here) or the action is dead code. Both are
|
|
103
|
+
findings. Do not quietly invent a command to fill the gap: a command with no
|
|
104
|
+
blueprint entry has no evidence, no policy and no actor, and you will not
|
|
105
|
+
notice which.
|
|
106
|
+
|
|
107
|
+
Two legacy shapes to expect and *not* reproduce:
|
|
108
|
+
|
|
109
|
+
- **`*_form` + `*` action pairs** (`create_stripe_refund_form` +
|
|
110
|
+
`create_stripe_refund`, `issue_payment_form` + `issue_payment`). The `_form` half
|
|
111
|
+
is a GET that renders a modal — it is a *screen*, not a capability. It collapses
|
|
112
|
+
into the screen; only the second half is a `pikkuFunc`. Porting both doubles your
|
|
113
|
+
command count with phantoms.
|
|
114
|
+
- **A `member_action` that only redirects** to another action. That is routing.
|
|
115
|
+
- **An action disabled in the production environment is not a live capability.**
|
|
116
|
+
Grep the environment guards before porting:
|
|
117
|
+
```bash
|
|
118
|
+
grep -rn "env.production?\|env\.development?\|ENV\[" app/admin/*.rb
|
|
119
|
+
```
|
|
120
|
+
On Applause, both halves of `create_stripe_refund` open with
|
|
121
|
+
`return redirect_to … if Rails.env.production?` — the admin refund screen has
|
|
122
|
+
never run in production, and refunds are actually issued in the Stripe dashboard.
|
|
123
|
+
Porting it faithfully would ship a prominent button for a capability the business
|
|
124
|
+
does not use through this app, and quietly move refunds into a surface nobody has
|
|
125
|
+
ever tested. Whether it should now exist is a **product decision**, not a port.
|
|
126
|
+
Check the guard is on the *mutating* half too: if the form is blocked and the POST
|
|
127
|
+
is not, you have found a hole rather than a dead feature.
|
|
128
|
+
|
|
129
|
+
## Stage 3 — Permissions (the part legacy skipped)
|
|
130
|
+
|
|
131
|
+
Generated admins authenticate and then trust. The whole admin sits behind one
|
|
132
|
+
"is an admin" check, and every action inside it is equally reachable — refunds,
|
|
133
|
+
impersonation and editing an FAQ all guarded identically.
|
|
134
|
+
|
|
135
|
+
- Read `policies.json` for the real rule per command. If the blueprint says the
|
|
136
|
+
policy is `enforcedBy: nothing`, that is a **gap you are now closing**, not a
|
|
137
|
+
behaviour to port.
|
|
138
|
+
- With Better Auth's `admin()` plugin, `user.role` is the platform role and
|
|
139
|
+
`session.impersonated_by` is set during impersonation. Both are yours already.
|
|
140
|
+
- **Money and identity actions deserve their own permission**, not the blanket one.
|
|
141
|
+
If the blueprint offers no rule, that is a `decisionsNeeded` entry — ask, do not
|
|
142
|
+
invent.
|
|
143
|
+
- **Impersonation:** Better Auth's `impersonateUser`/`stopImpersonating` replace the
|
|
144
|
+
hand-rolled version. If the legacy audit table recorded only the *start* of an
|
|
145
|
+
impersonation (no `ended_at`, no session id), do not port it — the Fabric audit
|
|
146
|
+
table already answers "what did they do while impersonating", which was the whole
|
|
147
|
+
question it failed to answer.
|
|
148
|
+
|
|
149
|
+
## Stage 4 — Screens
|
|
150
|
+
|
|
151
|
+
- The list query is `pikkuSessionlessFunc` + `readonly: true`; filters and scopes
|
|
152
|
+
are **input fields on one function**, not one function per scope. Legacy needs a
|
|
153
|
+
method per scope because the DSL has no parameters. You do not.
|
|
154
|
+
- Columns come from the query's return type. If a column exists in the DSL but not
|
|
155
|
+
in the type, the DSL was computing it in Ruby per row — that is an N+1 wearing a
|
|
156
|
+
column, and it belongs in the query.
|
|
157
|
+
- Reuse the app's Mantine theme. An admin styled differently from the product is
|
|
158
|
+
how design systems fork.
|
|
159
|
+
- **Server-computed charts** (chartkick/groupdate and friends) do not port. The
|
|
160
|
+
aggregation becomes a real query; the plot becomes a chart component. This is the
|
|
161
|
+
one genuinely expensive screen in most admins — cost it separately.
|
|
162
|
+
- **Drag-and-drop reordering** (`acts_as_list`, sortable tables) is custom logic,
|
|
163
|
+
not a table. Port the position semantics deliberately.
|
|
164
|
+
|
|
165
|
+
## Stage 5 — Verify and report
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
pikku all && pikku fabric validate --json
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Per resource, the parity report records:
|
|
172
|
+
|
|
173
|
+
- **Ported** — screens + which blueprint commands back them.
|
|
174
|
+
- **Deliberately not ported** — with the reason. Expect: `*_form` halves, dead
|
|
175
|
+
actions, single-member enums, screens over dropped tables.
|
|
176
|
+
- **Now authorized** — every action that was guarded by "reached an /admin URL"
|
|
177
|
+
and now has a real permission. This is the port's dividend; name it.
|
|
178
|
+
- **Still open** — actions whose rule the blueprint could not settle.
|
|
179
|
+
|
|
180
|
+
## Red Flags
|
|
181
|
+
|
|
182
|
+
| Thought | Reality |
|
|
183
|
+
|---|---|
|
|
184
|
+
| "The admin is just CRUD, scaffold it last" | Run the count. It was 44% of commands on a real app, and those commands exist nowhere else. |
|
|
185
|
+
| "I'll read the DSL and write the commands" | The blueprint already has them, with evidence, actors and policies. Map; don't re-derive. |
|
|
186
|
+
| "One function per scope" | A scope is a filter argument. The DSL needed a method because it has no parameters. |
|
|
187
|
+
| "`action_item` count = capability count" | Buttons aren't capabilities. Count `member_action`/`collection_action`. |
|
|
188
|
+
| "Port `create_stripe_refund_form` too" | It is a GET that renders a modal. It is a screen. Only the non-`_form` half is a command. |
|
|
189
|
+
| "Admins are admins; one permission is fine" | That is the legacy bug. Refunds and FAQ edits are not the same risk. |
|
|
190
|
+
| "The admin action isn't in commands.json, I'll add it" | Stop. Either the archaeology missed it (fix the blueprint) or it is dead. Both are findings. |
|
|
191
|
+
| "I'll restyle the admin, it's internal" | An admin off the product's theme is how a design system forks. |
|
|
192
|
+
|
|
193
|
+
## Quick Reference
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
# 1. how much of the app is actually the admin?
|
|
197
|
+
node -e "const c=require('./.knowledge/commands.json').commands;console.log(c.filter(x=>(x.evidence||[]).some(e=>(e.file||'').match(/admin/))).length+'/'+c.length)"
|
|
198
|
+
|
|
199
|
+
# 2. inventory the capabilities (not the buttons)
|
|
200
|
+
grep -rhoE "(member_action|collection_action) :[a-z_]+" app/admin/*.rb | sort -u
|
|
201
|
+
|
|
202
|
+
# 3. per resource: map actions -> commands.json, then build the screen
|
|
203
|
+
# 4. verify
|
|
204
|
+
pikku all && pikku fabric validate --json
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
## Related skills
|
|
208
|
+
|
|
209
|
+
- **pikku-software-archaeology** — produces the `.knowledge/` blueprint this needs.
|
|
210
|
+
- **pikku-blueprint-to-fabric** — the parent port; run this per-domain alongside it.
|
|
211
|
+
- **pikku-auth** — roles, ban and impersonation, and the scopes that gate them.
|
|
212
|
+
- **pikku-fabric** — screens, theme, Mantine conventions.
|
|
@@ -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**.
|