@agent-surface/cli 0.10.0 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +64 -23
  2. package/dist/bin.js +63 -41
  3. package/dist/bin.js.map +1 -1
  4. package/dist/check-XU3FYNGJ.js +122 -0
  5. package/dist/check-XU3FYNGJ.js.map +1 -0
  6. package/dist/chunk-2FG527AM.js +740 -0
  7. package/dist/chunk-2FG527AM.js.map +1 -0
  8. package/dist/chunk-AFLVTBI6.js +316 -0
  9. package/dist/chunk-AFLVTBI6.js.map +1 -0
  10. package/dist/chunk-DYDSJM7R.js +170 -0
  11. package/dist/chunk-DYDSJM7R.js.map +1 -0
  12. package/dist/{chunk-FYEXHWGG.js → chunk-QIVOZAWX.js} +52 -2
  13. package/dist/chunk-QIVOZAWX.js.map +1 -0
  14. package/dist/index.d.ts +36 -2
  15. package/dist/init-ODFEGU3P.js +141 -0
  16. package/dist/init-ODFEGU3P.js.map +1 -0
  17. package/dist/{ink-HBPOQTRS.js → ink-P23VKP4H.js} +102 -33
  18. package/dist/ink-P23VKP4H.js.map +1 -0
  19. package/dist/inspect-6XKULUH2.js +108 -0
  20. package/dist/inspect-6XKULUH2.js.map +1 -0
  21. package/dist/snapshot-YGEDJVTG.js +59 -0
  22. package/dist/snapshot-YGEDJVTG.js.map +1 -0
  23. package/package.json +4 -4
  24. package/dist/capabilities-OLFYMHCL.js +0 -37
  25. package/dist/capabilities-OLFYMHCL.js.map +0 -1
  26. package/dist/check-IN4XKAND.js +0 -84
  27. package/dist/check-IN4XKAND.js.map +0 -1
  28. package/dist/chunk-4AEQKM2X.js +0 -498
  29. package/dist/chunk-4AEQKM2X.js.map +0 -1
  30. package/dist/chunk-A27Y7ALQ.js +0 -51
  31. package/dist/chunk-A27Y7ALQ.js.map +0 -1
  32. package/dist/chunk-FYEXHWGG.js.map +0 -1
  33. package/dist/chunk-ODUIFFPM.js +0 -104
  34. package/dist/chunk-ODUIFFPM.js.map +0 -1
  35. package/dist/coverage-HCHLJTDD.js +0 -133
  36. package/dist/coverage-HCHLJTDD.js.map +0 -1
  37. package/dist/ink-HBPOQTRS.js.map +0 -1
  38. package/dist/inspect-NJNB6CAS.js +0 -213
  39. package/dist/inspect-NJNB6CAS.js.map +0 -1
  40. package/dist/snapshot-JQAB73OV.js +0 -38
  41. package/dist/snapshot-JQAB73OV.js.map +0 -1
@@ -0,0 +1,740 @@
1
+ import {
2
+ UsageError,
3
+ createSurfaceRunner
4
+ } from "./chunk-QIVOZAWX.js";
5
+
6
+ // src/baseline.ts
7
+ import { mkdirSync, readFileSync, writeFileSync } from "fs";
8
+ import { dirname, join, resolve } from "path";
9
+ import { serializeSurfaceSnapshot } from "@agent-surface/testing";
10
+ var DEFAULT_BASELINE_DIR = ".agent-surface";
11
+ function baselineDirFor(configPath, configured) {
12
+ return resolve(dirname(configPath), configured ?? DEFAULT_BASELINE_DIR);
13
+ }
14
+ function baselinePath(dir, scenario) {
15
+ return join(dir, `${scenario}.json`);
16
+ }
17
+ function normalize(snapshot) {
18
+ return serializeSurfaceSnapshot(snapshot);
19
+ }
20
+ function readBaseline(path) {
21
+ try {
22
+ return JSON.parse(readFileSync(path, "utf8"));
23
+ } catch {
24
+ return void 0;
25
+ }
26
+ }
27
+ function writeBaseline(path, value) {
28
+ mkdirSync(dirname(path), { recursive: true });
29
+ writeFileSync(path, `${JSON.stringify(value, null, 2)}
30
+ `, "utf8");
31
+ }
32
+ var PATH_SEGMENT = /([^.[\]]+)|\[(\d+)\]/g;
33
+ function subjectFor(document, path) {
34
+ let node = document;
35
+ let subject;
36
+ for (const match of path.matchAll(PATH_SEGMENT)) {
37
+ if (typeof node !== "object" || node === null) return subject;
38
+ const record = node;
39
+ const candidate = record["capabilityId"] ?? record["procedureId"];
40
+ if (typeof candidate === "string") subject = candidate;
41
+ const key = match[1] ?? match[2];
42
+ if (key === void 0) return subject;
43
+ node = record[key];
44
+ }
45
+ if (typeof node === "object" && node !== null) {
46
+ const record = node;
47
+ const candidate = record["capabilityId"] ?? record["procedureId"];
48
+ if (typeof candidate === "string") subject = candidate;
49
+ }
50
+ return subject;
51
+ }
52
+ function annotate(entries, after, before) {
53
+ return entries.map((entry) => {
54
+ const subject = subjectFor(after, entry.path) ?? subjectFor(before, entry.path);
55
+ return subject ? { ...entry, subject } : entry;
56
+ });
57
+ }
58
+ function diff(before, after, path = "") {
59
+ if (Object.is(before, after)) return [];
60
+ const bothArrays = Array.isArray(before) && Array.isArray(after);
61
+ const bothObjects = !bothArrays && typeof before === "object" && typeof after === "object" && before !== null && after !== null;
62
+ if (bothArrays) {
63
+ const entries = [];
64
+ const max = Math.max(before.length, after.length);
65
+ for (let i = 0; i < max; i++) {
66
+ const at = `${path}[${i}]`;
67
+ if (i >= before.length) entries.push({ path: at, kind: "added", after: after[i] });
68
+ else if (i >= after.length) entries.push({ path: at, kind: "removed", before: before[i] });
69
+ else entries.push(...diff(before[i], after[i], at));
70
+ }
71
+ return entries;
72
+ }
73
+ if (bothObjects) {
74
+ const entries = [];
75
+ const beforeRecord = before;
76
+ const afterRecord = after;
77
+ const keys = /* @__PURE__ */ new Set([...Object.keys(beforeRecord), ...Object.keys(afterRecord)]);
78
+ for (const key of [...keys].sort()) {
79
+ const at = path ? `${path}.${key}` : key;
80
+ if (!(key in beforeRecord)) {
81
+ entries.push({ path: at, kind: "added", after: afterRecord[key] });
82
+ } else if (!(key in afterRecord)) {
83
+ entries.push({ path: at, kind: "removed", before: beforeRecord[key] });
84
+ } else {
85
+ entries.push(...diff(beforeRecord[key], afterRecord[key], at));
86
+ }
87
+ }
88
+ return entries;
89
+ }
90
+ if (JSON.stringify(before) === JSON.stringify(after)) return [];
91
+ return [{ path: path || "<root>", kind: "changed", before, after }];
92
+ }
93
+ function formatValue(value) {
94
+ if (value === void 0) return "\u2014";
95
+ const text = typeof value === "string" ? value : JSON.stringify(value);
96
+ return text.length > 120 ? `${text.slice(0, 117)}\u2026` : text;
97
+ }
98
+
99
+ // src/coverage.ts
100
+ import { existsSync, readFileSync as readFileSync2 } from "fs";
101
+ import { join as join2 } from "path";
102
+ var ALLOWLIST_FILE = "coverage-allow.json";
103
+ var UNREAD_ALLOWLIST_FILE = "unresolved-allow.json";
104
+ function allowlistPathFor(baselineDir) {
105
+ return join2(baselineDir, ALLOWLIST_FILE);
106
+ }
107
+ function unreadAllowlistPathFor(baselineDir) {
108
+ return join2(baselineDir, UNREAD_ALLOWLIST_FILE);
109
+ }
110
+ function readAllowlist(path, keyName = "capabilityId") {
111
+ if (!existsSync(path)) return {};
112
+ let parsed;
113
+ try {
114
+ parsed = JSON.parse(readFileSync2(path, "utf8"));
115
+ } catch (error) {
116
+ throw new Error(
117
+ `could not parse ${path}: ${error instanceof Error ? error.message : String(error)}`
118
+ );
119
+ }
120
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
121
+ throw new Error(`${path} must be a JSON object of { "${keyName}": "reason" }`);
122
+ }
123
+ const allowlist = {};
124
+ for (const [id, reason] of Object.entries(parsed)) {
125
+ if (typeof reason !== "string" || reason.trim() === "") {
126
+ throw new Error(`${path}: "${id}" needs a non-empty reason string`);
127
+ }
128
+ allowlist[id] = reason;
129
+ }
130
+ return allowlist;
131
+ }
132
+ function unreadKey(entry) {
133
+ return `${entry.origin.file}#${entry.reason ?? "unknown"}`;
134
+ }
135
+ function buildCoverageReport(input) {
136
+ const unreached = [];
137
+ const allowed = [];
138
+ for (const id of [...input.authored].sort()) {
139
+ if (input.reachedIds.has(id)) continue;
140
+ if (id in input.allowlist) {
141
+ allowed.push(id);
142
+ continue;
143
+ }
144
+ unreached.push({ capabilityId: id, origin: input.origins.get(id) ?? { file: "?", line: 0 } });
145
+ }
146
+ const staleAllowlist = Object.keys(input.allowlist).filter((id) => input.reachedIds.has(id) || !input.authored.has(id)).sort();
147
+ const unreadAllowlist = input.unreadAllowlist ?? {};
148
+ const unread = [];
149
+ const allowedUnread = /* @__PURE__ */ new Set();
150
+ for (const entry of input.unresolved) {
151
+ const key = unreadKey(entry);
152
+ if (key in unreadAllowlist) allowedUnread.add(key);
153
+ else unread.push(entry);
154
+ }
155
+ const stillUnread = new Set(input.unresolved.map(unreadKey));
156
+ const staleUnreadAllowlist = Object.keys(unreadAllowlist).filter((key) => !stillUnread.has(key)).sort();
157
+ const unaccounted = [...input.reachedIds].filter((id) => !input.authored.has(id)).sort();
158
+ const domainReached = unaccounted.filter((id) => id.startsWith("domain:"));
159
+ const undeclared = unaccounted.filter((id) => !id.startsWith("domain:"));
160
+ return {
161
+ authored: input.authored.size,
162
+ reached: [...input.authored].filter((id) => input.reachedIds.has(id)).length,
163
+ scenarios: input.scenarios,
164
+ ...input.scope ? { scope: input.scope } : {},
165
+ allowlistOutOfScope: input.allowlistOutOfScope ?? 0,
166
+ unreached,
167
+ undeclared,
168
+ domainReached,
169
+ unresolved: unread,
170
+ allowed,
171
+ staleAllowlist,
172
+ allowlistPath: input.allowlistPath,
173
+ allowedUnread: [...allowedUnread].sort(),
174
+ staleUnreadAllowlist,
175
+ unreadAllowlistPath: input.unreadAllowlistPath
176
+ };
177
+ }
178
+ function coverageExitCode(report, options = {}) {
179
+ if (report.unreached.length > 0) return 1;
180
+ if (report.unresolved.length > 0 && !options.allowUnresolved) return 1;
181
+ if (report.staleAllowlist.length > 0) return 1;
182
+ if (report.staleUnreadAllowlist.length > 0) return 1;
183
+ return 0;
184
+ }
185
+
186
+ // src/extract.ts
187
+ import { existsSync as existsSync2 } from "fs";
188
+ import { dirname as dirname2, isAbsolute, join as join3, relative, resolve as resolve2 } from "path";
189
+ import ts from "typescript";
190
+ var UNRESOLVED_ID = "<unresolved>";
191
+ function findTsconfig(from) {
192
+ return ts.findConfigFile(resolve2(from), ts.sys.fileExists, "tsconfig.json");
193
+ }
194
+ function readProgramFiles(tsconfigPath) {
195
+ const read = ts.readConfigFile(tsconfigPath, ts.sys.readFile);
196
+ if (read.error) {
197
+ throw new Error(
198
+ `could not read ${tsconfigPath}: ${ts.flattenDiagnosticMessageText(read.error.messageText, " ")}`
199
+ );
200
+ }
201
+ const parsed = ts.parseJsonConfigFileContent(
202
+ read.config,
203
+ ts.sys,
204
+ dirname2(tsconfigPath)
205
+ );
206
+ if (parsed.errors.length > 0 && parsed.fileNames.length === 0) {
207
+ throw new Error(
208
+ `could not resolve any files from ${tsconfigPath}: ${parsed.errors.map((error) => ts.flattenDiagnosticMessageText(error.messageText, " ")).join("; ")}`
209
+ );
210
+ }
211
+ return { fileNames: parsed.fileNames, options: parsed.options };
212
+ }
213
+ function calleeName(call) {
214
+ if (ts.isIdentifier(call.expression)) return call.expression.text;
215
+ if (ts.isPropertyAccessExpression(call.expression)) return call.expression.name.text;
216
+ return void 0;
217
+ }
218
+ function propertyName(name) {
219
+ if (ts.isIdentifier(name) || ts.isStringLiteral(name)) return name.text;
220
+ return void 0;
221
+ }
222
+ function propertyOf(object, wanted) {
223
+ for (const property of object.properties) {
224
+ if (ts.isPropertyAssignment(property) && propertyName(property.name) === wanted) {
225
+ return property.initializer;
226
+ }
227
+ if (ts.isShorthandPropertyAssignment(property) && property.name.text === wanted) {
228
+ return property.name;
229
+ }
230
+ }
231
+ return void 0;
232
+ }
233
+ function hasSpread(object) {
234
+ return object.properties.some((property) => ts.isSpreadAssignment(property));
235
+ }
236
+ var CAPABILITY_GROUPS = ["observations", "actions"];
237
+ function spreadKeys(expression, source, depth = 0) {
238
+ if (depth > 1) return void 0;
239
+ if (ts.isParenthesizedExpression(expression)) {
240
+ return spreadKeys(expression.expression, source, depth);
241
+ }
242
+ if (ts.isConditionalExpression(expression)) {
243
+ const whenTrue = spreadKeys(expression.whenTrue, source, depth);
244
+ const whenFalse = spreadKeys(expression.whenFalse, source, depth);
245
+ if (!whenTrue || !whenFalse) return void 0;
246
+ return [...whenTrue, ...whenFalse];
247
+ }
248
+ const resolved = objectLiteralFor(expression, source);
249
+ if (!resolved.object) return void 0;
250
+ const keys = [];
251
+ for (const property of resolved.object.properties) {
252
+ if (ts.isSpreadAssignment(property)) {
253
+ const nested = spreadKeys(property.expression, source, depth + 1);
254
+ if (!nested) return void 0;
255
+ keys.push(...nested);
256
+ continue;
257
+ }
258
+ const name = ts.isPropertyAssignment(property) || ts.isMethodDeclaration(property) ? propertyName(property.name) : ts.isShorthandPropertyAssignment(property) ? property.name.text : void 0;
259
+ if (name === void 0) return void 0;
260
+ keys.push(name);
261
+ }
262
+ return keys;
263
+ }
264
+ function literalText(node) {
265
+ if (!node) return void 0;
266
+ if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) return node.text;
267
+ if (ts.isParenthesizedExpression(node)) return literalText(node.expression);
268
+ if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.PlusToken) {
269
+ const left = literalText(node.left);
270
+ const right = literalText(node.right);
271
+ if (left !== void 0 && right !== void 0) return left + right;
272
+ }
273
+ return void 0;
274
+ }
275
+ function describeConstruct(node) {
276
+ if (ts.isCallExpression(node)) {
277
+ const callee = calleeName(node);
278
+ return callee ? `built by ${callee}()` : "built by a call expression";
279
+ }
280
+ if (ts.isIdentifier(node)) return `a variable (${node.text}) this extractor could not follow`;
281
+ if (ts.isConditionalExpression(node)) return "a conditional expression";
282
+ if (ts.isTemplateExpression(node)) return "a template with substitutions";
283
+ if (ts.isPropertyAccessExpression(node)) return "a property access";
284
+ return "a non-literal expression";
285
+ }
286
+ function objectLiteralFor(expression, source) {
287
+ if (ts.isObjectLiteralExpression(expression)) return { object: expression };
288
+ if (ts.isIdentifier(expression)) {
289
+ const target = expression.text;
290
+ let found;
291
+ const visit = (node) => {
292
+ if (found) return;
293
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.name.text === target && node.initializer && ts.isObjectLiteralExpression(node.initializer)) {
294
+ found = node.initializer;
295
+ return;
296
+ }
297
+ ts.forEachChild(node, visit);
298
+ };
299
+ visit(source);
300
+ if (found) return { object: found };
301
+ return {
302
+ note: `the config is \`${target}\`, which is not a same-module object literal \u2014 the extractor follows one hop only`
303
+ };
304
+ }
305
+ return { note: `the config is ${describeConstruct(expression)}` };
306
+ }
307
+ var GRANULAR_HOOKS = /* @__PURE__ */ new Set(["useAgentAction", "useAgentObservation"]);
308
+ function capabilitiesFromGroup(group, kind, componentType, componentPartial, emit, source) {
309
+ if (!group) return;
310
+ const resolved = objectLiteralFor(group, source);
311
+ if (!resolved.object) {
312
+ emit.push({
313
+ capabilityId: `view:${componentType}.${UNRESOLVED_ID}`,
314
+ kind,
315
+ origin: emit.origin(group),
316
+ resolution: "unresolved",
317
+ reason: "dynamic-group",
318
+ note: `\`${kind}s\` on "${componentType}" is not an object literal: ${resolved.note}`
319
+ });
320
+ return;
321
+ }
322
+ for (const property of resolved.object.properties) {
323
+ if (ts.isSpreadAssignment(property)) {
324
+ emit.push({
325
+ capabilityId: `view:${componentType}.${UNRESOLVED_ID}`,
326
+ kind,
327
+ origin: emit.origin(property),
328
+ resolution: "unresolved",
329
+ reason: "spread-members",
330
+ note: `\`${kind}s\` on "${componentType}" spreads another object, which may contribute capabilities this inventory cannot name`
331
+ });
332
+ continue;
333
+ }
334
+ const name = ts.isPropertyAssignment(property) || ts.isMethodDeclaration(property) ? propertyName(property.name) : ts.isShorthandPropertyAssignment(property) ? property.name.text : void 0;
335
+ if (name === void 0) {
336
+ emit.push({
337
+ capabilityId: `view:${componentType}.${UNRESOLVED_ID}`,
338
+ kind,
339
+ origin: emit.origin(property),
340
+ resolution: "unresolved",
341
+ reason: "computed-name",
342
+ note: `a capability on "${componentType}" has a computed name`
343
+ });
344
+ continue;
345
+ }
346
+ const capability = {
347
+ capabilityId: `view:${componentType}.${name}`,
348
+ kind,
349
+ origin: emit.origin(property),
350
+ resolution: "static"
351
+ };
352
+ const notes = [];
353
+ if (componentPartial) notes.push(componentPartial);
354
+ const value = ts.isPropertyAssignment(property) ? property.initializer : void 0;
355
+ const definition = value && ts.isCallExpression(value) && value.arguments.length > 0 ? value.arguments[0] : value;
356
+ if (definition && ts.isObjectLiteralExpression(definition)) {
357
+ const description = literalText(propertyOf(definition, "description"));
358
+ if (description !== void 0) capability.description = description;
359
+ else notes.push("description is not a string literal");
360
+ if (kind === "action") {
361
+ const effect = literalText(propertyOf(definition, "effect"));
362
+ if (effect !== void 0) capability.effect = effect;
363
+ else notes.push("effect is not a string literal");
364
+ }
365
+ if (hasSpread(definition)) notes.push("the definition spreads another object");
366
+ } else {
367
+ notes.push(
368
+ value ? `the definition is ${describeConstruct(value)}` : "the definition is not an object literal"
369
+ );
370
+ }
371
+ if (notes.length > 0) {
372
+ capability.resolution = "partial";
373
+ capability.note = notes.join("; ");
374
+ }
375
+ emit.push(capability);
376
+ }
377
+ }
378
+ function visitCall(call, emit, source, deferred, enclosing) {
379
+ const callee = calleeName(call);
380
+ if (callee === void 0) return;
381
+ if (GRANULAR_HOOKS.has(callee)) {
382
+ emit.push({
383
+ capabilityId: UNRESOLVED_ID,
384
+ kind: callee === "useAgentAction" ? "action" : "observation",
385
+ origin: emit.origin(call),
386
+ resolution: "unresolved",
387
+ reason: "granular-hook",
388
+ note: `${callee}() registers against a render-scope link, so its component type is not at this call site`
389
+ });
390
+ return;
391
+ }
392
+ if (callee !== "useAgentComponent" && callee !== "register") return;
393
+ const argument = call.arguments[0];
394
+ if (!argument) return;
395
+ const resolved = objectLiteralFor(argument, source);
396
+ if (!resolved.object) {
397
+ emit.push({
398
+ capabilityId: UNRESOLVED_ID,
399
+ kind: "action",
400
+ origin: emit.origin(call),
401
+ resolution: "unresolved",
402
+ reason: "dynamic-config",
403
+ note: `${callee}() call site could not be read: ${resolved.note}`
404
+ });
405
+ return;
406
+ }
407
+ const config = resolved.object;
408
+ const typeNode = propertyOf(config, "type");
409
+ const type = literalText(typeNode);
410
+ if (type === void 0) {
411
+ if (callee === "register" && typeNode === void 0) return;
412
+ const slot = enclosing && typeNode && ts.isIdentifier(typeNode) ? parameterSlot(typeNode.text, enclosing.fn) : void 0;
413
+ if (slot && enclosing?.name) {
414
+ deferred.push({ config, source, emit, wrapperName: enclosing.name, slot, site: call });
415
+ return;
416
+ }
417
+ emit.push({
418
+ capabilityId: UNRESOLVED_ID,
419
+ kind: "action",
420
+ origin: emit.origin(call),
421
+ resolution: "unresolved",
422
+ reason: "dynamic-type",
423
+ note: `\`type\` is not a string literal, so no capability id on this component can be determined`
424
+ });
425
+ return;
426
+ }
427
+ const componentPartial = hasSpread(config) ? "the component config spreads another object, so some metadata here may be dynamic" : void 0;
428
+ for (const property of config.properties) {
429
+ if (!ts.isSpreadAssignment(property)) continue;
430
+ const keys = spreadKeys(property.expression, source);
431
+ if (keys && !keys.some((key) => CAPABILITY_GROUPS.includes(key))) {
432
+ continue;
433
+ }
434
+ emit.push({
435
+ capabilityId: `view:${type}.${UNRESOLVED_ID}`,
436
+ kind: "action",
437
+ origin: emit.origin(property),
438
+ resolution: "unresolved",
439
+ reason: "spread-members",
440
+ 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`
441
+ });
442
+ }
443
+ capabilitiesFromGroup(
444
+ propertyOf(config, "observations"),
445
+ "observation",
446
+ type,
447
+ componentPartial,
448
+ emit,
449
+ source
450
+ );
451
+ capabilitiesFromGroup(
452
+ propertyOf(config, "actions"),
453
+ "action",
454
+ type,
455
+ componentPartial,
456
+ emit,
457
+ source
458
+ );
459
+ }
460
+ function functionLike(node) {
461
+ if (ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isArrowFunction(node) || ts.isMethodDeclaration(node)) {
462
+ return node;
463
+ }
464
+ return void 0;
465
+ }
466
+ function parameterSlot(name, fn) {
467
+ for (const [index, parameter] of fn.parameters.entries()) {
468
+ if (ts.isIdentifier(parameter.name)) {
469
+ if (parameter.name.text === name) return { index };
470
+ continue;
471
+ }
472
+ if (ts.isObjectBindingPattern(parameter.name)) {
473
+ for (const element of parameter.name.elements) {
474
+ if (!ts.isIdentifier(element.name) || element.name.text !== name) continue;
475
+ const property = element.propertyName && ts.isIdentifier(element.propertyName) ? element.propertyName.text : name;
476
+ return { index, property };
477
+ }
478
+ }
479
+ }
480
+ return void 0;
481
+ }
482
+ function callsWrapper(site, wrapper, compilerOptions) {
483
+ const callee = site.call.expression;
484
+ if (!ts.isIdentifier(callee) || callee.text !== wrapper.wrapperName) return false;
485
+ if (site.source.fileName === wrapper.source.fileName) {
486
+ return true;
487
+ }
488
+ for (const statement of site.source.statements) {
489
+ if (!ts.isImportDeclaration(statement)) continue;
490
+ const clause = statement.importClause;
491
+ if (!clause) continue;
492
+ const named = clause.name?.text === wrapper.wrapperName || clause.namedBindings && ts.isNamedImports(clause.namedBindings) && clause.namedBindings.elements.some((element) => element.name.text === wrapper.wrapperName);
493
+ if (!named) continue;
494
+ if (!ts.isStringLiteral(statement.moduleSpecifier)) continue;
495
+ const resolved = ts.resolveModuleName(
496
+ statement.moduleSpecifier.text,
497
+ site.source.fileName,
498
+ compilerOptions,
499
+ ts.sys
500
+ ).resolvedModule;
501
+ if (resolved?.resolvedFileName === wrapper.source.fileName) return true;
502
+ }
503
+ return false;
504
+ }
505
+ function extractCapabilities(options) {
506
+ const root = resolve2(options.root);
507
+ const tsconfigPath = options.tsconfig ? isAbsolute(options.tsconfig) ? options.tsconfig : join3(root, options.tsconfig) : findTsconfig(root);
508
+ if (!tsconfigPath || !existsSync2(tsconfigPath)) {
509
+ throw new Error(
510
+ `no tsconfig.json found from ${root} \u2014 \`capabilities\` reads the TypeScript program, so it needs one (pass --tsconfig to point at it)`
511
+ );
512
+ }
513
+ const { fileNames, options: compilerOptions } = readProgramFiles(tsconfigPath);
514
+ const program = ts.createProgram(fileNames, compilerOptions);
515
+ const capabilities = [];
516
+ let filesAnalyzed = 0;
517
+ let filesOutsideRoot = 0;
518
+ const deferred = [];
519
+ const callsByName = /* @__PURE__ */ new Map();
520
+ for (const source of program.getSourceFiles()) {
521
+ if (source.isDeclarationFile) continue;
522
+ if (source.fileName.includes("/node_modules/")) continue;
523
+ if (!isInside(root, source.fileName)) {
524
+ filesOutsideRoot += 1;
525
+ continue;
526
+ }
527
+ filesAnalyzed += 1;
528
+ const emit = {
529
+ push: (capability) => capabilities.push(capability),
530
+ origin: (node) => ({
531
+ file: relative(root, source.fileName),
532
+ line: source.getLineAndCharacterOfPosition(node.getStart(source)).line + 1
533
+ })
534
+ };
535
+ let pendingName;
536
+ const visit = (node, enclosing) => {
537
+ const fn = functionLike(node);
538
+ if (fn) {
539
+ const named = ts.isFunctionDeclaration(node) && node.name ? node.name.text : pendingName;
540
+ enclosing = { fn, ...named ? { name: named } : {} };
541
+ pendingName = void 0;
542
+ } else if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name)) {
543
+ pendingName = node.name.text;
544
+ }
545
+ if (ts.isCallExpression(node)) {
546
+ visitCall(node, emit, source, deferred, enclosing);
547
+ if (ts.isIdentifier(node.expression)) {
548
+ const name = node.expression.text;
549
+ const sites = callsByName.get(name) ?? [];
550
+ sites.push({ call: node, source });
551
+ callsByName.set(name, sites);
552
+ }
553
+ }
554
+ ts.forEachChild(node, (child) => visit(child, enclosing));
555
+ };
556
+ visit(source);
557
+ }
558
+ for (const wrapper of deferred) {
559
+ const sites = (callsByName.get(wrapper.wrapperName) ?? []).filter(
560
+ (site) => callsWrapper(site, wrapper, compilerOptions)
561
+ );
562
+ const types = /* @__PURE__ */ new Map();
563
+ const dynamic = [];
564
+ for (const site of sites) {
565
+ const argument = site.call.arguments[wrapper.slot.index];
566
+ const value = wrapper.slot.property && argument && ts.isObjectLiteralExpression(argument) ? propertyOf(argument, wrapper.slot.property) : argument;
567
+ const text = value ? literalText(value) : void 0;
568
+ if (text !== void 0) types.set(text, site.call);
569
+ else dynamic.push(site);
570
+ }
571
+ for (const type of [...types.keys()].sort()) {
572
+ const componentPartial = hasSpread(wrapper.config) ? "the component config spreads another object, so some metadata here may be dynamic" : void 0;
573
+ for (const [group, kind] of [
574
+ ["observations", "observation"],
575
+ ["actions", "action"]
576
+ ]) {
577
+ capabilitiesFromGroup(
578
+ propertyOf(wrapper.config, group),
579
+ kind,
580
+ type,
581
+ componentPartial,
582
+ wrapper.emit,
583
+ wrapper.source
584
+ );
585
+ }
586
+ }
587
+ if (types.size === 0 || dynamic.length > 0) {
588
+ wrapper.emit.push({
589
+ capabilityId: UNRESOLVED_ID,
590
+ kind: "action",
591
+ origin: wrapper.emit.origin(wrapper.site),
592
+ resolution: "unresolved",
593
+ reason: "dynamic-type",
594
+ 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`
595
+ });
596
+ }
597
+ }
598
+ capabilities.sort(
599
+ (a, b) => a.capabilityId.localeCompare(b.capabilityId) || a.origin.file.localeCompare(b.origin.file) || a.origin.line - b.origin.line
600
+ );
601
+ return {
602
+ capabilities,
603
+ tsconfig: tsconfigPath,
604
+ root,
605
+ filesAnalyzed,
606
+ filesOutsideRoot,
607
+ domain: "not-analyzed"
608
+ };
609
+ }
610
+ function isInside(root, file) {
611
+ const rel = relative(root, file);
612
+ return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
613
+ }
614
+ function authoredIds(inventory) {
615
+ const ids = /* @__PURE__ */ new Set();
616
+ for (const capability of inventory.capabilities) {
617
+ if (capability.resolution === "unresolved") continue;
618
+ if (capability.capabilityId.endsWith(UNRESOLVED_ID)) continue;
619
+ ids.add(capability.capabilityId);
620
+ }
621
+ return ids;
622
+ }
623
+ function unresolved(inventory) {
624
+ return inventory.capabilities.filter((capability) => capability.resolution === "unresolved");
625
+ }
626
+
627
+ // src/analysis.ts
628
+ import { dirname as dirname3 } from "path";
629
+ import { matchesScope } from "@agent-surface/core/explain";
630
+ function readInventory(options) {
631
+ if (options.depth === "runtime") return void 0;
632
+ return extractCapabilities({
633
+ root: dirname3(options.configPath),
634
+ ...options.tsconfig ? { tsconfig: options.tsconfig } : {}
635
+ });
636
+ }
637
+ async function mountScenarios(options, onEach) {
638
+ if (options.depth === "static") return void 0;
639
+ const runner = await createSurfaceRunner(options.configPath);
640
+ try {
641
+ if (options.scenario && !runner.scenarioNames.includes(options.scenario)) {
642
+ throw new UsageError(
643
+ `unknown scenario "${options.scenario}" \u2014 this config defines ` + runner.scenarioNames.map((name) => `"${name}"`).join(", ")
644
+ );
645
+ }
646
+ const scenarios = options.scenario ? [options.scenario] : runner.scenarioNames;
647
+ const results = [];
648
+ const failures = [];
649
+ for (const scenario of scenarios) {
650
+ let result;
651
+ try {
652
+ result = await runner.collect({
653
+ scenario,
654
+ ...options.scope ? { scope: options.scope } : {}
655
+ });
656
+ } catch (error) {
657
+ failures.push({
658
+ scenario,
659
+ message: error instanceof Error ? error.message : String(error)
660
+ });
661
+ continue;
662
+ }
663
+ results.push(result);
664
+ await onEach?.(result);
665
+ }
666
+ return {
667
+ scenarios,
668
+ results,
669
+ failures,
670
+ baselineDir: baselineDirFor(
671
+ options.configPath,
672
+ options.baselineDir ?? runner.config.baselineDir
673
+ )
674
+ };
675
+ } finally {
676
+ await runner.close();
677
+ }
678
+ }
679
+ function componentTypeOf(capabilityId) {
680
+ const withoutPlane = capabilityId.replace(/^(view|domain):/, "");
681
+ const dot = withoutPlane.lastIndexOf(".");
682
+ return dot === -1 ? withoutPlane : withoutPlane.slice(0, dot);
683
+ }
684
+ function joinCoverage(inventory, runtime, options) {
685
+ if (!inventory || !runtime) return void 0;
686
+ if (runtime.failures.length > 0) return void 0;
687
+ const inScope = (capabilityId) => matchesScope(componentTypeOf(capabilityId), options.scope);
688
+ const origins = /* @__PURE__ */ new Map();
689
+ for (const capability of inventory.capabilities) {
690
+ if (!origins.has(capability.capabilityId)) {
691
+ origins.set(capability.capabilityId, capability.origin);
692
+ }
693
+ }
694
+ const authored = new Set([...authoredIds(inventory)].filter(inScope));
695
+ const reachedIds = /* @__PURE__ */ new Set();
696
+ for (const result of runtime.results) {
697
+ for (const capability of result.explanation.capabilities) {
698
+ reachedIds.add(capability.capabilityId);
699
+ }
700
+ }
701
+ const allowlistPath = allowlistPathFor(runtime.baselineDir);
702
+ const wholeAllowlist = readAllowlist(allowlistPath);
703
+ const allowlist = Object.fromEntries(
704
+ Object.entries(wholeAllowlist).filter(([id]) => inScope(id))
705
+ );
706
+ const unreadAllowlistPath = unreadAllowlistPathFor(runtime.baselineDir);
707
+ return buildCoverageReport({
708
+ unreadAllowlist: readAllowlist(unreadAllowlistPath, "file#reason"),
709
+ unreadAllowlistPath,
710
+ authored,
711
+ origins,
712
+ reachedIds,
713
+ scenarios: runtime.scenarios,
714
+ ...options.scope ? { scope: options.scope } : {},
715
+ unresolved: unresolved(inventory),
716
+ allowlist,
717
+ allowlistOutOfScope: Object.keys(wholeAllowlist).length - Object.keys(allowlist).length,
718
+ allowlistPath
719
+ });
720
+ }
721
+
722
+ export {
723
+ baselinePath,
724
+ normalize,
725
+ readBaseline,
726
+ writeBaseline,
727
+ annotate,
728
+ diff,
729
+ formatValue,
730
+ unreadKey,
731
+ coverageExitCode,
732
+ findTsconfig,
733
+ extractCapabilities,
734
+ authoredIds,
735
+ unresolved,
736
+ readInventory,
737
+ mountScenarios,
738
+ joinCoverage
739
+ };
740
+ //# sourceMappingURL=chunk-2FG527AM.js.map