@pikku/skills 0.12.61 → 0.12.63

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pikku/skills",
3
- "version": "0.12.61",
3
+ "version": "0.12.63",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/pikkujs/pikku.git",
@@ -47,10 +47,12 @@ wireAddon({
47
47
  name: string, // Namespace for addon functions (e.g. 'todos')
48
48
  package: string, // NPM package name (e.g. '@pikku/addon-todos')
49
49
  rpcEndpoint?: string, // Optional remote RPC endpoint for distributed execution
50
+ uses?: Record<string, string>, // alias the addon declared → name this app wired it under (only declared aliases; PKU355)
50
51
  auth?: boolean, // Require a session for every function in the addon
51
52
  mcp?: boolean | string[], // true: every function the addon declared mcp: true; a list: the tools this app offers, typed against the addon's function names
52
53
  expose?: boolean | string[], // opt-in: what rpc.exposed / POST /rpc may reach. true = every function the addon declared expose: true, a list = those of them named (typed; PKU343 on unknown or undeclared names), unset/false = none
53
54
  tags?: string[], // Tags applied to all addon functions
55
+ ui?: boolean, // Mount the screens the addon declares with definePlugin
54
56
  scopes?: string[], // Required of every function, on top of its own
55
57
  secretOverrides?: Record<string, string>, // Remap secret names (and grant them)
56
58
  variableOverrides?: Record<string, string>, // Remap variable names
@@ -159,6 +161,12 @@ export const createSingletonServices = pikkuAddonServices(
159
161
  )
160
162
  ```
161
163
 
164
+ Export the call as it is. A cast around it (`pikkuAddonServices(...) as unknown
165
+ as CreateSingletonServices<...>`, `<T>`, `!`) hides the factory
166
+ from the inspector, which would leave it unregistered and the addon's workflows
167
+ failing at runtime; the build fails with `PKU564` instead. Fix the type the cast
168
+ was hiding. `satisfies` and parentheses are fine: the inspector reads through them.
169
+
162
170
  `secrets` and `variables` arrive **typed against the addon's own declarations**,
163
171
  and a secret is a `SecretValue` — `.reveal()` is the only way to the plaintext
164
172
  (see `pikku-services`). `pikkuAddonConfig` is the matching factory for the addon's
@@ -318,6 +326,116 @@ version. The SDK carries only the remote RPCs, the `expose: true` workflows and
318
326
  the types they reach. Changing a published `remote: true` contract needs a new
319
327
  `version` on the function, which `pikku versions check` enforces.
320
328
 
329
+ ### Calling another addon
330
+
331
+ Declare it in the addon's **package.json** (not pikku.config.json), by the
332
+ alias your code calls it by and a range:
333
+
334
+ ```json
335
+ "pikku": { "uses": { "stripe": "@pikkustudio/addon-stripe@^0.12.0" } }
336
+ ```
337
+
338
+ Then call it typed, with no cast and no hand-written wrapper:
339
+
340
+ ```typescript
341
+ const charge = await rpc.invoke('stripe:charge', { amountCents })
342
+ await rpc.startWorkflow('stripe:refund', { chargeId: charge.chargeId })
343
+ ```
344
+
345
+ `pikku all` in the addon types `stripe:*` from the dependency's SDK (its remote
346
+ surface). A package in the same workspace is read from its own
347
+ `<outDir>/sdk`, so run `pikku all` there first (PKU338 if missing, PKU354 if
348
+ its version is outside the range); anything else is fetched from the registry
349
+ and pinned in `pikku-addons.lock`. Never cast `rpc` or call by the package
350
+ name; the alias is the only name that works.
351
+
352
+ The app wiring your addon maps the alias with `uses`, local or remote alike:
353
+
354
+ ```typescript
355
+ wireRemoteAddon({
356
+ name: 'payments',
357
+ package: '@pikkustudio/addon-stripe@^0.12.0',
358
+ serverUrl,
359
+ })
360
+ wireAddon({
361
+ name: 'billing',
362
+ package: '@acme/addon-billing',
363
+ uses: { stripe: 'payments' },
364
+ })
365
+ ```
366
+
367
+ | Code | The app's `pikku all` fails when |
368
+ | ------ | ------------------------------------------------------------------- |
369
+ | PKU348 | an alias the addon declared is not mapped |
370
+ | PKU347 | it is mapped to a name not wired, or wired from another package |
371
+ | PKU355 | `uses` maps a name the addon did not declare |
372
+ | PKU354 | the mapped instance's version is outside the range |
373
+ | PKU356 | the addon's `pikku.uses` entry is malformed (alias, type, no range) |
374
+
375
+ At runtime an unmapped alias, or the wired name called directly, throws
376
+ `AddonNotUsedError` (403) for invoke, workflows and agents.
377
+
378
+ ### Studio screens
379
+
380
+ An addon declares the screens it adds to Pikku Studio with `definePlugin`, in
381
+ `src/screens.ts` (`pikku new addon-ui <name>` scaffolds one). It replaces
382
+ `defineScreens`, which no longer exists.
383
+
384
+ ```typescript
385
+ import { definePlugin } from '@pikku/core/addon'
386
+
387
+ export default definePlugin({
388
+ title: 'invoices__title', // message key: the addon's name in Studio
389
+ screens: [
390
+ {
391
+ path: '/invoices',
392
+ title: 'invoices_list__title', // message key, also the nav label
393
+ scope: 'project', // 'studio' → /<path>, 'team' → /<team>/<path>,
394
+ // 'project' → /<team>/<project>[/<target>]/<path>
395
+ nav: {
396
+ area: 'project',
397
+ icon: 'receipt',
398
+ order: 30,
399
+ badge: 'unpaidCount',
400
+ },
401
+ needsApp: true, // project screens only; default false
402
+ scopes: ['invoices:read'], // hides the screen; NOT access control
403
+ component: () => import('./screens/Invoices.js'),
404
+ },
405
+ {
406
+ path: '/invoices/print',
407
+ title: 'invoices_print__title',
408
+ scope: 'project',
409
+ layout: 'bare',
410
+ app: 'print',
411
+ },
412
+ ],
413
+ })
414
+ ```
415
+
416
+ - A screen is a lazy `component` or an `app`, never both. `nav` is optional:
417
+ without it the screen is routed but not listed. `nav.area` is `'rail'`,
418
+ `'team'`, `'project'` or `'account'`; `badge` names one of the addon's
419
+ functions that returns a count.
420
+ - `replaces: true` takes over the core screen at the same path (first-party
421
+ addons only; Studio enforces that). `layout: 'bare'` renders without the shell.
422
+ - Titles are message keys in the addon's `messages/{locale}.json`, found by
423
+ convention next to its `package.json`; there is no field for them.
424
+ - `scopes` only hide the screen. The addon's functions must enforce the same
425
+ scopes.
426
+
427
+ The inspector reads every field statically, so each must be a literal.
428
+ `pikku all` fails on:
429
+
430
+ | Code | When |
431
+ | ------ | ------------------------------------------------------------ |
432
+ | PKU345 | a field is not a literal (variable, call, spread, bad value) |
433
+ | PKU357 | two screens of the addon use the same path |
434
+ | PKU358 | a title key is missing from `messages/en.json` |
435
+ | PKU359 | `nav.badge` names no function of the addon |
436
+
437
+ A key missing from another locale's catalog is a warning.
438
+
321
439
  ### Database tables
322
440
 
323
441
  An addon may **ship** tables. It must never **create** them: it runs inside the
@@ -5,7 +5,7 @@
5
5
  - Package Structure
6
6
  - pikku.config.json
7
7
  - package.json (key fields)
8
- - Platform service contributors (`pikku.contributors`)
8
+ - Platform service contributors (`defineContributors`)
9
9
  - tsconfig.json (key fields)
10
10
 
11
11
  `npx pikku new addon` scaffolds these files. You rarely hand-edit them — consult this when wiring exports or config by hand.
@@ -81,6 +81,12 @@ package that declared them, and the extra segment is what stops a linked addon's
81
81
  }
82
82
  ```
83
83
 
84
+ **`pikku.uses` names the addons this addon calls**, as
85
+ `{ "<alias>": "<package>@<range>" }`. The addon calls `<alias>:<fn>`, typed from
86
+ that package's SDK, and the app wiring it maps each alias with `wireAddon`
87
+ `uses` (see "Calling another addon" in the skill). The older list form,
88
+ `["kysely"]`, names keys the app must map, with no SDK or version check.
89
+
84
90
  **`imports` names `dist`, never the source tree.** `files: ["dist"]` is the whole
85
91
  published package, and `pikku dist` copies `.pikku` and `types` into it — so a
86
92
  `#pikku/*` target under `./.pikku/` resolves for the author and for nobody else.
@@ -99,57 +105,72 @@ live in a file that never imports `#pikku` — see "Functions" in the skill.
99
105
  writes `@my-org/addon-todos/.pikku/rpc/...`, exactly as it would in an
100
106
  application, and the leaf stays the package's own business.
101
107
 
102
- ## Platform service contributors (`pikku.contributors`)
108
+ ## Platform service contributors (`defineContributors`)
103
109
 
104
- An addon can ship code that runs in the **host's** generated
105
- `createPlatformServices` — a log source over the host's database, a client
106
- built from a host secret. `wireAddon` has no field for it; the package declares
107
- it in its own `package.json`, the same shape as the host's `dev.contributors`:
110
+ An addon can ship code that builds services for the **host's** target — a log
111
+ source over the host's database, a client built from a host secret. The addon
112
+ registers them in code with `defineContributors`; nothing goes in `package.json`
113
+ and `wireAddon` of the addon itself has no field for it. A host that wants one
114
+ opts in by name with `wireAddon({ contributors: ['log-sources'] })`, and gets
115
+ nothing it did not list.
108
116
 
109
- ```json
110
- {
111
- "exports": {
112
- "./contributors": {
113
- "types": "./dist/src/contributors.d.ts",
114
- "import": "./dist/src/contributors.js"
115
- }
116
- },
117
- "pikku": {
118
- "contributors": [
119
- { "module": "@my-org/addon-logs/contributors", "export": "logSources" }
120
- ]
121
- }
122
- }
117
+ ```ts
118
+ // src/contributors.ts
119
+ import { defineContributors } from '@pikku/core/contributors'
120
+ import { logSourcesDev } from './log-sources-dev.js'
121
+ import { logSourcesDeploy } from './log-sources-deploy.js'
122
+
123
+ export const contributors = defineContributors({
124
+ 'log-sources': { dev: logSourcesDev, deploy: logSourcesDeploy },
125
+ })
123
126
  ```
124
127
 
125
- `module` resolves from the addon's package directory. Every app that wires the
126
- addon (not `wireRemoteAddon`) picks the contributors up: `pikku all` emits them
127
- into `.pikku/pikku-dev-platform-services.gen.ts` for `pikku dev`, and
128
- `pikku deploy plan` / `apply` hands them to the provider adapter, after the
129
- app's own `deploy.contributors`.
130
-
131
- Declare what a contributor reads and sets, rather than relying on list order:
132
-
133
128
  ```ts
134
- export const logSources: PlatformServiceContributor = {
135
- name: 'log-sources',
129
+ // src/log-sources-deploy.ts — each variant in its OWN module
130
+ import type { Contributor } from '@pikku/core/contributors'
131
+ import { kyselyLogSource } from './log-source.js'
132
+
133
+ export const logSourcesDeploy: Contributor = {
136
134
  needs: ['kysely'], // runtime.kysely, or a contributor's services.kysely
137
135
  provides: ['logSources', 'logSources.workspace'],
138
- imports: [`import { kyselyLogSource } from '@my-org/addon-logs/log-source'`],
139
- emit: () => [
140
- ` services.logSources = { workspace: kyselyLogSource(runtime.kysely) }`,
141
- ],
136
+ run: (services, _env, runtime) => {
137
+ services.logSources = { workspace: kyselyLogSource(runtime.kysely) }
138
+ },
142
139
  }
143
140
  ```
144
141
 
145
- Contributors are sorted so each runs after every contributor providing one of
146
- its `needs`; a dotted need (`logSources.stage`) is met by a provider of the key
147
- or of its prefix. Emitted lines read services the entry built before them as
148
- `runtime.<name>` — `pikku dev` has logger, variables, secrets, schema and (with
149
- `db`) kysely; the standalone entry has logger, kysely (with a database) and its
150
- scheduler/queue/workflow/trigger services and event hub. Codegen **fails** on a
151
- need nothing meets, on two contributors providing the same key, and on a cycle,
152
- naming the contributors involved.
142
+ Rules the inspector and the build enforce:
143
+
144
+ - `defineContributors` takes an object literal keyed by contributor name; each
145
+ `dev` / `deploy` value must be a reference to an `export const`. Anything else
146
+ is PKU360; a name defined twice is PKU361.
147
+ - **Each variant lives in its own module.** The build imports the module that
148
+ exports the selected variant, so a `dev` and a `deploy` variant sharing a file
149
+ both reach every bundle, production included.
150
+ - A contributor with no variant for a target is simply not part of it.
151
+ - Run `pikku all` in the addon: it writes
152
+ `.pikku/contributors/pikku-contributors-meta.gen.json`, which a host reads to
153
+ opt in (an unknown name is PKU363). Ship `.pikku` in `files`.
154
+ - `pikku.contributors` in `package.json` is removed and is now an error
155
+ (PKU364), not silently ignored.
156
+ - Contributors run after every contributor providing one of their `needs`; a
157
+ dotted need (`logSources.stage`) is met by a provider of the key or its
158
+ prefix. `run` reads services the target built before it as `runtime.<name>` —
159
+ `pikku dev` has logger, variables, secrets, schema and (with `db`) kysely; the
160
+ standalone entry has logger, kysely (with a database) and its
161
+ scheduler/queue/workflow/trigger services and event hub.
162
+ - Two contributors providing the same key (after `when` / `requires` filtering)
163
+ is a build error naming both, except that a target's baseline services may be
164
+ replaced. A decorator declares `provides: []`. A contributor providing what it
165
+ needs is a cycle. An unmet `needs` is a build error.
166
+ - `requires: ['cloudflare']` on a contributor a host opted in to, for a target
167
+ that cannot supply it, is a build error; use `when` to say "not here".
168
+ - `onServerStart` / `onServerStop` run only where the server lifecycle runs
169
+ (dev, serve, standalone, generic server entry, Cloudflare container). On a
170
+ Worker `onServerStart` is a build error and `onServerStop` a warning.
171
+ - Fail closed: a contributor whose bindings are missing at startup throws.
172
+
173
+ Full reference: `docs/contributors.md` in the pikku repo.
153
174
 
154
175
  ## tsconfig.json (key fields)
155
176
 
@@ -10,6 +10,18 @@ Read this when a screen needs data no function returns yet, or returns in a diff
10
10
 
11
11
  The app loads its mocks with `registerPikkuMocks()` from the generated `mocks.gen.ts` beside `api.gen.ts`, called when `import.meta.env.DEV || import.meta.env.VITE_MOCK`. It covers the app's `.mocks/` and every wired addon's, so never glob `.mocks` or `node_modules` by hand.
12
12
 
13
+ ## Adding a mock
14
+
15
+ `pikku mocks add <rpc>` does the whole job. It checks the RPC is a real function, writes `.mocks/<rpc with : as .>/<state>.json` and `<state>.meta.json`, keeps exactly one default (the first mock is the default, `--default` moves it) and prints the stub hook to use. The response comes from `--body '<json>'` or `--from <file>`; an error mock and an empty mock of a list have a default body.
16
+
17
+ ```
18
+ pikku mocks add classes:month --body '[{"id":"c1","name":"Yoga"}]' --flag classMonth
19
+ pikku mocks add classes:month --state empty
20
+ pikku mocks add classes:month --state error --status 500
21
+ ```
22
+
23
+ `--flag <name>` declares the flag in the project's `defineFeatureFlags({ ... })` file when it is not there yet, or prints the entry to add by hand. Nothing is overwritten without `--force`. A body that differs from the function's output is written anyway and reported, because a stub may stand in for data the function does not return yet.
24
+
13
25
  ## Addon mocks
14
26
 
15
27
  An addon ships default mocks in its own package as `.mocks/<function>/<scenario>.json`, named without a namespace, and lists `.mocks` in `files`. An app that wires the addon as `studio` gets them as `studio:<function>`. The app's `.mocks/studio.<function>/` adds scenarios; one with the same name replaces the addon's, and a `default: true` in the app wins over the addon's default. Put mocks for an addon's functions in the addon, and only the app's own variations in the app.
@@ -166,29 +166,31 @@ it is worth writing as a browser step on §7's scenario and running
166
166
  `pikku scenario run local --spawn --run browser`: same clicks, same assertions,
167
167
  in the repo, green or red on every future run.
168
168
 
169
- **A host can inject platform services into `pikku dev`** the way a deploy does, with
170
- `dev.contributors` in `pikku.config.json`:
171
-
172
- ```json
173
- {
174
- "dev": {
175
- "contributors": [
176
- { "module": "./dev/email-contributor.ts", "export": "email" }
177
- ]
178
- }
179
- }
169
+ **A project can take platform services from an addon's contributors.** The addon
170
+ registers them with `defineContributors` (an object keyed by name, each with an
171
+ optional `dev` and `deploy` variant that is a plain `Contributor`), and the
172
+ project opts in by name:
173
+
174
+ ```ts
175
+ wireAddon({
176
+ name: 'inbox',
177
+ package: '@my-org/addon-inbox',
178
+ contributors: ['inbox'],
179
+ })
180
180
  ```
181
181
 
182
- The module exports a `PlatformServiceContributor` (`{ name, needs?, provides?, imports?, emit }`, the
183
- type `@pikku/deploy` takes). Addons the project wires add their own, declared in their
184
- package.json as `pikku.contributors` (see the pikku-addon skill's package manifest
185
- reference). Contributors run in the order their `needs`/`provides` require, and the
186
- generated `createPlatformServices(env, runtime)` gets the local services as `runtime`;
187
- an unmet need, a key provided twice or a cycle fails `pikku all`. `pikku all` writes `.pikku/pikku-dev-platform-services.gen.ts`
188
- from them, and `pikku dev` merges what it returns over the local services before
182
+ Nothing is injected without that list. `pikku all` resolves the `dev` variants
183
+ (dropping what `when` or `requires` rules out, ordering by `needs`/`provides`)
184
+ and writes `.pikku/contributors/pikku-dev-contributors.gen.ts`; `pikku dev` and
185
+ `pikku serve` merge what it builds over the local services before
189
186
  `createSingletonServices` runs, so a contributed `emailService` beats
190
- `LocalEmailService` (and the test-mode spy). Nothing is written without
191
- contributors, `pikku serve` never loads it, and `NODE_ENV=production` ignores it. An import a contributor names by `file://` URL or absolute path is written as the package export that points at it, or as a path relative to `.pikku/`, so the file typechecks under the project's tsconfig as long as the lines a contributor emits are typed.
187
+ `LocalEmailService` (and the test-mode spy). Both skip the file under
188
+ `NODE_ENV=production`. A contributor's `onServerStart` runs before the app's
189
+ `beforeStart`; its `onServerStop` after the app's `afterStop`. An unmet need, a
190
+ key provided twice or a cycle fails `pikku all`, and so does a declared database
191
+ (`createConfig` or `db/<dialect>`) with no `db` in the pikku config. A host that
192
+ injects an addon (CI, Studio) writes a gitignored wiring file containing
193
+ `wireAddon({ name, package, contributors })`. Reference: `docs/contributors.md`.
192
194
 
193
195
  ## 6a. Close the changeset against its plan, not against your memory
194
196
 
@@ -25,12 +25,16 @@ with Node, or a self-contained executable compiled with `bun build --compile`.
25
25
  factories the deploy codegen expects, and hand-rolling an `ExportedHandler` is
26
26
  how a worker deploy fails at runtime instead of at build.
27
27
 
28
- Platform services a host injects into the bundle come from contributors:
29
- `deploy.contributors` in `pikku.config.json` (`[{ "module", "export"? }]`, the
30
- same shape as `dev.contributors`) plus the `pikku.contributors` every wired addon
31
- declares in its package.json. `plan` and `apply` pass them all to the adapter,
32
- which orders them by `needs`/`provides` and, on standalone, runs them after the
33
- database is open, with `runtime.logger` and `runtime.kysely` in reach.
28
+ Platform services an addon builds for the bundle come from contributors the
29
+ project opted in to with `wireAddon({ contributors: [...] })`; the addon
30
+ registers them with `defineContributors`. `plan` and `apply` select the `deploy`
31
+ variant of each, drop what `when` rules out, order them by `needs`/`provides`
32
+ and, on standalone, run them after the database is open, with `runtime.logger`
33
+ and `runtime.kysely` in reach. Standalone, the generic server entry and the
34
+ Cloudflare adapter support contributors; a provider without `contributorSupport`
35
+ fails the build when a project opts in to one with a deploy variant. On a
36
+ Cloudflare Worker a contributor with `onServerStart` is a build error. See
37
+ `docs/contributors.md`.
34
38
 
35
39
  Always run `plan` before `apply`, and read it. It names what will be created,
36
40
  updated and deleted — the deletions are the reason to look.
@@ -74,6 +74,12 @@ service's paths and size limit; omitting it does not disable it.
74
74
  - **Do not take `services` as a named parameter and cast in the body.** The
75
75
  inspector reads the destructuring pattern to build the manifest; a cast makes
76
76
  the service invisible to it, so tree-shaking drops what the function needs.
77
+ - **Do not cast a service factory.** `pikkuServices(...) as X` (or `as unknown
78
+ as`, `!`, `<X>`) hides the call from the inspector,
79
+ so the factory would not be registered; the build fails with `PKU564`
80
+ instead. Export the direct `pikkuServices(...)` / `pikkuWireServices(...)` /
81
+ `pikkuConfig(...)` call and fix the type mismatch the cast was hiding.
82
+ `satisfies` and parentheses are fine: the inspector reads through them.
77
83
  - **Do not hand-roll an audit table.** `auditLog.write()` on an `audit: true`
78
84
  function enriches the event with the function id, wire type, trace id and user
79
85
  identity; an `insertInto('audit_log')` of your own gets none of that, and a
@@ -90,8 +90,9 @@ pikku ui theme apply --preset quorum --structure monopro
90
90
  | `--structure <id>` | swap in another preset's structure (depth) under this brand |
91
91
  | `--primary`, `--secondary`, `--accent` | brand colours, hex |
92
92
  | `--font-heading`, `--font-body` | Google Fonts families |
93
- | `--page` | light-mode page colour (`structure.page`) — paper, linen, bone instead of white |
94
- | `--ink` | light-mode text colour (`structure.ink`) |
93
+ | `--mode light\|dark` | `light` (default). `dark` makes `--page`/`--ink` the dark ground and its text (see Dark designs) |
94
+ | `--page` | page colour (`structure.page`) — paper, linen, bone instead of white; with `--mode dark`, the dark ground |
95
+ | `--ink` | text colour (`structure.ink`); with `--mode dark`, the text on the dark ground |
95
96
 
96
97
  Flags are flat scalars, one value each; there is no nested `--colors` object. One call writes the
97
98
  theme, makes it active, regenerates `themes/index.ts` (so the running app reloads) and re-brands
@@ -102,6 +103,27 @@ Fine-grained structure tweaks the flags do not cover (a shadow scale, the radius
102
103
  the active `themes/<id>.json` `structure`, followed by `pikku ui theme apply` to regenerate
103
104
  `theme.css`; keep them in the theme, not in components.
104
105
 
106
+ ## Dark designs
107
+
108
+ `--page`/`--ink` are light-mode colours unless you pass `--mode dark`. For a design that is dark
109
+ (a dark reference, a dark ground in the brief) always pass `--mode dark` with its ground and text:
110
+
111
+ ```bash
112
+ pikku ui theme apply --preset linear --mode dark \
113
+ --primary '#c6ff3d' --secondary '#22262d' --page '#14161a' --ink '#eef1f4'
114
+ ```
115
+
116
+ Every other token is derived for that ground: card and popover a step lighter, muted a further
117
+ step, muted text a mid tone of the ink at 4.5:1 or better, border and input visibly above the card,
118
+ and each `-foreground` black or white by contrast against its actual fill (so a lime primary gets a
119
+ dark label). The same values are written to `:root` and `.dark` (with `color-scheme: dark`), so a
120
+ stored scheme or `class="dark"` never switches to the preset's unrelated greys. Without
121
+ `--mode dark`, a dark `--page` is derived as a light ground and comes out wrong.
122
+
123
+ After every apply the command prints a contrast report (text/page, muted-text/page,
124
+ primary-foreground/primary, border/page, input/page) and warns on any pair under its target; fix
125
+ the colours and re-run, the exit code stays 0.
126
+
105
127
  ## Colour rule
106
128
 
107
129
  Every colour in app code comes from the theme: a semantic utility (`bg-primary`, `text-foreground`,
@@ -20,6 +20,9 @@ has no extractor: you read the design off the source and pass it to `pikku ui th
20
20
  pikku ui theme apply --preset breeze --primary '#0B6E99' --font-heading 'Manrope' --font-body 'Manrope' --page '#F7F5F0' --ink '#1C2A33'
21
21
  ```
22
22
 
23
+ If the reference is dark, add `--mode dark` (then `--page`/`--ink` are the dark ground and its
24
+ text); without it a dark page is derived as a light one.
25
+
23
26
  You are the art director: what you read is signal, not a finished theme. Check page and ink
24
27
  against each other and against the primary for 4.5:1 before applying.
25
28
 
@@ -167,6 +167,36 @@ cron expression can decide which tasks fire. `pikku-deploy` has the comparison.
167
167
  Enabling the RPC endpoint says the endpoint exists, not who may call it — each
168
168
  `expose: true` function is still gated by its own `auth`, permissions and scopes.
169
169
 
170
+ ## A function nothing uses: wire it or disable it
171
+
172
+ `pikku verify` fails on an app function that is not wired, exposed, or called
173
+ by name (`rpc.invoke`, a workflow step, an agent tool) — `[PKU563]`, naming the
174
+ function and its file. `pikku all` still builds; only verify fails. Addon
175
+ functions are never flagged, in the addon or in an app that uses it.
176
+
177
+ Workflows are checked too. A workflow is used when it is wired, exposed, or
178
+ started by a literal name: `rpc.startWorkflow('name')`,
179
+ `workflowService.startWorkflow` / `runToCompletion('name')`,
180
+ `rpc.invoke('name')`, another workflow's step, or a client helper. A workflow
181
+ started only through a computed name is flagged.
182
+
183
+ To keep a function in the source but out of the build, mark it `disabled`:
184
+
185
+ ```typescript
186
+ export const oldReport = pikkuSessionlessFunc({
187
+ disabled: 'replaced by liveReport', // or `true`
188
+ func: async () => 'old',
189
+ })
190
+ ```
191
+
192
+ A disabled function is left out of everything: meta, RPC maps, clients, MCP,
193
+ CLI, HTTP, addon SDKs. It cannot be registered at runtime either. Anything
194
+ that still references it — a wiring, an `rpc.invoke('oldReport')`, a workflow
195
+ step, an agent tool — fails the build with `[PKU562]`, naming both sides.
196
+ `pikku all` lists the disabled functions once, with file and reason.
197
+
198
+ The value must be `true` or a string literal; a computed value is refused.
199
+
170
200
  ## What NOT to do
171
201
 
172
202
  - Do not import a wiring factory from `@pikku/core/*`. It compiles and silently