@agent-surface/cli 0.13.0 → 0.15.0
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/dist/bin.js +41 -14
- package/dist/bin.js.map +1 -1
- package/dist/{check-TJIX7NDE.js → check-VWRYNYLN.js} +66 -48
- package/dist/check-VWRYNYLN.js.map +1 -0
- package/dist/chunk-A5UCBF7D.js +246 -0
- package/dist/chunk-A5UCBF7D.js.map +1 -0
- package/dist/chunk-GYYWHZPM.js +532 -0
- package/dist/chunk-GYYWHZPM.js.map +1 -0
- package/dist/chunk-NFK3XWWH.js +1475 -0
- package/dist/chunk-NFK3XWWH.js.map +1 -0
- package/dist/chunk-Y2LSPEVK.js +61 -0
- package/dist/chunk-Y2LSPEVK.js.map +1 -0
- package/dist/index.d.ts +3 -2
- package/dist/{init-6XV64LK2.js → init-BVZR6CRS.js} +62 -40
- package/dist/init-BVZR6CRS.js.map +1 -0
- package/dist/{ink-QR7X7TAC.js → ink-ZCQ26EY4.js} +71 -17
- package/dist/ink-ZCQ26EY4.js.map +1 -0
- package/dist/{inspect-NJ4J3MPP.js → inspect-3YFPCYQJ.js} +88 -104
- package/dist/inspect-3YFPCYQJ.js.map +1 -0
- package/dist/snapshot-5QKLZKZI.js +130 -0
- package/dist/snapshot-5QKLZKZI.js.map +1 -0
- package/package.json +4 -4
- package/dist/check-TJIX7NDE.js.map +0 -1
- package/dist/chunk-3AJ343NA.js +0 -178
- package/dist/chunk-3AJ343NA.js.map +0 -1
- package/dist/chunk-IALBMW3R.js +0 -278
- package/dist/chunk-IALBMW3R.js.map +0 -1
- package/dist/chunk-L7GHSC2Z.js +0 -465
- package/dist/chunk-L7GHSC2Z.js.map +0 -1
- package/dist/chunk-UGCLJ5JX.js +0 -724
- package/dist/chunk-UGCLJ5JX.js.map +0 -1
- package/dist/chunk-VX6GBEP3.js +0 -539
- package/dist/chunk-VX6GBEP3.js.map +0 -1
- package/dist/init-6XV64LK2.js.map +0 -1
- package/dist/ink-QR7X7TAC.js.map +0 -1
- package/dist/inspect-NJ4J3MPP.js.map +0 -1
- package/dist/snapshot-ZGTAF2Y5.js +0 -77
- package/dist/snapshot-ZGTAF2Y5.js.map +0 -1
|
@@ -0,0 +1,1475 @@
|
|
|
1
|
+
// src/render/model.ts
|
|
2
|
+
function flatRows(view) {
|
|
3
|
+
return view.groups.flatMap((group) => group.rows);
|
|
4
|
+
}
|
|
5
|
+
function pathOf(capabilityId) {
|
|
6
|
+
return capabilityId.replace(/^(view|domain):/, "");
|
|
7
|
+
}
|
|
8
|
+
function leafOf(capabilityId) {
|
|
9
|
+
const withoutPlane = pathOf(capabilityId);
|
|
10
|
+
const dot = withoutPlane.lastIndexOf(".");
|
|
11
|
+
return dot === -1 ? withoutPlane : withoutPlane.slice(dot + 1);
|
|
12
|
+
}
|
|
13
|
+
function actionFlags(action) {
|
|
14
|
+
const flags = [];
|
|
15
|
+
if (action.idempotent) flags.push("idempotent");
|
|
16
|
+
if (action.reversible) flags.push("reversible");
|
|
17
|
+
if (action.confirmation !== "never") flags.push(`confirmation:${action.confirmation}`);
|
|
18
|
+
return flags;
|
|
19
|
+
}
|
|
20
|
+
function procedureFlags(procedure) {
|
|
21
|
+
const flags = [];
|
|
22
|
+
if (procedure.confirmation !== "never") flags.push(`confirmation:${procedure.confirmation}`);
|
|
23
|
+
for (const field of procedure.boundFields) {
|
|
24
|
+
flags.push(`${field.path} bound${field.locked ? "+locked" : ""}`);
|
|
25
|
+
}
|
|
26
|
+
return flags;
|
|
27
|
+
}
|
|
28
|
+
function explanationIndex(explanation) {
|
|
29
|
+
const index = /* @__PURE__ */ new Map();
|
|
30
|
+
for (const capability of explanation.capabilities) {
|
|
31
|
+
index.set(`${capability.capabilityId}\0${capability.registrationId}`, capability);
|
|
32
|
+
}
|
|
33
|
+
return index;
|
|
34
|
+
}
|
|
35
|
+
function buildView(result, options = {}) {
|
|
36
|
+
const { snapshot, explanation } = result;
|
|
37
|
+
const index = explanationIndex(explanation);
|
|
38
|
+
const groups = [];
|
|
39
|
+
const counts = { callable: 0, disabled: 0, hidden: 0 };
|
|
40
|
+
const enrich = (row, capabilityId, registrationId) => {
|
|
41
|
+
const explained = index.get(`${capabilityId}\0${registrationId}`);
|
|
42
|
+
if (options.explain && explained) {
|
|
43
|
+
row.policies = explained.policies;
|
|
44
|
+
row.availability = explained.availability;
|
|
45
|
+
}
|
|
46
|
+
return row;
|
|
47
|
+
};
|
|
48
|
+
for (const component of snapshot.components) {
|
|
49
|
+
const rows = [];
|
|
50
|
+
for (const observation of component.observations) {
|
|
51
|
+
rows.push(
|
|
52
|
+
enrich(
|
|
53
|
+
rowFor(observation, "observation", void 0, [], options, {
|
|
54
|
+
input: void 0,
|
|
55
|
+
output: observation.outputSchema
|
|
56
|
+
}),
|
|
57
|
+
observation.capabilityId,
|
|
58
|
+
component.registrationId
|
|
59
|
+
)
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
for (const action of component.actions) {
|
|
63
|
+
rows.push(
|
|
64
|
+
enrich(
|
|
65
|
+
rowFor(action, "action", action.effect, actionFlags(action), options, {
|
|
66
|
+
input: action.inputSchema,
|
|
67
|
+
output: action.outputSchema
|
|
68
|
+
}),
|
|
69
|
+
action.capabilityId,
|
|
70
|
+
component.registrationId
|
|
71
|
+
)
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
groups.push({
|
|
75
|
+
heading: component.instanceId === "default" ? component.type : `${component.type}@${component.instanceId}`,
|
|
76
|
+
rows
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
if (snapshot.procedures.length > 0) {
|
|
80
|
+
groups.push({
|
|
81
|
+
heading: "authoritative (domain)",
|
|
82
|
+
rows: snapshot.procedures.map(
|
|
83
|
+
(procedure) => enrich(
|
|
84
|
+
{
|
|
85
|
+
capabilityId: procedure.procedureId,
|
|
86
|
+
name: procedure.procedureId.replace(/^domain:/, ""),
|
|
87
|
+
path: pathOf(procedure.procedureId),
|
|
88
|
+
kind: "procedure",
|
|
89
|
+
plane: "domain",
|
|
90
|
+
outcome: procedure.available ? "expose" : "disable",
|
|
91
|
+
description: procedure.description,
|
|
92
|
+
...procedure.unavailableReason ? { reason: procedure.unavailableReason } : {},
|
|
93
|
+
effect: procedure.effect,
|
|
94
|
+
flags: procedureFlags(procedure),
|
|
95
|
+
tags: [procedure.effect, ...procedureFlags(procedure)],
|
|
96
|
+
...options.schemas ? { schemas: { input: procedure.inputSchema, output: procedure.outputSchema } } : {}
|
|
97
|
+
},
|
|
98
|
+
procedure.procedureId,
|
|
99
|
+
procedure.registrationId
|
|
100
|
+
)
|
|
101
|
+
)
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
const hidden = explanation.capabilities.filter((c) => c.outcome === "hide");
|
|
105
|
+
if (hidden.length > 0) {
|
|
106
|
+
groups.push({
|
|
107
|
+
heading: "hidden by policy (absent from the snapshot)",
|
|
108
|
+
rows: hidden.map((capability) => ({
|
|
109
|
+
capabilityId: capability.capabilityId,
|
|
110
|
+
name: leafOf(capability.capabilityId),
|
|
111
|
+
path: pathOf(capability.capabilityId),
|
|
112
|
+
kind: capability.kind,
|
|
113
|
+
plane: capability.plane,
|
|
114
|
+
outcome: "hide",
|
|
115
|
+
description: capability.description,
|
|
116
|
+
// No reason line, deliberately. The reason a hidden capability carries
|
|
117
|
+
// is its *availability* reason — "The drawer is not open" — and printing
|
|
118
|
+
// that under a row marked `hidden` says the UI declined when authority
|
|
119
|
+
// did. Authority hides, state discloses (D11/D12), and the two must
|
|
120
|
+
// never look alike. Why it was hidden is a policy question, which is
|
|
121
|
+
// what `--explain` answers.
|
|
122
|
+
//
|
|
123
|
+
// A hidden capability has no snapshot entry, so there is no effect to
|
|
124
|
+
// report — the table prints an em dash rather than inventing one. The
|
|
125
|
+
// capability path already carries the component type; only a non-default
|
|
126
|
+
// instance adds anything.
|
|
127
|
+
flags: capability.component.instanceId === "default" ? [] : [`@${capability.component.instanceId}`],
|
|
128
|
+
tags: [`${capability.component.type}@${capability.component.instanceId}`],
|
|
129
|
+
...options.explain ? { policies: capability.policies, availability: capability.availability } : {}
|
|
130
|
+
}))
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
for (const capability of explanation.capabilities) {
|
|
134
|
+
if (capability.outcome === "expose") counts.callable += 1;
|
|
135
|
+
else if (capability.outcome === "disable") counts.disabled += 1;
|
|
136
|
+
else counts.hidden += 1;
|
|
137
|
+
}
|
|
138
|
+
return {
|
|
139
|
+
scenario: result.scenario,
|
|
140
|
+
...snapshot.route?.path ? { route: snapshot.route.path } : {},
|
|
141
|
+
...result.scope ? { scope: result.scope } : {},
|
|
142
|
+
groups,
|
|
143
|
+
counts,
|
|
144
|
+
rejections: result.rejections ?? [],
|
|
145
|
+
explained: options.explain === true
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
function rowFor(descriptor, kind, effect, flags, options, schemas) {
|
|
149
|
+
return {
|
|
150
|
+
capabilityId: descriptor.capabilityId,
|
|
151
|
+
name: descriptor.name,
|
|
152
|
+
path: pathOf(descriptor.capabilityId),
|
|
153
|
+
kind,
|
|
154
|
+
plane: "view",
|
|
155
|
+
outcome: descriptor.available ? "expose" : "disable",
|
|
156
|
+
description: descriptor.description,
|
|
157
|
+
...descriptor.unavailableReason ? { reason: descriptor.unavailableReason } : {},
|
|
158
|
+
...effect ? { effect } : {},
|
|
159
|
+
flags,
|
|
160
|
+
// The grouped detail view prints one combined list, the way it always has.
|
|
161
|
+
tags: effect ? [effect, ...flags] : [kind, ...flags],
|
|
162
|
+
...options.schemas ? { schemas } : {}
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// src/coverage.ts
|
|
167
|
+
import { existsSync, readFileSync } from "fs";
|
|
168
|
+
import { join } from "path";
|
|
169
|
+
var ALLOWLIST_FILE = "coverage-allow.json";
|
|
170
|
+
var UNREAD_ALLOWLIST_FILE = "unresolved-allow.json";
|
|
171
|
+
function allowlistPathFor(baselineDir) {
|
|
172
|
+
return join(baselineDir, ALLOWLIST_FILE);
|
|
173
|
+
}
|
|
174
|
+
function unreadAllowlistPathFor(baselineDir) {
|
|
175
|
+
return join(baselineDir, UNREAD_ALLOWLIST_FILE);
|
|
176
|
+
}
|
|
177
|
+
function readAllowlist(path, keyName = "capabilityId") {
|
|
178
|
+
if (!existsSync(path)) return {};
|
|
179
|
+
let parsed;
|
|
180
|
+
try {
|
|
181
|
+
parsed = JSON.parse(readFileSync(path, "utf8"));
|
|
182
|
+
} catch (error) {
|
|
183
|
+
throw new Error(
|
|
184
|
+
`could not parse ${path}: ${error instanceof Error ? error.message : String(error)}`
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
188
|
+
throw new Error(`${path} must be a JSON object of { "${keyName}": "reason" }`);
|
|
189
|
+
}
|
|
190
|
+
const allowlist = {};
|
|
191
|
+
for (const [id, reason] of Object.entries(parsed)) {
|
|
192
|
+
if (typeof reason !== "string" || reason.trim() === "") {
|
|
193
|
+
throw new Error(`${path}: "${id}" needs a non-empty reason string`);
|
|
194
|
+
}
|
|
195
|
+
allowlist[id] = reason;
|
|
196
|
+
}
|
|
197
|
+
return allowlist;
|
|
198
|
+
}
|
|
199
|
+
function unreadKey(entry) {
|
|
200
|
+
return `${entry.origin.file}#${entry.reason ?? "unknown"}#${entry.origin.site}`;
|
|
201
|
+
}
|
|
202
|
+
function buildCoverageReport(input) {
|
|
203
|
+
const unreached = [];
|
|
204
|
+
const allowed = [];
|
|
205
|
+
for (const id of [...input.authored].sort()) {
|
|
206
|
+
if (input.reachedIds.has(id)) continue;
|
|
207
|
+
if (id in input.allowlist) {
|
|
208
|
+
allowed.push(id);
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
unreached.push({ capabilityId: id, origin: input.origins.get(id) ?? { file: "?", line: 0 } });
|
|
212
|
+
}
|
|
213
|
+
const staleAllowlist = Object.keys(input.allowlist).filter((id) => input.reachedIds.has(id) || !input.authored.has(id)).sort();
|
|
214
|
+
const unreadAllowlist = input.unreadAllowlist ?? {};
|
|
215
|
+
const unread = [];
|
|
216
|
+
const allowedUnread = /* @__PURE__ */ new Set();
|
|
217
|
+
for (const entry of input.unresolved) {
|
|
218
|
+
const key = unreadKey(entry);
|
|
219
|
+
if (key in unreadAllowlist) allowedUnread.add(key);
|
|
220
|
+
else unread.push(entry);
|
|
221
|
+
}
|
|
222
|
+
const stillUnread = new Set(input.unresolved.map(unreadKey));
|
|
223
|
+
const staleUnreadAllowlist = Object.keys(unreadAllowlist).filter((key) => !stillUnread.has(key)).sort();
|
|
224
|
+
const unaccounted = [...input.reachedIds].filter((id) => !input.authored.has(id)).sort();
|
|
225
|
+
const domainReached = [...input.reachedIds].filter((id) => id.startsWith("domain:")).sort();
|
|
226
|
+
const unmanifestedDomain = input.domainAuthoritative ? unaccounted.filter((id) => id.startsWith("domain:")) : [];
|
|
227
|
+
const undeclared = unaccounted.filter((id) => !id.startsWith("domain:"));
|
|
228
|
+
return {
|
|
229
|
+
authored: input.authored.size,
|
|
230
|
+
reached: [...input.authored].filter((id) => input.reachedIds.has(id)).length,
|
|
231
|
+
scenarios: input.scenarios,
|
|
232
|
+
...input.scope ? { scope: input.scope } : {},
|
|
233
|
+
allowlistOutOfScope: input.allowlistOutOfScope ?? 0,
|
|
234
|
+
unreached,
|
|
235
|
+
undeclared,
|
|
236
|
+
domainReached,
|
|
237
|
+
unmanifestedDomain,
|
|
238
|
+
domainAuthoritative: input.domainAuthoritative === true,
|
|
239
|
+
unresolved: unread,
|
|
240
|
+
allowed,
|
|
241
|
+
staleAllowlist,
|
|
242
|
+
allowlistPath: input.allowlistPath,
|
|
243
|
+
allowedUnread: [...allowedUnread].sort(),
|
|
244
|
+
staleUnreadAllowlist,
|
|
245
|
+
unreadAllowlistPath: input.unreadAllowlistPath
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
function coverageExitCode(report, options = {}) {
|
|
249
|
+
if (report.unreached.length > 0) return 1;
|
|
250
|
+
if (report.unmanifestedDomain.length > 0) return 1;
|
|
251
|
+
if (report.unresolved.length > 0 && !options.allowUnresolved) return 1;
|
|
252
|
+
if (report.staleAllowlist.length > 0) return 1;
|
|
253
|
+
if (report.staleUnreadAllowlist.length > 0) return 1;
|
|
254
|
+
return 0;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// src/extract.ts
|
|
258
|
+
import { createHash } from "crypto";
|
|
259
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
260
|
+
import { dirname, isAbsolute, join as join2, relative, resolve } from "path";
|
|
261
|
+
import ts from "typescript";
|
|
262
|
+
var UNRESOLVED_ID = "<unresolved>";
|
|
263
|
+
function findTsconfig(from) {
|
|
264
|
+
return ts.findConfigFile(resolve(from), ts.sys.fileExists, "tsconfig.json");
|
|
265
|
+
}
|
|
266
|
+
function readLiteralConfigScope(configPath) {
|
|
267
|
+
const source = ts.createSourceFile(
|
|
268
|
+
configPath,
|
|
269
|
+
readFileSync2(configPath, "utf8"),
|
|
270
|
+
ts.ScriptTarget.Latest,
|
|
271
|
+
true,
|
|
272
|
+
configPath.endsWith("x") ? ts.ScriptKind.TSX : ts.ScriptKind.TS
|
|
273
|
+
);
|
|
274
|
+
let scope;
|
|
275
|
+
const visit = (node) => {
|
|
276
|
+
if (scope) return;
|
|
277
|
+
if (ts.isPropertyAssignment(node) && propertyName(node.name) === "scope" && ts.isArrayLiteralExpression(node.initializer)) {
|
|
278
|
+
const values = node.initializer.elements.map((entry) => literalText(entry));
|
|
279
|
+
if (values.every((value) => value !== void 0)) scope = values;
|
|
280
|
+
}
|
|
281
|
+
ts.forEachChild(node, visit);
|
|
282
|
+
};
|
|
283
|
+
visit(source);
|
|
284
|
+
return scope;
|
|
285
|
+
}
|
|
286
|
+
function readProgramFiles(tsconfigPath) {
|
|
287
|
+
const read = ts.readConfigFile(tsconfigPath, ts.sys.readFile);
|
|
288
|
+
if (read.error) {
|
|
289
|
+
throw new Error(
|
|
290
|
+
`could not read ${tsconfigPath}: ${ts.flattenDiagnosticMessageText(read.error.messageText, " ")}`
|
|
291
|
+
);
|
|
292
|
+
}
|
|
293
|
+
const parsed = ts.parseJsonConfigFileContent(
|
|
294
|
+
read.config,
|
|
295
|
+
ts.sys,
|
|
296
|
+
dirname(tsconfigPath)
|
|
297
|
+
);
|
|
298
|
+
if (parsed.errors.length > 0 && parsed.fileNames.length === 0) {
|
|
299
|
+
throw new Error(
|
|
300
|
+
`could not resolve any files from ${tsconfigPath}: ${parsed.errors.map((error) => ts.flattenDiagnosticMessageText(error.messageText, " ")).join("; ")}`
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
return { fileNames: parsed.fileNames, options: parsed.options };
|
|
304
|
+
}
|
|
305
|
+
var REGISTRATION_HOOKS = /* @__PURE__ */ new Set(["useAgentComponent", "useAgentAction", "useAgentObservation"]);
|
|
306
|
+
function isRegistrationModule(specifier) {
|
|
307
|
+
return specifier.startsWith("@agent-surface/");
|
|
308
|
+
}
|
|
309
|
+
var NO_IMPORTS = { locals: /* @__PURE__ */ new Map(), namespaces: /* @__PURE__ */ new Set() };
|
|
310
|
+
function importedRegistrations(source) {
|
|
311
|
+
const locals = /* @__PURE__ */ new Map();
|
|
312
|
+
const namespaces = /* @__PURE__ */ new Set();
|
|
313
|
+
for (const statement of source.statements) {
|
|
314
|
+
if (!ts.isImportDeclaration(statement)) continue;
|
|
315
|
+
if (!ts.isStringLiteral(statement.moduleSpecifier)) continue;
|
|
316
|
+
if (!isRegistrationModule(statement.moduleSpecifier.text)) continue;
|
|
317
|
+
const clause = statement.importClause;
|
|
318
|
+
if (!clause || clause.isTypeOnly || !clause.namedBindings) continue;
|
|
319
|
+
if (ts.isNamespaceImport(clause.namedBindings)) {
|
|
320
|
+
namespaces.add(clause.namedBindings.name.text);
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
for (const element of clause.namedBindings.elements) {
|
|
324
|
+
if (element.isTypeOnly) continue;
|
|
325
|
+
const imported = (element.propertyName ?? element.name).text;
|
|
326
|
+
if (REGISTRATION_HOOKS.has(imported)) locals.set(element.name.text, imported);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
return { locals, namespaces };
|
|
330
|
+
}
|
|
331
|
+
function renamedRegistrationExports(source, imports) {
|
|
332
|
+
const renamed = [];
|
|
333
|
+
for (const statement of source.statements) {
|
|
334
|
+
if (!ts.isExportDeclaration(statement) || statement.isTypeOnly) continue;
|
|
335
|
+
const clause = statement.exportClause;
|
|
336
|
+
if (!clause || !ts.isNamedExports(clause)) continue;
|
|
337
|
+
const from = statement.moduleSpecifier;
|
|
338
|
+
const fromOurs = from !== void 0 && ts.isStringLiteral(from) && isRegistrationModule(from.text);
|
|
339
|
+
if (from && !fromOurs) continue;
|
|
340
|
+
for (const element of clause.elements) {
|
|
341
|
+
if (element.isTypeOnly) continue;
|
|
342
|
+
const local = (element.propertyName ?? element.name).text;
|
|
343
|
+
const hook = fromOurs ? REGISTRATION_HOOKS.has(local) ? local : void 0 : imports.locals.get(local);
|
|
344
|
+
if (hook === void 0 || element.name.text === hook) continue;
|
|
345
|
+
renamed.push({ node: element, hook, exported: element.name.text });
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return renamed;
|
|
349
|
+
}
|
|
350
|
+
function namespaceMember(object, member, imports) {
|
|
351
|
+
return ts.isIdentifier(object) && imports.namespaces.has(object.text) && REGISTRATION_HOOKS.has(member);
|
|
352
|
+
}
|
|
353
|
+
function calleeName(call, imports = NO_IMPORTS) {
|
|
354
|
+
const callee = call.expression;
|
|
355
|
+
if (ts.isIdentifier(callee)) return imports.locals.get(callee.text) ?? callee.text;
|
|
356
|
+
if (ts.isPropertyAccessExpression(callee)) {
|
|
357
|
+
if (namespaceMember(callee.expression, callee.name.text, imports)) return callee.name.text;
|
|
358
|
+
return callee.name.text;
|
|
359
|
+
}
|
|
360
|
+
if (ts.isElementAccessExpression(callee)) {
|
|
361
|
+
const member = literalText(callee.argumentExpression);
|
|
362
|
+
if (member !== void 0 && namespaceMember(callee.expression, member, imports)) return member;
|
|
363
|
+
}
|
|
364
|
+
return void 0;
|
|
365
|
+
}
|
|
366
|
+
function propertyName(name) {
|
|
367
|
+
if (ts.isIdentifier(name) || ts.isStringLiteral(name)) return name.text;
|
|
368
|
+
return void 0;
|
|
369
|
+
}
|
|
370
|
+
function propertyOf(object, wanted) {
|
|
371
|
+
for (const property of object.properties) {
|
|
372
|
+
if (ts.isPropertyAssignment(property) && propertyName(property.name) === wanted) {
|
|
373
|
+
return property.initializer;
|
|
374
|
+
}
|
|
375
|
+
if (ts.isShorthandPropertyAssignment(property) && property.name.text === wanted) {
|
|
376
|
+
return property.name;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
return void 0;
|
|
380
|
+
}
|
|
381
|
+
function hasSpread(object) {
|
|
382
|
+
return object.properties.some((property) => ts.isSpreadAssignment(property));
|
|
383
|
+
}
|
|
384
|
+
var CAPABILITY_GROUPS = ["observations", "actions"];
|
|
385
|
+
function spreadKeys(expression, source, depth = 0) {
|
|
386
|
+
if (depth > 1) return void 0;
|
|
387
|
+
if (ts.isParenthesizedExpression(expression)) {
|
|
388
|
+
return spreadKeys(expression.expression, source, depth);
|
|
389
|
+
}
|
|
390
|
+
if (ts.isConditionalExpression(expression)) {
|
|
391
|
+
const whenTrue = spreadKeys(expression.whenTrue, source, depth);
|
|
392
|
+
const whenFalse = spreadKeys(expression.whenFalse, source, depth);
|
|
393
|
+
if (!whenTrue || !whenFalse) return void 0;
|
|
394
|
+
return [.../* @__PURE__ */ new Set([...whenTrue, ...whenFalse])];
|
|
395
|
+
}
|
|
396
|
+
const resolved = objectLiteralFor(expression, source);
|
|
397
|
+
if (!resolved.object) return void 0;
|
|
398
|
+
const keys = [];
|
|
399
|
+
for (const property of resolved.object.properties) {
|
|
400
|
+
if (ts.isSpreadAssignment(property)) {
|
|
401
|
+
const nested = spreadKeys(property.expression, source, depth + 1);
|
|
402
|
+
if (!nested) return void 0;
|
|
403
|
+
keys.push(...nested);
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
406
|
+
const name = ts.isPropertyAssignment(property) || ts.isMethodDeclaration(property) ? propertyName(property.name) : ts.isShorthandPropertyAssignment(property) ? property.name.text : void 0;
|
|
407
|
+
if (name === void 0) return void 0;
|
|
408
|
+
keys.push(name);
|
|
409
|
+
}
|
|
410
|
+
return [...new Set(keys)];
|
|
411
|
+
}
|
|
412
|
+
function literalText(node) {
|
|
413
|
+
if (!node) return void 0;
|
|
414
|
+
if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) return node.text;
|
|
415
|
+
if (ts.isParenthesizedExpression(node)) return literalText(node.expression);
|
|
416
|
+
if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.PlusToken) {
|
|
417
|
+
const left = literalText(node.left);
|
|
418
|
+
const right = literalText(node.right);
|
|
419
|
+
if (left !== void 0 && right !== void 0) return left + right;
|
|
420
|
+
}
|
|
421
|
+
return void 0;
|
|
422
|
+
}
|
|
423
|
+
function describeConstruct(node) {
|
|
424
|
+
if (ts.isCallExpression(node)) {
|
|
425
|
+
const callee = calleeName(node);
|
|
426
|
+
return callee ? `built by ${callee}()` : "built by a call expression";
|
|
427
|
+
}
|
|
428
|
+
if (ts.isIdentifier(node)) return `a variable (${node.text}) this extractor could not follow`;
|
|
429
|
+
if (ts.isConditionalExpression(node)) return "a conditional expression";
|
|
430
|
+
if (ts.isTemplateExpression(node)) return "a template with substitutions";
|
|
431
|
+
if (ts.isPropertyAccessExpression(node)) return "a property access";
|
|
432
|
+
return "a non-literal expression";
|
|
433
|
+
}
|
|
434
|
+
function objectLiteralFor(expression, source) {
|
|
435
|
+
if (ts.isObjectLiteralExpression(expression)) return { object: expression };
|
|
436
|
+
if (ts.isIdentifier(expression)) {
|
|
437
|
+
const target = expression.text;
|
|
438
|
+
let found;
|
|
439
|
+
const visit = (node) => {
|
|
440
|
+
if (found) return;
|
|
441
|
+
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.name.text === target && node.initializer && ts.isObjectLiteralExpression(node.initializer)) {
|
|
442
|
+
found = node.initializer;
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
ts.forEachChild(node, visit);
|
|
446
|
+
};
|
|
447
|
+
visit(source);
|
|
448
|
+
if (found) return { object: found };
|
|
449
|
+
return {
|
|
450
|
+
note: `the config is \`${target}\`, which is not a same-module object literal \u2014 the extractor follows one hop only`
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
return { note: `the config is ${describeConstruct(expression)}` };
|
|
454
|
+
}
|
|
455
|
+
var GRANULAR_HOOKS = /* @__PURE__ */ new Set(["useAgentAction", "useAgentObservation"]);
|
|
456
|
+
function capabilitiesFromGroup(group, kind, componentType, componentPartial, emit, source) {
|
|
457
|
+
if (!group) return;
|
|
458
|
+
const resolved = objectLiteralFor(group, source);
|
|
459
|
+
if (!resolved.object) {
|
|
460
|
+
emit.push({
|
|
461
|
+
capabilityId: `view:${componentType}.${UNRESOLVED_ID}`,
|
|
462
|
+
kind,
|
|
463
|
+
origin: emit.origin(group),
|
|
464
|
+
resolution: "unresolved",
|
|
465
|
+
reason: "dynamic-group",
|
|
466
|
+
note: `\`${kind}s\` on "${componentType}" is not an object literal: ${resolved.note}`
|
|
467
|
+
});
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
for (const property of resolved.object.properties) {
|
|
471
|
+
if (ts.isSpreadAssignment(property)) {
|
|
472
|
+
const keys = spreadKeys(property.expression, source);
|
|
473
|
+
if (keys === void 0) {
|
|
474
|
+
emit.push({
|
|
475
|
+
capabilityId: `view:${componentType}.${UNRESOLVED_ID}`,
|
|
476
|
+
kind,
|
|
477
|
+
origin: emit.origin(property),
|
|
478
|
+
resolution: "unresolved",
|
|
479
|
+
reason: "spread-members",
|
|
480
|
+
note: `\`${kind}s\` on "${componentType}" spreads another object, which may contribute capabilities this inventory cannot name`
|
|
481
|
+
});
|
|
482
|
+
continue;
|
|
483
|
+
}
|
|
484
|
+
for (const name2 of keys) {
|
|
485
|
+
const notes2 = [
|
|
486
|
+
...componentPartial ? [componentPartial] : [],
|
|
487
|
+
`\`${name2}\` is contributed by a spread, so its definition metadata or runtime presence may be dynamic`
|
|
488
|
+
];
|
|
489
|
+
emit.push({
|
|
490
|
+
capabilityId: `view:${componentType}.${name2}`,
|
|
491
|
+
kind,
|
|
492
|
+
origin: emit.origin(property),
|
|
493
|
+
resolution: "partial",
|
|
494
|
+
note: notes2.join("; ")
|
|
495
|
+
});
|
|
496
|
+
}
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
const name = ts.isPropertyAssignment(property) || ts.isMethodDeclaration(property) ? propertyName(property.name) : ts.isShorthandPropertyAssignment(property) ? property.name.text : void 0;
|
|
500
|
+
if (name === void 0) {
|
|
501
|
+
emit.push({
|
|
502
|
+
capabilityId: `view:${componentType}.${UNRESOLVED_ID}`,
|
|
503
|
+
kind,
|
|
504
|
+
origin: emit.origin(property),
|
|
505
|
+
resolution: "unresolved",
|
|
506
|
+
reason: "computed-name",
|
|
507
|
+
note: `a capability on "${componentType}" has a computed name`
|
|
508
|
+
});
|
|
509
|
+
continue;
|
|
510
|
+
}
|
|
511
|
+
const capability = {
|
|
512
|
+
capabilityId: `view:${componentType}.${name}`,
|
|
513
|
+
kind,
|
|
514
|
+
origin: emit.origin(property),
|
|
515
|
+
resolution: "static"
|
|
516
|
+
};
|
|
517
|
+
const notes = [];
|
|
518
|
+
if (componentPartial) notes.push(componentPartial);
|
|
519
|
+
const value = ts.isPropertyAssignment(property) ? property.initializer : void 0;
|
|
520
|
+
const definition = value && ts.isCallExpression(value) && value.arguments.length > 0 ? value.arguments[0] : value;
|
|
521
|
+
if (definition && ts.isObjectLiteralExpression(definition)) {
|
|
522
|
+
const description = literalText(propertyOf(definition, "description"));
|
|
523
|
+
if (description !== void 0) capability.description = description;
|
|
524
|
+
else notes.push("description is not a string literal");
|
|
525
|
+
if (kind === "action") {
|
|
526
|
+
const effect = literalText(propertyOf(definition, "effect"));
|
|
527
|
+
if (effect !== void 0) capability.effect = effect;
|
|
528
|
+
else notes.push("effect is not a string literal");
|
|
529
|
+
}
|
|
530
|
+
if (hasSpread(definition)) notes.push("the definition spreads another object");
|
|
531
|
+
} else {
|
|
532
|
+
notes.push(
|
|
533
|
+
value ? `the definition is ${describeConstruct(value)}` : "the definition is not an object literal"
|
|
534
|
+
);
|
|
535
|
+
}
|
|
536
|
+
if (notes.length > 0) {
|
|
537
|
+
capability.resolution = "partial";
|
|
538
|
+
capability.note = notes.join("; ");
|
|
539
|
+
}
|
|
540
|
+
emit.push(capability);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
function visitCall(call, emit, source, imports, deferred, enclosing) {
|
|
544
|
+
const callee = calleeName(call, imports);
|
|
545
|
+
if (callee === void 0) {
|
|
546
|
+
const object = ts.isElementAccessExpression(call.expression) ? call.expression.expression : void 0;
|
|
547
|
+
if (object && ts.isIdentifier(object) && imports.namespaces.has(object.text)) {
|
|
548
|
+
emit.push({
|
|
549
|
+
capabilityId: UNRESOLVED_ID,
|
|
550
|
+
kind: "action",
|
|
551
|
+
origin: emit.origin(call),
|
|
552
|
+
resolution: "unresolved",
|
|
553
|
+
reason: "dynamic-callee",
|
|
554
|
+
note: `a call reads a computed member of \`${object.text}\`, a namespace of this library \u2014 which export it calls, and so whether it registers anything, cannot be read here`
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
if (GRANULAR_HOOKS.has(callee)) {
|
|
560
|
+
emit.push({
|
|
561
|
+
capabilityId: UNRESOLVED_ID,
|
|
562
|
+
kind: callee === "useAgentAction" ? "action" : "observation",
|
|
563
|
+
origin: emit.origin(call),
|
|
564
|
+
resolution: "unresolved",
|
|
565
|
+
reason: "granular-hook",
|
|
566
|
+
note: `${callee}() registers against a render-scope link, so its component type is not at this call site`
|
|
567
|
+
});
|
|
568
|
+
return;
|
|
569
|
+
}
|
|
570
|
+
if (callee !== "useAgentComponent" && callee !== "register") return;
|
|
571
|
+
const argument = call.arguments[0];
|
|
572
|
+
if (!argument) return;
|
|
573
|
+
const resolved = objectLiteralFor(argument, source);
|
|
574
|
+
if (!resolved.object) {
|
|
575
|
+
emit.push({
|
|
576
|
+
capabilityId: UNRESOLVED_ID,
|
|
577
|
+
kind: "action",
|
|
578
|
+
origin: emit.origin(call),
|
|
579
|
+
resolution: "unresolved",
|
|
580
|
+
reason: "dynamic-config",
|
|
581
|
+
note: `${callee}() call site could not be read: ${resolved.note}`
|
|
582
|
+
});
|
|
583
|
+
return;
|
|
584
|
+
}
|
|
585
|
+
const config = resolved.object;
|
|
586
|
+
const typeNode = propertyOf(config, "type");
|
|
587
|
+
const type = literalText(typeNode);
|
|
588
|
+
if (type === void 0) {
|
|
589
|
+
if (callee === "register" && typeNode === void 0) return;
|
|
590
|
+
const slot = enclosing && typeNode && ts.isIdentifier(typeNode) ? parameterSlot(typeNode.text, enclosing.fn) : void 0;
|
|
591
|
+
if (slot && enclosing?.name) {
|
|
592
|
+
deferred.push({ config, source, emit, wrapperName: enclosing.name, slot, site: call });
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
emit.push({
|
|
596
|
+
capabilityId: UNRESOLVED_ID,
|
|
597
|
+
kind: "action",
|
|
598
|
+
origin: emit.origin(call),
|
|
599
|
+
resolution: "unresolved",
|
|
600
|
+
reason: "dynamic-type",
|
|
601
|
+
note: `\`type\` is not a string literal, so no capability id on this component can be determined`
|
|
602
|
+
});
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
const componentPartial = hasSpread(config) ? "the component config spreads another object, so some metadata here may be dynamic" : void 0;
|
|
606
|
+
for (const property of config.properties) {
|
|
607
|
+
if (!ts.isSpreadAssignment(property)) continue;
|
|
608
|
+
const keys = spreadKeys(property.expression, source);
|
|
609
|
+
if (keys && !keys.some((key) => CAPABILITY_GROUPS.includes(key))) {
|
|
610
|
+
continue;
|
|
611
|
+
}
|
|
612
|
+
emit.push({
|
|
613
|
+
capabilityId: `view:${type}.${UNRESOLVED_ID}`,
|
|
614
|
+
kind: "action",
|
|
615
|
+
origin: emit.origin(property),
|
|
616
|
+
resolution: "unresolved",
|
|
617
|
+
reason: "spread-members",
|
|
618
|
+
note: keys ? `"${type}" spreads ${describeConstruct(property.expression)}, which contributes \`${keys.filter((key) => CAPABILITY_GROUPS.includes(key)).join("`/`")}\` this inventory cannot name` : `"${type}" spreads ${describeConstruct(property.expression)}, whose keys this inventory cannot read \u2014 it may contribute capabilities not listed here`
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
capabilitiesFromGroup(
|
|
622
|
+
propertyOf(config, "observations"),
|
|
623
|
+
"observation",
|
|
624
|
+
type,
|
|
625
|
+
componentPartial,
|
|
626
|
+
emit,
|
|
627
|
+
source
|
|
628
|
+
);
|
|
629
|
+
capabilitiesFromGroup(
|
|
630
|
+
propertyOf(config, "actions"),
|
|
631
|
+
"action",
|
|
632
|
+
type,
|
|
633
|
+
componentPartial,
|
|
634
|
+
emit,
|
|
635
|
+
source
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
function functionLike(node) {
|
|
639
|
+
if (ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isArrowFunction(node) || ts.isMethodDeclaration(node)) {
|
|
640
|
+
return node;
|
|
641
|
+
}
|
|
642
|
+
return void 0;
|
|
643
|
+
}
|
|
644
|
+
function parameterSlot(name, fn) {
|
|
645
|
+
for (const [index, parameter] of fn.parameters.entries()) {
|
|
646
|
+
if (ts.isIdentifier(parameter.name)) {
|
|
647
|
+
if (parameter.name.text === name) return { index };
|
|
648
|
+
continue;
|
|
649
|
+
}
|
|
650
|
+
if (ts.isObjectBindingPattern(parameter.name)) {
|
|
651
|
+
for (const element of parameter.name.elements) {
|
|
652
|
+
if (!ts.isIdentifier(element.name) || element.name.text !== name) continue;
|
|
653
|
+
const property = element.propertyName && ts.isIdentifier(element.propertyName) ? element.propertyName.text : name;
|
|
654
|
+
return { index, property };
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
return void 0;
|
|
659
|
+
}
|
|
660
|
+
function callsWrapper(site, wrapper, compilerOptions) {
|
|
661
|
+
const callee = site.call.expression;
|
|
662
|
+
if (!ts.isIdentifier(callee) || callee.text !== wrapper.wrapperName) return false;
|
|
663
|
+
if (site.source.fileName === wrapper.source.fileName) {
|
|
664
|
+
return true;
|
|
665
|
+
}
|
|
666
|
+
for (const statement of site.source.statements) {
|
|
667
|
+
if (!ts.isImportDeclaration(statement)) continue;
|
|
668
|
+
const clause = statement.importClause;
|
|
669
|
+
if (!clause) continue;
|
|
670
|
+
const named = clause.name?.text === wrapper.wrapperName || clause.namedBindings && ts.isNamedImports(clause.namedBindings) && clause.namedBindings.elements.some((element) => element.name.text === wrapper.wrapperName);
|
|
671
|
+
if (!named) continue;
|
|
672
|
+
if (!ts.isStringLiteral(statement.moduleSpecifier)) continue;
|
|
673
|
+
const resolved = ts.resolveModuleName(
|
|
674
|
+
statement.moduleSpecifier.text,
|
|
675
|
+
site.source.fileName,
|
|
676
|
+
compilerOptions,
|
|
677
|
+
ts.sys
|
|
678
|
+
).resolvedModule;
|
|
679
|
+
if (resolved?.resolvedFileName === wrapper.source.fileName) return true;
|
|
680
|
+
}
|
|
681
|
+
return false;
|
|
682
|
+
}
|
|
683
|
+
function normalizedText(node, source) {
|
|
684
|
+
return node.getText(source).replace(/\s+/g, " ").trim();
|
|
685
|
+
}
|
|
686
|
+
function siteIdentity(source, node) {
|
|
687
|
+
const labels = [];
|
|
688
|
+
let enclosingCall = "";
|
|
689
|
+
let scope;
|
|
690
|
+
for (let parent = node.parent; parent && parent !== source; parent = parent.parent) {
|
|
691
|
+
if (!enclosingCall && ts.isCallExpression(parent)) {
|
|
692
|
+
enclosingCall = normalizedText(parent, source);
|
|
693
|
+
}
|
|
694
|
+
const named = (ts.isFunctionDeclaration(parent) || ts.isMethodDeclaration(parent)) && parent.name && (ts.isIdentifier(parent.name) || ts.isStringLiteral(parent.name)) ? parent.name.text : ts.isVariableDeclaration(parent) && ts.isIdentifier(parent.name) ? parent.name.text : void 0;
|
|
695
|
+
if (named !== void 0) {
|
|
696
|
+
labels.push(named);
|
|
697
|
+
scope ??= parent;
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
return { labels: labels.reverse(), enclosingCall, scope: scope ?? source };
|
|
701
|
+
}
|
|
702
|
+
function occurrence(scope, node, source) {
|
|
703
|
+
const text = normalizedText(node, source);
|
|
704
|
+
const start = node.getStart(source);
|
|
705
|
+
let rank = 0;
|
|
706
|
+
const visit = (candidate) => {
|
|
707
|
+
if (candidate.kind === node.kind && candidate.getStart(source) < start && normalizedText(candidate, source) === text) {
|
|
708
|
+
rank += 1;
|
|
709
|
+
}
|
|
710
|
+
ts.forEachChild(candidate, visit);
|
|
711
|
+
};
|
|
712
|
+
ts.forEachChild(scope, visit);
|
|
713
|
+
return rank;
|
|
714
|
+
}
|
|
715
|
+
function stableSite(source, node) {
|
|
716
|
+
const { labels, enclosingCall, scope } = siteIdentity(source, node);
|
|
717
|
+
return createHash("sha256").update(
|
|
718
|
+
`${labels.join("/")}\0${enclosingCall}\0${normalizedText(node, source)}\0${occurrence(
|
|
719
|
+
scope,
|
|
720
|
+
node,
|
|
721
|
+
source
|
|
722
|
+
)}`
|
|
723
|
+
).digest("hex").slice(0, 12);
|
|
724
|
+
}
|
|
725
|
+
var packageNameCache = /* @__PURE__ */ new Map();
|
|
726
|
+
function packageNameFor(file) {
|
|
727
|
+
let dir = dirname(file);
|
|
728
|
+
for (; ; ) {
|
|
729
|
+
if (packageNameCache.has(dir)) return packageNameCache.get(dir);
|
|
730
|
+
const packagePath = join2(dir, "package.json");
|
|
731
|
+
if (existsSync2(packagePath)) {
|
|
732
|
+
let name;
|
|
733
|
+
try {
|
|
734
|
+
const parsed = JSON.parse(readFileSync2(packagePath, "utf8"));
|
|
735
|
+
if (typeof parsed.name === "string") name = parsed.name;
|
|
736
|
+
} catch {
|
|
737
|
+
}
|
|
738
|
+
packageNameCache.set(dir, name);
|
|
739
|
+
return name;
|
|
740
|
+
}
|
|
741
|
+
const parent = dirname(dir);
|
|
742
|
+
if (parent === dir) return void 0;
|
|
743
|
+
dir = parent;
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
var IMPLEMENTATION_PACKAGES = /* @__PURE__ */ new Set([
|
|
747
|
+
"@agent-surface/core",
|
|
748
|
+
"@agent-surface/react",
|
|
749
|
+
"@agent-surface/orpc",
|
|
750
|
+
"@agent-surface/testing",
|
|
751
|
+
"@agent-surface/webmcp",
|
|
752
|
+
"@agent-surface/cli"
|
|
753
|
+
]);
|
|
754
|
+
function isAgentSurfaceImplementation(file) {
|
|
755
|
+
return IMPLEMENTATION_PACKAGES.has(packageNameFor(file) ?? "");
|
|
756
|
+
}
|
|
757
|
+
function extractCapabilities(options) {
|
|
758
|
+
const root = resolve(options.root);
|
|
759
|
+
const tsconfigPath = options.tsconfig ? isAbsolute(options.tsconfig) ? options.tsconfig : join2(root, options.tsconfig) : findTsconfig(root);
|
|
760
|
+
if (!tsconfigPath || !existsSync2(tsconfigPath)) {
|
|
761
|
+
throw new Error(
|
|
762
|
+
`no tsconfig.json found from ${root} \u2014 \`capabilities\` reads the TypeScript program, so it needs one (pass --tsconfig to point at it)`
|
|
763
|
+
);
|
|
764
|
+
}
|
|
765
|
+
const { fileNames, options: compilerOptions } = readProgramFiles(tsconfigPath);
|
|
766
|
+
const program = ts.createProgram(fileNames, compilerOptions);
|
|
767
|
+
const capabilities = [];
|
|
768
|
+
let filesAnalyzed = 0;
|
|
769
|
+
let filesOutsideRoot = 0;
|
|
770
|
+
const deferred = [];
|
|
771
|
+
const callsByName = /* @__PURE__ */ new Map();
|
|
772
|
+
for (const source of program.getSourceFiles()) {
|
|
773
|
+
if (source.isDeclarationFile) continue;
|
|
774
|
+
if (source.fileName.includes("/node_modules/")) continue;
|
|
775
|
+
if (!isInside(root, source.fileName) && isAgentSurfaceImplementation(source.fileName)) {
|
|
776
|
+
filesOutsideRoot += 1;
|
|
777
|
+
continue;
|
|
778
|
+
}
|
|
779
|
+
filesAnalyzed += 1;
|
|
780
|
+
const emit = {
|
|
781
|
+
push: (capability) => capabilities.push(capability),
|
|
782
|
+
origin: (node) => ({
|
|
783
|
+
file: relative(root, source.fileName),
|
|
784
|
+
line: source.getLineAndCharacterOfPosition(node.getStart(source)).line + 1,
|
|
785
|
+
site: stableSite(source, node)
|
|
786
|
+
})
|
|
787
|
+
};
|
|
788
|
+
const imports = importedRegistrations(source);
|
|
789
|
+
for (const renamed of renamedRegistrationExports(source, imports)) {
|
|
790
|
+
emit.push({
|
|
791
|
+
capabilityId: UNRESOLVED_ID,
|
|
792
|
+
kind: "action",
|
|
793
|
+
origin: emit.origin(renamed.node),
|
|
794
|
+
resolution: "unresolved",
|
|
795
|
+
reason: "dynamic-callee",
|
|
796
|
+
note: `${renamed.hook}() leaves this module as \`${renamed.exported}\`, so nothing at its call sites elsewhere proves they register anything \u2014 whatever they author is not in this catalog`
|
|
797
|
+
});
|
|
798
|
+
}
|
|
799
|
+
let pendingName;
|
|
800
|
+
const visit = (node, enclosing) => {
|
|
801
|
+
const fn = functionLike(node);
|
|
802
|
+
if (fn) {
|
|
803
|
+
const named = ts.isFunctionDeclaration(node) && node.name ? node.name.text : pendingName;
|
|
804
|
+
enclosing = { fn, ...named ? { name: named } : {} };
|
|
805
|
+
pendingName = void 0;
|
|
806
|
+
} else if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name)) {
|
|
807
|
+
pendingName = node.name.text;
|
|
808
|
+
}
|
|
809
|
+
if (ts.isCallExpression(node)) {
|
|
810
|
+
visitCall(node, emit, source, imports, deferred, enclosing);
|
|
811
|
+
if (ts.isIdentifier(node.expression)) {
|
|
812
|
+
const name = node.expression.text;
|
|
813
|
+
const sites = callsByName.get(name) ?? [];
|
|
814
|
+
sites.push({ call: node, source });
|
|
815
|
+
callsByName.set(name, sites);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
ts.forEachChild(node, (child) => visit(child, enclosing));
|
|
819
|
+
};
|
|
820
|
+
visit(source);
|
|
821
|
+
}
|
|
822
|
+
for (const wrapper of deferred) {
|
|
823
|
+
const sites = (callsByName.get(wrapper.wrapperName) ?? []).filter(
|
|
824
|
+
(site) => callsWrapper(site, wrapper, compilerOptions)
|
|
825
|
+
);
|
|
826
|
+
const types = /* @__PURE__ */ new Map();
|
|
827
|
+
const dynamic = [];
|
|
828
|
+
for (const site of sites) {
|
|
829
|
+
const argument = site.call.arguments[wrapper.slot.index];
|
|
830
|
+
const value = wrapper.slot.property && argument && ts.isObjectLiteralExpression(argument) ? propertyOf(argument, wrapper.slot.property) : argument;
|
|
831
|
+
const text = value ? literalText(value) : void 0;
|
|
832
|
+
if (text !== void 0) types.set(text, site.call);
|
|
833
|
+
else dynamic.push(site);
|
|
834
|
+
}
|
|
835
|
+
for (const type of [...types.keys()].sort()) {
|
|
836
|
+
const componentPartial = hasSpread(wrapper.config) ? "the component config spreads another object, so some metadata here may be dynamic" : void 0;
|
|
837
|
+
for (const [group, kind] of [
|
|
838
|
+
["observations", "observation"],
|
|
839
|
+
["actions", "action"]
|
|
840
|
+
]) {
|
|
841
|
+
capabilitiesFromGroup(
|
|
842
|
+
propertyOf(wrapper.config, group),
|
|
843
|
+
kind,
|
|
844
|
+
type,
|
|
845
|
+
componentPartial,
|
|
846
|
+
wrapper.emit,
|
|
847
|
+
wrapper.source
|
|
848
|
+
);
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
if (types.size === 0 || dynamic.length > 0) {
|
|
852
|
+
wrapper.emit.push({
|
|
853
|
+
capabilityId: UNRESOLVED_ID,
|
|
854
|
+
kind: "action",
|
|
855
|
+
origin: wrapper.emit.origin(wrapper.site),
|
|
856
|
+
resolution: "unresolved",
|
|
857
|
+
reason: "dynamic-type",
|
|
858
|
+
note: types.size === 0 ? `\`type\` is a parameter of ${wrapper.wrapperName}(), and no call site of it in this program passes a string literal` : `\`type\` is a parameter of ${wrapper.wrapperName}(); ${types.size} call site${types.size === 1 ? "" : "s"} resolved, ${dynamic.length} pass${dynamic.length === 1 ? "es" : ""} a non-literal`
|
|
859
|
+
});
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
capabilities.sort(
|
|
863
|
+
(a, b) => a.capabilityId.localeCompare(b.capabilityId) || a.origin.file.localeCompare(b.origin.file) || a.origin.line - b.origin.line
|
|
864
|
+
);
|
|
865
|
+
return {
|
|
866
|
+
capabilities,
|
|
867
|
+
tsconfig: tsconfigPath,
|
|
868
|
+
root,
|
|
869
|
+
filesAnalyzed,
|
|
870
|
+
filesOutsideRoot,
|
|
871
|
+
domain: "not-analyzed"
|
|
872
|
+
};
|
|
873
|
+
}
|
|
874
|
+
function isInside(root, file) {
|
|
875
|
+
const rel = relative(root, file);
|
|
876
|
+
return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
|
|
877
|
+
}
|
|
878
|
+
function authoredIds(inventory) {
|
|
879
|
+
const ids = /* @__PURE__ */ new Set();
|
|
880
|
+
for (const capability of inventory.capabilities) {
|
|
881
|
+
if (capability.resolution === "unresolved") continue;
|
|
882
|
+
if (capability.capabilityId.endsWith(UNRESOLVED_ID)) continue;
|
|
883
|
+
ids.add(capability.capabilityId);
|
|
884
|
+
}
|
|
885
|
+
return ids;
|
|
886
|
+
}
|
|
887
|
+
function unresolved(inventory) {
|
|
888
|
+
return inventory.capabilities.filter((capability) => capability.resolution === "unresolved");
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
// src/render/summary.ts
|
|
892
|
+
import { relative as relative2 } from "path";
|
|
893
|
+
var LABEL_WIDTH = 14;
|
|
894
|
+
var STATUS_WIDTH = 7;
|
|
895
|
+
var READING_SOURCE = "reading the source";
|
|
896
|
+
function mountingLabel(scenarios, index) {
|
|
897
|
+
const position = scenarios.length > 1 ? ` (${index + 1} of ${scenarios.length})` : "";
|
|
898
|
+
return `mounting ${scenarios[index]}${position}`;
|
|
899
|
+
}
|
|
900
|
+
function reportGrid(blocks, minimum = LABEL_WIDTH) {
|
|
901
|
+
return {
|
|
902
|
+
label: Math.max(minimum, ...blocks.flatMap((b) => b.rows.map((row) => row.label.length + 2))),
|
|
903
|
+
statuses: blocks.some((block) => block.rows.some((row) => row.status))
|
|
904
|
+
};
|
|
905
|
+
}
|
|
906
|
+
function displayPath(path) {
|
|
907
|
+
const rel = relative2(process.cwd(), path);
|
|
908
|
+
return rel && !rel.startsWith("..") ? rel : path;
|
|
909
|
+
}
|
|
910
|
+
var DEPTH_TEXT = {
|
|
911
|
+
full: "full \u2014 the source is read and every scenario is mounted",
|
|
912
|
+
static: "static \u2014 the source only; nothing is mounted",
|
|
913
|
+
runtime: "runtime \u2014 the scenarios only; the source is not read"
|
|
914
|
+
};
|
|
915
|
+
function runContextRows(context) {
|
|
916
|
+
const rows = [
|
|
917
|
+
{ label: "Config", text: displayPath(context.configPath) },
|
|
918
|
+
{ label: "Depth", text: DEPTH_TEXT[context.depth] },
|
|
919
|
+
{
|
|
920
|
+
label: "Scope",
|
|
921
|
+
text: context.scope && context.scope.length > 0 ? `${context.scope.join(" \xB7 ")} \u2014 every count below is relative to it` : "whole surface \u2014 no component-type prefix filter"
|
|
922
|
+
}
|
|
923
|
+
];
|
|
924
|
+
if (context.scenarios) {
|
|
925
|
+
const declared = context.declaredScenarios ?? context.scenarios;
|
|
926
|
+
const named = context.scenarios.length < declared.length;
|
|
927
|
+
rows.push({
|
|
928
|
+
label: "Scenarios",
|
|
929
|
+
text: `${named ? `${context.scenarios.length} of ${declared.length}` : context.scenarios.length} \u2014 ${context.scenarios.join(", ")}`
|
|
930
|
+
});
|
|
931
|
+
}
|
|
932
|
+
return rows;
|
|
933
|
+
}
|
|
934
|
+
function catalogRows(inventory, options = {}) {
|
|
935
|
+
const resolved = inventory.capabilities.filter((c) => c.resolution !== "unresolved");
|
|
936
|
+
const unreadEntries = unresolved(inventory);
|
|
937
|
+
const dynamicMetadata = resolved.filter((c) => c.resolution === "partial").length;
|
|
938
|
+
const authored = authoredIds(inventory).size + (options.domainCapabilities ?? 0);
|
|
939
|
+
return [
|
|
940
|
+
{
|
|
941
|
+
label: "STATUS",
|
|
942
|
+
tone: unreadEntries.length > 0 ? "warn" : "good",
|
|
943
|
+
text: unreadEntries.length > 0 ? `INCOMPLETE \u2014 ${unreadEntries.length} unread capability identit${unreadEntries.length === 1 ? "y" : "ies"}` : "COMPLETE \u2014 every capability identity resolved"
|
|
944
|
+
},
|
|
945
|
+
{
|
|
946
|
+
label: "Capabilities",
|
|
947
|
+
text: `${authored} authored (upper bound) \xB7 ${resolved.length} resolved call site${resolved.length === 1 ? "" : "s"}`
|
|
948
|
+
},
|
|
949
|
+
{
|
|
950
|
+
label: "Program",
|
|
951
|
+
text: `${inventory.filesAnalyzed} file${inventory.filesAnalyzed === 1 ? "" : "s"} analyzed` + (inventory.filesOutsideRoot > 0 ? ` \xB7 ${inventory.filesOutsideRoot} agent-surface implementation file${inventory.filesOutsideRoot === 1 ? "" : "s"} excluded` : "")
|
|
952
|
+
},
|
|
953
|
+
{
|
|
954
|
+
label: "Metadata",
|
|
955
|
+
text: `${dynamicMetadata} call site${dynamicMetadata === 1 ? "" : "s"} partially read` + (dynamicMetadata > 0 ? " \xB7 identity remains resolved" : "")
|
|
956
|
+
},
|
|
957
|
+
{
|
|
958
|
+
// Three different statements, and only one of them is a number. "Nobody
|
|
959
|
+
// looked" and "there is nothing to look at" must not read alike (OQ-1).
|
|
960
|
+
label: "Domain",
|
|
961
|
+
...options.mounted && options.domainCapabilities === void 0 ? { tone: "warn" } : {},
|
|
962
|
+
text: options.domainCapabilities !== void 0 ? `${options.domainCapabilities} manifest capabilit${options.domainCapabilities === 1 ? "y" : "ies"}` : options.mounted ? "no authoritative oRPC manifest configured \u2014 that plane has no denominator" : "not analyzed at static depth; full depth reads the oRPC manifest"
|
|
963
|
+
}
|
|
964
|
+
];
|
|
965
|
+
}
|
|
966
|
+
function runHeaderBlocks(title, context, inventory, domainCapabilities) {
|
|
967
|
+
return [
|
|
968
|
+
{ title, rows: runContextRows(context) },
|
|
969
|
+
...inventory ? [
|
|
970
|
+
{
|
|
971
|
+
title: "STATIC CATALOG",
|
|
972
|
+
rows: catalogRows(inventory, {
|
|
973
|
+
...domainCapabilities === void 0 ? {} : { domainCapabilities },
|
|
974
|
+
...context.depth === "static" ? {} : { mounted: true }
|
|
975
|
+
})
|
|
976
|
+
}
|
|
977
|
+
] : []
|
|
978
|
+
];
|
|
979
|
+
}
|
|
980
|
+
function componentOf(capabilityId) {
|
|
981
|
+
const path = capabilityId.replace(/^(view|domain):/, "");
|
|
982
|
+
const dot = path.lastIndexOf(".");
|
|
983
|
+
return dot === -1 ? path : path.slice(0, dot);
|
|
984
|
+
}
|
|
985
|
+
function catalogDetailParts(inventory, options = {}) {
|
|
986
|
+
const parts = [];
|
|
987
|
+
const resolved = inventory.capabilities.filter((c) => c.resolution !== "unresolved");
|
|
988
|
+
const unreadEntries = unresolved(inventory);
|
|
989
|
+
const components = /* @__PURE__ */ new Map();
|
|
990
|
+
for (const capability of resolved) {
|
|
991
|
+
const component = componentOf(capability.capabilityId);
|
|
992
|
+
const current = components.get(component) ?? { ids: /* @__PURE__ */ new Set(), sites: 0, partial: 0 };
|
|
993
|
+
current.ids.add(capability.capabilityId);
|
|
994
|
+
current.sites += 1;
|
|
995
|
+
if (capability.resolution === "partial") current.partial += 1;
|
|
996
|
+
components.set(component, current);
|
|
997
|
+
}
|
|
998
|
+
if (components.size > 0) {
|
|
999
|
+
parts.push({
|
|
1000
|
+
kind: "table",
|
|
1001
|
+
title: `COMPONENTS (${components.size})`,
|
|
1002
|
+
headers: ["COMPONENT", "CAPABILITIES", "CALL SITES", "DYNAMIC META"],
|
|
1003
|
+
rows: [...components.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([component, data]) => ({
|
|
1004
|
+
cells: [
|
|
1005
|
+
`view:${component}`,
|
|
1006
|
+
String(data.ids.size),
|
|
1007
|
+
String(data.sites),
|
|
1008
|
+
data.partial > 0 ? String(data.partial) : NONE
|
|
1009
|
+
],
|
|
1010
|
+
note: [...data.ids].sort().join(" \xB7 ")
|
|
1011
|
+
}))
|
|
1012
|
+
});
|
|
1013
|
+
}
|
|
1014
|
+
if (unreadEntries.length > 0) {
|
|
1015
|
+
const groups = /* @__PURE__ */ new Map();
|
|
1016
|
+
for (const entry of unreadEntries) {
|
|
1017
|
+
const key = `${entry.origin.file}\0${entry.reason ?? "unknown"}`;
|
|
1018
|
+
groups.set(key, (groups.get(key) ?? 0) + 1);
|
|
1019
|
+
}
|
|
1020
|
+
parts.push({
|
|
1021
|
+
kind: "table",
|
|
1022
|
+
title: `UNREAD SITES (${unreadEntries.length})`,
|
|
1023
|
+
lead: "Counts above are a floor until these sites are resolved or explicitly accepted.",
|
|
1024
|
+
headers: ["FILE", "REASON", "SITES"],
|
|
1025
|
+
rows: [...groups.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([key, count]) => {
|
|
1026
|
+
const [file, reason] = key.split("\0");
|
|
1027
|
+
return { cells: [file ?? "?", reason ?? "unknown", String(count)] };
|
|
1028
|
+
})
|
|
1029
|
+
});
|
|
1030
|
+
parts.push({
|
|
1031
|
+
kind: "note",
|
|
1032
|
+
title: "ALLOWLIST KEYS",
|
|
1033
|
+
lines: unreadEntries.map((entry) => ` allowlist key: ${unreadKey(entry)}`)
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
if (options.detail) {
|
|
1037
|
+
const byId = [...resolved].sort((a, b) => a.capabilityId.localeCompare(b.capabilityId));
|
|
1038
|
+
if (byId.length > 0) {
|
|
1039
|
+
parts.push({
|
|
1040
|
+
kind: "table",
|
|
1041
|
+
title: `CAPABILITY DETAILS (${byId.length} call sites)`,
|
|
1042
|
+
headers: ["CAPABILITY", "KIND", "ORIGIN", "READ"],
|
|
1043
|
+
rows: byId.map((capability) => ({
|
|
1044
|
+
cells: [
|
|
1045
|
+
capability.capabilityId,
|
|
1046
|
+
capability.kind,
|
|
1047
|
+
`${capability.origin.file}:${capability.origin.line}`,
|
|
1048
|
+
capability.resolution
|
|
1049
|
+
],
|
|
1050
|
+
...capability.note ? { note: capability.note } : {}
|
|
1051
|
+
}))
|
|
1052
|
+
});
|
|
1053
|
+
}
|
|
1054
|
+
if (unreadEntries.length > 0) {
|
|
1055
|
+
parts.push({ kind: "findings", sections: [unreadSection(unreadEntries)] });
|
|
1056
|
+
}
|
|
1057
|
+
} else if (unreadEntries.length > 0 || resolved.length > 0) {
|
|
1058
|
+
parts.push({
|
|
1059
|
+
kind: "note",
|
|
1060
|
+
muted: true,
|
|
1061
|
+
lines: ["Details: re-run with --detail for origins, per-site notes, and diagnostics."]
|
|
1062
|
+
});
|
|
1063
|
+
}
|
|
1064
|
+
return parts;
|
|
1065
|
+
}
|
|
1066
|
+
function scenarioStats(result) {
|
|
1067
|
+
const counts = { expose: 0, disable: 0, hide: 0 };
|
|
1068
|
+
for (const capability of result.explanation.capabilities) counts[capability.outcome] += 1;
|
|
1069
|
+
return {
|
|
1070
|
+
scenario: result.scenario,
|
|
1071
|
+
...result.snapshot.route?.path ? { route: result.snapshot.route.path } : {},
|
|
1072
|
+
callable: counts.expose,
|
|
1073
|
+
disabled: counts.disable,
|
|
1074
|
+
hidden: counts.hide,
|
|
1075
|
+
rejected: result.rejections.length
|
|
1076
|
+
};
|
|
1077
|
+
}
|
|
1078
|
+
var NONE = "\u2014";
|
|
1079
|
+
function scenarioTable(stats, options = {}) {
|
|
1080
|
+
const headers = ["SCENARIO", "ROUTE", "CALLABLE", "DISABLED", "HIDDEN", "REJECTED"];
|
|
1081
|
+
if (options.baselines) headers.push("BASELINE");
|
|
1082
|
+
return {
|
|
1083
|
+
headers,
|
|
1084
|
+
rows: stats.map((entry) => ({
|
|
1085
|
+
// The baseline column already says `did not mount`, so the note carries
|
|
1086
|
+
// only what a reader cannot get anywhere else: why.
|
|
1087
|
+
...entry.failed ? {
|
|
1088
|
+
note: `${options.baselines ? "" : "did not mount \u2014 "}${entry.failure ?? "the scenario threw during mount"}`
|
|
1089
|
+
} : {},
|
|
1090
|
+
cells: [
|
|
1091
|
+
entry.scenario,
|
|
1092
|
+
entry.route ?? NONE,
|
|
1093
|
+
...entry.failed ? [NONE, NONE, NONE, NONE] : [
|
|
1094
|
+
String(entry.callable),
|
|
1095
|
+
String(entry.disabled),
|
|
1096
|
+
String(entry.hidden),
|
|
1097
|
+
entry.rejected > 0 ? String(entry.rejected) : NONE
|
|
1098
|
+
],
|
|
1099
|
+
...options.baselines ? [entry.failed ? "did not mount" : entry.baseline ?? NONE] : []
|
|
1100
|
+
]
|
|
1101
|
+
}))
|
|
1102
|
+
};
|
|
1103
|
+
}
|
|
1104
|
+
var RANK = { hide: 0, disable: 1, expose: 2 };
|
|
1105
|
+
function trackReach(reach, rows, scenario) {
|
|
1106
|
+
for (const row of rows) {
|
|
1107
|
+
const current = reach.get(row.capabilityId);
|
|
1108
|
+
if (!current) {
|
|
1109
|
+
reach.set(row.capabilityId, {
|
|
1110
|
+
capabilityId: row.capabilityId,
|
|
1111
|
+
best: row.outcome,
|
|
1112
|
+
...row.effect ? { effect: row.effect } : {},
|
|
1113
|
+
flags: row.flags,
|
|
1114
|
+
...row.reason ? { note: row.reason } : {},
|
|
1115
|
+
scenarios: [scenario]
|
|
1116
|
+
});
|
|
1117
|
+
continue;
|
|
1118
|
+
}
|
|
1119
|
+
current.scenarios.push(scenario);
|
|
1120
|
+
if (!current.effect && row.effect) current.effect = row.effect;
|
|
1121
|
+
if (current.flags.length === 0 && row.flags.length > 0) current.flags = row.flags;
|
|
1122
|
+
if (RANK[row.outcome] > RANK[current.best]) {
|
|
1123
|
+
current.best = row.outcome;
|
|
1124
|
+
if (row.reason) current.note = row.reason;
|
|
1125
|
+
else delete current.note;
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
function neverCallable(reach) {
|
|
1130
|
+
return [...reach.values()].filter((entry) => entry.best !== "expose").sort((a, b) => a.capabilityId.localeCompare(b.capabilityId));
|
|
1131
|
+
}
|
|
1132
|
+
var MUTATING = /* @__PURE__ */ new Set(["server-mutation", "external-side-effect", "destructive"]);
|
|
1133
|
+
function riskOf(entries) {
|
|
1134
|
+
const present = entries.filter((entry) => entry.outcome !== "hide");
|
|
1135
|
+
return {
|
|
1136
|
+
present: present.length,
|
|
1137
|
+
destructive: present.filter((entry) => entry.effect === "destructive").length,
|
|
1138
|
+
mutating: present.filter((entry) => entry.effect && MUTATING.has(entry.effect)).length,
|
|
1139
|
+
confirmed: present.filter(
|
|
1140
|
+
(entry) => entry.flags.some((flag) => flag.startsWith("confirmation:"))
|
|
1141
|
+
).length,
|
|
1142
|
+
bound: present.filter((entry) => entry.flags.some((flag) => flag.includes(" bound"))).length
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
function riskParts(risk) {
|
|
1146
|
+
return [
|
|
1147
|
+
...risk.destructive > 0 ? [`${risk.destructive} destructive`] : [],
|
|
1148
|
+
...risk.mutating > risk.destructive ? [`${risk.mutating - risk.destructive} mutating`] : [],
|
|
1149
|
+
...risk.confirmed > 0 ? [`${risk.confirmed} confirmation-gated`] : []
|
|
1150
|
+
];
|
|
1151
|
+
}
|
|
1152
|
+
function riskClause(rows) {
|
|
1153
|
+
const parts = riskParts(riskOf(rows));
|
|
1154
|
+
return parts.join(", ");
|
|
1155
|
+
}
|
|
1156
|
+
function riskText(reach) {
|
|
1157
|
+
const risk = riskOf([...reach.values()].map((entry) => ({ ...entry, outcome: entry.best })));
|
|
1158
|
+
const parts = riskParts(risk);
|
|
1159
|
+
if (risk.present === 0) {
|
|
1160
|
+
return "nothing is on the surface at all \u2014 every capability was hidden by policy";
|
|
1161
|
+
}
|
|
1162
|
+
if (parts.length === 0) {
|
|
1163
|
+
return `nothing mutating or destructive is on the surface \xB7 ${risk.present} read-only or local-state capabilit${risk.present === 1 ? "y" : "ies"}`;
|
|
1164
|
+
}
|
|
1165
|
+
return [...parts, ...risk.bound > 0 ? [`${risk.bound} with bound input`] : []].join(" \xB7 ");
|
|
1166
|
+
}
|
|
1167
|
+
function surfaceSummaryRows(input) {
|
|
1168
|
+
const rows = [];
|
|
1169
|
+
const coverage = input.coverage;
|
|
1170
|
+
if (coverage) {
|
|
1171
|
+
rows.push({
|
|
1172
|
+
label: "Reach",
|
|
1173
|
+
tone: coverage.unreached.length > 0 ? "bad" : "good",
|
|
1174
|
+
text: `${coverage.reached}/${coverage.authored} authored capabilit${coverage.authored === 1 ? "y" : "ies"} reached` + (coverage.unreached.length > 0 ? ` \xB7 ${coverage.unreached.length} unreached` : "") + (coverage.allowed.length > 0 ? ` \xB7 ${coverage.allowed.length} allowlisted` : "")
|
|
1175
|
+
});
|
|
1176
|
+
}
|
|
1177
|
+
const mounted = input.reach.size;
|
|
1178
|
+
const dark = neverCallable(input.reach);
|
|
1179
|
+
if (mounted > 0) {
|
|
1180
|
+
const stuck = dark.filter((entry) => entry.best === "disable").length;
|
|
1181
|
+
const why = [
|
|
1182
|
+
...stuck > 0 ? [`${stuck} disabled`] : [],
|
|
1183
|
+
...dark.length > stuck ? [`${dark.length - stuck} hidden`] : []
|
|
1184
|
+
].join(", ");
|
|
1185
|
+
rows.push({
|
|
1186
|
+
label: "Callable",
|
|
1187
|
+
tone: dark.length > 0 ? "warn" : "good",
|
|
1188
|
+
text: `${mounted - dark.length}/${mounted} mounted capabilit${mounted === 1 ? "y is" : "ies are"} callable in at least one scenario` + (dark.length > 0 ? ` \xB7 ${dark.length} never callable (${why})` : "")
|
|
1189
|
+
});
|
|
1190
|
+
rows.push({ label: "Risk", text: riskText(input.reach) });
|
|
1191
|
+
}
|
|
1192
|
+
if (coverage) {
|
|
1193
|
+
if (coverage.domainReached.length > 0 || coverage.domainAuthoritative) {
|
|
1194
|
+
rows.push({
|
|
1195
|
+
label: "Domain",
|
|
1196
|
+
tone: coverage.unmanifestedDomain.length > 0 ? "bad" : "good",
|
|
1197
|
+
text: coverage.unmanifestedDomain.length > 0 ? `${coverage.unmanifestedDomain.length} mounted capabilit${coverage.unmanifestedDomain.length === 1 ? "y is" : "ies are"} absent from the oRPC manifest` : `${coverage.domainReached.length} capabilit${coverage.domainReached.length === 1 ? "y" : "ies"} reached${coverage.domainAuthoritative ? " against the authoritative oRPC manifest" : " and held apart \u2014 configure the manifest to cover that plane"}`
|
|
1198
|
+
});
|
|
1199
|
+
}
|
|
1200
|
+
rows.push({
|
|
1201
|
+
label: "Catalog",
|
|
1202
|
+
tone: coverage.unresolved.length > 0 ? "warn" : "good",
|
|
1203
|
+
text: coverage.unresolved.length > 0 ? `${coverage.unresolved.length} unread call site${coverage.unresolved.length === 1 ? "" : "s"} \u2014 every count above is a floor` : `every call site read${coverage.allowedUnread.length > 0 ? ` \xB7 ${coverage.allowedUnread.length} allowlisted` : ""}`
|
|
1204
|
+
});
|
|
1205
|
+
}
|
|
1206
|
+
rows.push({
|
|
1207
|
+
label: "Scenarios",
|
|
1208
|
+
tone: input.failures > 0 ? "bad" : void 0,
|
|
1209
|
+
text: `${input.scenarios.filter((entry) => !entry.failed).length} mounted` + (input.failures > 0 ? ` \xB7 ${input.failures} did not mount` : "")
|
|
1210
|
+
});
|
|
1211
|
+
rows.push({ label: "Verdict", tone: verdictTone(input), text: verdictText(input) });
|
|
1212
|
+
return rows;
|
|
1213
|
+
}
|
|
1214
|
+
function verdictTone(input) {
|
|
1215
|
+
if (input.failures > 0) return "bad";
|
|
1216
|
+
if (!input.coverage) return "warn";
|
|
1217
|
+
const clean = input.coverage.unreached.length === 0 && input.coverage.unresolved.length === 0 && input.coverage.staleAllowlist.length === 0 && input.coverage.staleUnreadAllowlist.length === 0 && input.coverage.unmanifestedDomain.length === 0;
|
|
1218
|
+
return clean ? "good" : "bad";
|
|
1219
|
+
}
|
|
1220
|
+
function verdictText(input) {
|
|
1221
|
+
if (input.failures > 0) {
|
|
1222
|
+
return "a scenario did not mount, so no coverage verdict was computed at all";
|
|
1223
|
+
}
|
|
1224
|
+
const coverage = input.coverage;
|
|
1225
|
+
if (!coverage) {
|
|
1226
|
+
return input.depth === "runtime" ? "the source was not read at this depth \u2014 a statement about these scenarios only" : "no coverage verdict at this depth";
|
|
1227
|
+
}
|
|
1228
|
+
if (coverage.unreached.length > 0) {
|
|
1229
|
+
return `${coverage.unreached.length} authored capabilit${coverage.unreached.length === 1 ? "y is" : "ies are"} reached by no scenario`;
|
|
1230
|
+
}
|
|
1231
|
+
if (coverage.unresolved.length > 0) {
|
|
1232
|
+
return "every capability the catalog could read is reached \u2014 and the catalog has holes in it";
|
|
1233
|
+
}
|
|
1234
|
+
return coverage.allowed.length > 0 ? "no new coverage gaps \u2014 the allowlist still holds the known ones" : "every authored capability is reached by a scenario";
|
|
1235
|
+
}
|
|
1236
|
+
function checkMatrixRows(input) {
|
|
1237
|
+
const rows = [];
|
|
1238
|
+
const coverage = input.coverage;
|
|
1239
|
+
if (coverage) {
|
|
1240
|
+
rows.push({
|
|
1241
|
+
label: "Coverage",
|
|
1242
|
+
status: coverage.unreached.length > 0 || coverage.staleAllowlist.length > 0 ? "FAIL" : coverage.allowed.length > 0 ? "WARN" : "PASS",
|
|
1243
|
+
text: `${coverage.reached}/${coverage.authored} authored capabilities reached` + (coverage.unreached.length > 0 ? ` \xB7 ${coverage.unreached.length} unreached` : "") + (coverage.allowed.length > 0 ? ` \xB7 ${coverage.allowed.length} unreached allowlisted` : "") + (coverage.staleAllowlist.length > 0 ? ` \xB7 ${coverage.staleAllowlist.length} stale allowlist entr${coverage.staleAllowlist.length === 1 ? "y" : "ies"}` : "")
|
|
1244
|
+
});
|
|
1245
|
+
const unread = coverage.unresolved.length;
|
|
1246
|
+
const accepted = coverage.allowedUnread.length;
|
|
1247
|
+
rows.push({
|
|
1248
|
+
label: "Catalog",
|
|
1249
|
+
status: coverage.staleUnreadAllowlist.length > 0 || unread > 0 && !input.unresolvedAllowed ? "FAIL" : unread > 0 || accepted > 0 ? "WARN" : "PASS",
|
|
1250
|
+
text: coverage.staleUnreadAllowlist.length > 0 ? `${coverage.staleUnreadAllowlist.length} stale unread allowlist entr${coverage.staleUnreadAllowlist.length === 1 ? "y" : "ies"}` : unread > 0 ? `${unread} unread static site${unread === 1 ? "" : "s"}${input.unresolvedAllowed ? " accepted by --allow-unresolved" : ""}` : accepted > 0 ? `${accepted} unread static site${accepted === 1 ? "" : "s"} allowlisted` : "all static sites resolved"
|
|
1251
|
+
});
|
|
1252
|
+
rows.push({
|
|
1253
|
+
label: "Domain",
|
|
1254
|
+
status: coverage.unmanifestedDomain.length > 0 ? "FAIL" : coverage.domainAuthoritative ? "PASS" : "WARN",
|
|
1255
|
+
text: coverage.unmanifestedDomain.length > 0 ? `${coverage.unmanifestedDomain.length} mounted capabilit${coverage.unmanifestedDomain.length === 1 ? "y" : "ies"} absent from manifest` : coverage.domainAuthoritative ? `${coverage.domainReached.length} manifest capabilit${coverage.domainReached.length === 1 ? "y" : "ies"} reached` : "authoritative manifest not configured"
|
|
1256
|
+
});
|
|
1257
|
+
} else {
|
|
1258
|
+
rows.push({
|
|
1259
|
+
label: "Coverage",
|
|
1260
|
+
status: input.status === "ERROR" ? "ERROR" : "WARN",
|
|
1261
|
+
text: input.status === "ERROR" ? "no verdict; runtime analysis incomplete" : "not evaluated \u2014 statement about these scenarios only; re-run with --depth full"
|
|
1262
|
+
});
|
|
1263
|
+
}
|
|
1264
|
+
const baselineOk = input.baselineCurrent === input.baselineTotal && input.scenarioManifestOk;
|
|
1265
|
+
rows.push({
|
|
1266
|
+
label: "Baselines",
|
|
1267
|
+
status: baselineOk ? "PASS" : "FAIL",
|
|
1268
|
+
text: `${input.baselineCurrent}/${input.baselineTotal} scenario baselines current` + (input.scenarioManifestOk ? "" : " \xB7 scenario manifest differs")
|
|
1269
|
+
});
|
|
1270
|
+
const mounted = input.stats.filter((entry) => !entry.failed).length;
|
|
1271
|
+
rows.push({
|
|
1272
|
+
label: "Runtime",
|
|
1273
|
+
status: input.mountFailures > 0 ? "ERROR" : input.rejected > 0 ? "FAIL" : "PASS",
|
|
1274
|
+
text: input.mountFailures > 0 ? `${input.mountFailures} scenario${input.mountFailures === 1 ? "" : "s"} did not mount` : input.rejected > 0 ? `${input.rejected} registration${input.rejected === 1 ? "" : "s"} rejected` : `${mounted} scenario${mounted === 1 ? "" : "s"} mounted`
|
|
1275
|
+
});
|
|
1276
|
+
return rows;
|
|
1277
|
+
}
|
|
1278
|
+
function checkOverviewParts(input) {
|
|
1279
|
+
const table = scenarioTable(input.stats, { baselines: true });
|
|
1280
|
+
return [
|
|
1281
|
+
{
|
|
1282
|
+
kind: "blocks",
|
|
1283
|
+
blocks: [
|
|
1284
|
+
{
|
|
1285
|
+
title: `SURFACE CHECK ${input.status}`,
|
|
1286
|
+
rows: input.context ? runContextRows(input.context) : []
|
|
1287
|
+
},
|
|
1288
|
+
{ rows: checkMatrixRows(input) }
|
|
1289
|
+
]
|
|
1290
|
+
},
|
|
1291
|
+
{
|
|
1292
|
+
kind: "table",
|
|
1293
|
+
title: `SCENARIOS (${input.stats.length})`,
|
|
1294
|
+
headers: table.headers,
|
|
1295
|
+
rows: table.rows
|
|
1296
|
+
}
|
|
1297
|
+
];
|
|
1298
|
+
}
|
|
1299
|
+
function coverageSections(report, options = {}) {
|
|
1300
|
+
const sections = [];
|
|
1301
|
+
if (report.unreached.length > 0) {
|
|
1302
|
+
sections.push({
|
|
1303
|
+
title: "UNREACHED",
|
|
1304
|
+
gloss: "authored, and no scenario mounts it",
|
|
1305
|
+
count: report.unreached.length,
|
|
1306
|
+
headers: ["CAPABILITY", "ORIGIN"],
|
|
1307
|
+
rows: report.unreached.map((entry) => ({
|
|
1308
|
+
cells: [entry.capabilityId, `${entry.origin.file}:${entry.origin.line}`]
|
|
1309
|
+
})),
|
|
1310
|
+
hint: `add a scenario that mounts them, delete the dead component, or record the decision in ${displayPath(report.allowlistPath)}`
|
|
1311
|
+
});
|
|
1312
|
+
}
|
|
1313
|
+
if (report.undeclared.length > 0) {
|
|
1314
|
+
if (!options.compact || options.detail) {
|
|
1315
|
+
sections.push({
|
|
1316
|
+
title: "UNDECLARED",
|
|
1317
|
+
gloss: "present at runtime with no static origin \u2014 a dynamic registration, or a gap here",
|
|
1318
|
+
count: report.undeclared.length,
|
|
1319
|
+
tone: "notice",
|
|
1320
|
+
lines: report.undeclared
|
|
1321
|
+
});
|
|
1322
|
+
} else {
|
|
1323
|
+
sections.push({
|
|
1324
|
+
title: "NOTICE",
|
|
1325
|
+
gloss: `${report.undeclared.length} runtime capabilit${report.undeclared.length === 1 ? "y has" : "ies have"} no static origin; re-run with --detail to list them`,
|
|
1326
|
+
count: 0,
|
|
1327
|
+
tone: "notice"
|
|
1328
|
+
});
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
if (report.unmanifestedDomain.length > 0) {
|
|
1332
|
+
sections.push({
|
|
1333
|
+
title: "UNMANIFESTED DOMAIN",
|
|
1334
|
+
gloss: "mounted, but absent from the authoritative oRPC manifest",
|
|
1335
|
+
count: report.unmanifestedDomain.length,
|
|
1336
|
+
lines: report.unmanifestedDomain,
|
|
1337
|
+
hint: "add them to the manifest, or stop mounting a router the manifest does not describe"
|
|
1338
|
+
});
|
|
1339
|
+
}
|
|
1340
|
+
if (report.staleAllowlist.length > 0) {
|
|
1341
|
+
sections.push({
|
|
1342
|
+
title: "STALE ALLOWLIST",
|
|
1343
|
+
gloss: "a scenario reaches these now, so delete them before the list rots",
|
|
1344
|
+
count: report.staleAllowlist.length,
|
|
1345
|
+
lines: report.staleAllowlist,
|
|
1346
|
+
hint: `delete these keys from ${displayPath(report.allowlistPath)}`
|
|
1347
|
+
});
|
|
1348
|
+
}
|
|
1349
|
+
if (report.staleUnreadAllowlist.length > 0) {
|
|
1350
|
+
sections.push({
|
|
1351
|
+
title: "STALE UNREAD ALLOWLIST",
|
|
1352
|
+
gloss: "the extractor reads these now, so delete them before the list rots",
|
|
1353
|
+
count: report.staleUnreadAllowlist.length,
|
|
1354
|
+
lines: report.staleUnreadAllowlist,
|
|
1355
|
+
hint: `delete these keys from ${displayPath(report.unreadAllowlistPath)}`
|
|
1356
|
+
});
|
|
1357
|
+
}
|
|
1358
|
+
if (report.unresolved.length > 0) {
|
|
1359
|
+
sections.push(unreadSection(report.unresolved, report.unreadAllowlistPath));
|
|
1360
|
+
}
|
|
1361
|
+
return sections;
|
|
1362
|
+
}
|
|
1363
|
+
function unreadSection(entries, allowlistPath) {
|
|
1364
|
+
return {
|
|
1365
|
+
title: "UNREAD CALL SITES",
|
|
1366
|
+
gloss: "the catalog is incomplete, so every count above is a floor",
|
|
1367
|
+
count: entries.length,
|
|
1368
|
+
lines: entries.flatMap((entry) => [
|
|
1369
|
+
`${entry.origin.file}:${entry.origin.line}`,
|
|
1370
|
+
` ${entry.note ?? "the extractor could not read this call site"}`,
|
|
1371
|
+
` allowlist key: ${unreadKey(entry)}`
|
|
1372
|
+
]),
|
|
1373
|
+
hint: allowlistPath ? `make the call site readable, or accept each key in ${displayPath(allowlistPath)}` : "make the call site readable, or accept each key in .agent-surface/unresolved-allow.json"
|
|
1374
|
+
};
|
|
1375
|
+
}
|
|
1376
|
+
function firstUsefulStackFrame(stack) {
|
|
1377
|
+
if (!stack) return void 0;
|
|
1378
|
+
const frames = stack.split("\n").map((line) => line.trim()).filter((line) => line.startsWith("at "));
|
|
1379
|
+
return frames.find((line) => !line.includes("node_modules")) ?? frames[0];
|
|
1380
|
+
}
|
|
1381
|
+
function failureLines(failure) {
|
|
1382
|
+
const message = failure.message.trim() || "Unknown scenario failure (no message)";
|
|
1383
|
+
const componentFrames = (failure.componentStack ?? "").split("\n").map((line) => line.trim()).filter(Boolean).slice(0, 6);
|
|
1384
|
+
const frame = firstUsefulStackFrame(failure.stack);
|
|
1385
|
+
return [
|
|
1386
|
+
failure.scenario,
|
|
1387
|
+
` ${message}`,
|
|
1388
|
+
...failure.cause ? [` caused by: ${failure.cause}`] : [],
|
|
1389
|
+
...componentFrames.length > 0 ? [" React component stack:", ...componentFrames.map((line) => ` ${line}`)] : [],
|
|
1390
|
+
...frame ? [` ${frame}`] : []
|
|
1391
|
+
];
|
|
1392
|
+
}
|
|
1393
|
+
function failureSection(failures) {
|
|
1394
|
+
return {
|
|
1395
|
+
title: "DID NOT MOUNT",
|
|
1396
|
+
gloss: "these scenarios threw, and were skipped",
|
|
1397
|
+
count: failures.length,
|
|
1398
|
+
lines: failures.flatMap(failureLines)
|
|
1399
|
+
};
|
|
1400
|
+
}
|
|
1401
|
+
function noVerdictSection(failures) {
|
|
1402
|
+
return {
|
|
1403
|
+
title: "NO COVERAGE VERDICT",
|
|
1404
|
+
gloss: "a scenario did not mount, so nothing reached anything",
|
|
1405
|
+
count: failures.length,
|
|
1406
|
+
lines: [
|
|
1407
|
+
"Every capability those scenarios would have surfaced would be reported unreached,",
|
|
1408
|
+
"so no verdict is printed at all. Fix the mount, or name a scenario that works."
|
|
1409
|
+
]
|
|
1410
|
+
};
|
|
1411
|
+
}
|
|
1412
|
+
function neverCallableSection(entries) {
|
|
1413
|
+
const stuck = entries.filter((entry) => entry.best === "disable").length;
|
|
1414
|
+
const hidden = entries.length - stuck;
|
|
1415
|
+
return {
|
|
1416
|
+
title: "NEVER CALLABLE",
|
|
1417
|
+
gloss: "every scenario mounted these, and none of them could call one",
|
|
1418
|
+
count: entries.length,
|
|
1419
|
+
tone: "notice",
|
|
1420
|
+
headers: ["CAPABILITY", "BEST STATE", "WHY"],
|
|
1421
|
+
rows: entries.map((entry) => ({
|
|
1422
|
+
cells: [
|
|
1423
|
+
entry.capabilityId,
|
|
1424
|
+
entry.best === "disable" ? "disabled" : "hidden",
|
|
1425
|
+
entry.best === "disable" ? entry.note ?? "the UI reported it unavailable in every scenario" : "a policy hid it in every scenario"
|
|
1426
|
+
]
|
|
1427
|
+
})),
|
|
1428
|
+
hint: [
|
|
1429
|
+
...stuck > 0 ? [
|
|
1430
|
+
"add a scenario that reaches the state these need \u2014 an open drawer, a filled list, a selected row"
|
|
1431
|
+
] : [],
|
|
1432
|
+
...hidden > 0 ? [
|
|
1433
|
+
`add a scenario whose consumer carries the authority ${stuck > 0 ? "the hidden ones are" : "these are"} waiting for`
|
|
1434
|
+
] : []
|
|
1435
|
+
].join("; ")
|
|
1436
|
+
};
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
export {
|
|
1440
|
+
flatRows,
|
|
1441
|
+
buildView,
|
|
1442
|
+
ALLOWLIST_FILE,
|
|
1443
|
+
UNREAD_ALLOWLIST_FILE,
|
|
1444
|
+
allowlistPathFor,
|
|
1445
|
+
unreadAllowlistPathFor,
|
|
1446
|
+
readAllowlist,
|
|
1447
|
+
buildCoverageReport,
|
|
1448
|
+
coverageExitCode,
|
|
1449
|
+
findTsconfig,
|
|
1450
|
+
readLiteralConfigScope,
|
|
1451
|
+
extractCapabilities,
|
|
1452
|
+
authoredIds,
|
|
1453
|
+
unresolved,
|
|
1454
|
+
LABEL_WIDTH,
|
|
1455
|
+
STATUS_WIDTH,
|
|
1456
|
+
READING_SOURCE,
|
|
1457
|
+
mountingLabel,
|
|
1458
|
+
reportGrid,
|
|
1459
|
+
displayPath,
|
|
1460
|
+
catalogRows,
|
|
1461
|
+
runHeaderBlocks,
|
|
1462
|
+
catalogDetailParts,
|
|
1463
|
+
scenarioStats,
|
|
1464
|
+
scenarioTable,
|
|
1465
|
+
trackReach,
|
|
1466
|
+
neverCallable,
|
|
1467
|
+
riskClause,
|
|
1468
|
+
surfaceSummaryRows,
|
|
1469
|
+
checkOverviewParts,
|
|
1470
|
+
coverageSections,
|
|
1471
|
+
failureSection,
|
|
1472
|
+
noVerdictSection,
|
|
1473
|
+
neverCallableSection
|
|
1474
|
+
};
|
|
1475
|
+
//# sourceMappingURL=chunk-NFK3XWWH.js.map
|