@pikku/inspector 0.12.84 → 0.12.86
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 +70 -0
- package/dist/add/add-feature.js +33 -0
- package/dist/add/add-wire-addon.js +11 -0
- package/dist/error-codes.d.ts +1 -0
- package/dist/error-codes.js +1 -0
- package/dist/inspector.js +3 -1
- package/dist/types.d.ts +26 -0
- package/dist/utils/load-addon-functions-meta.js +12 -0
- package/dist/utils/post-process.d.ts +25 -0
- package/dist/utils/post-process.js +73 -0
- package/dist/utils/serialize-inspector-state.d.ts +1 -0
- package/dist/utils/serialize-inspector-state.js +2 -0
- package/dist/utils/serialize-mcp-json.d.ts +9 -1
- package/dist/utils/serialize-mcp-json.js +21 -2
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,73 @@
|
|
|
1
|
+
## 0.12.86
|
|
2
|
+
|
|
3
|
+
### Patch Changes
|
|
4
|
+
|
|
5
|
+
- f05fb4e: A scenario that belongs to no feature is now reported.
|
|
6
|
+
|
|
7
|
+
Features are what the guide is written against: prose cites a feature, and the compiler fills that citation with the evidence from the scenarios the feature owns. A scenario nobody listed is therefore invisible — it runs, it captures screenshots and recordings, and none of it ever reaches a page. That failure is silent today, and it is the single reason two real projects have hundreds of captures bound to nothing.
|
|
8
|
+
|
|
9
|
+
The inspector now walks every feature's scenario list, and any scenario not claimed by one is reported as `PKU682`. It is an error rather than a critical, so `pikku all` keeps passing by default and the list is there to work through; `--fail-on-error` makes it a wall once a project has caught up.
|
|
10
|
+
|
|
11
|
+
Membership reads the scenario each entry names, not the entry itself. A `data` the AST cannot evaluate — `'a'.repeat(64)`, a call, anything computed — leaves the entry partial for the console but says nothing about who owns the scenario, and the check no longer treats the two as the same thing.
|
|
12
|
+
|
|
13
|
+
An entry that names no scenario at all — a spread, a `.map()` — does leave membership unknown. That only matters once something is already unowned, so the check reports it then, as a warn naming both the unowned scenarios and the features that could be hiding them, rather than accusing a scenario that may well be in one of those arrays.
|
|
14
|
+
|
|
15
|
+
- b4a895e: `pikku scenario guide` writes the user guide a scenario suite already contains. A feature reads as a page and a scenario as a section, and a run leaves screenshots behind with the captions their author took them under — the command joins that to the editorial prose a project checks in under `docs/` and writes markdown. It renders no HTML, ships no components, resolves no asset URLs and calls no model: an image is an ordinary relative ``, and whoever consumes the markdown rewrites the paths.
|
|
16
|
+
|
|
17
|
+
A page cites a feature by leaving the marker pair where the block belongs:
|
|
18
|
+
|
|
19
|
+
```markdown
|
|
20
|
+
---
|
|
21
|
+
title: Deployments
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
A deployment is one tracked shipment of your app.
|
|
25
|
+
|
|
26
|
+
<!-- pikku:guide feature=deploymentsFeature -->
|
|
27
|
+
<!-- /pikku:guide -->
|
|
28
|
+
|
|
29
|
+
## Does my app go down during a deploy?
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
That one line does both halves of the job. It says _where_ the block goes, which a frontmatter list cannot express, and it is what the coverage gate counts to decide _whether_ a feature is documented at all. The mapping stays many-to-many and falls out of the union of every marker in the tree. A rebuild rewrites exactly the regions between the markers, so every sentence a human wrote around them survives.
|
|
33
|
+
|
|
34
|
+
**The steps are evidence, not content.** A generated block is the scenario's title, the description its author wrote, and the shots it filed — never a numbered Given/When/Then ladder, which is a test report and not something anybody arrives at a documentation page wanting. The sentences are still what the guide is kept honest against: `docs/.guide.lock` records a hash of each feature's step sentences and artifact ids, deliberately not of the image bytes. Restyling a UI changes every screenshot and no sentence; inserting or renaming a step changes what the prose was describing, and the page is reported stale. The lock is generated and checked in, so no hash is ever typed or merged by hand — and a tree whose lock is untracked reports every page as current forever.
|
|
35
|
+
|
|
36
|
+
Every registered feature has to be cited by some page, and a feature that is pure plumbing says so rather than being written about — `pikkuFeature({ document: false })`, threaded through the inspector and `FeatureMeta`. An uncited feature fails the command by name; `--allow-undocumented` downgrades that one failure to a report. A page citing a feature id that is not registered stays an error either way.
|
|
37
|
+
|
|
38
|
+
A guide is only written out of a run that can stand behind it. A run that failed or was killed halfway is refused, because a page is a claim that the product does what it says. So is a narrowed one: `pikku scenario run --flows`/`--features`/`--tags` leaves out scenarios the suite has, and a guide built from it would describe those flows as though they do not exist. `ScenarioRunRecord.selection` records the filters a run was selected with, since nothing in the results afterwards can tell a suite of forty from forty that were asked for.
|
|
39
|
+
|
|
40
|
+
Results are joined to features by `featureId`, falling back to the display name only for records written before that field existed — a title is rewritten freely and two features may share one.
|
|
41
|
+
|
|
42
|
+
Emission is deterministic — identical inputs give byte-identical output, and no timestamp goes in that did not come from the run record. Frontmatter the compiler does not own (`slug`, `draft`, `sidebar_position`, anything else a docs site reads) passes through untouched, and a source written with CRLF line endings is read as having frontmatter.
|
|
43
|
+
|
|
44
|
+
- Updated dependencies [b4a895e]
|
|
45
|
+
- Updated dependencies [b4a895e]
|
|
46
|
+
- @pikku/core@0.12.118
|
|
47
|
+
|
|
48
|
+
## 0.12.85
|
|
49
|
+
|
|
50
|
+
### Patch Changes
|
|
51
|
+
|
|
52
|
+
- b312867: A project can now serve several MCP endpoints, one per connector.
|
|
53
|
+
|
|
54
|
+
Until now every MCP tool in a project was pooled onto a single `/mcp`, so a hub offering three connectors offered one endpoint listing all three connectors' tools at once. A client pointed at it saw tools it had no business calling, and the only way to give a connector an endpoint of its own was to give it a deployment of its own — three deploys, three bills, three service graphs.
|
|
55
|
+
|
|
56
|
+
`wireAddon` gains `mcpEndpoint`. `true` serves that instance's tools at `/mcp/<name>`; a string is the path, used as given. Leaving it unset keeps the tools on the shared endpoint, which is where they have always been, so nothing existing moves.
|
|
57
|
+
|
|
58
|
+
A surfaced instance now gets its own manifest (`.pikku/mcp/mcp.<name>.gen.json`, carrying the path it answers on), its own deploy unit (`mcp-<name>`, routed on that path), and its own MCP server — with its own tool list, so a client pointed at one endpoint never sees another's tools. The plumbing for the per-surface manifest and unit already existed in `deploy apply`; nothing had ever produced one.
|
|
59
|
+
|
|
60
|
+
`pikku dev` mounts every endpoint the generated tree describes, not just the default one. Without that a project that moved its tools onto their own endpoints would have served nothing locally at all — the default manifest it reads is empty precisely because they moved — and the only way to try a connector would have been to deploy it.
|
|
61
|
+
|
|
62
|
+
The node and bun transports take `mcpSurfaces` alongside `mcpJson` and mount each at its own path, longest path first. `/mcp` claims everything beneath `/mcp/`, so without that ordering the default endpoint answers `/mcp/weather` and the surface's tools are unreachable.
|
|
63
|
+
|
|
64
|
+
OAuth discovery is split between the endpoints rather than duplicated across them. RFC 9728 folds a resource's path into its well-known route, so each endpoint's own document is already distinct, but the path-less `/.well-known/oauth-protected-resource` predates that and describes whichever resource answers it. Only the default endpoint claims it — otherwise every unit registers the same route and the provider's router decides which resource a client is told about, and in dev a client probing it is described whichever surface sorted first.
|
|
65
|
+
|
|
66
|
+
- Updated dependencies [87971bd]
|
|
67
|
+
- Updated dependencies [b312867]
|
|
68
|
+
- Updated dependencies [51bd35a]
|
|
69
|
+
- @pikku/core@0.12.116
|
|
70
|
+
|
|
1
71
|
## 0.12.84
|
|
2
72
|
|
|
3
73
|
### Patch Changes
|
package/dist/add/add-feature.js
CHANGED
|
@@ -96,6 +96,16 @@ const stringProperty = (config, name) => {
|
|
|
96
96
|
const value = getProperty(config, name);
|
|
97
97
|
return value && ts.isStringLiteralLike(value) ? value.text : undefined;
|
|
98
98
|
};
|
|
99
|
+
const booleanProperty = (config, name) => {
|
|
100
|
+
const value = getProperty(config, name);
|
|
101
|
+
if (!value) {
|
|
102
|
+
return undefined;
|
|
103
|
+
}
|
|
104
|
+
if (value.kind === ts.SyntaxKind.TrueKeyword) {
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
return value.kind === ts.SyntaxKind.FalseKeyword ? false : undefined;
|
|
108
|
+
};
|
|
99
109
|
const stringArrayProperty = (config, name) => {
|
|
100
110
|
const value = getProperty(config, name);
|
|
101
111
|
if (!value || !ts.isArrayLiteralExpression(value)) {
|
|
@@ -106,6 +116,16 @@ const stringArrayProperty = (config, name) => {
|
|
|
106
116
|
.map((e) => e.text);
|
|
107
117
|
return values.length === value.elements.length ? values : undefined;
|
|
108
118
|
};
|
|
119
|
+
const mentionedScenario = (element, checker) => {
|
|
120
|
+
if (ts.isIdentifier(element)) {
|
|
121
|
+
return scenarioName(element, checker);
|
|
122
|
+
}
|
|
123
|
+
if (ts.isObjectLiteralExpression(element)) {
|
|
124
|
+
const reference = getProperty(element, 'scenario');
|
|
125
|
+
return reference ? scenarioName(reference, checker) : undefined;
|
|
126
|
+
}
|
|
127
|
+
return undefined;
|
|
128
|
+
};
|
|
109
129
|
const readEntry = (element, checker) => {
|
|
110
130
|
if (ts.isIdentifier(element)) {
|
|
111
131
|
const scenario = scenarioName(element, checker);
|
|
@@ -164,6 +184,8 @@ export const addFeature = (logger, node, checker, state) => {
|
|
|
164
184
|
exportedName,
|
|
165
185
|
entries: [],
|
|
166
186
|
unresolvedEntries: 0,
|
|
187
|
+
mentions: [],
|
|
188
|
+
unnamedEntries: 0,
|
|
167
189
|
hasBefore: false,
|
|
168
190
|
hasAfter: false,
|
|
169
191
|
};
|
|
@@ -172,6 +194,10 @@ export const addFeature = (logger, node, checker, state) => {
|
|
|
172
194
|
const name = stringProperty(config, 'name');
|
|
173
195
|
const description = stringProperty(config, 'description');
|
|
174
196
|
const tags = stringArrayProperty(config, 'tags');
|
|
197
|
+
const document = booleanProperty(config, 'document');
|
|
198
|
+
if (document !== undefined) {
|
|
199
|
+
feature.document = document;
|
|
200
|
+
}
|
|
175
201
|
if (name !== undefined) {
|
|
176
202
|
feature.name = name;
|
|
177
203
|
}
|
|
@@ -186,6 +212,13 @@ export const addFeature = (logger, node, checker, state) => {
|
|
|
186
212
|
const scenarios = getProperty(config, 'scenarios');
|
|
187
213
|
if (scenarios && ts.isArrayLiteralExpression(scenarios)) {
|
|
188
214
|
for (const element of scenarios.elements) {
|
|
215
|
+
const mention = mentionedScenario(element, checker);
|
|
216
|
+
if (mention) {
|
|
217
|
+
feature.mentions.push(mention);
|
|
218
|
+
}
|
|
219
|
+
else {
|
|
220
|
+
feature.unnamedEntries += 1;
|
|
221
|
+
}
|
|
189
222
|
const entry = readEntry(element, checker);
|
|
190
223
|
if (entry) {
|
|
191
224
|
feature.entries.push(entry);
|
|
@@ -95,6 +95,7 @@ export function addWireAddon(node, state, logger) {
|
|
|
95
95
|
let pkg;
|
|
96
96
|
let rpcEndpoint;
|
|
97
97
|
let mcp;
|
|
98
|
+
let mcpEndpoint;
|
|
98
99
|
let auth;
|
|
99
100
|
let tags;
|
|
100
101
|
let scopes;
|
|
@@ -125,6 +126,15 @@ export function addWireAddon(node, state, logger) {
|
|
|
125
126
|
? prop.initializer.kind === ts.SyntaxKind.TrueKeyword
|
|
126
127
|
: parseStringArray(prop.initializer);
|
|
127
128
|
}
|
|
129
|
+
else if (key === 'mcpEndpoint') {
|
|
130
|
+
if (ts.isStringLiteral(prop.initializer)) {
|
|
131
|
+
mcpEndpoint = prop.initializer.text;
|
|
132
|
+
}
|
|
133
|
+
else if (prop.initializer.kind === ts.SyntaxKind.TrueKeyword ||
|
|
134
|
+
prop.initializer.kind === ts.SyntaxKind.FalseKeyword) {
|
|
135
|
+
mcpEndpoint = prop.initializer.kind === ts.SyntaxKind.TrueKeyword;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
128
138
|
else if (key === 'auth' &&
|
|
129
139
|
(prop.initializer.kind === ts.SyntaxKind.TrueKeyword ||
|
|
130
140
|
prop.initializer.kind === ts.SyntaxKind.FalseKeyword)) {
|
|
@@ -175,6 +185,7 @@ export function addWireAddon(node, state, logger) {
|
|
|
175
185
|
file: node.getSourceFile().fileName,
|
|
176
186
|
rpcEndpoint,
|
|
177
187
|
mcp,
|
|
188
|
+
mcpEndpoint,
|
|
178
189
|
auth,
|
|
179
190
|
tags,
|
|
180
191
|
scopes,
|
package/dist/error-codes.d.ts
CHANGED
|
@@ -25,6 +25,7 @@ export declare enum ErrorCode {
|
|
|
25
25
|
SCENARIO_NOT_EXTRACTABLE = "PKU679",
|
|
26
26
|
SCENARIO_HAS_NO_ASSERTION = "PKU680",
|
|
27
27
|
SCENARIO_STEP_PROSE_NAMES_ACTOR = "PKU681",
|
|
28
|
+
SCENARIO_HAS_NO_FEATURE = "PKU682",
|
|
28
29
|
WORKFLOW_ORCHESTRATOR_NOT_CONFIGURED = "PKU600",
|
|
29
30
|
INVALID_DSL_WORKFLOW = "PKU641",
|
|
30
31
|
WORKFLOW_GRAPH_ADDON_NOT_WIRED = "PKU642",
|
package/dist/error-codes.js
CHANGED
|
@@ -27,6 +27,7 @@ export var ErrorCode;
|
|
|
27
27
|
ErrorCode["SCENARIO_NOT_EXTRACTABLE"] = "PKU679";
|
|
28
28
|
ErrorCode["SCENARIO_HAS_NO_ASSERTION"] = "PKU680";
|
|
29
29
|
ErrorCode["SCENARIO_STEP_PROSE_NAMES_ACTOR"] = "PKU681";
|
|
30
|
+
ErrorCode["SCENARIO_HAS_NO_FEATURE"] = "PKU682";
|
|
30
31
|
ErrorCode["WORKFLOW_ORCHESTRATOR_NOT_CONFIGURED"] = "PKU600";
|
|
31
32
|
ErrorCode["INVALID_DSL_WORKFLOW"] = "PKU641";
|
|
32
33
|
ErrorCode["WORKFLOW_GRAPH_ADDON_NOT_WIRED"] = "PKU642";
|
package/dist/inspector.js
CHANGED
|
@@ -7,7 +7,7 @@ import { getFilesAndMethods } from './utils/get-files-and-methods.js';
|
|
|
7
7
|
import { findCommonAncestor } from './utils/find-root-dir.js';
|
|
8
8
|
import { createSourceFileCacheHost } from './source-file-cache.js';
|
|
9
9
|
import { createNestedProjectFilter } from './utils/nested-project-filter.js';
|
|
10
|
-
import { aggregateRequiredServices, stampAuthHandlerServices, validateAgentModels, validateAgentToolReferences, validateAgentScorerReferences, validateSecretOverrides, validateVariableOverrides, validateCredentialOverrides, validateRemoteAddonDependencies, validateRemoteAddonAuth, validateScopeReferences, validateSystemRoleScopes, validatePersonaRoles, computeResolvedIOTypes, computeMiddlewareGroupsMeta, computePermissionsGroupsMeta, computeRequiredSchemas, validateNoSecretAliasServices, registerDerivedOAuth2AppSecrets, validateSecretUsage, computeDiagnostics, validateSchemaReferences, validateSchemaWiringSeparation, validateScenarioServices, validateScenarioSteps, validateWorkflowGraphAddons, } from './utils/post-process.js';
|
|
10
|
+
import { aggregateRequiredServices, stampAuthHandlerServices, validateAgentModels, validateAgentToolReferences, validateAgentScorerReferences, validateSecretOverrides, validateVariableOverrides, validateCredentialOverrides, validateRemoteAddonDependencies, validateRemoteAddonAuth, validateScopeReferences, validateSystemRoleScopes, validatePersonaRoles, computeResolvedIOTypes, computeMiddlewareGroupsMeta, computePermissionsGroupsMeta, computeRequiredSchemas, validateNoSecretAliasServices, registerDerivedOAuth2AppSecrets, validateSecretUsage, computeDiagnostics, validateSchemaReferences, validateSchemaWiringSeparation, validateScenarioServices, validateScenarioSteps, validateScenarioFeatures, validateWorkflowGraphAddons, } from './utils/post-process.js';
|
|
11
11
|
import { validateExposedFunctionsGated } from './utils/validate-exposed-functions-gated.js';
|
|
12
12
|
import { validateTagsResolveToMiddleware } from './utils/validate-tags-resolve-to-middleware.js';
|
|
13
13
|
import { annotateHttpRouteAuth } from './utils/annotate-http-route-auth.js';
|
|
@@ -115,6 +115,7 @@ export function getInitialInspectorState(rootDir) {
|
|
|
115
115
|
toolsMeta: {},
|
|
116
116
|
promptsMeta: {},
|
|
117
117
|
files: new Set(),
|
|
118
|
+
surfaces: {},
|
|
118
119
|
},
|
|
119
120
|
agents: {
|
|
120
121
|
agentsMeta: {},
|
|
@@ -404,6 +405,7 @@ export const inspect = async (logger, routeFiles, options = {}) => {
|
|
|
404
405
|
validateWorkflowGraphAddons(logger, state);
|
|
405
406
|
validateScenarioServices(logger, state);
|
|
406
407
|
validateScenarioSteps(logger, state);
|
|
408
|
+
validateScenarioFeatures(logger, state);
|
|
407
409
|
// Needs every route, every function and every wireAddon declaration
|
|
408
410
|
// together, so it can only run once the whole program is inspected.
|
|
409
411
|
annotateHttpRouteAuth(state);
|
package/dist/types.d.ts
CHANGED
|
@@ -455,6 +455,8 @@ export interface InspectorFeature {
|
|
|
455
455
|
name?: string;
|
|
456
456
|
description?: string;
|
|
457
457
|
tags?: string[];
|
|
458
|
+
/** `document: false` on the config, and only that — an absent flag documents. */
|
|
459
|
+
document?: boolean;
|
|
458
460
|
entries: InspectorFeatureEntry[];
|
|
459
461
|
/**
|
|
460
462
|
* Entries that are not literal — a spread, a `.map()`, a call. Counted rather
|
|
@@ -462,6 +464,17 @@ export interface InspectorFeature {
|
|
|
462
464
|
* quietly showing a short feature.
|
|
463
465
|
*/
|
|
464
466
|
unresolvedEntries: number;
|
|
467
|
+
/**
|
|
468
|
+
* Every scenario the array names, including entries whose `data` could not be
|
|
469
|
+
* read. Membership is knowable there even when the entry is not — which is
|
|
470
|
+
* what the check for a scenario belonging to no feature compares against.
|
|
471
|
+
*/
|
|
472
|
+
mentions: string[];
|
|
473
|
+
/**
|
|
474
|
+
* Entries that name no scenario at all — a spread, a `.map()`. Only these
|
|
475
|
+
* leave membership genuinely unknown.
|
|
476
|
+
*/
|
|
477
|
+
unnamedEntries: number;
|
|
465
478
|
/**
|
|
466
479
|
* Feature hooks run once around the whole group, never per scenario, and are
|
|
467
480
|
* runtime-only — so only their presence is recorded.
|
|
@@ -589,6 +602,13 @@ export interface InspectorState {
|
|
|
589
602
|
* when the declared value was not a statically-knowable literal.
|
|
590
603
|
*/
|
|
591
604
|
mcp?: boolean | string[];
|
|
605
|
+
/**
|
|
606
|
+
* Serves this instance's MCP tools on an endpoint of their own rather
|
|
607
|
+
* than the project's default one. `true` means `/mcp/<name>`; a string
|
|
608
|
+
* is the path as given. Absent leaves the tools on the default
|
|
609
|
+
* endpoint, which is where they have always been.
|
|
610
|
+
*/
|
|
611
|
+
mcpEndpoint?: boolean | string;
|
|
592
612
|
/**
|
|
593
613
|
* The addon's own gates. `runPikkuFunc` applies these to every function
|
|
594
614
|
* in the package, on every wiring path — not just `namespace:function`.
|
|
@@ -631,6 +651,12 @@ export interface InspectorState {
|
|
|
631
651
|
toolsMeta: MCPToolMeta;
|
|
632
652
|
promptsMeta: MCPPromptMeta;
|
|
633
653
|
files: Set<string>;
|
|
654
|
+
/**
|
|
655
|
+
* Every MCP endpoint the project serves beyond the default one, as
|
|
656
|
+
* surface name -> the path it is served on. A meta entry carrying a
|
|
657
|
+
* `surface` is served by the endpoint named here and by no other.
|
|
658
|
+
*/
|
|
659
|
+
surfaces: Record<string, string>;
|
|
634
660
|
};
|
|
635
661
|
agents: {
|
|
636
662
|
agentsMeta: AgentsMeta;
|
|
@@ -153,6 +153,17 @@ export async function loadAddonFunctionsMeta(logger, state) {
|
|
|
153
153
|
// decides which of the addon's functions a model gets to see.
|
|
154
154
|
if (decl.mcp) {
|
|
155
155
|
const selected = Array.isArray(decl.mcp) ? new Set(decl.mcp) : null;
|
|
156
|
+
// `mcpEndpoint` gives this instance an endpoint to itself, which is
|
|
157
|
+
// what lets one project expose several connectors: a client pointed at
|
|
158
|
+
// one surface lists that surface's tools and no others. Unset keeps
|
|
159
|
+
// the tools on the project's single endpoint.
|
|
160
|
+
const surface = decl.mcpEndpoint ? namespace : undefined;
|
|
161
|
+
if (surface) {
|
|
162
|
+
state.mcpEndpoints.surfaces[surface] =
|
|
163
|
+
typeof decl.mcpEndpoint === 'string'
|
|
164
|
+
? decl.mcpEndpoint
|
|
165
|
+
: `/mcp/${namespace}`;
|
|
166
|
+
}
|
|
156
167
|
for (const [funcName, funcMeta] of Object.entries(meta)) {
|
|
157
168
|
if (selected ? !selected.has(funcName) : !funcMeta.mcp) {
|
|
158
169
|
continue;
|
|
@@ -165,6 +176,7 @@ export async function loadAddonFunctionsMeta(logger, state) {
|
|
|
165
176
|
inputSchema: funcMeta.inputSchemaName ?? null,
|
|
166
177
|
outputSchema: funcMeta.outputSchemaName ?? null,
|
|
167
178
|
tags: funcMeta.tags,
|
|
179
|
+
...(surface ? { surface } : {}),
|
|
168
180
|
};
|
|
169
181
|
}
|
|
170
182
|
// A name the addon does not publish is a tool the app believes it
|
|
@@ -129,6 +129,31 @@ export declare const proseOpensWithActor: (prose: string, actor: string) => bool
|
|
|
129
129
|
* singletons the step actually needs.
|
|
130
130
|
*/
|
|
131
131
|
export declare function validateScenarioSteps(logger: InspectorLogger, state: InspectorState | Omit<InspectorState, 'typesLookup'>): void;
|
|
132
|
+
/**
|
|
133
|
+
* Reports scenarios that no `pikkuFeature` lists.
|
|
134
|
+
*
|
|
135
|
+
* A feature is what a run stamps onto each result, and the only handle anything
|
|
136
|
+
* downstream has on a scenario: the console groups by it and the guide cites it.
|
|
137
|
+
* A scenario in no feature still runs and still passes — it simply files its
|
|
138
|
+
* results, and any screenshot or recording it took, where nothing can reach
|
|
139
|
+
* them. That is not a style violation, it is work being thrown away, and it is
|
|
140
|
+
* invisible precisely because the suite is green.
|
|
141
|
+
*
|
|
142
|
+
* It is an `error` rather than a `critical` because every suite written before
|
|
143
|
+
* the rule has some, and a hard failure on a CLI bump would strand them with no
|
|
144
|
+
* way to defer. `--fail-on-error` is how a project opts in once it has bound
|
|
145
|
+
* what it has.
|
|
146
|
+
*
|
|
147
|
+
* Membership reads the scenario each entry names, not the entry itself: a
|
|
148
|
+
* `data` the AST cannot evaluate leaves the entry partial for the console but
|
|
149
|
+
* says nothing about who owns the scenario.
|
|
150
|
+
*
|
|
151
|
+
* An entry that names no scenario at all (a spread, a `.map()`) does leave
|
|
152
|
+
* membership unknown. That only matters when something is already unowned, so
|
|
153
|
+
* the check reports it then and downgrades to a warn naming both sides, rather
|
|
154
|
+
* than accusing a scenario that may well be in that array.
|
|
155
|
+
*/
|
|
156
|
+
export declare function validateScenarioFeatures(logger: InspectorLogger, state: InspectorState | Omit<InspectorState, 'typesLookup'>): void;
|
|
132
157
|
/**
|
|
133
158
|
* A `pikkuWorkflowGraph` node that references the `graph:` namespace (e.g.
|
|
134
159
|
* `graph:editFields`) needs @pikku/addon-graph wired — otherwise the RPC never
|
|
@@ -943,6 +943,79 @@ export function validateScenarioSteps(logger, state) {
|
|
|
943
943
|
}
|
|
944
944
|
}
|
|
945
945
|
}
|
|
946
|
+
/**
|
|
947
|
+
* Reports scenarios that no `pikkuFeature` lists.
|
|
948
|
+
*
|
|
949
|
+
* A feature is what a run stamps onto each result, and the only handle anything
|
|
950
|
+
* downstream has on a scenario: the console groups by it and the guide cites it.
|
|
951
|
+
* A scenario in no feature still runs and still passes — it simply files its
|
|
952
|
+
* results, and any screenshot or recording it took, where nothing can reach
|
|
953
|
+
* them. That is not a style violation, it is work being thrown away, and it is
|
|
954
|
+
* invisible precisely because the suite is green.
|
|
955
|
+
*
|
|
956
|
+
* It is an `error` rather than a `critical` because every suite written before
|
|
957
|
+
* the rule has some, and a hard failure on a CLI bump would strand them with no
|
|
958
|
+
* way to defer. `--fail-on-error` is how a project opts in once it has bound
|
|
959
|
+
* what it has.
|
|
960
|
+
*
|
|
961
|
+
* Membership reads the scenario each entry names, not the entry itself: a
|
|
962
|
+
* `data` the AST cannot evaluate leaves the entry partial for the console but
|
|
963
|
+
* says nothing about who owns the scenario.
|
|
964
|
+
*
|
|
965
|
+
* An entry that names no scenario at all (a spread, a `.map()`) does leave
|
|
966
|
+
* membership unknown. That only matters when something is already unowned, so
|
|
967
|
+
* the check reports it then and downgrades to a warn naming both sides, rather
|
|
968
|
+
* than accusing a scenario that may well be in that array.
|
|
969
|
+
*/
|
|
970
|
+
export function validateScenarioFeatures(logger, state) {
|
|
971
|
+
const features = state.workflows.featureFiles;
|
|
972
|
+
if (!features) {
|
|
973
|
+
return;
|
|
974
|
+
}
|
|
975
|
+
const owned = new Set();
|
|
976
|
+
for (const feature of features.values()) {
|
|
977
|
+
for (const mention of feature.mentions) {
|
|
978
|
+
owned.add(mention);
|
|
979
|
+
}
|
|
980
|
+
}
|
|
981
|
+
const files = state.workflows.files;
|
|
982
|
+
const unowned = [];
|
|
983
|
+
for (const [workflowName, meta] of Object.entries(state.workflows.meta)) {
|
|
984
|
+
if (!meta.scenario) {
|
|
985
|
+
continue;
|
|
986
|
+
}
|
|
987
|
+
const exported = files?.get(workflowName)?.exportedName;
|
|
988
|
+
if (owned.has(workflowName) || (exported && owned.has(exported))) {
|
|
989
|
+
continue;
|
|
990
|
+
}
|
|
991
|
+
unowned.push(workflowName);
|
|
992
|
+
}
|
|
993
|
+
if (unowned.length === 0) {
|
|
994
|
+
return;
|
|
995
|
+
}
|
|
996
|
+
const unreadable = [...features.values()]
|
|
997
|
+
.filter((feature) => feature.unnamedEntries > 0)
|
|
998
|
+
.map((feature) => feature.exportedName);
|
|
999
|
+
if (unreadable.length > 0) {
|
|
1000
|
+
logger.diagnostic({
|
|
1001
|
+
severity: 'warn',
|
|
1002
|
+
code: ErrorCode.SCENARIO_HAS_NO_FEATURE,
|
|
1003
|
+
message: `${unowned.map((name) => `'${name}'`).join(', ')} appear in no feature's \`scenarios\` array, ` +
|
|
1004
|
+
`but ${unreadable.map((name) => `'${name}'`).join(', ')} build theirs with a spread or a call, so one of them may still be listed there. ` +
|
|
1005
|
+
`List those scenarios literally and this becomes a definite answer either way.`,
|
|
1006
|
+
});
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
for (const name of unowned) {
|
|
1010
|
+
logger.diagnostic({
|
|
1011
|
+
severity: 'error',
|
|
1012
|
+
code: ErrorCode.SCENARIO_HAS_NO_FEATURE,
|
|
1013
|
+
message: `Scenario '${name}' is listed by no pikkuFeature, so nothing downstream can reach it — ` +
|
|
1014
|
+
`a run stamps no feature onto its results, the console groups it under none, and a guide cannot cite it. ` +
|
|
1015
|
+
`Add it to a feature's \`scenarios\` array.`,
|
|
1016
|
+
});
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
946
1019
|
/**
|
|
947
1020
|
* A `pikkuWorkflowGraph` node that references the `graph:` namespace (e.g.
|
|
948
1021
|
* `graph:editFields`) needs @pikku/addon-graph wired — otherwise the RPC never
|
|
@@ -212,6 +212,7 @@ export interface SerializableInspectorState {
|
|
|
212
212
|
toolsMeta: InspectorState['mcpEndpoints']['toolsMeta'];
|
|
213
213
|
promptsMeta: InspectorState['mcpEndpoints']['promptsMeta'];
|
|
214
214
|
files: string[];
|
|
215
|
+
surfaces?: InspectorState['mcpEndpoints']['surfaces'];
|
|
215
216
|
};
|
|
216
217
|
agents: {
|
|
217
218
|
invokedAgentsByFile?: Array<[string, string[]]>;
|
|
@@ -92,6 +92,7 @@ export function serializeInspectorState(state) {
|
|
|
92
92
|
toolsMeta: state.mcpEndpoints.toolsMeta,
|
|
93
93
|
promptsMeta: state.mcpEndpoints.promptsMeta,
|
|
94
94
|
files: Array.from(state.mcpEndpoints.files),
|
|
95
|
+
surfaces: state.mcpEndpoints.surfaces,
|
|
95
96
|
},
|
|
96
97
|
agents: {
|
|
97
98
|
agentsMeta: state.agents?.agentsMeta ?? {},
|
|
@@ -278,6 +279,7 @@ export function deserializeInspectorState(data) {
|
|
|
278
279
|
toolsMeta: data.mcpEndpoints.toolsMeta,
|
|
279
280
|
promptsMeta: data.mcpEndpoints.promptsMeta,
|
|
280
281
|
files: new Set(data.mcpEndpoints.files),
|
|
282
|
+
surfaces: data.mcpEndpoints.surfaces ?? {},
|
|
281
283
|
},
|
|
282
284
|
agents: {
|
|
283
285
|
agentsMeta: data.agents?.agentsMeta || {},
|
|
@@ -1,2 +1,10 @@
|
|
|
1
1
|
import type { InspectorLogger, InspectorState } from '../types.js';
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Serializes one MCP endpoint's manifest.
|
|
4
|
+
*
|
|
5
|
+
* A project can serve several endpoints, and a wiring belongs to exactly one of
|
|
6
|
+
* them: the one named by its `surface`. Passing no surface serializes the
|
|
7
|
+
* default endpoint, which is every wiring that named none — so a project with
|
|
8
|
+
* no surfaces at all gets the single manifest it always got.
|
|
9
|
+
*/
|
|
10
|
+
export declare const serializeMCPJson: (logger: InspectorLogger, state: InspectorState, surface?: string) => string;
|
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
import { resolveFunctionMeta } from './resolve-function-meta.js';
|
|
2
|
-
|
|
2
|
+
/**
|
|
3
|
+
* Serializes one MCP endpoint's manifest.
|
|
4
|
+
*
|
|
5
|
+
* A project can serve several endpoints, and a wiring belongs to exactly one of
|
|
6
|
+
* them: the one named by its `surface`. Passing no surface serializes the
|
|
7
|
+
* default endpoint, which is every wiring that named none — so a project with
|
|
8
|
+
* no surfaces at all gets the single manifest it always got.
|
|
9
|
+
*/
|
|
10
|
+
export const serializeMCPJson = (logger, state, surface) => {
|
|
3
11
|
const { mcpEndpoints, functions, schemas } = state;
|
|
4
12
|
const { typesMap } = functions;
|
|
5
13
|
const { resourcesMeta, toolsMeta, promptsMeta } = mcpEndpoints;
|
|
14
|
+
const onThisSurface = (meta) => (meta.surface ?? undefined) === surface;
|
|
6
15
|
const tools = [];
|
|
7
16
|
const resources = [];
|
|
8
17
|
const prompts = [];
|
|
@@ -37,6 +46,8 @@ export const serializeMCPJson = (logger, state) => {
|
|
|
37
46
|
return schema;
|
|
38
47
|
};
|
|
39
48
|
for (const [name, endpointMeta] of Object.entries(resourcesMeta)) {
|
|
49
|
+
if (!onThisSurface(endpointMeta))
|
|
50
|
+
continue;
|
|
40
51
|
const functionMeta = resolveFunctionMeta(state, endpointMeta.pikkuFuncId);
|
|
41
52
|
if (!functionMeta) {
|
|
42
53
|
logger.warn(`Function ${endpointMeta.pikkuFuncId} not found in functionsMeta. Skipping resource ${name}.`);
|
|
@@ -56,6 +67,8 @@ export const serializeMCPJson = (logger, state) => {
|
|
|
56
67
|
});
|
|
57
68
|
}
|
|
58
69
|
for (const [name, endpointMeta] of Object.entries(toolsMeta)) {
|
|
70
|
+
if (!onThisSurface(endpointMeta))
|
|
71
|
+
continue;
|
|
59
72
|
const functionMeta = resolveFunctionMeta(state, endpointMeta.pikkuFuncId);
|
|
60
73
|
if (!functionMeta) {
|
|
61
74
|
logger.warn(`Function ${endpointMeta.pikkuFuncId} not found in functionsMeta. Skipping tool ${name}.`);
|
|
@@ -74,6 +87,8 @@ export const serializeMCPJson = (logger, state) => {
|
|
|
74
87
|
});
|
|
75
88
|
}
|
|
76
89
|
for (const [name, endpointMeta] of Object.entries(promptsMeta)) {
|
|
90
|
+
if (!onThisSurface(endpointMeta))
|
|
91
|
+
continue;
|
|
77
92
|
const functionMeta = resolveFunctionMeta(state, endpointMeta.pikkuFuncId);
|
|
78
93
|
if (!functionMeta) {
|
|
79
94
|
logger.warn(`Function ${endpointMeta.pikkuFuncId} not found in functionsMeta. Skipping prompt ${name}.`);
|
|
@@ -101,5 +116,9 @@ export const serializeMCPJson = (logger, state) => {
|
|
|
101
116
|
arguments: argumentsArray,
|
|
102
117
|
});
|
|
103
118
|
}
|
|
104
|
-
|
|
119
|
+
// The path only travels in a surface's own manifest: the default endpoint
|
|
120
|
+
// stays at whatever the runtime mounts, which is what every existing project
|
|
121
|
+
// already relies on.
|
|
122
|
+
const mcpPath = surface ? mcpEndpoints.surfaces?.[surface] : undefined;
|
|
123
|
+
return JSON.stringify({ ...(mcpPath ? { mcpPath } : {}), tools, resources, prompts }, null, 2);
|
|
105
124
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pikku/inspector",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.86",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/pikkujs/pikku.git",
|
|
@@ -55,10 +55,10 @@
|
|
|
55
55
|
"zod-to-ts": "^2.1.0"
|
|
56
56
|
},
|
|
57
57
|
"peerDependencies": {
|
|
58
|
-
"@pikku/core": "^0.12.
|
|
58
|
+
"@pikku/core": "^0.12.118"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
|
-
"@pikku/core": "^0.12.
|
|
61
|
+
"@pikku/core": "^0.12.118",
|
|
62
62
|
"@types/node": "^24.13.3"
|
|
63
63
|
},
|
|
64
64
|
"engines": {
|