@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.
- package/README.md +1 -1
- package/dist/skills.gen.js +2 -2
- package/dist/snippets.js +1 -1
- package/package.json +1 -1
- package/skills/pikku-architect/SKILL.md +1 -1
- package/skills/pikku-architect/references/plan-defects.md +7 -7
- package/skills/pikku-auth/references/better-auth.md +92 -96
- package/skills/pikku-build/SKILL.md +9 -9
- package/skills/pikku-build/references/app.md +56 -61
- package/skills/pikku-build/references/design.md +14 -14
- package/skills/pikku-build/references/feature.md +3 -3
- package/skills/pikku-build/references/mocks.md +6 -0
- package/skills/pikku-build/references/multi-app.md +16 -16
- package/skills/pikku-build/references/openapi.md +34 -34
- package/skills/pikku-build/references/platform.md +4 -5
- package/skills/pikku-build/references/post-clone.md +1 -1
- package/skills/pikku-build/references/quick.md +63 -36
- package/skills/pikku-build/references/scenarios.md +17 -16
- package/skills/pikku-build/references/ship.md +14 -16
- package/skills/pikku-build/references/theming.md +2 -2
- package/skills/pikku-changes/SKILL.md +2 -5
- package/skills/pikku-concepts/SKILL.md +26 -27
- package/skills/pikku-concepts/references/language.md +6 -6
- package/skills/pikku-deploy/references/nextjs.md +1 -1
- package/skills/pikku-guide/SKILL.md +18 -17
- package/skills/pikku-i18n/references/messages.md +5 -6
- package/skills/pikku-knowledge/SKILL.md +3 -3
- package/skills/pikku-meta/references/audit.md +1 -1
- package/skills/pikku-permissions/SKILL.md +1 -1
- package/skills/pikku-react/SKILL.md +18 -20
- package/skills/pikku-react/references/client.md +2 -37
- package/skills/pikku-report/SKILL.md +10 -10
- package/skills/pikku-scenario/SKILL.md +63 -66
- package/skills/pikku-scenario/references/browser.md +5 -5
- package/skills/pikku-scenario/references/coverage.md +4 -4
- package/skills/pikku-scenario/references/steps.md +31 -30
- package/skills/pikku-scenario/references/{persona-run.md → virtual-user-run.md} +38 -43
- package/skills/pikku-scenario/references/virtual-users.md +114 -0
- package/skills/pikku-seo/SKILL.md +1 -1
- package/skills/pikku-services/references/audit-wire-service.md +2 -2
- package/skills/pikku-services/references/audit.md +1 -2
- package/skills/pikku-services/references/services.md +1 -1
- package/skills/pikku-software-archaeology/SKILL.md +13 -13
- package/skills/pikku-software-archaeology/references/blueprint.schema.json +9 -9
- package/skills/pikku-theme/SKILL.md +1 -1
- package/skills/pikku-workflow/SKILL.md +1 -1
- package/skills/pikku-admin-to-fabric/SKILL.md +0 -198
- package/skills/pikku-blueprint-to-fabric/SKILL.md +0 -362
- package/skills/pikku-blueprint-to-fabric/scripts/inventory.mjs +0 -367
- package/skills/pikku-fabric/SKILL.md +0 -331
- package/skills/pikku-fabric/references/database.md +0 -128
- package/skills/pikku-fabric/references/debugging.md +0 -114
- package/skills/pikku-fabric/references/deploy.md +0 -179
- package/skills/pikku-scenario/references/personas.md +0 -123
|
@@ -1,17 +1,17 @@
|
|
|
1
|
-
# Running a
|
|
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
|
-
- [
|
|
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
|
|
14
|
-
app's real auth and works the API in character, driven by a model. A
|
|
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
|
|
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
|
|
28
|
-
|
|
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
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
|
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
|
|
49
|
-
| `--goals` | Comma-separated, in your words — run _alongside_ the
|
|
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
|
|
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
|
|
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
|
-
##
|
|
79
|
+
## The key
|
|
80
80
|
|
|
81
|
-
|
|
82
|
-
|
|
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. **`
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
122
|
-
from a
|
|
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
|
|
131
|
-
|
|
|
132
|
-
| `pikku
|
|
133
|
-
| `pikku
|
|
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
|
|
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
|
|
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
|
|
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 `
|
|
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
|
|
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
|
-
> **
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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,
|
|
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
|
|
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:
|
|
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
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
-
"
|
|
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
|
|
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 `
|
|
287
|
+
(plus `virtualUser`, which is scenario-only — see `pikku-scenario`).
|
|
288
288
|
|
|
289
289
|
### Parallel fan-out
|
|
290
290
|
|