@pikku/inspector 0.12.47 → 0.12.49

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/CHANGELOG.md +218 -0
  2. package/LICENSE +106 -0
  3. package/README.md +28 -2
  4. package/dist/add/add-feature.d.ts +19 -0
  5. package/dist/add/add-feature.js +200 -0
  6. package/dist/add/add-functions.js +38 -9
  7. package/dist/add/add-workflow.js +22 -29
  8. package/dist/add/scenario-instrumentation.d.ts +24 -0
  9. package/dist/add/scenario-instrumentation.js +29 -0
  10. package/dist/error-codes.d.ts +3 -0
  11. package/dist/error-codes.js +3 -0
  12. package/dist/index.d.ts +3 -0
  13. package/dist/index.js +1 -0
  14. package/dist/inspector.js +39 -2
  15. package/dist/types.d.ts +43 -0
  16. package/dist/utils/extract-function-name.js +27 -0
  17. package/dist/utils/post-process.d.ts +26 -0
  18. package/dist/utils/post-process.js +92 -0
  19. package/dist/utils/serialize-inspector-state.d.ts +2 -1
  20. package/dist/utils/serialize-inspector-state.js +2 -0
  21. package/dist/utils/workflow/dsl/extract-dsl-workflow.js +203 -19
  22. package/dist/utils/workflow/dsl/patterns.d.ts +21 -0
  23. package/dist/utils/workflow/dsl/patterns.js +30 -0
  24. package/dist/utils/workflow/dsl/validation.d.ts +5 -0
  25. package/dist/utils/workflow/dsl/validation.js +9 -0
  26. package/dist/utils/workflow/graph/convert-dsl-to-graph.js +36 -0
  27. package/dist/utils/workflow/graph/workflow-graph.types.d.ts +5 -1
  28. package/dist/visit.js +2 -0
  29. package/package.json +4 -3
  30. package/src/add/add-feature.test.ts +306 -0
  31. package/src/add/add-feature.ts +253 -0
  32. package/src/add/add-functions.ts +47 -9
  33. package/src/add/add-scenario-step.test.ts +311 -0
  34. package/src/add/add-workflow-silent-drops.test.ts +98 -0
  35. package/src/add/add-workflow.ts +25 -37
  36. package/src/add/scenario-instrumentation.test.ts +89 -0
  37. package/src/add/scenario-instrumentation.ts +36 -0
  38. package/src/add/scenario-marker.test.ts +89 -0
  39. package/src/add/scenario-not-extractable.test.ts +159 -0
  40. package/src/add/scenario-services.test.ts +103 -0
  41. package/src/add/scenario-skip.test.ts +83 -0
  42. package/src/error-codes.ts +3 -0
  43. package/src/index.ts +6 -0
  44. package/src/inspector.ts +50 -1
  45. package/src/tsconfig-paths.test.ts +90 -0
  46. package/src/types.ts +45 -0
  47. package/src/utils/extract-function-name.ts +36 -0
  48. package/src/utils/post-process.test.ts +30 -1
  49. package/src/utils/post-process.ts +103 -0
  50. package/src/utils/serialize-inspector-state.ts +8 -1
  51. package/src/utils/workflow/dsl/extract-dsl-workflow.ts +236 -22
  52. package/src/utils/workflow/dsl/patterns.ts +38 -0
  53. package/src/utils/workflow/dsl/validation.ts +12 -0
  54. package/src/utils/workflow/graph/convert-dsl-to-graph-skip.test.ts +35 -0
  55. package/src/utils/workflow/graph/convert-dsl-to-graph.ts +36 -0
  56. package/src/utils/workflow/graph/workflow-graph.types.ts +5 -0
  57. package/src/visit.ts +2 -0
  58. package/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md CHANGED
@@ -1,3 +1,221 @@
1
+ ## 0.12.49
2
+
3
+ ### Patch Changes
4
+
5
+ - 426610a: Scenario instrumentation is no longer scaffolded into projects, and no longer deploys.
6
+
7
+ `scaffold.scenarios` generated four functions — `pikkuScenarioTakeLiveCoverage`, `pikkuScenarioResetLiveCoverage`, `pikkuScenarioResetStubs`, `pikkuScenarioGetStubCalls` — into the project's own source. As project source they were indistinguishable from application code: registered in the app bootstrap, listed in the app's function and RPC meta, and shipped `expose: true` inside every deployed bundle. Coverage and stub inspection are things you do to a development server; production carried two endpoints that fingerprint the build and one that resets a global tracker, gated only by whether a metadata file happened to sit beside the bundle.
8
+
9
+ `pikku dev` now registers the implementations itself, after the app bootstrap. Nothing is generated, nothing is written to the project, and a bundle cannot carry what was never in its bootstrap — `pikku serve` and every deployed unit have no trace of them. The scenario runner reaches them over `/rpc/<name>` exactly as before.
10
+
11
+ Also:
12
+ - The inspector ignores these four names wherever it finds them, so a project that has not regenerated — and still has the scaffolded file checked in — stops deploying it immediately. Codegen deletes the retired scaffold on its next run.
13
+ - They no longer count towards a project's function total, so `pikku scenario --coverage` stops reporting four permanently-uncovered functions that were never the project's to cover.
14
+ - The instrumentation no longer carries schemas (there was nothing to validate but one optional string), which drops the `zod` dependency the scaffold silently required of every project that enabled it.
15
+ - They are registered sessionless, so `scaffold.scenarios: true` — as opposed to `'auth'` — now genuinely means "no session required". As a sessioned `pikkuFunc` with `auth: false`, it demanded a session anyway and logged a warning saying so.
16
+
17
+ - 09973b9: Scenarios, features and steps no longer reach a deployment.
18
+
19
+ Steps were already held back from the app bootstrap, so a deployed server never imported a step body. Everything _about_ a scenario still travelled with the application: a `pikkuScenario(...)` is a function, so its name, schemas and hashes sat in the app function meta; the schemas it and its steps validate against sat in the app's `register.gen.ts` — on one project 458 of the 582 registered schemas belonged to tests; its name sat in the internal RPC meta; and because a scenario is _also_ a workflow, the inspector synthesised a `wf-orchestrator-<scenario>` queue worker for each one. The deploy analyzer, which reads inspector state rather than the partitioned codegen output, then read all of it back as application code: a unit per scenario, a `WorkflowDefinition` per scenario, and a real queue per scenario. A 13-scenario suite turned into 13 production queues named after tests, waiting for a provider to create them.
20
+
21
+ The existing scenario/app partition is now applied everywhere it was missing. `FunctionRuntimeMeta` gains a `scenario` marker (the counterpart of `scenarioStep`) so a scenario body is recognisable without walking the workflow graph; scenario bodies join their steps on the scenario side of the function-meta and registration split; schemas only a scenario or step needs are written and registered under `.pikku/scenarios/schemas/` and imported by the scenario bootstrap alone; scenario names are dropped from the internal RPC meta; no orchestrator queue worker is synthesised for a scenario; and the deploy analyzer drops both scenario functions and scenario workflows before it decides what a deployment contains.
22
+
23
+ The MCP metas are keyed by wiring rather than by function, so a scenario wired as an MCP tool, resource or prompt was the one id that still reached the manifest after the function and workflow filters — as an endpoint on the gateway plus a gateway dependency on a unit that was never emitted. Those ids are now filtered too.
24
+
25
+ `scenarioSchemaDirectory` is rejected when it resolves to the same directory as `schemaDirectory`. A schema write owns its directory — it emits `register.gen.ts` and prunes every schema file its own required-set does not name — so sharing one would replace the application register with the scenario-only one and delete the app's schema files, which nothing downstream can detect.
26
+
27
+ Nothing changes for `pikku scenario run` — the scenario bootstrap still registers every scenario, feature, step, meta and schema. What changes is that a bundle stops carrying them.
28
+
29
+ - Updated dependencies [8a2c993]
30
+ - Updated dependencies [a261006]
31
+ - Updated dependencies [09973b9]
32
+ - @pikku/core@0.12.71
33
+
34
+ ## 0.12.48
35
+
36
+ ### Patch Changes
37
+
38
+ - 04b8607: Make the unmodellable-for-of diagnostic tell you what is actually wrong. It always blamed the iterable ("its iterable must be a data array"), which was false whenever the real cause was a `workflow.do` nested inside an `if`/`switch` — a DSL fanout body is a flat list of steps with no branch member, so the diagnostic now says so and points at `.filter` or `pikkuWorkflowComplexFunc`. Also stops the same diagnostic firing on a `for-of` that contains no workflow call at all: a loop that only massages locals has no step to lose, so erroring on it was a false positive.
39
+ - 7c8f015: Hard-error on a for-of a DSL workflow can't model, instead of silently dropping it.
40
+
41
+ A `pikkuWorkflowFunc` (DSL) whose body contained a computed/counting loop — e.g. `for (const i of [...Array(n).keys()])` — used to serialize with **zero steps**: the DSL extractor only models a for-of as a sequential fanout over a data-array identifier/field (`data.items`), so a non-path iterable made `extractSequentialFanout` return null and the extractor dropped the loop _and every `workflow.do` inside it_. The invoked functions then never entered `invokedFunctions`, so they got no `addFunction()` registration and threw `Function not found` at runtime — a silent codegen footgun that bricked every prod sandbox create.
42
+
43
+ Now the extractor pushes a validation error naming the offending for-of, so `pikkuWorkflowFunc` reports `INVALID_DSL_WORKFLOW` at codegen. A genuine control-flow/counting loop belongs in `pikkuWorkflowComplexFunc`, which falls back to the basic AST walk that _does_ register loop-invoked functions.
44
+
45
+ - 5962e51: Give the inspector's program the project's tsconfig path mappings.
46
+
47
+ The inspector builds its own TypeScript program rather than reusing the project's, which is what keeps a cold run affordable — but it hand-rolled its compiler options and so had no `paths`. An import the project resolves only through a `paths` entry therefore did not resolve inside the inspector, and the factory the inspected function was passed to came back as `any`. A function whose input type comes from that factory's contextual type — rather than an explicit generic or a named `input:` schema — then recorded no input at all, silently, with no diagnostic.
48
+
49
+ `inspect` takes a new `tsconfig` option (the CLI passes `config.tsconfig`, including on a setup-only run, where `schemaConfig` is absent). Only resolution keys are copied across — `baseUrl`, `paths`, `rootDirs`, `pathsBasePath`. Type-inference options such as `strict` deliberately stay the inspector's own, because changing those would change the types it records.
50
+
51
+ This was latent rather than live: the generic path reads type _nodes_, the schema path reads an identifier, output types come from the handler body, and JSON schemas are generated by a separate tsconfig-aware program — so nothing in the tree reached the contextual-type fallback. Regenerating the e2e project and the `functions` template produces byte-identical output.
52
+
53
+ - 5962e51: Add `pikkuFeature`, a grouping primitive for scenarios.
54
+
55
+ A feature groups scenarios the way gherkin's `Feature:` groups `Scenario:`, and gets `Examples:` for free as an ordinary loop:
56
+
57
+ ```ts
58
+ export const credentialFeature = pikkuFeature({
59
+ name: 'Credential API',
60
+ tags: ['credential'],
61
+ before: startsMockOAuthServer,
62
+ after: stopsMockOAuthServer,
63
+ scenarios: [
64
+ credentialLazyLoadScenario,
65
+ ...['stripe', 'google', 'hmac-key'].map((name) => ({
66
+ scenario: credentialRoundTripScenario,
67
+ data: { name },
68
+ })),
69
+ ],
70
+ })
71
+ ```
72
+
73
+ - Scenarios are referenced by **imported identifier**, not by string name, so a renamed or deleted scenario is a compile error rather than a silent skip. A `{ scenario, data }` entry's `data` is typed against that scenario's own input.
74
+ - Feature hooks run **once around the whole group** (`before → a → b → c → after`), not per scenario, and `after` runs in a `finally`. Per-scenario setup stays the scenario's own `before`; gherkin's `Background:` is deliberately not expressible.
75
+ - A scenario's effective tags are its own plus its feature's, so `--tags credential` selects through the feature.
76
+ - New `--features` selector on `pikku scenario run`, and `pikku scenario list` now prints features with their scenarios indented. Every filter narrows the same plan, so narrowing a feature to two of its five scenarios still runs its hooks exactly once around those two.
77
+ - The **feature is the run unit**: `--flows` on a scenario whose every feature entry carries `data` errors and names the features containing it, because the feature is what supplies that data. A scenario referenced bare anywhere, or in no feature at all, still runs standalone.
78
+
79
+ `pikkuFeature` infers its scenario list with a `const` generic, so `CoreFeature['scenarios']` is `readonly` — otherwise the emitted `addFeature(id, feature)` call does not typecheck.
80
+
81
+ Membership is resolved at runtime by object identity — `pikkuScenario` returns its config verbatim, so a feature holds the very object that was registered. That is what lets the scenario list be built by a loop, which no static analysis could enumerate. It also means a scenario constructed inline inside a feature is never registered, and is reported as unresolved rather than silently running as something else.
82
+
83
+ - 5962e51: Add `before` / `after` hooks to `pikkuScenario`, and make an unextractable scenario a hard error.
84
+
85
+ A scenario config now takes `before` and `after`. Both have the same signature as `func` — `(services, data, wire)` — with the return value discarded, so there is no new type to learn and a hook reaches the app the same way the body does, through `wire.actors`:
86
+
87
+ ```ts
88
+ export const credentialScenario = pikkuScenario({
89
+ title: 'A credential is loaded on first use',
90
+ tags: ['scenario', 'credential'],
91
+ before: resetsCredentials,
92
+ after: removesInstalledAddon,
93
+ func: async (services, data, { scenario, actors }) => { ... },
94
+ })
95
+ ```
96
+
97
+ - `before` throwing skips the body and fails the run, but `after` still runs.
98
+ - `after` always runs, in a `finally`. Throwing fails a run that would otherwise have passed; on an already-failed run it attaches as the `cause` and never replaces the original error.
99
+ - Neither runs when the run is suspended or waiting — teardown only fires at a terminal outcome.
100
+ - Hooks are not ladder rows: the runner records nothing for them, and a failure is labelled by phase via the new `ScenarioHookError`.
101
+ - Hooks are scenario-only. A `before`/`after` on a `pikkuWorkflowFunc` never runs — a workflow is durable and resumable, so a callback that reran on every replay would have no honest meaning.
102
+
103
+ Two fixes that scenarios needed to be safe to write:
104
+ - A closure in a complex-workflow or scenario body is no longer held to the DSL statement whitelist. A single `try`/`catch` inside any callback previously failed extraction, and the fallback path understands `do`/`sleep` but not `step`/`given`/`when`/`then` — so the scenario registered with **zero steps** and passed vacuously, with no diagnostic. Plain DSL workflows still descend into callbacks, which is what validates fanout bodies.
105
+ - New `PKU679`: a scenario that fails DSL extraction is now a critical error and refuses to register, instead of silently registering empty. A scenario that declares no input parameter at all is legitimate and still extracts.
106
+
107
+ - a436645: Redesign the console's scenarios screen as living documentation of a project's BDD features.
108
+
109
+ The inspector now statically extracts `pikkuFeature` declarations — name, description, tags, the scenarios each one groups (including `{ scenario, data }` examples), and whether it declares `before`/`after` — and the CLI writes them to `<outDir>/scenarios/features.gen.json`, which `MetaService.getFeaturesMeta()` reads and the console addon returns from `getAllMeta`.
110
+
111
+ The scenarios page reads that back as a document: features on the left, and on the right the selected feature's scenarios, each rendered as the given/when/then ladder of prose its author actually wrote, with repeats shown as `for each x in xs`, `Examples:` tables for parameterised entries, skip reasons stated rather than hidden, and each scenario's cast of personas inline. The Flows/Personas segmented control is gone; tags filter the document the same way `pikku scenario run --tags` filters a run.
112
+
113
+ - 47478a4: Let a scenario declare why it is held out of a default run.
114
+
115
+ `pikkuScenario({ skip: 'why' })` keeps the scenario in the plan and reports it as `SKIP <name> (<reason>)` on the ladder, instead of the alternatives available until now: deleting it, commenting it out, or leaving it red. Naming it directly with `--flows` clears the quarantine and runs it; selecting the feature it belongs to does not, because a feature is a group and running the group should not silently drag a quarantined member in.
116
+
117
+ The run report's `skipped` list now carries a reason per scenario rather than assuming `--no-browser`, so a browser scenario held back on a machine with no browser reads differently from one the project quarantined itself.
118
+
119
+ `@pikku/console` gains a test id on the addon detail page's Setup tab, which was previously only reachable through its translated label.
120
+
121
+ - 2434f1b: Fix two silent losses in scenario step extraction.
122
+
123
+ Destructuring a scenario step result (`const { threadId } = await scenario.given(...)`) dropped the
124
+ step from the graph without a diagnostic — the step stayed fully typed and present in the step map,
125
+ then failed at runtime with `Function not found`. It now reports PKU679, like every other step form
126
+ the DSL cannot model.
127
+
128
+ A constant referenced as step input (`{ resourceId: RESOURCE_ID }`) was serialized as
129
+ `{ $ref: 'trigger', path: 'RESOURCE_ID' }` — a read of a trigger field that does not exist. A `const`
130
+ with a literal initializer is now inlined as that literal.
131
+
132
+ - 5962e51: Add `template` to `pikkuScenarioStep`, so a step's reported prose names the values it was called with.
133
+
134
+ `description` documents what a step does, for the console and for whoever reads the source. `template` is what a reader of the report sees, with `{placeholders}` filled from the input the step was actually called with:
135
+
136
+ ```ts
137
+ export const seesAddonCard = pikkuScenarioStep<
138
+ { packageName: string; state?: 'installed' | 'available' },
139
+ { visible: true },
140
+ true
141
+ >({
142
+ name: 'seesAddonCard',
143
+ description: 'sees an addon in the gallery',
144
+ template: 'sees {state} addon {packageName}',
145
+ browser: true,
146
+ func: async (_services, { packageName, state }, { browser }) => { … },
147
+ })
148
+ ```
149
+
150
+ ```
151
+ Then the admin sees at least 10 addons on offer ✓ 3ms
152
+ When the admin searches for stripe ✓ 10ms
153
+ Then the admin sees available addon @pikku/addon-stripe ✓ 77ms
154
+ ```
155
+
156
+ Previously the only way to get that was a `description` at every call site, which meant writing the sentence once per call rather than once per step — and a call site that forgot it reported the same sentence three times in a row.
157
+ - A placeholder with no recorded value renders as nothing and the surrounding whitespace collapses, so an omitted optional input reads as a shorter sentence rather than leaking a literal `{state}` into the report. Type placeholder values so they read as words (`state?: 'installed' | 'available'`, not `installed?: boolean`).
158
+ - A call-site `description` still wins, the same way it already won over the step's `description`.
159
+ - `renderStepTemplate` is exported from `@pikku/core/workflow` alongside `composeStepProse`, so the CLI reporter and the console render identically.
160
+
161
+ Scenario steps now record their input on the run (`inlineStep` persisted `null` for every inline step, so there was nothing for a reporter to interpolate). This is what `getRunSteps` already exposes as `data` for RPC steps.
162
+
163
+ A step called from a loop gets its template too. Its durable name is built at runtime (`sees @pikku/addon-todos`) from a declaration the static meta records verbatim (`sees ${packageName}`), so the two can never match by name — it used to fall back to the bare name, with no keyword, actor or template:
164
+
165
+ ```
166
+ sees @pikku/addon-console ✓ 85ms
167
+ Then the admin sees installed addon @pikku/addon-console ✓ 92ms
168
+ ```
169
+
170
+ The join is by **step function**. A scenario step is dispatched by name exactly as an RPC is, so it now records that name in the run's existing `rpcName` slot — no new field, no schema change in any workflow store. Nothing dispatches off that value anywhere; step identity always comes from the code being replayed.
171
+
172
+ To keep the slot honest, a scenario step is now its own **kind of RPC**, alongside public / private / remote: `FunctionMeta.scenarioStep` marks it, and `rpcExposed` refuses it even if something marks it `expose: true`. Steps were already left out of the RPC registry; this makes "never network-callable" a property the runtime enforces rather than one the registration path happens to produce.
173
+
174
+ `collectScenarioStepProse` now returns `{ byStepName, byStepFunc }` rather than a bare `Map`, and `buildStepLadder` takes that. The step name still wins; the function index only decides steps recorded under a name no declaration carries, and a function called from several sites that disagree on their prose is left out rather than guessed at.
175
+
176
+ - 5962e51: Add `pikkuScenarioStep` — named, typed scenario steps whose body is an ordinary pikku function.
177
+
178
+ A scenario step is referenced by typed string name, the same way `workflow.do` references an RPC, and is checked against a generated `FlattenedScenarioStepMap`:
179
+
180
+ ```ts
181
+ export const buysAnApple = pikkuScenarioStep({
182
+ name: 'buysAnApple',
183
+ description: 'buys an apple',
184
+ func: async (services, data: { qty: number }) => { ... },
185
+ })
186
+
187
+ await scenario.given('buys an apple', 'buysAnApple', { qty: 1 }, { actor: actors.shopper })
188
+ // renders: Given the shopper buys an apple
189
+ ```
190
+
191
+ - `given`/`when`/`then` are sugar over `step`, setting only the prose prefix. The runner renders a step ladder from the recorded run.
192
+ - Steps default to `retries: 0` — a failed assertion is not retried.
193
+ - Steps are deliberately **not** registered as RPCs, so a browser-driving step is never network-callable.
194
+ - `browser: true` steps receive a browser handle on the wire. `@pikku/playwright` is a new package providing the Playwright-backed provider, signing each actor's browser context in through the same actor path the HTTP actors use. Without a provider, `pikku scenario run --no-browser` **skips** browser scenarios instead of failing them.
195
+ - New diagnostics: PKU677 (a `browser: true` step called without an actor) and PKU678 (a step target that is not a static string literal).
196
+ - Fixes `--no-<flag>` boolean negation in the CLI command parser, which previously parsed as an unknown option.
197
+ - Fixes PKU673 (a scenario func destructuring services), which never fired because it ran before function meta existed; it now runs in post-processing.
198
+ - Fixes scenario/workflow steps nested in `for...of` and `Promise.all` being dropped from workflow meta.
199
+
200
+ - Updated dependencies [539ee0b]
201
+ - Updated dependencies [a1a6816]
202
+ - Updated dependencies [dc3e11e]
203
+ - Updated dependencies [24da616]
204
+ - Updated dependencies [04bfe3f]
205
+ - Updated dependencies [5962e51]
206
+ - Updated dependencies [5962e51]
207
+ - Updated dependencies [cd6453c]
208
+ - Updated dependencies [a436645]
209
+ - Updated dependencies [46cf63e]
210
+ - Updated dependencies [9e666bc]
211
+ - Updated dependencies [1c841d8]
212
+ - Updated dependencies [47478a4]
213
+ - Updated dependencies [9e666bc]
214
+ - Updated dependencies [5962e51]
215
+ - Updated dependencies [5962e51]
216
+ - Updated dependencies [61b9bf8]
217
+ - @pikku/core@0.12.70
218
+
1
219
  ## 0.12.47
2
220
 
3
221
  ### Patch Changes
package/LICENSE ADDED
@@ -0,0 +1,106 @@
1
+ Business Source License 1.1
2
+
3
+ Parameters
4
+
5
+ Licensor: Yasser Fadl and Pikku contributors
6
+ Licensed Work: TypeScript inspector for Pikku — discovers functions and wirings to drive code generation (@pikku/inspector)
7
+ Pikku Console (@pikku/console)
8
+ The Licensed Work is (c) 2021 - present Yasser Fadl and Pikku contributors
9
+ Additional Use Grant: You may make production use of the Licensed Work for any purpose,
10
+ including creating free or open source software. However, you may
11
+ not offer as a commercial product or service any software featuring
12
+ a visual or graphical interface for authoring, editing, building,
13
+ or managing Pikku code, Pikku functions, Pikku handlers, or Pikku
14
+ workflows.
15
+ Change Date: Four years from the date the Licensed Work was first made available
16
+ Change License: MIT License
17
+
18
+ For information about alternative licensing arrangements, contact yasser.fadl@gmail.com
19
+
20
+ Notice
21
+
22
+ The Business Source License (this document, or the "License") is not an Open Source
23
+ license. However, the Licensed Work will eventually be made available under an Open
24
+ Source License, as stated in this License.
25
+
26
+ License text copyright (c) 2017 MariaDB Corporation Ab, All Rights Reserved.
27
+ "Business Source License" is a trademark of MariaDB Corporation Ab.
28
+
29
+ -----------------------------------------------------------------------------
30
+
31
+ Business Source License 1.1
32
+
33
+ Terms
34
+
35
+ The Licensor hereby grants you the right to copy, modify, create derivative works,
36
+ redistribute, and make non-production use of the Licensed Work. The Licensor may make
37
+ an Additional Use Grant, above, permitting limited production use.
38
+
39
+ Effective on the Change Date, or the fourth anniversary of the first publicly
40
+ available distribution of a specific version of the Licensed Work under this License,
41
+ whichever comes first, the Licensor hereby grants you rights under the terms of the
42
+ Change License, and the rights granted in the paragraph above terminate.
43
+
44
+ If your use of the Licensed Work does not comply with the requirements currently in
45
+ effect as described in this License, you must purchase a commercial license from the
46
+ Licensor, its affiliated entities, or authorized resellers, or you must refrain from
47
+ using the Licensed Work.
48
+
49
+ All copies of the original and modified Licensed Work, and derivative works of the
50
+ Licensed Work, are subject to this License. This License applies separately for each
51
+ version of the Licensed Work and the Change Date may vary for each version of the
52
+ Licensed Work released by Licensor.
53
+
54
+ You must conspicuously display this License on each original or modified copy of the
55
+ Licensed Work. If you receive the Licensed Work in original or modified form from a
56
+ third party, the terms and conditions set forth in this License apply to your use of
57
+ that work.
58
+
59
+ Any use of the Licensed Work in violation of this License will automatically terminate
60
+ your rights under this License for the current and all other versions of the Licensed
61
+ Work.
62
+
63
+ This License does not grant you any right in any trademark or logo of Licensor or its
64
+ affiliates (provided that you may use a trademark or logo of Licensor as expressly
65
+ required by this License).
66
+
67
+ TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON AN "AS IS"
68
+ BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS, EXPRESS OR IMPLIED,
69
+ INCLUDING (WITHOUT LIMITATION) WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
70
+ PURPOSE, NON-INFRINGEMENT, AND TITLE.
71
+
72
+ MariaDB hereby grants you permission to use this License's text to license your works,
73
+ and to refer to it using the trademark "Business Source License", as long as you
74
+ comply with the Covenants of Licensor below.
75
+
76
+ Covenants of Licensor
77
+
78
+ In consideration of the right to use this License's text and the "Business Source
79
+ License" name and trademark, Licensor covenants to MariaDB, and to all other
80
+ recipients of the licensed work to be provided by Licensor:
81
+
82
+ 1. To specify as the Change License the GPL Version 2.0 or any later version, or a
83
+ license that is compatible with GPL Version 2.0 or a later version, where
84
+ "compatible" means that software provided under the Change License can be included
85
+ in a program with software provided under GPL Version 2.0 or a later version.
86
+ Licensor may specify additional Change Licenses without limitation.
87
+
88
+ 2. To either: (a) specify an additional grant of rights to use that does not impose
89
+ any additional restriction on the right granted in this License, as the Additional
90
+ Use Grant; or (b) insert the text "None".
91
+
92
+ 3. To specify a Change Date.
93
+
94
+ 4. Not to modify this License in any other way.
95
+
96
+ -----------------------------------------------------------------------------
97
+
98
+ Note: Every other Pikku package — @pikku/core, the runtimes, the services, the clients,
99
+ and @pikku/skills — is licensed under the MIT License. Each package ships its own LICENSE
100
+ file stating its terms; that file governs, and this one covers only the Licensed Work
101
+ named above.
102
+
103
+ In particular, the Pikku agent skills (@pikku/skills) are MIT. They are the instruction
104
+ set coding agents read to build, wire and deploy Pikku projects, and they are meant to be
105
+ adopted, forked and extended by any harness — so their terms do not depend on the CLI
106
+ that happens to install them.
package/README.md CHANGED
@@ -1,3 +1,29 @@
1
- # Pikku CLI
1
+ # @pikku/inspector
2
2
 
3
- This package is part of @pikku/mono and is responsible for all the intropsection and file generations.
3
+ The TypeScript inspector behind the Pikku CLI. Walks your source with the
4
+ TypeScript compiler API to discover functions, wirings and their types, and
5
+ produces the state the CLI generates code from.
6
+
7
+ Most users get this transitively via `@pikku/cli`; install it directly only if
8
+ you are building your own tooling on top of Pikku's introspection.
9
+
10
+ ## Install
11
+
12
+ ```bash
13
+ npm install @pikku/inspector
14
+ ```
15
+
16
+ ## Usage
17
+
18
+ ```typescript
19
+ import { inspect, getInitialInspectorState } from '@pikku/inspector'
20
+
21
+ const state = getInitialInspectorState()
22
+ await inspect(['src'], tsconfigPath, state)
23
+ ```
24
+
25
+ Diagnostics come back as `CodedDiagnostic` values with a stable `ErrorCode`.
26
+
27
+ ## Docs
28
+
29
+ https://pikku.dev/docs
@@ -0,0 +1,19 @@
1
+ import type { AddWiring } from '../types.js';
2
+ /**
3
+ * Inspector for `pikkuFeature()` calls.
4
+ *
5
+ * A feature is the document structure the console renders — its name,
6
+ * description, tags and the order of its scenarios are what a reader sees — so
7
+ * everything the config states literally is extracted here.
8
+ *
9
+ * What cannot be extracted is a scenarios entry that is not literal:
10
+ * `...['stripe', 'google'].map((name) => ({ scenario, data: { name } }))` is
11
+ * ordinary TypeScript, and no AST walk can enumerate it. Those entries are
12
+ * counted in `unresolvedEntries` rather than dropped, and membership for them
13
+ * is resolved at runtime by object identity (`resolveFeatureScenarios`), where
14
+ * the array has actually been evaluated.
15
+ *
16
+ * The export location is recorded either way, which is what the CLI needs to
17
+ * emit its `addFeature(...)` wiring line.
18
+ */
19
+ export declare const addFeature: AddWiring;
@@ -0,0 +1,200 @@
1
+ import * as ts from 'typescript';
2
+ import { extractFunctionName } from '../utils/extract-function-name.js';
3
+ import { ErrorCode } from '../error-codes.js';
4
+ /**
5
+ * Reads a fully literal expression into a plain value.
6
+ *
7
+ * Used for a `{ scenario, data }` entry's `data`, which is gherkin's
8
+ * `Examples:` row. Anything not literal yields `undefined` — a computed row
9
+ * belongs to the runtime-resolved portion, not to the extracted meta.
10
+ */
11
+ const literalValue = (node) => {
12
+ if (ts.isStringLiteralLike(node)) {
13
+ return node.text;
14
+ }
15
+ if (ts.isNumericLiteral(node)) {
16
+ return Number(node.text);
17
+ }
18
+ if (node.kind === ts.SyntaxKind.TrueKeyword) {
19
+ return true;
20
+ }
21
+ if (node.kind === ts.SyntaxKind.FalseKeyword) {
22
+ return false;
23
+ }
24
+ if (node.kind === ts.SyntaxKind.NullKeyword) {
25
+ return null;
26
+ }
27
+ if (ts.isPrefixUnaryExpression(node) &&
28
+ node.operator === ts.SyntaxKind.MinusToken &&
29
+ ts.isNumericLiteral(node.operand)) {
30
+ return -Number(node.operand.text);
31
+ }
32
+ if (ts.isArrayLiteralExpression(node)) {
33
+ const values = [];
34
+ for (const element of node.elements) {
35
+ const value = literalValue(element);
36
+ if (value === undefined) {
37
+ return undefined;
38
+ }
39
+ values.push(value);
40
+ }
41
+ return values;
42
+ }
43
+ if (ts.isObjectLiteralExpression(node)) {
44
+ const value = {};
45
+ for (const property of node.properties) {
46
+ if (!ts.isPropertyAssignment(property)) {
47
+ return undefined;
48
+ }
49
+ const key = ts.isIdentifier(property.name) || ts.isStringLiteralLike(property.name)
50
+ ? property.name.text
51
+ : undefined;
52
+ if (key === undefined) {
53
+ return undefined;
54
+ }
55
+ const propertyValue = literalValue(property.initializer);
56
+ if (propertyValue === undefined) {
57
+ return undefined;
58
+ }
59
+ value[key] = propertyValue;
60
+ }
61
+ return value;
62
+ }
63
+ return undefined;
64
+ };
65
+ /**
66
+ * Resolves a scenario reference to the name it is declared under, following an
67
+ * import alias back to the original export so `{ x as y }` records `x`.
68
+ */
69
+ const scenarioName = (node, checker) => {
70
+ if (!ts.isIdentifier(node)) {
71
+ return undefined;
72
+ }
73
+ const symbol = checker.getSymbolAtLocation(node);
74
+ if (!symbol) {
75
+ return node.text;
76
+ }
77
+ const resolved = symbol.flags & ts.SymbolFlags.Alias
78
+ ? checker.getAliasedSymbol(symbol)
79
+ : symbol;
80
+ return resolved.getName() || node.text;
81
+ };
82
+ const getProperty = (config, name) => {
83
+ const property = config.properties.find((p) => ts.isPropertyAssignment(p) &&
84
+ ts.isIdentifier(p.name) &&
85
+ p.name.text === name);
86
+ return property && ts.isPropertyAssignment(property)
87
+ ? property.initializer
88
+ : undefined;
89
+ };
90
+ const hasProperty = (config, name) => config.properties.some((p) => (ts.isPropertyAssignment(p) ||
91
+ ts.isMethodDeclaration(p) ||
92
+ ts.isShorthandPropertyAssignment(p)) &&
93
+ ts.isIdentifier(p.name) &&
94
+ p.name.text === name);
95
+ const stringProperty = (config, name) => {
96
+ const value = getProperty(config, name);
97
+ return value && ts.isStringLiteralLike(value) ? value.text : undefined;
98
+ };
99
+ const stringArrayProperty = (config, name) => {
100
+ const value = getProperty(config, name);
101
+ if (!value || !ts.isArrayLiteralExpression(value)) {
102
+ return undefined;
103
+ }
104
+ const values = value.elements
105
+ .filter(ts.isStringLiteralLike)
106
+ .map((e) => e.text);
107
+ return values.length === value.elements.length ? values : undefined;
108
+ };
109
+ const readEntry = (element, checker) => {
110
+ if (ts.isIdentifier(element)) {
111
+ const scenario = scenarioName(element, checker);
112
+ return scenario ? { scenario } : undefined;
113
+ }
114
+ if (ts.isObjectLiteralExpression(element)) {
115
+ const reference = getProperty(element, 'scenario');
116
+ if (!reference) {
117
+ return undefined;
118
+ }
119
+ const scenario = scenarioName(reference, checker);
120
+ if (!scenario) {
121
+ return undefined;
122
+ }
123
+ const dataNode = getProperty(element, 'data');
124
+ if (!dataNode) {
125
+ return { scenario };
126
+ }
127
+ const data = literalValue(dataNode);
128
+ return data === undefined ? undefined : { scenario, data };
129
+ }
130
+ return undefined;
131
+ };
132
+ /**
133
+ * Inspector for `pikkuFeature()` calls.
134
+ *
135
+ * A feature is the document structure the console renders — its name,
136
+ * description, tags and the order of its scenarios are what a reader sees — so
137
+ * everything the config states literally is extracted here.
138
+ *
139
+ * What cannot be extracted is a scenarios entry that is not literal:
140
+ * `...['stripe', 'google'].map((name) => ({ scenario, data: { name } }))` is
141
+ * ordinary TypeScript, and no AST walk can enumerate it. Those entries are
142
+ * counted in `unresolvedEntries` rather than dropped, and membership for them
143
+ * is resolved at runtime by object identity (`resolveFeatureScenarios`), where
144
+ * the array has actually been evaluated.
145
+ *
146
+ * The export location is recorded either way, which is what the CLI needs to
147
+ * emit its `addFeature(...)` wiring line.
148
+ */
149
+ export const addFeature = (logger, node, checker, state) => {
150
+ if (!ts.isCallExpression(node)) {
151
+ return;
152
+ }
153
+ const expression = node.expression;
154
+ if (!ts.isIdentifier(expression) || expression.text !== 'pikkuFeature') {
155
+ return;
156
+ }
157
+ const { exportedName } = extractFunctionName(node, checker, state.rootDir);
158
+ if (!exportedName) {
159
+ logger.critical(ErrorCode.MISSING_NAME, `A pikkuFeature() must be assigned to an export — the export identifier is the feature's id.`);
160
+ return;
161
+ }
162
+ const feature = {
163
+ path: node.getSourceFile().fileName,
164
+ exportedName,
165
+ entries: [],
166
+ unresolvedEntries: 0,
167
+ hasBefore: false,
168
+ hasAfter: false,
169
+ };
170
+ const [config] = node.arguments;
171
+ if (config && ts.isObjectLiteralExpression(config)) {
172
+ const name = stringProperty(config, 'name');
173
+ const description = stringProperty(config, 'description');
174
+ const tags = stringArrayProperty(config, 'tags');
175
+ if (name !== undefined) {
176
+ feature.name = name;
177
+ }
178
+ if (description !== undefined) {
179
+ feature.description = description;
180
+ }
181
+ if (tags !== undefined) {
182
+ feature.tags = tags;
183
+ }
184
+ feature.hasBefore = hasProperty(config, 'before');
185
+ feature.hasAfter = hasProperty(config, 'after');
186
+ const scenarios = getProperty(config, 'scenarios');
187
+ if (scenarios && ts.isArrayLiteralExpression(scenarios)) {
188
+ for (const element of scenarios.elements) {
189
+ const entry = readEntry(element, checker);
190
+ if (entry) {
191
+ feature.entries.push(entry);
192
+ }
193
+ else {
194
+ feature.unresolvedEntries += 1;
195
+ }
196
+ }
197
+ }
198
+ }
199
+ state.workflows.featureFiles.set(exportedName, feature);
200
+ };