@pikku/skills 0.12.10 → 0.12.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +819 -0
- package/dist/skills.gen.js +1 -1
- package/package.json +4 -4
- package/skills/pikku-addon/SKILL.md +17 -11
- package/skills/pikku-addon/references/addon-package-manifest.md +2 -2
- package/skills/pikku-agent/SKILL.md +4 -5
- package/skills/pikku-audit/SKILL.md +28 -13
- package/skills/pikku-aws/SKILL.md +2 -2
- package/skills/pikku-better-auth/SKILL.md +97 -17
- package/skills/pikku-build-app/SKILL.md +621 -0
- package/skills/pikku-build-app/references/multi-app.md +117 -0
- package/skills/pikku-build-app/references/ship.md +98 -0
- package/skills/pikku-build-app/references/theming.md +70 -0
- package/skills/pikku-build-platform/SKILL.md +239 -0
- package/skills/pikku-build-quick/SKILL.md +238 -0
- package/skills/pikku-cli/SKILL.md +7 -7
- package/skills/pikku-cli/references/complete-example.md +1 -1
- package/skills/pikku-concepts/SKILL.md +5 -2
- package/skills/pikku-concepts/references/concept-mapping.md +1 -1
- package/skills/pikku-config/SKILL.md +5 -3
- package/skills/pikku-deploy-azure/SKILL.md +5 -4
- package/skills/pikku-emails/SKILL.md +28 -7
- package/skills/pikku-fabric/SKILL.md +27 -3
- package/skills/pikku-fabric-debug/SKILL.md +1 -1
- package/skills/pikku-feature/SKILL.md +5 -4
- package/skills/pikku-http/SKILL.md +4 -4
- package/skills/pikku-http/references/http-options.md +13 -13
- package/skills/pikku-i18n/SKILL.md +2 -1
- package/skills/pikku-info/SKILL.md +1 -1
- package/skills/pikku-knowledge/SKILL.md +13 -13
- package/skills/pikku-mcp/SKILL.md +4 -4
- package/skills/pikku-middleware/SKILL.md +5 -5
- package/skills/pikku-middleware/references/middleware-patterns.md +14 -8
- package/skills/pikku-n8n-import/SKILL.md +12 -12
- package/skills/pikku-n8n-import/SPEC.md +3 -0
- package/skills/pikku-n8n-import/references/code-translation.md +26 -22
- package/skills/pikku-n8n-import/references/loops-and-control.md +7 -7
- package/skills/pikku-paraglide/SKILL.md +11 -6
- package/skills/pikku-permissions/SKILL.md +19 -15
- package/skills/pikku-product-second-opinion/README.md +3 -3
- package/skills/pikku-product-second-opinion/example/sample-report.md +15 -12
- package/skills/pikku-product-second-opinion/references/report-template.md +10 -7
- package/skills/pikku-queue/SKILL.md +1 -1
- package/skills/pikku-react/SKILL.md +53 -13
- package/skills/pikku-realtime/SKILL.md +52 -21
- package/skills/pikku-rpc/SKILL.md +4 -2
- package/skills/pikku-rtl/SKILL.md +1 -1
- package/skills/pikku-scenario/SKILL.md +131 -20
- package/skills/pikku-schedule/SKILL.md +6 -1
- package/skills/pikku-schema-ajv/SKILL.md +2 -2
- package/skills/pikku-schema-cfworker/SKILL.md +1 -1
- package/skills/pikku-security/SKILL.md +9 -5
- package/skills/pikku-services/SKILL.md +27 -18
- package/skills/pikku-services/references/audit-wire-service.md +14 -8
- package/skills/pikku-software-archaeology/scripts/validate.mjs +146 -69
- package/skills/pikku-tag-middleware/SKILL.md +1 -0
- package/skills/pikku-template-clone/SKILL.md +2 -1
- package/skills/pikku-trigger/SKILL.md +3 -3
- package/skills/pikku-websocket/SKILL.md +4 -3
- package/skills/pikku-workflow/SKILL.md +2 -2
- package/skills/pikku-workflow/references/workflow-reference.md +24 -10
|
@@ -38,7 +38,7 @@ pikku info tags --verbose # Understand project organization
|
|
|
38
38
|
### `pikkuServices(factory)` — singleton services (created once at startup)
|
|
39
39
|
|
|
40
40
|
```typescript
|
|
41
|
-
import { pikkuServices } from '#pikku'
|
|
41
|
+
import { pikkuServices } from '#pikku/function'
|
|
42
42
|
import { ConsoleLogger } from '@pikku/core/services'
|
|
43
43
|
import { JoseJWTService } from '@pikku/jose'
|
|
44
44
|
|
|
@@ -61,7 +61,7 @@ export const createSingletonServices = pikkuServices(
|
|
|
61
61
|
### `pikkuWireServices(factory)` — per-request services (fresh per HTTP request, queue job, CLI command, etc.)
|
|
62
62
|
|
|
63
63
|
```typescript
|
|
64
|
-
import { pikkuWireServices } from '#pikku'
|
|
64
|
+
import { pikkuWireServices } from '#pikku/function'
|
|
65
65
|
|
|
66
66
|
export const createWireServices = pikkuWireServices(
|
|
67
67
|
async (singletonServices, wire) => {
|
|
@@ -157,13 +157,16 @@ const getUser = pikkuFunc({
|
|
|
157
157
|
|
|
158
158
|
**Never write a `if (!service) throw ...` existence guard in a function body.** It is dead code, and it defeats the platform.
|
|
159
159
|
|
|
160
|
-
Optionality lives in exactly one place — `services.ts` / the `SingletonServices` declaration — and it means
|
|
160
|
+
Optionality lives in exactly one place — `services.ts` / the `SingletonServices` declaration — and it means _"this may not be created"_, not _"this may be missing at call time"_. A service is optional precisely because **nothing destructures it**, and the generated `requiredSingletonServices` manifest therefore never marks it for creation. The moment any wired function destructures it, Pikku creates it and guarantees it is there.
|
|
161
161
|
|
|
162
162
|
The types enforce this rather than merely documenting it. The inspector records the services destructured by every wired `func`, `permissions` **and** `middleware`, and emits them as `RequiredSingletonServices`. The generated function types then default their service parameter to:
|
|
163
163
|
|
|
164
164
|
```typescript
|
|
165
|
-
export type WiredSingletonServices = RequiredSingletonServices &
|
|
166
|
-
|
|
165
|
+
export type WiredSingletonServices = RequiredSingletonServices &
|
|
166
|
+
SingletonServices
|
|
167
|
+
export type WiredServices = SecretlessServices<
|
|
168
|
+
RequiredSingletonServices & Services
|
|
169
|
+
>
|
|
167
170
|
```
|
|
168
171
|
|
|
169
172
|
The `SecretlessServices<...>` wrapper is why `secrets` never appears in a
|
|
@@ -223,21 +226,21 @@ const createSingletonServices = pikkuServices(async (config) => {
|
|
|
223
226
|
|
|
224
227
|
### Built-in Services
|
|
225
228
|
|
|
226
|
-
| Service
|
|
227
|
-
|
|
|
228
|
-
| `ConsoleLogger`
|
|
229
|
-
| `JoseJWTService`
|
|
230
|
-
| `LocalSecretService`
|
|
231
|
-
| `LocalVariablesService`
|
|
232
|
-
| `PinoLogger`
|
|
233
|
-
| `createInvocationAudit`
|
|
234
|
-
| `createAuditedKysely`
|
|
229
|
+
| Service | Package | Purpose |
|
|
230
|
+
| ----------------------- | ---------------------- | --------------------------------------- |
|
|
231
|
+
| `ConsoleLogger` | `@pikku/core/services` | Console-based logging |
|
|
232
|
+
| `JoseJWTService` | `@pikku/jose` | JWT sign/verify via jose |
|
|
233
|
+
| `LocalSecretService` | `@pikku/core/services` | Local development secrets |
|
|
234
|
+
| `LocalVariablesService` | `@pikku/core/services` | Local environment variables |
|
|
235
|
+
| `PinoLogger` | `@pikku/pino` | Structured logging via Pino |
|
|
236
|
+
| `createInvocationAudit` | `@pikku/core/services` | Per-request audit buffer |
|
|
237
|
+
| `createAuditedKysely` | `@pikku/kysely` | Auto-capture DB queries as audit events |
|
|
235
238
|
|
|
236
239
|
## Complete Example
|
|
237
240
|
|
|
238
241
|
```typescript
|
|
239
242
|
// services.ts
|
|
240
|
-
import { pikkuServices, pikkuWireServices } from '#pikku'
|
|
243
|
+
import { pikkuServices, pikkuWireServices } from '#pikku/function'
|
|
241
244
|
import { ConsoleLogger } from '@pikku/core/services'
|
|
242
245
|
import { JoseJWTService } from '@pikku/jose'
|
|
243
246
|
|
|
@@ -249,9 +252,15 @@ class TodoStore {
|
|
|
249
252
|
this.todos.set(todo.id, todo)
|
|
250
253
|
return todo
|
|
251
254
|
}
|
|
252
|
-
async get(id: string) {
|
|
253
|
-
|
|
254
|
-
|
|
255
|
+
async get(id: string) {
|
|
256
|
+
return this.todos.get(id)
|
|
257
|
+
}
|
|
258
|
+
async list() {
|
|
259
|
+
return [...this.todos.values()]
|
|
260
|
+
}
|
|
261
|
+
async delete(id: string) {
|
|
262
|
+
this.todos.delete(id)
|
|
263
|
+
}
|
|
255
264
|
}
|
|
256
265
|
|
|
257
266
|
export const createSingletonServices = pikkuServices(async (config) => {
|
|
@@ -9,13 +9,15 @@ Pair with `createAuditedKysely` to auto-capture every Kysely query as an audit e
|
|
|
9
9
|
import { createInvocationAudit } from '@pikku/core/services'
|
|
10
10
|
import { createAuditedKysely } from '@pikku/kysely'
|
|
11
11
|
|
|
12
|
-
export const createWireServices = pikkuWireServices(
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
})
|
|
12
|
+
export const createWireServices = pikkuWireServices(
|
|
13
|
+
async (singletonServices, wire) => {
|
|
14
|
+
const audit = createInvocationAudit(singletonServices.audit, wire)
|
|
15
|
+
const kysely = singletonServices.kysely
|
|
16
|
+
? createAuditedKysely(singletonServices.kysely, { audit })
|
|
17
|
+
: undefined
|
|
18
|
+
return { audit, ...(kysely ? { kysely } : {}) }
|
|
19
|
+
}
|
|
20
|
+
)
|
|
19
21
|
```
|
|
20
22
|
|
|
21
23
|
The `audit` wire service is typed as `AuditLog` (from `@pikku/core`). Functions that emit custom events use it directly:
|
|
@@ -24,7 +26,11 @@ The `audit` wire service is typed as `AuditLog` (from `@pikku/core`). Functions
|
|
|
24
26
|
const deleteUser = pikkuFunc({
|
|
25
27
|
func: async ({ audit }, { userId }) => {
|
|
26
28
|
// The user identity comes from the wire session — the payload is metadata.
|
|
27
|
-
await audit.write({
|
|
29
|
+
await audit.write({
|
|
30
|
+
type: 'user.deleted',
|
|
31
|
+
source: 'explicit',
|
|
32
|
+
metadata: { userId },
|
|
33
|
+
})
|
|
28
34
|
// ...
|
|
29
35
|
},
|
|
30
36
|
})
|
|
@@ -5,118 +5,182 @@
|
|
|
5
5
|
//
|
|
6
6
|
// Usage: node validate.mjs <path-to-.knowledge-dir>
|
|
7
7
|
|
|
8
|
-
import { readFileSync, existsSync } from 'node:fs'
|
|
9
|
-
import { join, dirname } from 'node:path'
|
|
10
|
-
import { fileURLToPath } from 'node:url'
|
|
8
|
+
import { readFileSync, existsSync } from 'node:fs'
|
|
9
|
+
import { join, dirname } from 'node:path'
|
|
10
|
+
import { fileURLToPath } from 'node:url'
|
|
11
11
|
|
|
12
|
-
const here = dirname(fileURLToPath(import.meta.url))
|
|
13
|
-
const schemaDoc = JSON.parse(
|
|
12
|
+
const here = dirname(fileURLToPath(import.meta.url))
|
|
13
|
+
const schemaDoc = JSON.parse(
|
|
14
|
+
readFileSync(join(here, '..', 'references', 'blueprint.schema.json'), 'utf8')
|
|
15
|
+
)
|
|
14
16
|
|
|
15
|
-
const dir = process.argv[2]
|
|
16
|
-
if (!dir) {
|
|
17
|
+
const dir = process.argv[2]
|
|
18
|
+
if (!dir) {
|
|
19
|
+
console.error('usage: node validate.mjs <.knowledge dir>')
|
|
20
|
+
process.exit(2)
|
|
21
|
+
}
|
|
17
22
|
|
|
18
|
-
const errors = []
|
|
19
|
-
const warnings = []
|
|
23
|
+
const errors = []
|
|
24
|
+
const warnings = []
|
|
20
25
|
|
|
21
26
|
// --- minimal JSON-Schema-subset validator (type, required, properties, items, enum, minItems, pattern, $ref -> $defs) ---
|
|
22
27
|
function resolveRef(ref) {
|
|
23
|
-
const m = /^#\/\$defs\/(\w+)$/.exec(ref)
|
|
24
|
-
if (!m || !schemaDoc.$defs[m[1]]) throw new Error(`unresolvable $ref ${ref}`)
|
|
25
|
-
return schemaDoc.$defs[m[1]]
|
|
28
|
+
const m = /^#\/\$defs\/(\w+)$/.exec(ref)
|
|
29
|
+
if (!m || !schemaDoc.$defs[m[1]]) throw new Error(`unresolvable $ref ${ref}`)
|
|
30
|
+
return schemaDoc.$defs[m[1]]
|
|
26
31
|
}
|
|
27
32
|
|
|
28
33
|
function check(value, schema, path) {
|
|
29
|
-
if (schema.$ref)
|
|
34
|
+
if (schema.$ref)
|
|
35
|
+
schema = { ...resolveRef(schema.$ref), ...schema, $ref: undefined }
|
|
30
36
|
if (schema.enum && !schema.enum.includes(value)) {
|
|
31
|
-
errors.push(
|
|
32
|
-
|
|
37
|
+
errors.push(
|
|
38
|
+
`${path}: expected one of [${schema.enum.join(', ')}], got ${JSON.stringify(value)}`
|
|
39
|
+
)
|
|
40
|
+
return
|
|
33
41
|
}
|
|
34
|
-
const t = schema.type
|
|
42
|
+
const t = schema.type
|
|
35
43
|
if (t === 'object') {
|
|
36
44
|
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
37
|
-
errors.push(`${path}: expected object`)
|
|
45
|
+
errors.push(`${path}: expected object`)
|
|
46
|
+
return
|
|
38
47
|
}
|
|
39
48
|
for (const req of schema.required || []) {
|
|
40
|
-
if (!(req in value))
|
|
49
|
+
if (!(req in value))
|
|
50
|
+
errors.push(`${path}: missing required field "${req}"`)
|
|
41
51
|
}
|
|
42
52
|
for (const [k, v] of Object.entries(value)) {
|
|
43
|
-
if (schema.properties?.[k]) check(v, schema.properties[k], `${path}.${k}`)
|
|
53
|
+
if (schema.properties?.[k]) check(v, schema.properties[k], `${path}.${k}`)
|
|
44
54
|
}
|
|
45
55
|
} else if (t === 'array') {
|
|
46
|
-
if (!Array.isArray(value)) {
|
|
56
|
+
if (!Array.isArray(value)) {
|
|
57
|
+
errors.push(`${path}: expected array`)
|
|
58
|
+
return
|
|
59
|
+
}
|
|
47
60
|
if (schema.minItems && value.length < schema.minItems) {
|
|
48
|
-
errors.push(
|
|
61
|
+
errors.push(
|
|
62
|
+
`${path}: needs at least ${schema.minItems} item(s), has ${value.length}`
|
|
63
|
+
)
|
|
49
64
|
}
|
|
50
|
-
if (schema.items)
|
|
65
|
+
if (schema.items)
|
|
66
|
+
value.forEach((v, i) => check(v, schema.items, `${path}[${i}]`))
|
|
51
67
|
} else if (t === 'string') {
|
|
52
|
-
if (typeof value !== 'string') {
|
|
68
|
+
if (typeof value !== 'string') {
|
|
69
|
+
errors.push(`${path}: expected string`)
|
|
70
|
+
return
|
|
71
|
+
}
|
|
53
72
|
if (schema.pattern && !new RegExp(schema.pattern).test(value)) {
|
|
54
|
-
errors.push(`${path}: "${value}" does not match ${schema.pattern}`)
|
|
73
|
+
errors.push(`${path}: "${value}" does not match ${schema.pattern}`)
|
|
55
74
|
}
|
|
56
75
|
} else if (t === 'boolean' && typeof value !== 'boolean') {
|
|
57
|
-
errors.push(`${path}: expected boolean`)
|
|
76
|
+
errors.push(`${path}: expected boolean`)
|
|
58
77
|
} else if (t === 'number' && typeof value !== 'number') {
|
|
59
|
-
errors.push(`${path}: expected number`)
|
|
78
|
+
errors.push(`${path}: expected number`)
|
|
60
79
|
}
|
|
61
80
|
}
|
|
62
81
|
|
|
63
82
|
// --- load + per-file validation ---
|
|
64
83
|
// Files marked `x-optional` (the frontend layer) only validate when present, so a
|
|
65
84
|
// backend-only repo does not fail for lacking them.
|
|
66
|
-
const docs = {}
|
|
85
|
+
const docs = {}
|
|
67
86
|
for (const [filename, fileSchema] of Object.entries(schemaDoc.files)) {
|
|
68
|
-
const p = join(dir, filename)
|
|
87
|
+
const p = join(dir, filename)
|
|
69
88
|
if (!existsSync(p)) {
|
|
70
|
-
if (!fileSchema['x-optional']) errors.push(`${filename}: missing`)
|
|
71
|
-
continue
|
|
89
|
+
if (!fileSchema['x-optional']) errors.push(`${filename}: missing`)
|
|
90
|
+
continue
|
|
72
91
|
}
|
|
73
92
|
try {
|
|
74
|
-
docs[filename] = JSON.parse(readFileSync(p, 'utf8'))
|
|
93
|
+
docs[filename] = JSON.parse(readFileSync(p, 'utf8'))
|
|
75
94
|
} catch (e) {
|
|
76
|
-
errors.push(`${filename}: invalid JSON (${e.message})`)
|
|
95
|
+
errors.push(`${filename}: invalid JSON (${e.message})`)
|
|
96
|
+
continue
|
|
77
97
|
}
|
|
78
|
-
check(docs[filename], fileSchema, filename)
|
|
98
|
+
check(docs[filename], fileSchema, filename)
|
|
79
99
|
}
|
|
80
|
-
if (!existsSync(join(dir, 'blueprint.md'))) errors.push('blueprint.md: missing')
|
|
100
|
+
if (!existsSync(join(dir, 'blueprint.md'))) errors.push('blueprint.md: missing')
|
|
81
101
|
|
|
82
102
|
// --- cross-file referential checks ---
|
|
83
103
|
if (docs['domains.json'] && docs['commands.json']) {
|
|
84
|
-
const domains = new Set(
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const
|
|
104
|
+
const domains = new Set(
|
|
105
|
+
(docs['domains.json'].domains || []).map((d) => d.name)
|
|
106
|
+
)
|
|
107
|
+
const commandNames = new Set(
|
|
108
|
+
(docs['commands.json'].commands || []).map((c) => c.name)
|
|
109
|
+
)
|
|
110
|
+
const queryNames = new Set(
|
|
111
|
+
(docs['queries.json']?.queries || []).map((q) => q.name)
|
|
112
|
+
)
|
|
113
|
+
const eventNames = new Set(
|
|
114
|
+
(docs['events.json']?.events || []).map((e) => e.name)
|
|
115
|
+
)
|
|
88
116
|
|
|
89
117
|
const wantDomain = (owner, d) => {
|
|
90
|
-
if (d && !domains.has(d))
|
|
91
|
-
|
|
118
|
+
if (d && !domains.has(d))
|
|
119
|
+
errors.push(`${owner}: domain "${d}" not defined in domains.json`)
|
|
120
|
+
}
|
|
92
121
|
for (const c of docs['commands.json'].commands || []) {
|
|
93
|
-
wantDomain(`commands.json:${c.name}`, c.domain)
|
|
122
|
+
wantDomain(`commands.json:${c.name}`, c.domain)
|
|
94
123
|
for (const ev of c.eventsProduced || []) {
|
|
95
|
-
if (!eventNames.has(ev))
|
|
124
|
+
if (!eventNames.has(ev))
|
|
125
|
+
warnings.push(
|
|
126
|
+
`commands.json:${c.name} produces "${ev}" which is not in events.json`
|
|
127
|
+
)
|
|
96
128
|
}
|
|
97
129
|
}
|
|
98
|
-
for (const q of docs['queries.json']?.queries || [])
|
|
99
|
-
|
|
100
|
-
for (const
|
|
130
|
+
for (const q of docs['queries.json']?.queries || [])
|
|
131
|
+
wantDomain(`queries.json:${q.name}`, q.domain)
|
|
132
|
+
for (const e of docs['entities.json']?.entities || [])
|
|
133
|
+
wantDomain(`entities.json:${e.name}`, e.domain)
|
|
134
|
+
for (const ev of docs['events.json']?.events || [])
|
|
135
|
+
wantDomain(`events.json:${ev.name}`, ev.domain)
|
|
101
136
|
|
|
102
137
|
for (const s of docs['api.json']?.surfaces || []) {
|
|
103
|
-
const { type, name } = s.mapsTo || {}
|
|
104
|
-
if (type === 'command' && !commandNames.has(name))
|
|
105
|
-
|
|
106
|
-
|
|
138
|
+
const { type, name } = s.mapsTo || {}
|
|
139
|
+
if (type === 'command' && !commandNames.has(name))
|
|
140
|
+
errors.push(
|
|
141
|
+
`api.json:${s.method || ''} ${s.path}: maps to unknown command "${name}"`
|
|
142
|
+
)
|
|
143
|
+
if (type === 'query' && !queryNames.has(name))
|
|
144
|
+
errors.push(
|
|
145
|
+
`api.json:${s.method || ''} ${s.path}: maps to unknown query "${name}"`
|
|
146
|
+
)
|
|
147
|
+
if (type === 'event-ingress' && !eventNames.has(name))
|
|
148
|
+
errors.push(
|
|
149
|
+
`api.json:${s.method || ''} ${s.path}: event-ingress maps to unknown event "${name}" (state-changing webhooks should map to a command instead)`
|
|
150
|
+
)
|
|
107
151
|
}
|
|
108
152
|
// every domain's listed concepts should exist
|
|
109
153
|
for (const d of docs['domains.json'].domains || []) {
|
|
110
|
-
for (const c of d.commands || [])
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
154
|
+
for (const c of d.commands || [])
|
|
155
|
+
if (!commandNames.has(c))
|
|
156
|
+
warnings.push(
|
|
157
|
+
`domains.json:${d.name}: lists command "${c}" not in commands.json`
|
|
158
|
+
)
|
|
159
|
+
for (const q of d.queries || [])
|
|
160
|
+
if (!queryNames.has(q))
|
|
161
|
+
warnings.push(
|
|
162
|
+
`domains.json:${d.name}: lists query "${q}" not in queries.json`
|
|
163
|
+
)
|
|
164
|
+
for (const e of d.events || [])
|
|
165
|
+
if (!eventNames.has(e))
|
|
166
|
+
warnings.push(
|
|
167
|
+
`domains.json:${d.name}: lists event "${e}" not in events.json`
|
|
168
|
+
)
|
|
169
|
+
const policyNames = new Set(
|
|
170
|
+
(docs['policies.json']?.policies || []).map((p) => p.name)
|
|
171
|
+
)
|
|
172
|
+
for (const p of d.policies || [])
|
|
173
|
+
if (!policyNames.has(p))
|
|
174
|
+
warnings.push(
|
|
175
|
+
`domains.json:${d.name}: lists policy "${p}" not in policies.json`
|
|
176
|
+
)
|
|
115
177
|
}
|
|
116
178
|
// commands with no policies and no preconditions are suspicious for mutating ops
|
|
117
179
|
for (const c of docs['commands.json'].commands || []) {
|
|
118
180
|
if (!(c.policies || []).length && !(c.preconditions || []).length) {
|
|
119
|
-
warnings.push(
|
|
181
|
+
warnings.push(
|
|
182
|
+
`commands.json:${c.name}: no policies or preconditions — really unguarded, or missed extraction?`
|
|
183
|
+
)
|
|
120
184
|
}
|
|
121
185
|
}
|
|
122
186
|
}
|
|
@@ -124,13 +188,15 @@ if (docs['domains.json'] && docs['commands.json']) {
|
|
|
124
188
|
// --- frontend layer cross-checks (only when the optional frontend files exist) ---
|
|
125
189
|
if (docs['frontend-components.json']) {
|
|
126
190
|
const componentNames = new Set(
|
|
127
|
-
(docs['frontend-components.json'].components || []).map((c) => c.name)
|
|
128
|
-
)
|
|
191
|
+
(docs['frontend-components.json'].components || []).map((c) => c.name)
|
|
192
|
+
)
|
|
129
193
|
// routes should reference components that were actually inventoried
|
|
130
194
|
for (const r of docs['frontend-routes.json']?.routes || []) {
|
|
131
195
|
for (const c of r.usesComponents || []) {
|
|
132
196
|
if (!componentNames.has(c)) {
|
|
133
|
-
warnings.push(
|
|
197
|
+
warnings.push(
|
|
198
|
+
`frontend-routes.json:${r.path}: uses component "${c}" not in frontend-components.json`
|
|
199
|
+
)
|
|
134
200
|
}
|
|
135
201
|
}
|
|
136
202
|
}
|
|
@@ -138,7 +204,9 @@ if (docs['frontend-components.json']) {
|
|
|
138
204
|
// port-risk is unactionable
|
|
139
205
|
for (const c of docs['frontend-components.json'].components || []) {
|
|
140
206
|
if (c.rebuild === 'custom-logic' && !c.customLogic) {
|
|
141
|
-
warnings.push(
|
|
207
|
+
warnings.push(
|
|
208
|
+
`frontend-components.json:${c.name}: rebuild=custom-logic but no customLogic description — port risk is unactionable`
|
|
209
|
+
)
|
|
142
210
|
}
|
|
143
211
|
}
|
|
144
212
|
// data-fetching queries named on routes should resolve to a real query/command
|
|
@@ -146,11 +214,13 @@ if (docs['frontend-components.json']) {
|
|
|
146
214
|
const known = new Set([
|
|
147
215
|
...(docs['queries.json']?.queries || []).map((q) => q.name),
|
|
148
216
|
...(docs['commands.json']?.commands || []).map((c) => c.name),
|
|
149
|
-
])
|
|
217
|
+
])
|
|
150
218
|
for (const r of docs['frontend-routes.json']?.routes || []) {
|
|
151
219
|
for (const d of r.dataFrom || []) {
|
|
152
220
|
if (!known.has(d)) {
|
|
153
|
-
warnings.push(
|
|
221
|
+
warnings.push(
|
|
222
|
+
`frontend-routes.json:${r.path}: reads "${d}" which is not a known query/command`
|
|
223
|
+
)
|
|
154
224
|
}
|
|
155
225
|
}
|
|
156
226
|
}
|
|
@@ -160,14 +230,21 @@ if (docs['frontend-components.json']) {
|
|
|
160
230
|
// an inconsistent UI with no specific design findings = under-extraction
|
|
161
231
|
// (guarded on frontend.json alone — independent of the component inventory)
|
|
162
232
|
if (docs['frontend.json']) {
|
|
163
|
-
const consistency = docs['frontend.json'].designSystemConsistency
|
|
164
|
-
const findingCount = (docs['frontend.json'].designFindings || []).length
|
|
165
|
-
if (
|
|
166
|
-
|
|
233
|
+
const consistency = docs['frontend.json'].designSystemConsistency
|
|
234
|
+
const findingCount = (docs['frontend.json'].designFindings || []).length
|
|
235
|
+
if (
|
|
236
|
+
(consistency === 'mixed' || consistency === 'ad-hoc') &&
|
|
237
|
+
findingCount === 0
|
|
238
|
+
) {
|
|
239
|
+
warnings.push(
|
|
240
|
+
`frontend.json: designSystemConsistency="${consistency}" but designFindings is empty — name the specific broken patterns (interaction/theming/cross-page/…)`
|
|
241
|
+
)
|
|
167
242
|
}
|
|
168
243
|
}
|
|
169
244
|
|
|
170
|
-
for (const w of warnings) console.log(`WARN ${w}`)
|
|
171
|
-
for (const e of errors) console.log(`ERROR ${e}`)
|
|
172
|
-
console.log(
|
|
173
|
-
|
|
245
|
+
for (const w of warnings) console.log(`WARN ${w}`)
|
|
246
|
+
for (const e of errors) console.log(`ERROR ${e}`)
|
|
247
|
+
console.log(
|
|
248
|
+
`\n${errors.length} error(s), ${warnings.length} warning(s) across ${Object.keys(docs).length} files`
|
|
249
|
+
)
|
|
250
|
+
process.exit(errors.length ? 1 : 0)
|
|
@@ -6,6 +6,7 @@ description: 'Deprecated — use pikku-middleware instead. Tag middleware (addTa
|
|
|
6
6
|
# Deprecated: use `pikku-middleware`
|
|
7
7
|
|
|
8
8
|
Tag middleware is covered in the **`pikku-middleware`** skill, which also covers:
|
|
9
|
+
|
|
9
10
|
- `addHTTPMiddleware` (global / prefix-based)
|
|
10
11
|
- `addTagMiddleware` (tag-scoped)
|
|
11
12
|
- Middleware execution order and priority
|
|
@@ -26,8 +26,9 @@ commit, separate from any feature work.
|
|
|
26
26
|
|
|
27
27
|
`create-pikku` keeps only the chosen package manager's lockfile and deletes
|
|
28
28
|
the other, and for yarn it may have written an **empty** `yarn.lock` as a
|
|
29
|
-
marker. Commit the lockfile
|
|
29
|
+
marker. Commit the lockfile _after_ the first install has filled it in —
|
|
30
30
|
committing the empty placeholder pins nothing.
|
|
31
|
+
|
|
31
32
|
3. **Rename template identifiers.** Update `name` in the root `package.json`
|
|
32
33
|
(and any `@project/*` or other placeholder names) to the real project.
|
|
33
34
|
4. **Drop template-only artifacts.** Remove any `TEMPLATE.md`, demo docs, or
|
|
@@ -45,7 +45,7 @@ handler, and a handler can exist before any source is wired.
|
|
|
45
45
|
Define the target function that handles trigger events:
|
|
46
46
|
|
|
47
47
|
```typescript
|
|
48
|
-
import { wireTrigger } from '#pikku'
|
|
48
|
+
import { wireTrigger } from '#pikku/trigger'
|
|
49
49
|
|
|
50
50
|
wireTrigger({
|
|
51
51
|
name: string, // Trigger name (matches source)
|
|
@@ -60,7 +60,7 @@ wireTrigger({
|
|
|
60
60
|
Define the event source that fires triggers:
|
|
61
61
|
|
|
62
62
|
```typescript
|
|
63
|
-
import { wireTriggerSource } from '#pikku'
|
|
63
|
+
import { wireTriggerSource } from '#pikku/trigger'
|
|
64
64
|
|
|
65
65
|
wireTriggerSource({
|
|
66
66
|
name: string, // Must match a wireTrigger name
|
|
@@ -80,7 +80,7 @@ up a listener, calls `trigger.invoke(...)` for each event it sees, and returns a
|
|
|
80
80
|
teardown function:
|
|
81
81
|
|
|
82
82
|
```typescript
|
|
83
|
-
import { pikkuTriggerFunc } from '#pikku'
|
|
83
|
+
import { pikkuTriggerFunc } from '#pikku/trigger'
|
|
84
84
|
|
|
85
85
|
const source = pikkuTriggerFunc<
|
|
86
86
|
InputType, // Configuration input
|
|
@@ -94,8 +94,9 @@ wireChannel({
|
|
|
94
94
|
name: 'todos',
|
|
95
95
|
route: '/todos',
|
|
96
96
|
onMessageWiring: {
|
|
97
|
-
action: {
|
|
98
|
-
|
|
97
|
+
action: {
|
|
98
|
+
// ← the field to route on
|
|
99
|
+
create: { func: createTodo }, // ← its possible values
|
|
99
100
|
list: { func: listTodos, auth: false },
|
|
100
101
|
},
|
|
101
102
|
},
|
|
@@ -129,7 +130,7 @@ wireChannel({
|
|
|
129
130
|
onMessageWiring: {
|
|
130
131
|
action: {
|
|
131
132
|
authenticate: { func: authenticate, auth: false }, // No session required
|
|
132
|
-
subscribe: { func: subscribeTodos },
|
|
133
|
+
subscribe: { func: subscribeTodos }, // Session required
|
|
133
134
|
create: { func: createTodo },
|
|
134
135
|
},
|
|
135
136
|
},
|
|
@@ -64,8 +64,8 @@ import {
|
|
|
64
64
|
pikkuWorkflowComplexFunc,
|
|
65
65
|
} from '#pikku/workflow/pikku-workflow-types.gen.js'
|
|
66
66
|
|
|
67
|
-
// WRONG —
|
|
68
|
-
import { pikkuWorkflowFunc } from '#pikku'
|
|
67
|
+
// WRONG — the function leaf does not re-export them (TS2305)
|
|
68
|
+
import { pikkuWorkflowFunc } from '#pikku/function'
|
|
69
69
|
```
|
|
70
70
|
|
|
71
71
|
## Defining a workflow
|
|
@@ -6,15 +6,15 @@ Whether a step runs **inline** (same process/session, no queue round-trip) or is
|
|
|
6
6
|
|
|
7
7
|
- **Steps default to inline.** Most steps don't need their own worker; running them inline avoids a queue round-trip per step, so a normally-started workflow executes its whole chain in one orchestrator pass.
|
|
8
8
|
- **`workflowQueued: true` opts a function out.** Set it on the **function config** (`pikkuFunc` / `pikkuSessionlessFunc`, same level as `auth`/`expose`) to dispatch that step via the queue — for expensive/long-running steps that deserve their own worker, retry isolation, and concurrency limits. `workflowRetries` and `workflowTimeout` sit alongside it.
|
|
9
|
-
- **Run-level `inline` is separate** and only controls whether the
|
|
9
|
+
- **Run-level `inline` is separate** and only controls whether the _whole run_ executes in-process without queue infrastructure (set automatically when there is no `queueService`, or via `startWorkflow(..., { inline: true })`). It governs sleep handling, not per-step dispatch.
|
|
10
10
|
|
|
11
11
|
The rule (`dispatchStep`):
|
|
12
12
|
|
|
13
|
-
| Function `workflowQueued` | `queueService` present? | Result
|
|
14
|
-
|
|
15
|
-
| default / `false`
|
|
16
|
-
| `true`
|
|
17
|
-
| `true`
|
|
13
|
+
| Function `workflowQueued` | `queueService` present? | Result |
|
|
14
|
+
| ------------------------- | ----------------------- | ----------------------- |
|
|
15
|
+
| default / `false` | any | **inline** |
|
|
16
|
+
| `true` | yes | **queued** (own worker) |
|
|
17
|
+
| `true` | no | **throws** |
|
|
18
18
|
|
|
19
19
|
```typescript
|
|
20
20
|
// Push this one expensive step onto the queue; every other step stays inline:
|
|
@@ -24,7 +24,9 @@ export const renderLargeReport = pikkuSessionlessFunc({
|
|
|
24
24
|
workflowTimeout: '5m',
|
|
25
25
|
input: ReportInput,
|
|
26
26
|
output: ReportOutput,
|
|
27
|
-
func: async (services, data) => {
|
|
27
|
+
func: async (services, data) => {
|
|
28
|
+
/* ... */
|
|
29
|
+
},
|
|
28
30
|
})
|
|
29
31
|
```
|
|
30
32
|
|
|
@@ -39,13 +41,25 @@ Usually auto-scaffolded via `scaffold.workflow`. To wire by hand:
|
|
|
39
41
|
|
|
40
42
|
```typescript
|
|
41
43
|
// Start a workflow
|
|
42
|
-
wireHTTP({
|
|
44
|
+
wireHTTP({
|
|
45
|
+
method: 'post',
|
|
46
|
+
route: '/onboard',
|
|
47
|
+
func: workflowStart('onboardUser'),
|
|
48
|
+
})
|
|
43
49
|
|
|
44
50
|
// Execute workflow steps (called by the orchestrator)
|
|
45
|
-
wireHTTP({
|
|
51
|
+
wireHTTP({
|
|
52
|
+
method: 'post',
|
|
53
|
+
route: '/onboard/run',
|
|
54
|
+
func: workflow('onboardUser'),
|
|
55
|
+
})
|
|
46
56
|
|
|
47
57
|
// Check workflow status
|
|
48
|
-
wireHTTP({
|
|
58
|
+
wireHTTP({
|
|
59
|
+
method: 'get',
|
|
60
|
+
route: '/onboard/status/:runId',
|
|
61
|
+
func: workflowStatus('onboardUser'),
|
|
62
|
+
})
|
|
49
63
|
```
|
|
50
64
|
|
|
51
65
|
## Suspend / resume example
|