@pikku/inspector 0.12.69 → 0.12.72
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 +57 -0
- package/dist/add/add-file-with-factory.js +96 -14
- package/dist/add/add-rpc-invocations.js +48 -0
- package/dist/error-codes.d.ts +1 -0
- package/dist/error-codes.js +1 -0
- package/dist/inspector.js +3 -0
- package/dist/types.d.ts +12 -0
- package/dist/utils/filter-inspector-state.js +38 -4
- package/dist/utils/post-process.d.ts +10 -0
- package/dist/utils/post-process.js +87 -0
- package/dist/utils/serialize-inspector-state.d.ts +3 -0
- package/dist/utils/serialize-inspector-state.js +6 -0
- package/package.json +3 -3
- package/src/add/add-file-with-factory.test.ts +134 -0
- package/src/add/add-file-with-factory.ts +109 -17
- package/src/add/add-rpc-invocations.test.ts +124 -0
- package/src/add/add-rpc-invocations.ts +52 -0
- package/src/add/add-scenario-step.test.ts +61 -0
- package/src/error-codes.ts +1 -0
- package/src/inspector.ts +3 -0
- package/src/types.ts +12 -0
- package/src/utils/filter-inspector-state.test.ts +132 -0
- package/src/utils/filter-inspector-state.ts +45 -4
- package/src/utils/post-process.test.ts +42 -0
- package/src/utils/post-process.ts +110 -0
- package/src/utils/serialize-inspector-state.ts +17 -0
- package/tsconfig.tsbuildinfo +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,60 @@
|
|
|
1
|
+
## 0.12.72
|
|
2
|
+
|
|
3
|
+
### Patch Changes
|
|
4
|
+
|
|
5
|
+
- 202cb72: Refuse a scenario step whose prose opens with its own actor (PKU681). The
|
|
6
|
+
reporter already renders the actor as the sentence's subject, so a step
|
|
7
|
+
authored as `'sam' creates the client` and run as `{ actor: actors.sam }`
|
|
8
|
+
reads "Given sam 'sam' creates the client" — and the hardcoded name desyncs
|
|
9
|
+
the moment the call site changes actor. Only the subject position is refused:
|
|
10
|
+
naming someone else mid-sentence, or an actor keyed after a role noun used as
|
|
11
|
+
a noun ("creates the admin client"), is left alone.
|
|
12
|
+
|
|
13
|
+
## 0.12.71
|
|
14
|
+
|
|
15
|
+
### Patch Changes
|
|
16
|
+
|
|
17
|
+
- 9015400: Derive an addon's required parent services from what its functions actually use.
|
|
18
|
+
|
|
19
|
+
An addon's functions are its published surface — a consumer calls them, nothing
|
|
20
|
+
wires them locally — so `usedFunctions` was empty for an addon's own build and no
|
|
21
|
+
function services were ever aggregated. The generated services map came out
|
|
22
|
+
claiming the addon needed nothing beyond the framework defaults, and
|
|
23
|
+
`requiredParentServices` was derived only from the second parameter of
|
|
24
|
+
`pikkuAddonServices`, which names what the factory reads rather than what its
|
|
25
|
+
functions destructure.
|
|
26
|
+
|
|
27
|
+
The visible cost was an addon that consumed `kysely` in all fifteen of its
|
|
28
|
+
functions and declared it nowhere: the consuming project was never told to supply
|
|
29
|
+
it, the addon lost it at runtime, and every query failed on the first call. The
|
|
30
|
+
only way out was to list the missing names under `forceRequiredServices`, which
|
|
31
|
+
is a detection escape hatch and was never meant to carry a contract.
|
|
32
|
+
|
|
33
|
+
An addon build now aggregates services from every declared function, and treats
|
|
34
|
+
as parent-provided whatever it uses but does not build itself. What the factory
|
|
35
|
+
builds is read from the object literals it returns, minus anything it took off
|
|
36
|
+
the parent bag first — a forwarded service is not a created one, which is how
|
|
37
|
+
`@pikku/addon-admin` returns `scopeService` and friends without claiming to own
|
|
38
|
+
them. Names destructured from the second parameter in the function body, rather
|
|
39
|
+
than in the parameter list, are picked up too.
|
|
40
|
+
|
|
41
|
+
- 8852a75: Register an agent invoked from a function body in the calling deployment unit.
|
|
42
|
+
|
|
43
|
+
`runAgent('houseAssistant', ...)` and `rpc.agent.run('houseAssistant', ...)` resolve against the in-process agent registry, but the deploy analyzer only ever put an agent's registration in its own `agent-*` unit. A function calling one landed in a separate unit whose bootstrap never registered it, so the deployed worker threw `AI agent not found: houseAssistant`.
|
|
44
|
+
|
|
45
|
+
The inspector now records a string-literal agent name passed to `runAgent` / `streamAgent` / `rpc.agent.run` / `rpc.agent.stream` in a function body under `agents.invokedAgentsByFile`, mirroring what it already does for `rpc.invoke` targets. The analyzer carries those names on the calling unit as `invokedAgents` and adds the `ai-model` / `ai-storage` service requirements, and per-unit codegen puts the agent — and its tools — into that unit's filter names so its wiring is generated there too. A dynamic (template-literal) agent name is warned about, as it is for `rpc.invoke`.
|
|
46
|
+
|
|
47
|
+
- Updated dependencies [f4e2e89]
|
|
48
|
+
- @pikku/core@0.12.102
|
|
49
|
+
|
|
50
|
+
## 0.12.70
|
|
51
|
+
|
|
52
|
+
### Patch Changes
|
|
53
|
+
|
|
54
|
+
- d64db40: Prune a filtered-out workflow's file entry, not just its meta.
|
|
55
|
+
|
|
56
|
+
`filterInspectorState` deleted a pruned workflow from `workflows.graphMeta` and `workflows.meta` but left it in `workflows.files`, which is what the wiring generator iterates. Per-unit deploy codegen therefore registered every workflow in the project into every unit, and — because a scenario is told apart from an app workflow by `graphMeta[name].source === 'scenario'`, which the same prune had just removed — each scenario was reclassified as an app workflow. That pulled scenario bodies, their steps, and whatever those import into deployed bundles; on Cloudflare a step reading a fixture off disk failed the whole publish with `No such module "node:fs"`.
|
|
57
|
+
|
|
1
58
|
## 0.12.69
|
|
2
59
|
|
|
3
60
|
### Patch Changes
|
|
@@ -10,6 +10,93 @@ const wrapperFunctionMap = {
|
|
|
10
10
|
pikkuAddonWireServices: 'CreateWireServices',
|
|
11
11
|
pikkuServerLifecycle: 'ServerLifecycle',
|
|
12
12
|
};
|
|
13
|
+
/**
|
|
14
|
+
* What an addon's `pikkuAddonServices` factory takes from the parent: the names
|
|
15
|
+
* destructured off its second parameter, either in the parameter list or from a
|
|
16
|
+
* `const { … } = existingServices` in the body.
|
|
17
|
+
*/
|
|
18
|
+
const extractForwardedServices = (functionNode) => {
|
|
19
|
+
const forwarded = [];
|
|
20
|
+
const secondParam = functionNode.parameters[1];
|
|
21
|
+
if (!secondParam)
|
|
22
|
+
return forwarded;
|
|
23
|
+
const collectBinding = (pattern) => {
|
|
24
|
+
for (const elem of pattern.elements) {
|
|
25
|
+
if (elem.dotDotDotToken)
|
|
26
|
+
continue;
|
|
27
|
+
const name = elem.propertyName && ts.isIdentifier(elem.propertyName)
|
|
28
|
+
? elem.propertyName.text
|
|
29
|
+
: ts.isIdentifier(elem.name)
|
|
30
|
+
? elem.name.text
|
|
31
|
+
: undefined;
|
|
32
|
+
if (name)
|
|
33
|
+
forwarded.push(name);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
if (ts.isObjectBindingPattern(secondParam.name)) {
|
|
37
|
+
collectBinding(secondParam.name);
|
|
38
|
+
return forwarded;
|
|
39
|
+
}
|
|
40
|
+
if (!ts.isIdentifier(secondParam.name))
|
|
41
|
+
return forwarded;
|
|
42
|
+
const paramName = secondParam.name.text;
|
|
43
|
+
const body = functionNode.body;
|
|
44
|
+
if (!ts.isBlock(body))
|
|
45
|
+
return forwarded;
|
|
46
|
+
const visit = (node) => {
|
|
47
|
+
if (ts.isVariableDeclaration(node) &&
|
|
48
|
+
ts.isObjectBindingPattern(node.name) &&
|
|
49
|
+
node.initializer &&
|
|
50
|
+
ts.isIdentifier(node.initializer) &&
|
|
51
|
+
node.initializer.text === paramName) {
|
|
52
|
+
collectBinding(node.name);
|
|
53
|
+
}
|
|
54
|
+
ts.forEachChild(node, visit);
|
|
55
|
+
};
|
|
56
|
+
ts.forEachChild(body, visit);
|
|
57
|
+
return forwarded;
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* The service names the factory's returned object literals name. A spread is
|
|
61
|
+
* skipped — it carries the parent's bag through rather than naming anything —
|
|
62
|
+
* and a name the factory took off the parent is a forward, not a creation.
|
|
63
|
+
*/
|
|
64
|
+
const extractReturnedServices = (functionNode) => {
|
|
65
|
+
const returned = [];
|
|
66
|
+
const collect = (expression) => {
|
|
67
|
+
while (ts.isParenthesizedExpression(expression)) {
|
|
68
|
+
expression = expression.expression;
|
|
69
|
+
}
|
|
70
|
+
if (!ts.isObjectLiteralExpression(expression))
|
|
71
|
+
return;
|
|
72
|
+
for (const property of expression.properties) {
|
|
73
|
+
if (ts.isSpreadAssignment(property))
|
|
74
|
+
continue;
|
|
75
|
+
const name = property.name;
|
|
76
|
+
if (name && (ts.isIdentifier(name) || ts.isStringLiteral(name))) {
|
|
77
|
+
returned.push(name.text);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
const body = functionNode.body;
|
|
82
|
+
if (!ts.isBlock(body)) {
|
|
83
|
+
collect(body);
|
|
84
|
+
return returned;
|
|
85
|
+
}
|
|
86
|
+
const visit = (node) => {
|
|
87
|
+
if (ts.isFunctionDeclaration(node) ||
|
|
88
|
+
ts.isFunctionExpression(node) ||
|
|
89
|
+
ts.isArrowFunction(node)) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (ts.isReturnStatement(node) && node.expression) {
|
|
93
|
+
collect(node.expression);
|
|
94
|
+
}
|
|
95
|
+
ts.forEachChild(node, visit);
|
|
96
|
+
};
|
|
97
|
+
ts.forEachChild(body, visit);
|
|
98
|
+
return returned;
|
|
99
|
+
};
|
|
13
100
|
export const addFileWithFactory = (node, checker, methods = new Map(), expectedTypeName, state) => {
|
|
14
101
|
if (ts.isVariableDeclaration(node)) {
|
|
15
102
|
const fileName = node.getSourceFile().fileName;
|
|
@@ -57,20 +144,15 @@ export const addFileWithFactory = (node, checker, methods = new Map(), expectedT
|
|
|
57
144
|
}
|
|
58
145
|
// Extract existing services an addon needs from the parent
|
|
59
146
|
// (second parameter of pikkuAddonServices callback)
|
|
60
|
-
if (wrapperFunctionName === 'pikkuAddonServices' &&
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
? elem.name.text
|
|
70
|
-
: undefined;
|
|
71
|
-
if (name) {
|
|
72
|
-
state.addonRequiredParentServices.push(name);
|
|
73
|
-
}
|
|
147
|
+
if (wrapperFunctionName === 'pikkuAddonServices' && functionNode) {
|
|
148
|
+
state.addonServicesFactorySeen = true;
|
|
149
|
+
const forwarded = new Set(extractForwardedServices(functionNode));
|
|
150
|
+
for (const name of forwarded) {
|
|
151
|
+
state.addonRequiredParentServices.push(name);
|
|
152
|
+
}
|
|
153
|
+
for (const name of extractReturnedServices(functionNode)) {
|
|
154
|
+
if (!forwarded.has(name)) {
|
|
155
|
+
state.addonCreatedServices.push(name);
|
|
74
156
|
}
|
|
75
157
|
}
|
|
76
158
|
}
|
|
@@ -42,6 +42,33 @@ function extractNamespace(functionRef) {
|
|
|
42
42
|
}
|
|
43
43
|
return null;
|
|
44
44
|
}
|
|
45
|
+
function agentInvocationName(expression) {
|
|
46
|
+
if (ts.isIdentifier(expression)) {
|
|
47
|
+
if (expression.text === 'runAgent')
|
|
48
|
+
return 'runAgent';
|
|
49
|
+
if (expression.text === 'streamAgent')
|
|
50
|
+
return 'streamAgent';
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
if (!ts.isPropertyAccessExpression(expression))
|
|
54
|
+
return null;
|
|
55
|
+
const method = expression.name.text;
|
|
56
|
+
if (method !== 'run' && method !== 'stream')
|
|
57
|
+
return null;
|
|
58
|
+
const agentReceiver = invocationReceiver(expression.expression);
|
|
59
|
+
if (!ts.isPropertyAccessExpression(agentReceiver) ||
|
|
60
|
+
agentReceiver.name.text !== 'agent') {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
const rpcReceiver = invocationReceiver(agentReceiver.expression);
|
|
64
|
+
const isRpc = ts.isIdentifier(rpcReceiver)
|
|
65
|
+
? rpcReceiver.text === 'rpc'
|
|
66
|
+
: ts.isPropertyAccessExpression(rpcReceiver) &&
|
|
67
|
+
rpcReceiver.name.text === 'rpc';
|
|
68
|
+
if (!isRpc)
|
|
69
|
+
return null;
|
|
70
|
+
return method === 'run' ? 'rpc.agent.run' : 'rpc.agent.stream';
|
|
71
|
+
}
|
|
45
72
|
/**
|
|
46
73
|
* Scan for rpc.invoke() calls to track which functions are actually being invoked
|
|
47
74
|
* Also detects addon usage via:
|
|
@@ -80,6 +107,27 @@ export function addRPCInvocations(node, state, logger) {
|
|
|
80
107
|
state.workflows.invokedWorkflows.add(workflowName);
|
|
81
108
|
}
|
|
82
109
|
}
|
|
110
|
+
const agentCall = agentInvocationName(expression);
|
|
111
|
+
if (agentCall) {
|
|
112
|
+
const [firstArg] = args;
|
|
113
|
+
if (firstArg && ts.isStringLiteral(firstArg)) {
|
|
114
|
+
const agentName = firstArg.text;
|
|
115
|
+
logger.debug(`• Found ${agentCall}() call: ${agentName}`);
|
|
116
|
+
const sourceFileName = node.getSourceFile().fileName;
|
|
117
|
+
let byFile = state.agents.invokedAgentsByFile.get(sourceFileName);
|
|
118
|
+
if (!byFile) {
|
|
119
|
+
byFile = new Set();
|
|
120
|
+
state.agents.invokedAgentsByFile.set(sourceFileName, byFile);
|
|
121
|
+
}
|
|
122
|
+
byFile.add(agentName);
|
|
123
|
+
}
|
|
124
|
+
else if (firstArg &&
|
|
125
|
+
(ts.isTemplateExpression(firstArg) ||
|
|
126
|
+
ts.isNoSubstitutionTemplateLiteral(firstArg))) {
|
|
127
|
+
logger.warn(`• Found dynamic agent invocation: ${firstArg.getText()}`);
|
|
128
|
+
logger.warn(`\tYou can only use string literals for agent names, with ' or " and not \``);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
83
131
|
// Check for rpc.invoke('...') calls
|
|
84
132
|
const invokeReceiver = ts.isPropertyAccessExpression(expression)
|
|
85
133
|
? invocationReceiver(expression.expression)
|
package/dist/error-codes.d.ts
CHANGED
|
@@ -24,6 +24,7 @@ export declare enum ErrorCode {
|
|
|
24
24
|
SCENARIO_STEP_TARGET_NOT_STATIC = "PKU678",
|
|
25
25
|
SCENARIO_NOT_EXTRACTABLE = "PKU679",
|
|
26
26
|
SCENARIO_HAS_NO_ASSERTION = "PKU680",
|
|
27
|
+
SCENARIO_STEP_PROSE_NAMES_ACTOR = "PKU681",
|
|
27
28
|
WORKFLOW_ORCHESTRATOR_NOT_CONFIGURED = "PKU600",
|
|
28
29
|
INVALID_DSL_WORKFLOW = "PKU641",
|
|
29
30
|
WORKFLOW_GRAPH_ADDON_NOT_WIRED = "PKU642",
|
package/dist/error-codes.js
CHANGED
|
@@ -26,6 +26,7 @@ export var ErrorCode;
|
|
|
26
26
|
ErrorCode["SCENARIO_STEP_TARGET_NOT_STATIC"] = "PKU678";
|
|
27
27
|
ErrorCode["SCENARIO_NOT_EXTRACTABLE"] = "PKU679";
|
|
28
28
|
ErrorCode["SCENARIO_HAS_NO_ASSERTION"] = "PKU680";
|
|
29
|
+
ErrorCode["SCENARIO_STEP_PROSE_NAMES_ACTOR"] = "PKU681";
|
|
29
30
|
ErrorCode["WORKFLOW_ORCHESTRATOR_NOT_CONFIGURED"] = "PKU600";
|
|
30
31
|
ErrorCode["INVALID_DSL_WORKFLOW"] = "PKU641";
|
|
31
32
|
ErrorCode["WORKFLOW_GRAPH_ADDON_NOT_WIRED"] = "PKU642";
|
package/dist/inspector.js
CHANGED
|
@@ -36,6 +36,8 @@ export function getInitialInspectorState(rootDir) {
|
|
|
36
36
|
wireServicesFactories: new Map(),
|
|
37
37
|
wireServicesMeta: new Map(),
|
|
38
38
|
addonRequiredParentServices: [],
|
|
39
|
+
addonCreatedServices: [],
|
|
40
|
+
addonServicesFactorySeen: false,
|
|
39
41
|
addonServerlessIncompatible: new Map(),
|
|
40
42
|
configFactories: new Map(),
|
|
41
43
|
serverLifecycleFactories: new Map(),
|
|
@@ -116,6 +118,7 @@ export function getInitialInspectorState(rootDir) {
|
|
|
116
118
|
agents: {
|
|
117
119
|
agentsMeta: {},
|
|
118
120
|
files: new Map(),
|
|
121
|
+
invokedAgentsByFile: new Map(),
|
|
119
122
|
},
|
|
120
123
|
scorers: {
|
|
121
124
|
scorersMeta: {},
|
package/dist/types.d.ts
CHANGED
|
@@ -448,6 +448,8 @@ export interface InspectorState {
|
|
|
448
448
|
wireServicesFactories: PathToNameAndType;
|
|
449
449
|
wireServicesMeta: Map<string, string[]>;
|
|
450
450
|
addonRequiredParentServices: string[];
|
|
451
|
+
addonCreatedServices: string[];
|
|
452
|
+
addonServicesFactorySeen: boolean;
|
|
451
453
|
addonServerlessIncompatible: Map<string, string[]>;
|
|
452
454
|
configFactories: PathToNameAndType;
|
|
453
455
|
serverLifecycleFactories: PathToNameAndType;
|
|
@@ -571,6 +573,16 @@ export interface InspectorState {
|
|
|
571
573
|
path: string;
|
|
572
574
|
exportedName: string;
|
|
573
575
|
}>;
|
|
576
|
+
/**
|
|
577
|
+
* Agent registry keys named by a `runAgent`/`rpc.agent.run` call in a
|
|
578
|
+
* function body, keyed by the file the call was written in.
|
|
579
|
+
*
|
|
580
|
+
* The lookup those calls perform is in-process, so the agent has to be
|
|
581
|
+
* registered in the same deployment unit as its caller. Without this the
|
|
582
|
+
* deploy analyzer only ever saw an agent as its own unit, and a caller in
|
|
583
|
+
* another unit failed at run time with "AI agent not found".
|
|
584
|
+
*/
|
|
585
|
+
invokedAgentsByFile: Map<string, Set<string>>;
|
|
574
586
|
};
|
|
575
587
|
scorers: {
|
|
576
588
|
scorersMeta: ScorerMeta;
|
|
@@ -2,6 +2,23 @@ import { parseVersionedId } from '@pikku/core/utils';
|
|
|
2
2
|
import { makeContextBasedId } from './extract-function-name.js';
|
|
3
3
|
import { aggregateRequiredServices } from './post-process.js';
|
|
4
4
|
import { IncompatibleDeployTargetError, resolveDeployTarget, } from './resolve-deploy-target.js';
|
|
5
|
+
/**
|
|
6
|
+
* Drop a workflow from every map that describes it. `files` and `graphFiles`
|
|
7
|
+
* are what the wiring generator iterates, and `graphMeta` is what tells a
|
|
8
|
+
* scenario apart from an app workflow — leaving a pruned workflow's file entry
|
|
9
|
+
* behind registers it anyway, and a scenario whose graphMeta has gone reads as
|
|
10
|
+
* an app workflow, which is how scenario steps reached deployed bundles.
|
|
11
|
+
*/
|
|
12
|
+
const pruneWorkflow = (workflows, name, pikkuFuncId) => {
|
|
13
|
+
delete workflows.graphMeta[name];
|
|
14
|
+
delete workflows.meta[name];
|
|
15
|
+
workflows.files.delete(name);
|
|
16
|
+
workflows.graphFiles.delete(name);
|
|
17
|
+
if (pikkuFuncId) {
|
|
18
|
+
workflows.files.delete(pikkuFuncId);
|
|
19
|
+
workflows.graphFiles.delete(pikkuFuncId);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
5
22
|
// Module-level Set to track warned groups across multiple filter calls
|
|
6
23
|
const globalWarnedGroups = new Set();
|
|
7
24
|
/**
|
|
@@ -334,6 +351,8 @@ export function filterInspectorState(state, filters, logger) {
|
|
|
334
351
|
...state.workflows,
|
|
335
352
|
graphMeta: JSON.parse(JSON.stringify(state.workflows?.graphMeta ?? {})),
|
|
336
353
|
meta: JSON.parse(JSON.stringify(state.workflows?.meta ?? {})),
|
|
354
|
+
files: new Map(state.workflows?.files ?? []),
|
|
355
|
+
graphFiles: new Map(state.workflows?.graphFiles ?? []),
|
|
337
356
|
},
|
|
338
357
|
channels: {
|
|
339
358
|
...state.channels,
|
|
@@ -366,6 +385,13 @@ export function filterInspectorState(state, filters, logger) {
|
|
|
366
385
|
...state.agents,
|
|
367
386
|
agentsMeta: JSON.parse(JSON.stringify(state.agents?.agentsMeta ?? {})),
|
|
368
387
|
files: new Map(),
|
|
388
|
+
invokedAgentsByFile: new Map([
|
|
389
|
+
...(state.agents?.invokedAgentsByFile ??
|
|
390
|
+
new Map()),
|
|
391
|
+
].map(([file, agents]) => [
|
|
392
|
+
file,
|
|
393
|
+
new Set(agents),
|
|
394
|
+
])),
|
|
369
395
|
},
|
|
370
396
|
// Scorers carry through every filter. They are named by the agents that
|
|
371
397
|
// survive it, and a deployment that quietly stopped grading would look
|
|
@@ -768,13 +794,11 @@ export function filterInspectorState(state, filters, logger) {
|
|
|
768
794
|
const pikkuFuncId = graphMeta?.pikkuFuncId ?? workflowMeta?.pikkuFuncId;
|
|
769
795
|
if (pikkuFuncId &&
|
|
770
796
|
!filteredState.serviceAggregation.usedFunctions.has(pikkuFuncId)) {
|
|
771
|
-
|
|
772
|
-
delete filteredState.workflows.meta[name];
|
|
797
|
+
pruneWorkflow(filteredState.workflows, name, pikkuFuncId);
|
|
773
798
|
}
|
|
774
799
|
else if (!pikkuFuncId) {
|
|
775
800
|
// No function ID found — prune it
|
|
776
|
-
|
|
777
|
-
delete filteredState.workflows.meta[name];
|
|
801
|
+
pruneWorkflow(filteredState.workflows, name, pikkuFuncId);
|
|
778
802
|
}
|
|
779
803
|
}
|
|
780
804
|
// Prune RPC meta to only include entries whose target function survived
|
|
@@ -853,6 +877,16 @@ export function filterInspectorState(state, filters, logger) {
|
|
|
853
877
|
if (filteredState.serviceAggregation.requiredServices.has('workflowService')) {
|
|
854
878
|
filteredState.serviceAggregation.requiredServices.add('queueService');
|
|
855
879
|
}
|
|
880
|
+
const keptFunctionFiles = new Set();
|
|
881
|
+
for (const file of filteredState.functions.files.values()) {
|
|
882
|
+
if (file?.path)
|
|
883
|
+
keptFunctionFiles.add(file.path);
|
|
884
|
+
}
|
|
885
|
+
for (const file of filteredState.agents.invokedAgentsByFile.keys()) {
|
|
886
|
+
if (!keptFunctionFiles.has(file)) {
|
|
887
|
+
filteredState.agents.invokedAgentsByFile.delete(file);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
856
890
|
if ((filteredState.rpc.wireAddonDeclarations?.size ?? 0) > 0) {
|
|
857
891
|
const referencedIds = new Set(filteredState.serviceAggregation.usedFunctions);
|
|
858
892
|
const keptFiles = new Set();
|
|
@@ -101,6 +101,16 @@ export declare function validateAgentModels(logger: InspectorLogger, state: Insp
|
|
|
101
101
|
* the check read `undefined` there and never fired.
|
|
102
102
|
*/
|
|
103
103
|
export declare function validateScenarioServices(logger: InspectorLogger, state: InspectorState | Omit<InspectorState, 'typesLookup'>): void;
|
|
104
|
+
/**
|
|
105
|
+
* Does this step's prose open by naming the actor who is already its subject?
|
|
106
|
+
*
|
|
107
|
+
* `composeStepProse` renders the actor as the sentence's subject, so a step
|
|
108
|
+
* authored as `'sam' creates the client` reads "Given sam 'sam' creates the
|
|
109
|
+
* client". Only the subject position counts: a persona keyed after a role noun
|
|
110
|
+
* ("admin") must still be able to say "creates the admin client", and naming
|
|
111
|
+
* someone mid-sentence — "sends sam an invite" — is ordinary prose.
|
|
112
|
+
*/
|
|
113
|
+
export declare const proseOpensWithActor: (prose: string, actor: string) => boolean;
|
|
104
114
|
/**
|
|
105
115
|
* Walk every scenario step, wherever it is nested, and validate the two things
|
|
106
116
|
* that can only be known once both the workflow meta and the function meta
|
|
@@ -313,6 +313,31 @@ export function aggregateRequiredServices(state) {
|
|
|
313
313
|
requiredServices.add(service);
|
|
314
314
|
}
|
|
315
315
|
}
|
|
316
|
+
// 8. When this project IS an addon, what it needs from its parent is whatever
|
|
317
|
+
// its functions use minus whatever its own factory builds. Reading only the
|
|
318
|
+
// factory's second parameter missed everything the functions destructure and
|
|
319
|
+
// the factory never names — `kysely` above all — which left the contract empty
|
|
320
|
+
// and let a consumer omit it and fail at the first query instead of at build.
|
|
321
|
+
if (state.addonServicesFactorySeen) {
|
|
322
|
+
// Every function an addon declares is part of its published surface — a
|
|
323
|
+
// consumer calls it — so none of them are reachable from a local wiring and
|
|
324
|
+
// `usedFunctions` is empty. Aggregating only from wirings left the services
|
|
325
|
+
// map claiming an addon needs nothing, which is what drove addons to list
|
|
326
|
+
// their own services under `forceRequiredServices` by hand.
|
|
327
|
+
for (const funcMeta of Object.values(state.functions.meta)) {
|
|
328
|
+
addServices(funcMeta?.services);
|
|
329
|
+
}
|
|
330
|
+
const createdSet = new Set(state.addonCreatedServices ?? []);
|
|
331
|
+
const parentServices = new Set(parentDeclared);
|
|
332
|
+
for (const service of requiredServices) {
|
|
333
|
+
if (!createdSet.has(service) &&
|
|
334
|
+
!internalServices.has(service) &&
|
|
335
|
+
!defaultServices.has(service)) {
|
|
336
|
+
parentServices.add(service);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
state.addonRequiredParentServices = [...parentServices];
|
|
340
|
+
}
|
|
316
341
|
}
|
|
317
342
|
export function validateSecretOverrides(logger, state) {
|
|
318
343
|
const { wireAddonDeclarations } = state.rpc;
|
|
@@ -734,6 +759,67 @@ export function validateScenarioServices(logger, state) {
|
|
|
734
759
|
}
|
|
735
760
|
}
|
|
736
761
|
}
|
|
762
|
+
/**
|
|
763
|
+
* Does this step's prose open by naming the actor who is already its subject?
|
|
764
|
+
*
|
|
765
|
+
* `composeStepProse` renders the actor as the sentence's subject, so a step
|
|
766
|
+
* authored as `'sam' creates the client` reads "Given sam 'sam' creates the
|
|
767
|
+
* client". Only the subject position counts: a persona keyed after a role noun
|
|
768
|
+
* ("admin") must still be able to say "creates the admin client", and naming
|
|
769
|
+
* someone mid-sentence — "sends sam an invite" — is ordinary prose.
|
|
770
|
+
*/
|
|
771
|
+
export const proseOpensWithActor = (prose, actor) => {
|
|
772
|
+
if (!actor) {
|
|
773
|
+
return false;
|
|
774
|
+
}
|
|
775
|
+
const escaped = actor.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
776
|
+
return new RegExp(`^["'“‘\\s]*${escaped}(?:['’]s)?\\b`, 'i').test(prose);
|
|
777
|
+
};
|
|
778
|
+
/**
|
|
779
|
+
* The prose that will actually render for one step, in the runtime's own
|
|
780
|
+
* precedence: a call-site description overrides the step's template, which
|
|
781
|
+
* overrides its declared description, which falls back to the durable name.
|
|
782
|
+
*
|
|
783
|
+
* Every candidate is returned rather than only the winner, because a template
|
|
784
|
+
* is authored where the actor is not yet known — so it is wrong at every call
|
|
785
|
+
* site that runs it as the persona it names, not just the winning one.
|
|
786
|
+
*/
|
|
787
|
+
const renderableProse = (step, stepMeta) => {
|
|
788
|
+
const description = step.options?.description;
|
|
789
|
+
if (description) {
|
|
790
|
+
return [step.stepName, description];
|
|
791
|
+
}
|
|
792
|
+
return [
|
|
793
|
+
step.stepName,
|
|
794
|
+
stepMeta?.scenarioStepTemplate,
|
|
795
|
+
stepMeta?.description,
|
|
796
|
+
].filter((prose) => typeof prose === 'string');
|
|
797
|
+
};
|
|
798
|
+
/**
|
|
799
|
+
* A step's prose must not open by naming the actor it already runs as (PKU681).
|
|
800
|
+
*
|
|
801
|
+
* The reporter renders the actor as the sentence's subject, so `'sam' creates
|
|
802
|
+
* the client` run as `actors.sam` reads "Given sam 'sam' creates the client" —
|
|
803
|
+
* and the hardcoded copy silently desyncs the moment the call site changes
|
|
804
|
+
* actor. Only the subject position is refused; naming someone mid-sentence is
|
|
805
|
+
* ordinary prose, and a persona keyed after a role noun can still say "creates
|
|
806
|
+
* the admin client".
|
|
807
|
+
*/
|
|
808
|
+
const validateStepProse = (logger, workflowName, step, stepMeta) => {
|
|
809
|
+
const actor = step.actor;
|
|
810
|
+
if (!actor) {
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
const offender = renderableProse(step, stepMeta).find((prose) => proseOpensWithActor(prose, actor));
|
|
814
|
+
if (offender === undefined) {
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
logger.critical(ErrorCode.SCENARIO_STEP_PROSE_NAMES_ACTOR, `Scenario '${workflowName}' names its actor in the step prose: '${offender}' runs as '${actor}'. ` +
|
|
818
|
+
`The reporter already renders the actor as the subject, so this reads ` +
|
|
819
|
+
`"${capitaliseFirst(step.phase)} ${actor} ${offender}". Drop the name and let ` +
|
|
820
|
+
`{ actor: actors.${actor} } supply it — the step reads as a bare third-person predicate.`);
|
|
821
|
+
};
|
|
822
|
+
const capitaliseFirst = (value) => value.charAt(0).toUpperCase() + value.slice(1);
|
|
737
823
|
/**
|
|
738
824
|
* Walk every scenario step, wherever it is nested, and validate the two things
|
|
739
825
|
* that can only be known once both the workflow meta and the function meta
|
|
@@ -765,6 +851,7 @@ export function validateScenarioSteps(logger, state) {
|
|
|
765
851
|
}
|
|
766
852
|
state.serviceAggregation.usedFunctions.add(step.stepFunc);
|
|
767
853
|
const stepMeta = state.functions.meta[step.stepFunc];
|
|
854
|
+
validateStepProse(logger, workflowName, step, stepMeta);
|
|
768
855
|
if (stepMeta?.scenarioStepRequiresActor && !step.actor) {
|
|
769
856
|
const why = stepMeta.scenarioStepSurfaces?.includes('browser')
|
|
770
857
|
? 'declares a browser binding'
|
|
@@ -57,6 +57,8 @@ export interface SerializableInspectorState {
|
|
|
57
57
|
]>;
|
|
58
58
|
wireServicesMeta: Array<[string, string[]]>;
|
|
59
59
|
addonRequiredParentServices: string[];
|
|
60
|
+
addonCreatedServices: string[];
|
|
61
|
+
addonServicesFactorySeen: boolean;
|
|
60
62
|
addonServerlessIncompatible: Array<[string, string[]]>;
|
|
61
63
|
configFactories: Array<[
|
|
62
64
|
string,
|
|
@@ -210,6 +212,7 @@ export interface SerializableInspectorState {
|
|
|
210
212
|
files: string[];
|
|
211
213
|
};
|
|
212
214
|
agents: {
|
|
215
|
+
invokedAgentsByFile?: Array<[string, string[]]>;
|
|
213
216
|
agentsMeta: InspectorState['agents']['agentsMeta'];
|
|
214
217
|
files: [string, {
|
|
215
218
|
path: string;
|
|
@@ -24,6 +24,8 @@ export function serializeInspectorState(state) {
|
|
|
24
24
|
wireServicesFactories: Array.from(state.wireServicesFactories.entries()),
|
|
25
25
|
wireServicesMeta: Array.from(state.wireServicesMeta.entries()),
|
|
26
26
|
addonRequiredParentServices: state.addonRequiredParentServices,
|
|
27
|
+
addonCreatedServices: state.addonCreatedServices,
|
|
28
|
+
addonServicesFactorySeen: state.addonServicesFactorySeen,
|
|
27
29
|
addonServerlessIncompatible: Array.from(state.addonServerlessIncompatible.entries()),
|
|
28
30
|
configFactories: Array.from(state.configFactories.entries()),
|
|
29
31
|
serverLifecycleFactories: Array.from(state.serverLifecycleFactories.entries()),
|
|
@@ -94,6 +96,7 @@ export function serializeInspectorState(state) {
|
|
|
94
96
|
agents: {
|
|
95
97
|
agentsMeta: state.agents?.agentsMeta ?? {},
|
|
96
98
|
files: Array.from(state.agents?.files?.entries() ?? []),
|
|
99
|
+
invokedAgentsByFile: Array.from((state.agents?.invokedAgentsByFile ?? new Map()).entries()).map(([file, agents]) => [file, Array.from(agents)]),
|
|
97
100
|
},
|
|
98
101
|
scorers: {
|
|
99
102
|
scorersMeta: state.scorers?.scorersMeta ?? {},
|
|
@@ -198,6 +201,8 @@ export function deserializeInspectorState(data) {
|
|
|
198
201
|
wireServicesFactories: new Map(data.wireServicesFactories),
|
|
199
202
|
wireServicesMeta: new Map(data.wireServicesMeta),
|
|
200
203
|
addonRequiredParentServices: data.addonRequiredParentServices || [],
|
|
204
|
+
addonCreatedServices: data.addonCreatedServices || [],
|
|
205
|
+
addonServicesFactorySeen: data.addonServicesFactorySeen ?? false,
|
|
201
206
|
addonServerlessIncompatible: new Map(data.addonServerlessIncompatible || []),
|
|
202
207
|
configFactories: new Map(data.configFactories),
|
|
203
208
|
serverLifecycleFactories: new Map(data.serverLifecycleFactories),
|
|
@@ -272,6 +277,7 @@ export function deserializeInspectorState(data) {
|
|
|
272
277
|
agents: {
|
|
273
278
|
agentsMeta: data.agents?.agentsMeta || {},
|
|
274
279
|
files: new Map(data.agents?.files || []),
|
|
280
|
+
invokedAgentsByFile: new Map((data.agents?.invokedAgentsByFile || []).map(([file, agents]) => [file, new Set(agents)])),
|
|
275
281
|
},
|
|
276
282
|
scorers: {
|
|
277
283
|
scorersMeta: data.scorers?.scorersMeta || {},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pikku/inspector",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.72",
|
|
4
4
|
"description": "TypeScript inspector for Pikku — discovers functions and wirings to drive code generation",
|
|
5
5
|
"author": "yasser.fadl@gmail.com",
|
|
6
6
|
"license": "BUSL-1.1",
|
|
@@ -45,10 +45,10 @@
|
|
|
45
45
|
"zod-to-ts": "^2.1.0"
|
|
46
46
|
},
|
|
47
47
|
"peerDependencies": {
|
|
48
|
-
"@pikku/core": "^0.12.
|
|
48
|
+
"@pikku/core": "^0.12.102"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
|
-
"@pikku/core": "^0.12.
|
|
51
|
+
"@pikku/core": "^0.12.102",
|
|
52
52
|
"@types/node": "^24.13.3"
|
|
53
53
|
},
|
|
54
54
|
"engines": {
|