@pikku/skills 0.12.54 → 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 (54) 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-architect/SKILL.md +1 -1
  6. package/skills/pikku-architect/references/plan-defects.md +7 -7
  7. package/skills/pikku-auth/references/better-auth.md +92 -96
  8. package/skills/pikku-build/SKILL.md +9 -9
  9. package/skills/pikku-build/references/app.md +56 -61
  10. package/skills/pikku-build/references/design.md +14 -14
  11. package/skills/pikku-build/references/feature.md +3 -3
  12. package/skills/pikku-build/references/mocks.md +6 -0
  13. package/skills/pikku-build/references/multi-app.md +16 -16
  14. package/skills/pikku-build/references/openapi.md +34 -34
  15. package/skills/pikku-build/references/platform.md +4 -5
  16. package/skills/pikku-build/references/post-clone.md +1 -1
  17. package/skills/pikku-build/references/quick.md +63 -36
  18. package/skills/pikku-build/references/scenarios.md +17 -16
  19. package/skills/pikku-build/references/ship.md +14 -16
  20. package/skills/pikku-build/references/theming.md +2 -2
  21. package/skills/pikku-changes/SKILL.md +2 -5
  22. package/skills/pikku-concepts/SKILL.md +26 -27
  23. package/skills/pikku-concepts/references/language.md +6 -6
  24. package/skills/pikku-deploy/references/nextjs.md +1 -1
  25. package/skills/pikku-guide/SKILL.md +18 -17
  26. package/skills/pikku-i18n/references/messages.md +5 -6
  27. package/skills/pikku-knowledge/SKILL.md +3 -3
  28. package/skills/pikku-meta/references/audit.md +1 -1
  29. package/skills/pikku-permissions/SKILL.md +1 -1
  30. package/skills/pikku-react/SKILL.md +18 -20
  31. package/skills/pikku-react/references/client.md +2 -37
  32. package/skills/pikku-report/SKILL.md +10 -10
  33. package/skills/pikku-scenario/SKILL.md +63 -66
  34. package/skills/pikku-scenario/references/browser.md +5 -5
  35. package/skills/pikku-scenario/references/coverage.md +4 -4
  36. package/skills/pikku-scenario/references/steps.md +31 -30
  37. package/skills/pikku-scenario/references/{persona-run.md → virtual-user-run.md} +38 -43
  38. package/skills/pikku-scenario/references/virtual-users.md +114 -0
  39. package/skills/pikku-seo/SKILL.md +1 -1
  40. package/skills/pikku-services/references/audit-wire-service.md +2 -2
  41. package/skills/pikku-services/references/audit.md +1 -2
  42. package/skills/pikku-services/references/services.md +1 -1
  43. package/skills/pikku-software-archaeology/SKILL.md +13 -13
  44. package/skills/pikku-software-archaeology/references/blueprint.schema.json +9 -9
  45. package/skills/pikku-theme/SKILL.md +1 -1
  46. package/skills/pikku-workflow/SKILL.md +1 -1
  47. package/skills/pikku-admin-to-fabric/SKILL.md +0 -198
  48. package/skills/pikku-blueprint-to-fabric/SKILL.md +0 -362
  49. package/skills/pikku-blueprint-to-fabric/scripts/inventory.mjs +0 -367
  50. package/skills/pikku-fabric/SKILL.md +0 -331
  51. package/skills/pikku-fabric/references/database.md +0 -128
  52. package/skills/pikku-fabric/references/debugging.md +0 -114
  53. package/skills/pikku-fabric/references/deploy.md +0 -179
  54. package/skills/pikku-scenario/references/personas.md +0 -123
@@ -1,17 +1,17 @@
1
- # Running a persona as a virtual user
1
+ # Running a virtual user against a stage
2
2
 
3
3
  ## Contents
4
4
 
5
5
  - [The shape of a run](#the-shape-of-a-run)
6
6
  - [The dispositions](#the-dispositions)
7
- - [Credentials, and which one wins](#credentials-and-which-one-wins)
7
+ - [The key](#the-key)
8
8
  - [Production is opt-in, twice](#production-is-opt-in-twice)
9
9
  - [The role check happens before the first step](#the-role-check-happens-before-the-first-step)
10
10
  - [The other subcommands](#the-other-subcommands)
11
11
  - [What NOT to do](#what-not-to-do)
12
12
 
13
- `pikku persona run <environment> <persona>` signs a declared persona in over the
14
- app's real auth and works the API in character, driven by a model. A persona
13
+ `pikku virtual-user run <environment> <virtualUserId>` signs a declared virtual user in over the
14
+ app's real auth and works the API in character, driven by a model. A virtual user
15
15
  while running **is** the virtual user — there is no second declaration for it.
16
16
 
17
17
  **It is not a test runner.** It asserts nothing, and a green run proves nothing:
@@ -20,33 +20,33 @@ not with this seed". Findings set exit code 1, so a run can gate a pipeline;
20
20
  giving up on a goal does not, because that is a user being a user.
21
21
 
22
22
  Everything it needs is already in the project — the catalogue is the function
23
- meta, the intents are the scenarios' own prose, the identity is the persona
23
+ meta, the intents are the scenarios' own prose, the identity is the virtual user
24
24
  signing in, the scopes come from their declared roles. The only new input is
25
25
  which person to be.
26
26
 
27
- Declaring personas — persona versus actor, `definePersonas`, materialised
28
- actors — is in the skill itself, under **Personas and actors**. This is the
27
+ Declaring virtual users — virtual user versus virtual user, `defineVirtualUsers`, materialised
28
+ virtual users — is in the skill itself, under **Virtual users and virtual users**. This is the
29
29
  running half.
30
30
 
31
31
  ## The shape of a run
32
32
 
33
33
  ```bash
34
- SCENARIO_ACTOR_SECRET=… pikku persona run local shopper
35
- SCENARIO_ACTOR_SECRET=… pikku persona run local shopper -d careless --seed 42
36
- SCENARIO_ACTOR_SECRET=… pikku persona run staging auditor \
34
+ pikku virtual-user run local shopper
35
+ pikku virtual-user run local shopper -d careless --seed 42
36
+ pikku virtual-user run staging auditor \
37
37
  --goals "reconcile the order totals" --steps 80 --out runs/auditor.json
38
38
  ```
39
39
 
40
40
  Both arguments are required positionals: the environment key from
41
- `environments`, then the persona id. A run needs a model — `--model`, or
41
+ `environments`, then the virtual user id. A run needs a model — `--model`, or
42
42
  `scenarios.model` in `pikku.config.json` — and an AI provider in the
43
43
  environment (`OPENAI_BASE_URL` + `OPENAI_API_KEY`, or `LITELLM_PROXY_URL` +
44
44
  `LITELLM_API_KEY`).
45
45
 
46
46
  | Flag | Effect |
47
47
  | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
48
- | `--disposition` / `-d` | How they behave. Overrides the persona's own |
49
- | `--goals` | Comma-separated, in your words — run _alongside_ the persona's own and the ones derived from scenarios |
48
+ | `--disposition` / `-d` | How they behave. Overrides the virtual user's own |
49
+ | `--goals` | Comma-separated, in your words — run _alongside_ the virtual user's own and the ones derived from scenarios |
50
50
  | `--steps` | Model turns before it stops (default 40) |
51
51
  | `--mutations` | Non-read calls before it stops |
52
52
  | `--duration` | Wall clock before it stops, e.g. `30m` |
@@ -60,9 +60,9 @@ environment (`OPENAI_BASE_URL` + `OPENAI_API_KEY`, or `LITELLM_PROXY_URL` +
60
60
  ## The dispositions
61
61
 
62
62
  A disposition is a bundle of instructions and mechanical dials (move weights,
63
- temperature, repeat and re-read rates). `tuning` on the persona adjusts those
63
+ temperature, repeat and re-read rates). `tuning` on the virtual user adjusts those
64
64
  dials without replacing the character — a tuned `careless` user is still
65
- careless. Passing `--disposition` drops the persona's `tuning`, because you
65
+ careless. Passing `--disposition` drops the virtual user's `tuning`, because you
66
66
  asked to run them differently rather than to bend their dials into another
67
67
  shape.
68
68
 
@@ -76,33 +76,29 @@ shape.
76
76
  | `adversarial` | Probing whether the boundaries are enforced. Inverted oracle — a 2xx from something it should not reach is the finding |
77
77
  | `accountable` | Doing the job for real. The **only** disposition production accepts |
78
78
 
79
- ## Credentials, and which one wins
79
+ ## The key
80
80
 
81
- Three variables, checked in this order. None of them belongs in
82
- `pikku.config.json`.
81
+ A run signs one short-lived statement per virtual user and presents it to the
82
+ app. The key comes from, in this order:
83
83
 
84
- 1. **`FABRIC_OPERATOR_TOKEN`** — what a deployed stage accepts. Asymmetric, and
85
- it needs no account the target would not otherwise have, so it wins over the
86
- other two when both are present.
87
- 2. **`PIKKU_PERSONA_SECRETS`** — `id=secret,id=secret`, already-derived
88
- per-persona credentials. Hand a run only the personas it should be able to
89
- be; asking for one outside the list is refused by name rather than falling
90
- through to the root. Mint them with `pikku persona secret [personas...]` —
91
- naming none mints all.
92
- 3. **`SCENARIO_ACTOR_SECRET`** — the root secret, which derives every persona's
93
- credential and is therefore entitled to all of them. Only `pikku dev` serves
94
- the endpoint it opens.
84
+ 1. **`VIRTUAL_USER_SIGNING_KEY`** in the environment — a deployed stage's
85
+ private key, held by whoever deploys it.
86
+ 2. **The file `pikku dev` wrote**, `.pikku-runtime/virtual-user/signing.key` —
87
+ so a local run needs nothing set.
88
+
89
+ Neither belongs in `pikku.config.json`. The app only ever holds the public half,
90
+ `VIRTUAL_USER_VERIFY_KEY`.
95
91
 
96
92
  ## Production is opt-in, twice
97
93
 
98
- A persona's `environments` omitted means every configured environment **except**
94
+ A virtual user's `environments` omitted means every configured environment **except**
99
95
  those flagged `production: true` — nothing reaches production by being
100
96
  forgotten. Naming one requires `disposition: 'accountable'`.
101
97
 
102
98
  That rule is checked twice on purpose: the inspector checks the declaration at
103
- build time, and sign-in re-checks the **effective** disposition — the persona's
99
+ build time, and sign-in re-checks the **effective** disposition — the virtual user's
104
100
  own, or whatever `--disposition` replaced it with — before the run starts. So
105
- `--disposition adversarial` cannot point an accountable persona at production.
101
+ `--disposition adversarial` cannot point an accountable virtual user at production.
106
102
  The build check trusts the file; the run check does not trust which artifact got
107
103
  deployed.
108
104
 
@@ -110,7 +106,7 @@ deployed.
110
106
  true` is a label a person wrote in `pikku.config.json`; nothing can tell from a
111
107
  url whether real customers are behind it. `--api-url` replaces the url and keeps
112
108
  the classification, so a non-production environment repointed at a production
113
- host is still treated as non-production, and an adversarial persona will happily
109
+ host is still treated as non-production, and an adversarial virtual user will happily
114
110
  run against it. The flag is for a target that only exists at run time — a
115
111
  freshly provisioned sandbox. Point it anywhere else and the guard above is not
116
112
  protecting you.
@@ -118,8 +114,8 @@ protecting you.
118
114
  ## The role check happens before the first step
119
115
 
120
116
  A run reads its own roles back from the stage and compares them to what the
121
- persona declared. It refuses on a mismatch, before anything runs — findings
122
- from a persona whose roles drifted are about the seed, and reading them as
117
+ virtual user declared. It refuses on a mismatch, before anything runs — findings
118
+ from a virtual user whose roles drifted are about the seed, and reading them as
123
119
  product bugs is how a whole run gets thrown away. A stage that reports no roles
124
120
  warns and runs unverified. `--skip-role-check` is for a target whose auth
125
121
  reports roles somewhere pikku cannot read; findings from such a run may be seed
@@ -127,21 +123,20 @@ drift.
127
123
 
128
124
  ## The other subcommands
129
125
 
130
- | Command | What it answers |
131
- | ------------------------------------ | ------------------------------------------------------------------------------------------ |
132
- | `pikku persona list` | Who is declared — who each one is, what they may do, what they want |
133
- | `pikku persona sync <environment>` | Which personas that environment will provision, with which roles, and why any were skipped |
134
- | `pikku persona secret [personas...]` | Mint per-persona credentials from the root secret |
126
+ | Command | What it answers |
127
+ | --------------------------------------- | ----------------------------------------------------------------------------------------------- |
128
+ | `pikku virtual-user list` | Who is declared — who each one is, what they may do, what they want |
129
+ | `pikku virtual-user sync <environment>` | Which virtual users that environment will provision, with which roles, and why any were skipped |
135
130
 
136
131
  `sync` **reports**; it does not provision. The CLI has no connection to a
137
132
  deployed environment's database, so the provisioning happens in the deployment —
138
- pass the generated personas to `provisionPersonas` from `@pikku/better-auth`.
133
+ pass the generated virtual users to `provisionVirtualUsers` from `@pikku/better-auth`.
139
134
 
140
135
  ## What NOT to do
141
136
 
142
137
  - **Do not treat a clean run as a pass.** Nothing was asserted. Use scenarios
143
138
  for the things that must hold.
144
- - **Do not run a persona declared `runnable: false`**, or one whose `account`
139
+ - **Do not run a virtual user declared `runnable: false`**, or one whose `account`
145
140
  names a provider. The first is someone who exists to be acted upon — running
146
141
  her races the scenario that bans her — and the second needs a human at a
147
142
  consent screen. Both are refused before sign-in rather than partway through.
@@ -154,5 +149,5 @@ pass the generated personas to `provisionPersonas` from `@pikku/better-auth`.
154
149
  ones the app marked as needing a human because they spend money.
155
150
  - **Do not read a finding from a run started with `--skip-role-check` as a
156
151
  product bug** until the roles are confirmed some other way.
157
- - **Do not expect `--goals` to replace the persona's goals.** They are appended;
152
+ - **Do not expect `--goals` to replace the virtual user's goals.** They are appended;
158
153
  a run that replaces Susan's goals is not Susan.
@@ -0,0 +1,114 @@
1
+ # Virtual users and virtual users
2
+
3
+ ## Contents
4
+
5
+ - [Declaring virtual users in TypeScript](#declaring-virtual-users-in-typescript)
6
+ - [Virtual users that call a third-party API](#virtual-users-that-call-a-third-party-api)
7
+ - [The same virtual users sign a human in](#the-same-virtual-users-sign-a-human-in)
8
+
9
+ A **virtual user** is a person your product is for; an **virtual user** is one body that signs in as them. Every entry materialises exactly one virtual user, so `virtualUsers.<id>` exists for each declared virtual user and there is no second way to declare a login.
10
+
11
+ - Two people of the same kind are two entries, not one virtual user with two logins — "you see yours, not theirs" is only testable with two customers.
12
+ - Never write an email address: each is derived from the virtual user id and `scenarios.emailDomain`, and a hand-written one signs in as somebody who was never created.
13
+ - `roles` is typechecked against `defineSystemRole`; an undeclared role is a build error.
14
+ - A person who is only ever acted _upon_ — the account an admin bans — sets `runnable: false`: declared and seeded, never signed in, because a run as them would race the scenario that acts on them.
15
+ - A virtual user holds only what is true of that kind of person for the app's whole lifetime (`name`, `jobTitle`, `description`, `personality`, `roles`, `goals`, `disposition`). What someone is trying to get done, and the circumstances they are doing it in, belong to the **scenario**, not to them.
16
+
17
+ ## Declaring virtual users in TypeScript
18
+
19
+ There may be **one `defineVirtualUsers` call in the whole codebase** — one place to
20
+ read the set from, one place to add to it. A second anywhere, including in the
21
+ same file, is a critical. Generated files are exempt and never claim the slot.
22
+
23
+ > [!WARNING]
24
+ > The declaration is **read from source, never evaluated** — the CLI writes it
25
+ > to JSON that a deployed stage carries without the app. So every value has to
26
+ > be statically knowable, and a value that is not comes out as `undefined`
27
+ > rather than as an error. Only `name` is checked, so a computed `personality`,
28
+ > `jobTitle` or `description` is dropped in silence and the virtual user runs with a
29
+ > blank temperament.
30
+
31
+ What that admits and what it does not:
32
+
33
+ ```typescript
34
+ personality: 'Wound up and short with it.' // read
35
+ personality: `Wound up and short with it.
36
+ Says what she wants in a few blunt words.` // read — no ${} in it
37
+ personality: 'Wound up. ' + 'Short with it.' // dropped, silently
38
+ personality: TEMPERAMENTS.impatient // dropped, silently
39
+ ```
40
+
41
+ A no-substitution template literal is a string literal as far as the reader is
42
+ concerned, so it is the way to write a long personality across several lines —
43
+ not a concatenation, and not a `prettier-ignore`d single line. Its newlines and
44
+ leading indentation are kept verbatim and reach the model that way, which is
45
+ harmless but worth knowing before you align it to the surrounding code.
46
+
47
+ One more thing worth knowing before writing a rich virtual user: **`virtualUser.converse`
48
+ builds its prompt from `name`, `jobTitle`, `personality` and the scenario's
49
+ `task` only.** Fields like `disposition`, `goals` and `roles` are read and
50
+ stored, and the console shows them, but they do not reach the conversing
51
+ virtual user's instructions. Anything that must shape how someone talks belongs in
52
+ `personality` or in the task.
53
+
54
+ A project that never declares a virtual user keeps working: a scenario that names no virtual user needs none.
55
+
56
+ - `environments.<name>.apiUrl` is required. `signInPath` defaults to `/auth/sign-in/virtual-user`, `rpcPath` to `/rpc`.
57
+ - **The signing key never goes in `pikku.config.json`.** `pikku dev` creates one under `.pikku-runtime/virtual-user/`; a deployed stage's deployer holds `VIRTUAL_USER_SIGNING_KEY`. `pikku scenario run` throws without a key.
58
+
59
+ ## Virtual users that call a third-party API
60
+
61
+ An addon generated from an upstream API (Dolibarr, a CRM, a calendar) calls it
62
+ with the signed-in user's own credential. A virtual user has none until one is stored
63
+ for it, so every step that reaches the addon fails with `No <X> session`. Give
64
+ the `virtualUser` plugin a `credentials` option, and each virtual user carries its upstream
65
+ credential into every session:
66
+
67
+ - `names: ['dolibarr']` — the credentials to carry
68
+ - `store: (name, value, userId) => credentialService.set(name, value, userId)`
69
+ - `remove: (name, userId) => credentialService.delete(name, userId)`
70
+
71
+ At each virtual user sign-in the plugin reads `VIRTUAL_USER_CREDENTIAL_<VIRTUAL_USER>_<NAME>` —
72
+ `dan` + `dolibarr` is `VIRTUAL_USER_CREDENTIAL_DAN_DOLIBARR` — and stores it with
73
+ `credentialService.set` for that virtual user. A bare value is stored as `{ token }`
74
+ (what a delegated or bearer credential holds); a JSON object is stored as-is,
75
+ e.g. `{"apiKey":"…"}` for an API-key credential. Unset means the virtual user has no
76
+ upstream credential: `remove` drops one stored at an earlier sign-in.
77
+
78
+ - **Values live in `.env` (or CI secrets), never in `virtual-users.ts` or code.**
79
+ Use a dedicated upstream test account per virtual user, not a real person's.
80
+ - `read` defaults to `process.env`; a Worker passes `(key) => variables.get(key)`.
81
+ - A malformed JSON value refuses the sign-in with a 500 that names the variable.
82
+ - A delegated token expires upstream like any other; re-signing the virtual user
83
+ in re-stores whatever the variable holds now.
84
+
85
+ ## The same virtual users sign a human in
86
+
87
+ Declared virtual users are not only for automated runs. `signInPath` is Better Auth's
88
+ `virtualUser` plugin (see `pikku-auth`, a separate install), which any caller can post to — so the
89
+ frontend can sign in as the **same** list, and an
90
+ app can be reviewed as each kind of user without anyone knowing a seed password.
91
+
92
+ It holds no credential. A client app that wants one-click sign-in posts only a
93
+ virtual user id to `/auth/sign-in/switch-virtual-user` itself; the server
94
+ resolves the address. One server piece serves both, in the auth config:
95
+
96
+ ```ts snippet:virtualUserSignIn
97
+
98
+ ```
99
+
100
+ It is always open under `pikku dev`. A deployed stage needs virtual user
101
+ sign-in opted in and an explicit `virtualUserSignIn.allowed`; production never
102
+ has the opt-in, so it lists nobody and refuses every virtual user sign-in.
103
+
104
+ If the list is empty, it is one of two things, and neither errors:
105
+
106
+ - **The gate is closed.** On a deployed stage that is the gate doing its job —
107
+ check the opt-in. Locally, check the virtual users declare an `email` (via
108
+ `scenarios.emailDomain`) and are not `runnable: false`.
109
+ - **`virtualUserSignIn` is missing, or the frontend calls another API.** A dev
110
+ proxy (`VITE_API_PROXY`, default `http://localhost:3000`) that points at
111
+ another project's API lists that project's virtual users, or none.
112
+
113
+ When the list has virtual users but a sign-in 404s, that virtual user has no `email`
114
+ or is `runnable: false`.
@@ -44,7 +44,7 @@ gate) — write real copy for THIS app, in the app's voice.
44
44
  - **Title**: unique per page, 50–60 characters, the page's primary topic first,
45
45
  brand at the end (`Topic — AppName`). The template's `__APP_TITLE__` placeholder
46
46
  (in `src/app-meta.ts` and `messages/en.json`) is filled with the app title at scaffold time
47
- by `fabric-new-app`; it must never survive in any page's title.
47
+ by `cloud-new-app`; it must never survive in any page's title.
48
48
  - **Description**: unique per page, 150–160 characters, states the concrete
49
49
  value of the page in plain language — a reason to click, not a keyword list.
50
50
  - **Dynamic public pages** (e.g. a public detail page) build both from loader
@@ -1,6 +1,6 @@
1
1
  # Audit Wire Service
2
2
 
3
- `createInvocationAudit` creates a per-request `InvocationAuditLog` that buffers audit events in memory and flushes them as a batch when the function-runner calls `closeWireServices` at the end of the request. If `singletonServices.audit` is not configured (local dev without Fabric), it returns a no-op `DisabledInvocationAudit` — no crash, events are silently dropped.
3
+ `createInvocationAudit` creates a per-request `InvocationAuditLog` that buffers audit events in memory and flushes them as a batch when the function-runner calls `closeWireServices` at the end of the request. If `singletonServices.audit` is not configured (local dev without a hosting platform), it returns a no-op `DisabledInvocationAudit` — no crash, events are silently dropped.
4
4
 
5
5
  Pair with `createAuditedKysely` to auto-capture every Kysely query as an audit event.
6
6
 
@@ -38,4 +38,4 @@ const deleteUser = pikkuFunc({
38
38
 
39
39
  `closeWireServices` (called automatically by the function-runner) invokes `audit.close()` → `singletonServices.audit.write(batch)` → platform-specific flush (e.g. CF Queue, libsql INSERT). No manual flushing needed.
40
40
 
41
- > **Fabric note:** Fabric provisions the audit queue and consumer worker automatically. The audit table schema is in `db/sqlite/0003-audit.sql` (starter-template). Run `pikku fabric validate` to confirm the migration is in place.
41
+ > **Note:** a hosting platform may provision the audit queue and consumer worker for you. The audit table schema is in `db/sqlite/0003-audit.sql` (starter-template). Run `pikku validate` to confirm the migration is in place.
@@ -10,7 +10,6 @@
10
10
  - [AuditEvent shape](#auditevent-shape)
11
11
  - [Do / Don't](#do-dont)
12
12
 
13
-
14
13
  ## Mental model — two layers
15
14
 
16
15
  - **`audit` (singleton `AuditService`)** — the durable **sink**. Write-only: `audit(event)` + optional `write(batch)`. Defaults to `NoopAuditService` (discards). Swap in a real sink to persist (see Sinks).
@@ -188,7 +187,7 @@ type AuditEvent = {
188
187
 
189
188
  `auditLog.write()` takes `Omit<AuditEvent, 'occurredAt'>` — you only supply `type`, `source`, and `metadata` (and `userIdentity` if overriding the session default).
190
189
 
191
- `userIdentity` is filled from the wire's session plus its `pikkuUserId`, and is left off entirely when all three are absent — which is what makes a cron or system invocation land with a null actor rather than an empty object.
190
+ `userIdentity` is filled from the wire's session plus its `pikkuUserId`, and is left off entirely when all three are absent — which is what makes a cron or system invocation land with a null virtual user rather than an empty object.
192
191
 
193
192
  ## Do / Don't
194
193
 
@@ -210,7 +210,7 @@ what makes the split real.
210
210
 
211
211
  ### Audit Wire Service
212
212
 
213
- `createInvocationAudit` + `createAuditedKysely` add per-request audit buffering that flushes on request close (no-op if `audit` is unconfigured). For the full pattern, no-op behavior, custom-event usage, and Fabric notes, read `references/audit-wire-service.md`.
213
+ `createInvocationAudit` + `createAuditedKysely` add per-request audit buffering that flushes on request close (no-op if `audit` is unconfigured). For the full pattern, no-op behavior, custom-event usage, and hosting notes, read `references/audit-wire-service.md`.
214
214
 
215
215
  ### Built-in Services
216
216
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: pikku-software-archaeology
3
- description: 'Use when reverse-engineering an existing repository into a `.knowledge/` Product Blueprint — recovering what product an undocumented or organically-grown codebase implements so it can be rebuilt cleanly (e.g. as a Pikku app) — and when turning that blueprint into a plain-language second opinion for the non-technical owner of the app. TRIGGER when: user says "extract a blueprint", "reverse engineer this app", "what does this codebase actually do as a product", "prepare this repo for a rewrite/migration", points at a legacy repo (any language) and asks for its domains, workflows, business rules or a rebuild plan, or asks "explain how my app works" / "what would you do differently" / "is this built well?" for a founder, PM or operator. Not the `knowledge/` notes of pikku-knowledge. DO NOT TRIGGER for: documenting code structure, API docs for a clean codebase, an engineer-facing code review, or rebuilding from an existing blueprint (use pikku-blueprint-to-fabric).'
3
+ description: 'Use when reverse-engineering an existing repository into a `.knowledge/` Product Blueprint — recovering what product an undocumented or organically-grown codebase implements so it can be rebuilt cleanly (e.g. as a Pikku app) — and when turning that blueprint into a plain-language second opinion for the non-technical owner of the app. TRIGGER when: user says "extract a blueprint", "reverse engineer this app", "what does this codebase actually do as a product", "prepare this repo for a rewrite/migration", points at a legacy repo (any language) and asks for its domains, workflows, business rules or a rebuild plan, or asks "explain how my app works" / "what would you do differently" / "is this built well?" for a founder, PM or operator. Not the `knowledge/` notes of pikku-knowledge. DO NOT TRIGGER for: documenting code structure, API docs for a clean codebase, an engineer-facing code review, or rebuilding from an existing blueprint.'
4
4
  installGroups: [core]
5
5
  ---
6
6
 
@@ -25,7 +25,7 @@ Everything goes in `<repo>/.knowledge/` (or a caller-specified directory):
25
25
 
26
26
  ```
27
27
  .knowledge/
28
- ├── product.json # purpose, actors, capabilities, terminology
28
+ ├── product.json # purpose, virtualUsers, capabilities, terminology
29
29
  ├── domains.json # business domains (NEVER folder names)
30
30
  ├── entities.json # domain entities: attributes, relationships, states, transitions
31
31
  ├── commands.json # state-changing actions (SendInvoice, not POST /invoices/:id/send)
@@ -77,13 +77,13 @@ Build an inventory before interpreting anything:
77
77
 
78
78
  Where intent hides, per ecosystem (read these first):
79
79
 
80
- | Ecosystem | Highest-yield locations |
81
- | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
82
- | Rails | `config/routes.rb`, model validations + callbacks + `aasm`/state machines, `app/policies` (Pundit) / `ability.rb` (CanCan), Sidekiq/ActiveJob workers, `db/schema.rb`, specs (esp. request + model specs) |
83
- | Express/Node | route registration files, middleware chains (auth!), inline `if` guards in handlers, SQL/ORM models, `jobs/`+crontab refs, webhook handlers |
84
- | Django | `urls.py`, model `Meta`/constraints/`clean()`, DRF serializers + permissions classes, celery tasks, admin.py (reveals internal workflows) |
85
- | Laravel | `routes/`, FormRequests (validation), Policies/Gates, Jobs + scheduler in `Kernel.php`, migrations |
86
- | Go | mux/router setup, middleware, struct tags, `cmd/` binaries (each is a component) |
80
+ | Ecosystem | Highest-yield locations |
81
+ | ------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
82
+ | Rails | `config/routes.rb`, model validations + callbacks + `aasm`/state machines, `app/policies` (Pundit) / `ability.rb` (CanCan), Sidekiq/ActiveJob workers, `db/schema.rb`, specs (esp. request + model specs) |
83
+ | Express/Node | route registration files, middleware chains (auth!), inline `if` guards in handlers, SQL/ORM models, `jobs/`+crontab refs, webhook handlers |
84
+ | Django | `urls.py`, model `Meta`/constraints/`clean()`, DRF serializers + permissions classes, celery tasks, admin.py (reveals internal workflows) |
85
+ | Laravel | `routes/`, FormRequests (validation), Policies/Gates, Jobs + scheduler in `Kernel.php`, migrations |
86
+ | Go | mux/router setup, middleware, struct tags, `cmd/` binaries (each is a component) |
87
87
  | Frontend (React/Vue/etc.) | router config / file-based routes (pages a user reaches), the component tree, the design-system import (`@mui/*`, Tailwind config, shadcn `components.json`) to judge consistency, the data layer (react-query/tRPC/fetch wrappers) to tie UI back to backend queries, charts/tables/editors (the `custom-logic` port risk), auth wiring |
88
88
 
89
89
  ### Phase 2 — Test excavation (do not skip)
@@ -91,7 +91,7 @@ Where intent hides, per ecosystem (read these first):
91
91
  Tests are the closest thing to an executable product spec. For every test file:
92
92
 
93
93
  - `describe`/`context`/`it` names → **scenarios** (attach to the matching workflow in `workflows.json` under `scenarios[]`, with `fromTest` set).
94
- - User-flow/journey harnesses (`pikkuUserFlow` stories, cucumber `.feature` files, Playwright journeys) are the highest-grade scenario source — they already ARE given/when/outcome sequences; extract them verbatim.
94
+ - User-flow/journey harnesses (`pikkuUserFlow` stories, `.feature` files, Playwright journeys) are the highest-grade scenario source — they already ARE given/when/outcome sequences; extract them verbatim.
95
95
  - Assertions → confirmations of policies and invariants (upgrade their `confidence` to `high`, add the test as evidence).
96
96
  - Fixtures/factories → entity attribute shapes and realistic example data.
97
97
  - Edge-case tests → business rules that exist **nowhere else in the code** (e.g. "replayed webhook events are idempotent" may only be stated in a test).
@@ -105,9 +105,9 @@ Run each lens over the surveyed material. Rules that counter the classic failure
105
105
 
106
106
  **Domains** — infer from data ownership, workflows, and vocabulary; NEVER from folder names. `controllers/` is not a domain; "Billing" is. A domain owns entities and the commands that mutate them.
107
107
 
108
- **Entities** — domain concepts, not tables. Include: attributes (from schema + serializers + fixtures), relationships, **lifecycle states and transitions** (grep status/state columns, then find every write to them — each write site is a transition with a trigger), ownership (which actor's rows), constraints.
108
+ **Entities** — domain concepts, not tables. Include: attributes (from schema + serializers + fixtures), relationships, **lifecycle states and transitions** (grep status/state columns, then find every write to them — each write site is a transition with a trigger), ownership (which virtual user's rows), constraints.
109
109
 
110
- **Commands** — every way state changes: routes, jobs, webhooks, CLI, admin consoles, DB triggers. Name them imperative `VerbNoun` in domain language: `POST /api/users/:id/status` → `ActivateUser`. For each: actor, preconditions (every `if (...) return 4xx` guard is a precondition or policy), effects, events produced. Convention: authentication/token issuance is a command (`LogInUser`, effect: "issues a session/JWT") even though it writes no rows — it changes the caller's security state.
110
+ **Commands** — every way state changes: routes, jobs, webhooks, CLI, admin consoles, DB triggers. Name them imperative `VerbNoun` in domain language: `POST /api/users/:id/status` → `ActivateUser`. For each: virtual user, preconditions (every `if (...) return 4xx` guard is a precondition or policy), effects, events produced. Convention: authentication/token issuance is a command (`LogInUser`, effect: "issues a session/JWT") even though it writes no rows — it changes the caller's security state.
111
111
 
112
112
  **Queries** — reads and views, `GetX`/`ListX`/`SearchX`. Record the tenancy scoping each applies (a missing `WHERE user_id=` that exists elsewhere is a gaps.json security entry).
113
113
 
@@ -142,7 +142,7 @@ Run each lens over the surveyed material. Rules that counter the classic failure
142
142
 
143
143
  ### Phase 4 — Cross-check, validate, synthesize
144
144
 
145
- 1. Cross-checks before writing: every command has an actor and at least one precondition or policy (or you explain why it is genuinely unguarded); every state transition appears in some command/workflow — if a state is only reachable by manual/DB intervention, record the transition with trigger `"none (manual/DB-only)"` AND add a gaps.json entry; every event has a producer; every api surface maps to a defined concept; every integration is used by some command/workflow.
145
+ 1. Cross-checks before writing: every command has a virtual user and at least one precondition or policy (or you explain why it is genuinely unguarded); every state transition appears in some command/workflow — if a state is only reachable by manual/DB intervention, record the transition with trigger `"none (manual/DB-only)"` AND add a gaps.json entry; every event has a producer; every api surface maps to a defined concept; every integration is used by some command/workflow.
146
146
  2. Write all 14 JSON files, sorting **unordered identity collections** by `name` (or `path`) — on first extraction too, not just re-runs. Arrays whose order carries meaning (`workflows[].steps`, and any other observed sequence) stay in their observed order: sorting them would rewrite the behavior you extracted. Run the validator: done means `0 error(s)` and exit 0, and every WARN explicitly reviewed and either fixed or justified in your summary.
147
147
  3. Write `blueprint.md`: product summary → domain map → per-domain narrative (entities/commands/events with the interesting rules) → workflows → integrations/architecture → invariants → gaps and open decisions → rebuild recommendation. Write it for the engineer who will rebuild the product and has never seen the legacy code.
148
148
 
@@ -36,7 +36,7 @@
36
36
  "required": [
37
37
  "name",
38
38
  "purpose",
39
- "actors",
39
+ "virtualUsers",
40
40
  "capabilities",
41
41
  "terminology",
42
42
  "evidence",
@@ -49,7 +49,7 @@
49
49
  "description": "1-3 sentences: what problem, for whom"
50
50
  },
51
51
  "targetUsers": { "type": "array", "items": { "type": "string" } },
52
- "actors": {
52
+ "virtualUsers": {
53
53
  "type": "array",
54
54
  "minItems": 1,
55
55
  "items": {
@@ -212,7 +212,7 @@
212
212
  },
213
213
  "ownership": {
214
214
  "type": "string",
215
- "description": "which actor/tenant owns rows of this entity"
215
+ "description": "which virtual user/tenant owns rows of this entity"
216
216
  },
217
217
  "constraints": { "type": "array", "items": { "type": "string" } },
218
218
  "evidence": { "$ref": "#/$defs/evidence" },
@@ -233,7 +233,7 @@
233
233
  "required": [
234
234
  "name",
235
235
  "domain",
236
- "actor",
236
+ "virtualUser",
237
237
  "input",
238
238
  "preconditions",
239
239
  "effects",
@@ -247,7 +247,7 @@
247
247
  "description": "imperative VerbNoun: ApproveInvoice"
248
248
  },
249
249
  "domain": { "$ref": "#/$defs/conceptName" },
250
- "actor": { "type": "string" },
250
+ "virtualUser": { "type": "string" },
251
251
  "trigger": {
252
252
  "type": "string",
253
253
  "description": "how it is invoked today (route, job, webhook, cron)"
@@ -305,7 +305,7 @@
305
305
  "required": [
306
306
  "name",
307
307
  "domain",
308
- "actor",
308
+ "virtualUser",
309
309
  "description",
310
310
  "returns",
311
311
  "evidence",
@@ -317,7 +317,7 @@
317
317
  "description": "GetX / ListX / SearchX"
318
318
  },
319
319
  "domain": { "$ref": "#/$defs/conceptName" },
320
- "actor": { "type": "string" },
320
+ "virtualUser": { "type": "string" },
321
321
  "description": { "type": "string" },
322
322
  "input": {
323
323
  "type": "array",
@@ -448,7 +448,7 @@
448
448
  "required": [
449
449
  "name",
450
450
  "kind",
451
- "actor",
451
+ "virtualUser",
452
452
  "trigger",
453
453
  "steps",
454
454
  "result",
@@ -458,7 +458,7 @@
458
458
  "properties": {
459
459
  "name": { "type": "string" },
460
460
  "kind": { "type": "string", "enum": ["user", "admin", "system"] },
461
- "actor": { "type": "string" },
461
+ "virtualUser": { "type": "string" },
462
462
  "trigger": { "type": "string" },
463
463
  "steps": {
464
464
  "type": "array",
@@ -184,4 +184,4 @@ and pass them to `pikku ui theme apply`. The recipe is in `references/from-a-ref
184
184
  ## Hosted only
185
185
 
186
186
  Interactive theme pickers, design extraction, stock images, site crawling, favicon generation, AI-generated logos and
187
- agent-driven build flows exist only in Pikku Studio and are not part of the CLI.
187
+ agent-driven build flows are not part of the CLI; a hosting platform may offer them.
@@ -284,7 +284,7 @@ compensated. The error arrives as `wire.graph.recoveringFrom`
284
284
  object (`{ success: false, reason }`) instead.
285
285
 
286
286
  Full step options: `description`, `retries`, `retryDelay`, `compensate: false`
287
- (plus `actor`, which is scenario-only — see `pikku-scenario`).
287
+ (plus `virtualUser`, which is scenario-only — see `pikku-scenario`).
288
288
 
289
289
  ### Parallel fan-out
290
290