@agent-surface/cli 0.11.0 → 0.12.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.
@@ -0,0 +1,984 @@
1
+ import {
2
+ UsageError,
3
+ createSurfaceRunner
4
+ } from "./chunk-QIVOZAWX.js";
5
+
6
+ // src/baseline.ts
7
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
8
+ import { dirname, join, resolve, sep } from "path";
9
+ import { serializeSurfaceSnapshot } from "@agent-surface/testing";
10
+ var DEFAULT_BASELINE_DIR = ".agent-surface";
11
+ var SCENARIO_MANIFEST_FILE = "scenarios.json";
12
+ function baselineDirFor(configPath, configured) {
13
+ return resolve(dirname(configPath), configured ?? DEFAULT_BASELINE_DIR);
14
+ }
15
+ function baselinePath(dir, scenario) {
16
+ const reserved = /* @__PURE__ */ new Set(["scenarios", "coverage-allow", "unresolved-allow"]);
17
+ if (scenario.length === 0 || scenario === "." || scenario === ".." || scenario.includes("/") || scenario.includes("\\") || scenario.includes("\0") || reserved.has(scenario)) {
18
+ throw new Error(`invalid scenario name ${JSON.stringify(scenario)} \u2014 use a filename-safe name`);
19
+ }
20
+ const root = resolve(dir);
21
+ const path = resolve(root, `${scenario}.json`);
22
+ if (!path.startsWith(`${root}${sep}`)) {
23
+ throw new Error(`scenario ${JSON.stringify(scenario)} escapes the baseline directory`);
24
+ }
25
+ return path;
26
+ }
27
+ function scenarioManifestPath(dir) {
28
+ return join(dir, SCENARIO_MANIFEST_FILE);
29
+ }
30
+ function normalize(snapshot) {
31
+ return serializeSurfaceSnapshot(snapshot);
32
+ }
33
+ function readBaseline(path) {
34
+ if (!existsSync(path)) return void 0;
35
+ try {
36
+ return JSON.parse(readFileSync(path, "utf8"));
37
+ } catch (error) {
38
+ throw new Error(
39
+ `could not read baseline ${path}: ${error instanceof Error ? error.message : String(error)}`
40
+ );
41
+ }
42
+ }
43
+ function writeBaseline(path, value) {
44
+ mkdirSync(dirname(path), { recursive: true });
45
+ writeFileSync(path, `${JSON.stringify(value, null, 2)}
46
+ `, "utf8");
47
+ }
48
+ function readScenarioManifest(dir) {
49
+ const path = scenarioManifestPath(dir);
50
+ const value = readBaseline(path);
51
+ if (value === void 0) return void 0;
52
+ if (typeof value !== "object" || value === null || !Array.isArray(value.scenarios) || !value.scenarios.every((name) => typeof name === "string")) {
53
+ throw new Error(`${path} must contain { "scenarios": string[] }`);
54
+ }
55
+ return [...value.scenarios].sort();
56
+ }
57
+ function writeScenarioManifest(dir, scenarios) {
58
+ writeBaseline(scenarioManifestPath(dir), { scenarios: [...scenarios].sort() });
59
+ }
60
+ var PATH_SEGMENT = /([^.[\]]+)|\[(\d+)\]/g;
61
+ function subjectFor(document, path) {
62
+ let node = document;
63
+ let subject;
64
+ for (const match of path.matchAll(PATH_SEGMENT)) {
65
+ if (typeof node !== "object" || node === null) return subject;
66
+ const record = node;
67
+ const candidate = record["capabilityId"] ?? record["procedureId"];
68
+ if (typeof candidate === "string") subject = candidate;
69
+ const key = match[1] ?? match[2];
70
+ if (key === void 0) return subject;
71
+ node = record[key];
72
+ }
73
+ if (typeof node === "object" && node !== null) {
74
+ const record = node;
75
+ const candidate = record["capabilityId"] ?? record["procedureId"];
76
+ if (typeof candidate === "string") subject = candidate;
77
+ }
78
+ return subject;
79
+ }
80
+ function annotate(entries, after, before) {
81
+ return entries.map((entry) => {
82
+ const subject = subjectFor(after, entry.path) ?? subjectFor(before, entry.path);
83
+ return subject ? { ...entry, subject } : entry;
84
+ });
85
+ }
86
+ function diff(before, after, path = "") {
87
+ if (Object.is(before, after)) return [];
88
+ const bothArrays = Array.isArray(before) && Array.isArray(after);
89
+ const bothObjects = !bothArrays && typeof before === "object" && typeof after === "object" && before !== null && after !== null;
90
+ if (bothArrays) {
91
+ const entries = [];
92
+ const max = Math.max(before.length, after.length);
93
+ for (let i = 0; i < max; i++) {
94
+ const at = `${path}[${i}]`;
95
+ if (i >= before.length) entries.push({ path: at, kind: "added", after: after[i] });
96
+ else if (i >= after.length) entries.push({ path: at, kind: "removed", before: before[i] });
97
+ else entries.push(...diff(before[i], after[i], at));
98
+ }
99
+ return entries;
100
+ }
101
+ if (bothObjects) {
102
+ const entries = [];
103
+ const beforeRecord = before;
104
+ const afterRecord = after;
105
+ const keys = /* @__PURE__ */ new Set([...Object.keys(beforeRecord), ...Object.keys(afterRecord)]);
106
+ for (const key of [...keys].sort()) {
107
+ const at = path ? `${path}.${key}` : key;
108
+ if (!(key in beforeRecord)) {
109
+ entries.push({ path: at, kind: "added", after: afterRecord[key] });
110
+ } else if (!(key in afterRecord)) {
111
+ entries.push({ path: at, kind: "removed", before: beforeRecord[key] });
112
+ } else {
113
+ entries.push(...diff(beforeRecord[key], afterRecord[key], at));
114
+ }
115
+ }
116
+ return entries;
117
+ }
118
+ if (JSON.stringify(before) === JSON.stringify(after)) return [];
119
+ return [{ path: path || "<root>", kind: "changed", before, after }];
120
+ }
121
+ function formatValue(value) {
122
+ if (value === void 0) return "\u2014";
123
+ const text = typeof value === "string" ? value : JSON.stringify(value);
124
+ return text.length > 120 ? `${text.slice(0, 117)}\u2026` : text;
125
+ }
126
+
127
+ // src/coverage.ts
128
+ import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
129
+ import { join as join2 } from "path";
130
+ var ALLOWLIST_FILE = "coverage-allow.json";
131
+ var UNREAD_ALLOWLIST_FILE = "unresolved-allow.json";
132
+ function allowlistPathFor(baselineDir) {
133
+ return join2(baselineDir, ALLOWLIST_FILE);
134
+ }
135
+ function unreadAllowlistPathFor(baselineDir) {
136
+ return join2(baselineDir, UNREAD_ALLOWLIST_FILE);
137
+ }
138
+ function readAllowlist(path, keyName = "capabilityId") {
139
+ if (!existsSync2(path)) return {};
140
+ let parsed;
141
+ try {
142
+ parsed = JSON.parse(readFileSync2(path, "utf8"));
143
+ } catch (error) {
144
+ throw new Error(
145
+ `could not parse ${path}: ${error instanceof Error ? error.message : String(error)}`
146
+ );
147
+ }
148
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
149
+ throw new Error(`${path} must be a JSON object of { "${keyName}": "reason" }`);
150
+ }
151
+ const allowlist = {};
152
+ for (const [id, reason] of Object.entries(parsed)) {
153
+ if (typeof reason !== "string" || reason.trim() === "") {
154
+ throw new Error(`${path}: "${id}" needs a non-empty reason string`);
155
+ }
156
+ allowlist[id] = reason;
157
+ }
158
+ return allowlist;
159
+ }
160
+ function unreadKey(entry) {
161
+ return `${entry.origin.file}#${entry.reason ?? "unknown"}#${entry.origin.site}`;
162
+ }
163
+ function buildCoverageReport(input) {
164
+ const unreached = [];
165
+ const allowed = [];
166
+ for (const id of [...input.authored].sort()) {
167
+ if (input.reachedIds.has(id)) continue;
168
+ if (id in input.allowlist) {
169
+ allowed.push(id);
170
+ continue;
171
+ }
172
+ unreached.push({ capabilityId: id, origin: input.origins.get(id) ?? { file: "?", line: 0 } });
173
+ }
174
+ const staleAllowlist = Object.keys(input.allowlist).filter((id) => input.reachedIds.has(id) || !input.authored.has(id)).sort();
175
+ const unreadAllowlist = input.unreadAllowlist ?? {};
176
+ const unread = [];
177
+ const allowedUnread = /* @__PURE__ */ new Set();
178
+ for (const entry of input.unresolved) {
179
+ const key = unreadKey(entry);
180
+ if (key in unreadAllowlist) allowedUnread.add(key);
181
+ else unread.push(entry);
182
+ }
183
+ const stillUnread = new Set(input.unresolved.map(unreadKey));
184
+ const staleUnreadAllowlist = Object.keys(unreadAllowlist).filter((key) => !stillUnread.has(key)).sort();
185
+ const unaccounted = [...input.reachedIds].filter((id) => !input.authored.has(id)).sort();
186
+ const domainReached = [...input.reachedIds].filter((id) => id.startsWith("domain:")).sort();
187
+ const unmanifestedDomain = input.domainAuthoritative ? unaccounted.filter((id) => id.startsWith("domain:")) : [];
188
+ const undeclared = unaccounted.filter((id) => !id.startsWith("domain:"));
189
+ return {
190
+ authored: input.authored.size,
191
+ reached: [...input.authored].filter((id) => input.reachedIds.has(id)).length,
192
+ scenarios: input.scenarios,
193
+ ...input.scope ? { scope: input.scope } : {},
194
+ allowlistOutOfScope: input.allowlistOutOfScope ?? 0,
195
+ unreached,
196
+ undeclared,
197
+ domainReached,
198
+ unmanifestedDomain,
199
+ domainAuthoritative: input.domainAuthoritative === true,
200
+ unresolved: unread,
201
+ allowed,
202
+ staleAllowlist,
203
+ allowlistPath: input.allowlistPath,
204
+ allowedUnread: [...allowedUnread].sort(),
205
+ staleUnreadAllowlist,
206
+ unreadAllowlistPath: input.unreadAllowlistPath
207
+ };
208
+ }
209
+ function coverageExitCode(report, options = {}) {
210
+ if (report.unreached.length > 0) return 1;
211
+ if (report.unmanifestedDomain.length > 0) return 1;
212
+ if (report.unresolved.length > 0 && !options.allowUnresolved) return 1;
213
+ if (report.staleAllowlist.length > 0) return 1;
214
+ if (report.staleUnreadAllowlist.length > 0) return 1;
215
+ return 0;
216
+ }
217
+
218
+ // src/extract.ts
219
+ import { createHash } from "crypto";
220
+ import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
221
+ import { dirname as dirname2, isAbsolute, join as join3, relative, resolve as resolve2 } from "path";
222
+ import ts from "typescript";
223
+ var UNRESOLVED_ID = "<unresolved>";
224
+ function findTsconfig(from) {
225
+ return ts.findConfigFile(resolve2(from), ts.sys.fileExists, "tsconfig.json");
226
+ }
227
+ function readLiteralConfigScope(configPath) {
228
+ const source = ts.createSourceFile(
229
+ configPath,
230
+ readFileSync3(configPath, "utf8"),
231
+ ts.ScriptTarget.Latest,
232
+ true,
233
+ configPath.endsWith("x") ? ts.ScriptKind.TSX : ts.ScriptKind.TS
234
+ );
235
+ let scope;
236
+ const visit = (node) => {
237
+ if (scope) return;
238
+ if (ts.isPropertyAssignment(node) && propertyName(node.name) === "scope" && ts.isArrayLiteralExpression(node.initializer)) {
239
+ const values = node.initializer.elements.map((entry) => literalText(entry));
240
+ if (values.every((value) => value !== void 0)) scope = values;
241
+ }
242
+ ts.forEachChild(node, visit);
243
+ };
244
+ visit(source);
245
+ return scope;
246
+ }
247
+ function readProgramFiles(tsconfigPath) {
248
+ const read = ts.readConfigFile(tsconfigPath, ts.sys.readFile);
249
+ if (read.error) {
250
+ throw new Error(
251
+ `could not read ${tsconfigPath}: ${ts.flattenDiagnosticMessageText(read.error.messageText, " ")}`
252
+ );
253
+ }
254
+ const parsed = ts.parseJsonConfigFileContent(
255
+ read.config,
256
+ ts.sys,
257
+ dirname2(tsconfigPath)
258
+ );
259
+ if (parsed.errors.length > 0 && parsed.fileNames.length === 0) {
260
+ throw new Error(
261
+ `could not resolve any files from ${tsconfigPath}: ${parsed.errors.map((error) => ts.flattenDiagnosticMessageText(error.messageText, " ")).join("; ")}`
262
+ );
263
+ }
264
+ return { fileNames: parsed.fileNames, options: parsed.options };
265
+ }
266
+ var REGISTRATION_HOOKS = /* @__PURE__ */ new Set(["useAgentComponent", "useAgentAction", "useAgentObservation"]);
267
+ function isRegistrationModule(specifier) {
268
+ return specifier.startsWith("@agent-surface/");
269
+ }
270
+ var NO_IMPORTS = { locals: /* @__PURE__ */ new Map(), namespaces: /* @__PURE__ */ new Set() };
271
+ function importedRegistrations(source) {
272
+ const locals = /* @__PURE__ */ new Map();
273
+ const namespaces = /* @__PURE__ */ new Set();
274
+ for (const statement of source.statements) {
275
+ if (!ts.isImportDeclaration(statement)) continue;
276
+ if (!ts.isStringLiteral(statement.moduleSpecifier)) continue;
277
+ if (!isRegistrationModule(statement.moduleSpecifier.text)) continue;
278
+ const clause = statement.importClause;
279
+ if (!clause || clause.isTypeOnly || !clause.namedBindings) continue;
280
+ if (ts.isNamespaceImport(clause.namedBindings)) {
281
+ namespaces.add(clause.namedBindings.name.text);
282
+ continue;
283
+ }
284
+ for (const element of clause.namedBindings.elements) {
285
+ if (element.isTypeOnly) continue;
286
+ const imported = (element.propertyName ?? element.name).text;
287
+ if (REGISTRATION_HOOKS.has(imported)) locals.set(element.name.text, imported);
288
+ }
289
+ }
290
+ return { locals, namespaces };
291
+ }
292
+ function renamedRegistrationExports(source, imports) {
293
+ const renamed = [];
294
+ for (const statement of source.statements) {
295
+ if (!ts.isExportDeclaration(statement) || statement.isTypeOnly) continue;
296
+ const clause = statement.exportClause;
297
+ if (!clause || !ts.isNamedExports(clause)) continue;
298
+ const from = statement.moduleSpecifier;
299
+ const fromOurs = from !== void 0 && ts.isStringLiteral(from) && isRegistrationModule(from.text);
300
+ if (from && !fromOurs) continue;
301
+ for (const element of clause.elements) {
302
+ if (element.isTypeOnly) continue;
303
+ const local = (element.propertyName ?? element.name).text;
304
+ const hook = fromOurs ? REGISTRATION_HOOKS.has(local) ? local : void 0 : imports.locals.get(local);
305
+ if (hook === void 0 || element.name.text === hook) continue;
306
+ renamed.push({ node: element, hook, exported: element.name.text });
307
+ }
308
+ }
309
+ return renamed;
310
+ }
311
+ function namespaceMember(object, member, imports) {
312
+ return ts.isIdentifier(object) && imports.namespaces.has(object.text) && REGISTRATION_HOOKS.has(member);
313
+ }
314
+ function calleeName(call, imports = NO_IMPORTS) {
315
+ const callee = call.expression;
316
+ if (ts.isIdentifier(callee)) return imports.locals.get(callee.text) ?? callee.text;
317
+ if (ts.isPropertyAccessExpression(callee)) {
318
+ if (namespaceMember(callee.expression, callee.name.text, imports)) return callee.name.text;
319
+ return callee.name.text;
320
+ }
321
+ if (ts.isElementAccessExpression(callee)) {
322
+ const member = literalText(callee.argumentExpression);
323
+ if (member !== void 0 && namespaceMember(callee.expression, member, imports)) return member;
324
+ }
325
+ return void 0;
326
+ }
327
+ function propertyName(name) {
328
+ if (ts.isIdentifier(name) || ts.isStringLiteral(name)) return name.text;
329
+ return void 0;
330
+ }
331
+ function propertyOf(object, wanted) {
332
+ for (const property of object.properties) {
333
+ if (ts.isPropertyAssignment(property) && propertyName(property.name) === wanted) {
334
+ return property.initializer;
335
+ }
336
+ if (ts.isShorthandPropertyAssignment(property) && property.name.text === wanted) {
337
+ return property.name;
338
+ }
339
+ }
340
+ return void 0;
341
+ }
342
+ function hasSpread(object) {
343
+ return object.properties.some((property) => ts.isSpreadAssignment(property));
344
+ }
345
+ var CAPABILITY_GROUPS = ["observations", "actions"];
346
+ function spreadKeys(expression, source, depth = 0) {
347
+ if (depth > 1) return void 0;
348
+ if (ts.isParenthesizedExpression(expression)) {
349
+ return spreadKeys(expression.expression, source, depth);
350
+ }
351
+ if (ts.isConditionalExpression(expression)) {
352
+ const whenTrue = spreadKeys(expression.whenTrue, source, depth);
353
+ const whenFalse = spreadKeys(expression.whenFalse, source, depth);
354
+ if (!whenTrue || !whenFalse) return void 0;
355
+ return [...whenTrue, ...whenFalse];
356
+ }
357
+ const resolved = objectLiteralFor(expression, source);
358
+ if (!resolved.object) return void 0;
359
+ const keys = [];
360
+ for (const property of resolved.object.properties) {
361
+ if (ts.isSpreadAssignment(property)) {
362
+ const nested = spreadKeys(property.expression, source, depth + 1);
363
+ if (!nested) return void 0;
364
+ keys.push(...nested);
365
+ continue;
366
+ }
367
+ const name = ts.isPropertyAssignment(property) || ts.isMethodDeclaration(property) ? propertyName(property.name) : ts.isShorthandPropertyAssignment(property) ? property.name.text : void 0;
368
+ if (name === void 0) return void 0;
369
+ keys.push(name);
370
+ }
371
+ return keys;
372
+ }
373
+ function literalText(node) {
374
+ if (!node) return void 0;
375
+ if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node)) return node.text;
376
+ if (ts.isParenthesizedExpression(node)) return literalText(node.expression);
377
+ if (ts.isBinaryExpression(node) && node.operatorToken.kind === ts.SyntaxKind.PlusToken) {
378
+ const left = literalText(node.left);
379
+ const right = literalText(node.right);
380
+ if (left !== void 0 && right !== void 0) return left + right;
381
+ }
382
+ return void 0;
383
+ }
384
+ function describeConstruct(node) {
385
+ if (ts.isCallExpression(node)) {
386
+ const callee = calleeName(node);
387
+ return callee ? `built by ${callee}()` : "built by a call expression";
388
+ }
389
+ if (ts.isIdentifier(node)) return `a variable (${node.text}) this extractor could not follow`;
390
+ if (ts.isConditionalExpression(node)) return "a conditional expression";
391
+ if (ts.isTemplateExpression(node)) return "a template with substitutions";
392
+ if (ts.isPropertyAccessExpression(node)) return "a property access";
393
+ return "a non-literal expression";
394
+ }
395
+ function objectLiteralFor(expression, source) {
396
+ if (ts.isObjectLiteralExpression(expression)) return { object: expression };
397
+ if (ts.isIdentifier(expression)) {
398
+ const target = expression.text;
399
+ let found;
400
+ const visit = (node) => {
401
+ if (found) return;
402
+ if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name) && node.name.text === target && node.initializer && ts.isObjectLiteralExpression(node.initializer)) {
403
+ found = node.initializer;
404
+ return;
405
+ }
406
+ ts.forEachChild(node, visit);
407
+ };
408
+ visit(source);
409
+ if (found) return { object: found };
410
+ return {
411
+ note: `the config is \`${target}\`, which is not a same-module object literal \u2014 the extractor follows one hop only`
412
+ };
413
+ }
414
+ return { note: `the config is ${describeConstruct(expression)}` };
415
+ }
416
+ var GRANULAR_HOOKS = /* @__PURE__ */ new Set(["useAgentAction", "useAgentObservation"]);
417
+ function capabilitiesFromGroup(group, kind, componentType, componentPartial, emit, source) {
418
+ if (!group) return;
419
+ const resolved = objectLiteralFor(group, source);
420
+ if (!resolved.object) {
421
+ emit.push({
422
+ capabilityId: `view:${componentType}.${UNRESOLVED_ID}`,
423
+ kind,
424
+ origin: emit.origin(group),
425
+ resolution: "unresolved",
426
+ reason: "dynamic-group",
427
+ note: `\`${kind}s\` on "${componentType}" is not an object literal: ${resolved.note}`
428
+ });
429
+ return;
430
+ }
431
+ for (const property of resolved.object.properties) {
432
+ if (ts.isSpreadAssignment(property)) {
433
+ emit.push({
434
+ capabilityId: `view:${componentType}.${UNRESOLVED_ID}`,
435
+ kind,
436
+ origin: emit.origin(property),
437
+ resolution: "unresolved",
438
+ reason: "spread-members",
439
+ note: `\`${kind}s\` on "${componentType}" spreads another object, which may contribute capabilities this inventory cannot name`
440
+ });
441
+ continue;
442
+ }
443
+ const name = ts.isPropertyAssignment(property) || ts.isMethodDeclaration(property) ? propertyName(property.name) : ts.isShorthandPropertyAssignment(property) ? property.name.text : void 0;
444
+ if (name === void 0) {
445
+ emit.push({
446
+ capabilityId: `view:${componentType}.${UNRESOLVED_ID}`,
447
+ kind,
448
+ origin: emit.origin(property),
449
+ resolution: "unresolved",
450
+ reason: "computed-name",
451
+ note: `a capability on "${componentType}" has a computed name`
452
+ });
453
+ continue;
454
+ }
455
+ const capability = {
456
+ capabilityId: `view:${componentType}.${name}`,
457
+ kind,
458
+ origin: emit.origin(property),
459
+ resolution: "static"
460
+ };
461
+ const notes = [];
462
+ if (componentPartial) notes.push(componentPartial);
463
+ const value = ts.isPropertyAssignment(property) ? property.initializer : void 0;
464
+ const definition = value && ts.isCallExpression(value) && value.arguments.length > 0 ? value.arguments[0] : value;
465
+ if (definition && ts.isObjectLiteralExpression(definition)) {
466
+ const description = literalText(propertyOf(definition, "description"));
467
+ if (description !== void 0) capability.description = description;
468
+ else notes.push("description is not a string literal");
469
+ if (kind === "action") {
470
+ const effect = literalText(propertyOf(definition, "effect"));
471
+ if (effect !== void 0) capability.effect = effect;
472
+ else notes.push("effect is not a string literal");
473
+ }
474
+ if (hasSpread(definition)) notes.push("the definition spreads another object");
475
+ } else {
476
+ notes.push(
477
+ value ? `the definition is ${describeConstruct(value)}` : "the definition is not an object literal"
478
+ );
479
+ }
480
+ if (notes.length > 0) {
481
+ capability.resolution = "partial";
482
+ capability.note = notes.join("; ");
483
+ }
484
+ emit.push(capability);
485
+ }
486
+ }
487
+ function visitCall(call, emit, source, imports, deferred, enclosing) {
488
+ const callee = calleeName(call, imports);
489
+ if (callee === void 0) {
490
+ const object = ts.isElementAccessExpression(call.expression) ? call.expression.expression : void 0;
491
+ if (object && ts.isIdentifier(object) && imports.namespaces.has(object.text)) {
492
+ emit.push({
493
+ capabilityId: UNRESOLVED_ID,
494
+ kind: "action",
495
+ origin: emit.origin(call),
496
+ resolution: "unresolved",
497
+ reason: "dynamic-callee",
498
+ 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`
499
+ });
500
+ }
501
+ return;
502
+ }
503
+ if (GRANULAR_HOOKS.has(callee)) {
504
+ emit.push({
505
+ capabilityId: UNRESOLVED_ID,
506
+ kind: callee === "useAgentAction" ? "action" : "observation",
507
+ origin: emit.origin(call),
508
+ resolution: "unresolved",
509
+ reason: "granular-hook",
510
+ note: `${callee}() registers against a render-scope link, so its component type is not at this call site`
511
+ });
512
+ return;
513
+ }
514
+ if (callee !== "useAgentComponent" && callee !== "register") return;
515
+ const argument = call.arguments[0];
516
+ if (!argument) return;
517
+ const resolved = objectLiteralFor(argument, source);
518
+ if (!resolved.object) {
519
+ emit.push({
520
+ capabilityId: UNRESOLVED_ID,
521
+ kind: "action",
522
+ origin: emit.origin(call),
523
+ resolution: "unresolved",
524
+ reason: "dynamic-config",
525
+ note: `${callee}() call site could not be read: ${resolved.note}`
526
+ });
527
+ return;
528
+ }
529
+ const config = resolved.object;
530
+ const typeNode = propertyOf(config, "type");
531
+ const type = literalText(typeNode);
532
+ if (type === void 0) {
533
+ if (callee === "register" && typeNode === void 0) return;
534
+ const slot = enclosing && typeNode && ts.isIdentifier(typeNode) ? parameterSlot(typeNode.text, enclosing.fn) : void 0;
535
+ if (slot && enclosing?.name) {
536
+ deferred.push({ config, source, emit, wrapperName: enclosing.name, slot, site: call });
537
+ return;
538
+ }
539
+ emit.push({
540
+ capabilityId: UNRESOLVED_ID,
541
+ kind: "action",
542
+ origin: emit.origin(call),
543
+ resolution: "unresolved",
544
+ reason: "dynamic-type",
545
+ note: `\`type\` is not a string literal, so no capability id on this component can be determined`
546
+ });
547
+ return;
548
+ }
549
+ const componentPartial = hasSpread(config) ? "the component config spreads another object, so some metadata here may be dynamic" : void 0;
550
+ for (const property of config.properties) {
551
+ if (!ts.isSpreadAssignment(property)) continue;
552
+ const keys = spreadKeys(property.expression, source);
553
+ if (keys && !keys.some((key) => CAPABILITY_GROUPS.includes(key))) {
554
+ continue;
555
+ }
556
+ emit.push({
557
+ capabilityId: `view:${type}.${UNRESOLVED_ID}`,
558
+ kind: "action",
559
+ origin: emit.origin(property),
560
+ resolution: "unresolved",
561
+ reason: "spread-members",
562
+ 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`
563
+ });
564
+ }
565
+ capabilitiesFromGroup(
566
+ propertyOf(config, "observations"),
567
+ "observation",
568
+ type,
569
+ componentPartial,
570
+ emit,
571
+ source
572
+ );
573
+ capabilitiesFromGroup(
574
+ propertyOf(config, "actions"),
575
+ "action",
576
+ type,
577
+ componentPartial,
578
+ emit,
579
+ source
580
+ );
581
+ }
582
+ function functionLike(node) {
583
+ if (ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isArrowFunction(node) || ts.isMethodDeclaration(node)) {
584
+ return node;
585
+ }
586
+ return void 0;
587
+ }
588
+ function parameterSlot(name, fn) {
589
+ for (const [index, parameter] of fn.parameters.entries()) {
590
+ if (ts.isIdentifier(parameter.name)) {
591
+ if (parameter.name.text === name) return { index };
592
+ continue;
593
+ }
594
+ if (ts.isObjectBindingPattern(parameter.name)) {
595
+ for (const element of parameter.name.elements) {
596
+ if (!ts.isIdentifier(element.name) || element.name.text !== name) continue;
597
+ const property = element.propertyName && ts.isIdentifier(element.propertyName) ? element.propertyName.text : name;
598
+ return { index, property };
599
+ }
600
+ }
601
+ }
602
+ return void 0;
603
+ }
604
+ function callsWrapper(site, wrapper, compilerOptions) {
605
+ const callee = site.call.expression;
606
+ if (!ts.isIdentifier(callee) || callee.text !== wrapper.wrapperName) return false;
607
+ if (site.source.fileName === wrapper.source.fileName) {
608
+ return true;
609
+ }
610
+ for (const statement of site.source.statements) {
611
+ if (!ts.isImportDeclaration(statement)) continue;
612
+ const clause = statement.importClause;
613
+ if (!clause) continue;
614
+ const named = clause.name?.text === wrapper.wrapperName || clause.namedBindings && ts.isNamedImports(clause.namedBindings) && clause.namedBindings.elements.some((element) => element.name.text === wrapper.wrapperName);
615
+ if (!named) continue;
616
+ if (!ts.isStringLiteral(statement.moduleSpecifier)) continue;
617
+ const resolved = ts.resolveModuleName(
618
+ statement.moduleSpecifier.text,
619
+ site.source.fileName,
620
+ compilerOptions,
621
+ ts.sys
622
+ ).resolvedModule;
623
+ if (resolved?.resolvedFileName === wrapper.source.fileName) return true;
624
+ }
625
+ return false;
626
+ }
627
+ function normalizedText(node, source) {
628
+ return node.getText(source).replace(/\s+/g, " ").trim();
629
+ }
630
+ function siteIdentity(source, node) {
631
+ const labels = [];
632
+ let enclosingCall = "";
633
+ let scope;
634
+ for (let parent = node.parent; parent && parent !== source; parent = parent.parent) {
635
+ if (!enclosingCall && ts.isCallExpression(parent)) {
636
+ enclosingCall = normalizedText(parent, source);
637
+ }
638
+ 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;
639
+ if (named !== void 0) {
640
+ labels.push(named);
641
+ scope ??= parent;
642
+ }
643
+ }
644
+ return { labels: labels.reverse(), enclosingCall, scope: scope ?? source };
645
+ }
646
+ function occurrence(scope, node, source) {
647
+ const text = normalizedText(node, source);
648
+ const start = node.getStart(source);
649
+ let rank = 0;
650
+ const visit = (candidate) => {
651
+ if (candidate.kind === node.kind && candidate.getStart(source) < start && normalizedText(candidate, source) === text) {
652
+ rank += 1;
653
+ }
654
+ ts.forEachChild(candidate, visit);
655
+ };
656
+ ts.forEachChild(scope, visit);
657
+ return rank;
658
+ }
659
+ function stableSite(source, node) {
660
+ const { labels, enclosingCall, scope } = siteIdentity(source, node);
661
+ return createHash("sha256").update(
662
+ `${labels.join("/")}\0${enclosingCall}\0${normalizedText(node, source)}\0${occurrence(
663
+ scope,
664
+ node,
665
+ source
666
+ )}`
667
+ ).digest("hex").slice(0, 12);
668
+ }
669
+ var packageNameCache = /* @__PURE__ */ new Map();
670
+ function packageNameFor(file) {
671
+ let dir = dirname2(file);
672
+ for (; ; ) {
673
+ if (packageNameCache.has(dir)) return packageNameCache.get(dir);
674
+ const packagePath = join3(dir, "package.json");
675
+ if (existsSync3(packagePath)) {
676
+ let name;
677
+ try {
678
+ const parsed = JSON.parse(readFileSync3(packagePath, "utf8"));
679
+ if (typeof parsed.name === "string") name = parsed.name;
680
+ } catch {
681
+ }
682
+ packageNameCache.set(dir, name);
683
+ return name;
684
+ }
685
+ const parent = dirname2(dir);
686
+ if (parent === dir) return void 0;
687
+ dir = parent;
688
+ }
689
+ }
690
+ var IMPLEMENTATION_PACKAGES = /* @__PURE__ */ new Set([
691
+ "@agent-surface/core",
692
+ "@agent-surface/react",
693
+ "@agent-surface/orpc",
694
+ "@agent-surface/testing",
695
+ "@agent-surface/webmcp",
696
+ "@agent-surface/cli"
697
+ ]);
698
+ function isAgentSurfaceImplementation(file) {
699
+ return IMPLEMENTATION_PACKAGES.has(packageNameFor(file) ?? "");
700
+ }
701
+ function extractCapabilities(options) {
702
+ const root = resolve2(options.root);
703
+ const tsconfigPath = options.tsconfig ? isAbsolute(options.tsconfig) ? options.tsconfig : join3(root, options.tsconfig) : findTsconfig(root);
704
+ if (!tsconfigPath || !existsSync3(tsconfigPath)) {
705
+ throw new Error(
706
+ `no tsconfig.json found from ${root} \u2014 \`capabilities\` reads the TypeScript program, so it needs one (pass --tsconfig to point at it)`
707
+ );
708
+ }
709
+ const { fileNames, options: compilerOptions } = readProgramFiles(tsconfigPath);
710
+ const program = ts.createProgram(fileNames, compilerOptions);
711
+ const capabilities = [];
712
+ let filesAnalyzed = 0;
713
+ let filesOutsideRoot = 0;
714
+ const deferred = [];
715
+ const callsByName = /* @__PURE__ */ new Map();
716
+ for (const source of program.getSourceFiles()) {
717
+ if (source.isDeclarationFile) continue;
718
+ if (source.fileName.includes("/node_modules/")) continue;
719
+ if (!isInside(root, source.fileName) && isAgentSurfaceImplementation(source.fileName)) {
720
+ filesOutsideRoot += 1;
721
+ continue;
722
+ }
723
+ filesAnalyzed += 1;
724
+ const emit = {
725
+ push: (capability) => capabilities.push(capability),
726
+ origin: (node) => ({
727
+ file: relative(root, source.fileName),
728
+ line: source.getLineAndCharacterOfPosition(node.getStart(source)).line + 1,
729
+ site: stableSite(source, node)
730
+ })
731
+ };
732
+ const imports = importedRegistrations(source);
733
+ for (const renamed of renamedRegistrationExports(source, imports)) {
734
+ emit.push({
735
+ capabilityId: UNRESOLVED_ID,
736
+ kind: "action",
737
+ origin: emit.origin(renamed.node),
738
+ resolution: "unresolved",
739
+ reason: "dynamic-callee",
740
+ 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`
741
+ });
742
+ }
743
+ let pendingName;
744
+ const visit = (node, enclosing) => {
745
+ const fn = functionLike(node);
746
+ if (fn) {
747
+ const named = ts.isFunctionDeclaration(node) && node.name ? node.name.text : pendingName;
748
+ enclosing = { fn, ...named ? { name: named } : {} };
749
+ pendingName = void 0;
750
+ } else if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name)) {
751
+ pendingName = node.name.text;
752
+ }
753
+ if (ts.isCallExpression(node)) {
754
+ visitCall(node, emit, source, imports, deferred, enclosing);
755
+ if (ts.isIdentifier(node.expression)) {
756
+ const name = node.expression.text;
757
+ const sites = callsByName.get(name) ?? [];
758
+ sites.push({ call: node, source });
759
+ callsByName.set(name, sites);
760
+ }
761
+ }
762
+ ts.forEachChild(node, (child) => visit(child, enclosing));
763
+ };
764
+ visit(source);
765
+ }
766
+ for (const wrapper of deferred) {
767
+ const sites = (callsByName.get(wrapper.wrapperName) ?? []).filter(
768
+ (site) => callsWrapper(site, wrapper, compilerOptions)
769
+ );
770
+ const types = /* @__PURE__ */ new Map();
771
+ const dynamic = [];
772
+ for (const site of sites) {
773
+ const argument = site.call.arguments[wrapper.slot.index];
774
+ const value = wrapper.slot.property && argument && ts.isObjectLiteralExpression(argument) ? propertyOf(argument, wrapper.slot.property) : argument;
775
+ const text = value ? literalText(value) : void 0;
776
+ if (text !== void 0) types.set(text, site.call);
777
+ else dynamic.push(site);
778
+ }
779
+ for (const type of [...types.keys()].sort()) {
780
+ const componentPartial = hasSpread(wrapper.config) ? "the component config spreads another object, so some metadata here may be dynamic" : void 0;
781
+ for (const [group, kind] of [
782
+ ["observations", "observation"],
783
+ ["actions", "action"]
784
+ ]) {
785
+ capabilitiesFromGroup(
786
+ propertyOf(wrapper.config, group),
787
+ kind,
788
+ type,
789
+ componentPartial,
790
+ wrapper.emit,
791
+ wrapper.source
792
+ );
793
+ }
794
+ }
795
+ if (types.size === 0 || dynamic.length > 0) {
796
+ wrapper.emit.push({
797
+ capabilityId: UNRESOLVED_ID,
798
+ kind: "action",
799
+ origin: wrapper.emit.origin(wrapper.site),
800
+ resolution: "unresolved",
801
+ reason: "dynamic-type",
802
+ 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`
803
+ });
804
+ }
805
+ }
806
+ capabilities.sort(
807
+ (a, b) => a.capabilityId.localeCompare(b.capabilityId) || a.origin.file.localeCompare(b.origin.file) || a.origin.line - b.origin.line
808
+ );
809
+ return {
810
+ capabilities,
811
+ tsconfig: tsconfigPath,
812
+ root,
813
+ filesAnalyzed,
814
+ filesOutsideRoot,
815
+ domain: "not-analyzed"
816
+ };
817
+ }
818
+ function isInside(root, file) {
819
+ const rel = relative(root, file);
820
+ return rel !== "" && !rel.startsWith("..") && !isAbsolute(rel);
821
+ }
822
+ function authoredIds(inventory) {
823
+ const ids = /* @__PURE__ */ new Set();
824
+ for (const capability of inventory.capabilities) {
825
+ if (capability.resolution === "unresolved") continue;
826
+ if (capability.capabilityId.endsWith(UNRESOLVED_ID)) continue;
827
+ ids.add(capability.capabilityId);
828
+ }
829
+ return ids;
830
+ }
831
+ function unresolved(inventory) {
832
+ return inventory.capabilities.filter((capability) => capability.resolution === "unresolved");
833
+ }
834
+
835
+ // src/analysis.ts
836
+ import { dirname as dirname3 } from "path";
837
+ import { matchesScope } from "@agent-surface/core/explain";
838
+ function readInventory(options) {
839
+ if (options.depth === "runtime") return void 0;
840
+ return extractCapabilities({
841
+ root: dirname3(options.configPath),
842
+ ...options.tsconfig ? { tsconfig: options.tsconfig } : {}
843
+ });
844
+ }
845
+ function staticConfigScope(options) {
846
+ return options.scope ?? readLiteralConfigScope(options.configPath);
847
+ }
848
+ async function mountScenarios(options, onEach) {
849
+ if (options.depth === "static") return void 0;
850
+ const runner = await createSurfaceRunner(options.configPath);
851
+ try {
852
+ if (options.scenario && !runner.scenarioNames.includes(options.scenario)) {
853
+ throw new UsageError(
854
+ `unknown scenario "${options.scenario}" \u2014 this config defines ` + runner.scenarioNames.map((name) => `"${name}"`).join(", ")
855
+ );
856
+ }
857
+ const scenarios = options.scenario ? [options.scenario] : runner.scenarioNames;
858
+ const effectiveScope = options.scope ?? runner.config.scope;
859
+ const results = [];
860
+ const failures = [];
861
+ for (const scenario of scenarios) {
862
+ let result;
863
+ try {
864
+ result = await runner.collect({
865
+ scenario,
866
+ ...options.scope ? { scope: options.scope } : {}
867
+ });
868
+ } catch (error) {
869
+ failures.push({
870
+ scenario,
871
+ message: error instanceof Error ? error.message : String(error)
872
+ });
873
+ continue;
874
+ }
875
+ results.push(result);
876
+ await onEach?.(result);
877
+ }
878
+ return {
879
+ scenarios,
880
+ declaredScenarios: runner.scenarioNames,
881
+ results,
882
+ failures,
883
+ baselineDir: baselineDirFor(
884
+ options.configPath,
885
+ options.baselineDir ?? runner.config.baselineDir
886
+ ),
887
+ ...effectiveScope ? { scope: effectiveScope } : {},
888
+ domainCapabilities: Object.keys(runner.config.manifest?.tools ?? {}).map((path) => `domain:${path}`).sort(),
889
+ domainManifestConfigured: runner.config.manifest !== void 0
890
+ };
891
+ } finally {
892
+ await runner.close();
893
+ }
894
+ }
895
+ function componentTypeOf(capabilityId) {
896
+ const withoutPlane = capabilityId.replace(/^(view|domain):/, "");
897
+ const dot = withoutPlane.lastIndexOf(".");
898
+ return dot === -1 ? withoutPlane : withoutPlane.slice(0, dot);
899
+ }
900
+ function scopeInventory(inventory, scope) {
901
+ if (!inventory || !scope) return inventory;
902
+ return {
903
+ ...inventory,
904
+ capabilities: inventory.capabilities.filter(
905
+ (capability) => capability.resolution === "unresolved" || matchesScope(componentTypeOf(capability.capabilityId), scope)
906
+ )
907
+ };
908
+ }
909
+ function scopeCapabilityIds(ids, scope) {
910
+ return scope ? ids.filter((id) => matchesScope(componentTypeOf(id), scope)) : ids;
911
+ }
912
+ function joinCoverage(inventory, runtime, options) {
913
+ if (!inventory || !runtime) return void 0;
914
+ if (runtime.failures.length > 0) return void 0;
915
+ const effectiveScope = options.scope ?? runtime.scope;
916
+ const inScope = (capabilityId) => matchesScope(componentTypeOf(capabilityId), effectiveScope);
917
+ const origins = /* @__PURE__ */ new Map();
918
+ for (const capability of inventory.capabilities) {
919
+ if (!origins.has(capability.capabilityId)) {
920
+ origins.set(capability.capabilityId, capability.origin);
921
+ }
922
+ }
923
+ const authored = new Set([...authoredIds(inventory)].filter(inScope));
924
+ for (const capabilityId of runtime.domainCapabilities) {
925
+ if (inScope(capabilityId)) authored.add(capabilityId);
926
+ if (!origins.has(capabilityId)) {
927
+ origins.set(capabilityId, { file: "oRPC manifest", line: 0 });
928
+ }
929
+ }
930
+ const reachedIds = /* @__PURE__ */ new Set();
931
+ for (const result of runtime.results) {
932
+ for (const capability of result.explanation.capabilities) {
933
+ reachedIds.add(capability.capabilityId);
934
+ }
935
+ }
936
+ const allowlistPath = allowlistPathFor(runtime.baselineDir);
937
+ const wholeAllowlist = readAllowlist(allowlistPath);
938
+ const allowlist = Object.fromEntries(
939
+ Object.entries(wholeAllowlist).filter(([id]) => inScope(id))
940
+ );
941
+ const unreadAllowlistPath = unreadAllowlistPathFor(runtime.baselineDir);
942
+ return buildCoverageReport({
943
+ unreadAllowlist: readAllowlist(unreadAllowlistPath, "file#reason#site"),
944
+ unreadAllowlistPath,
945
+ domainAuthoritative: runtime.domainManifestConfigured,
946
+ authored,
947
+ origins,
948
+ reachedIds,
949
+ scenarios: runtime.scenarios,
950
+ ...effectiveScope ? { scope: effectiveScope } : {},
951
+ unresolved: unresolved(inventory),
952
+ allowlist,
953
+ allowlistOutOfScope: Object.keys(wholeAllowlist).length - Object.keys(allowlist).length,
954
+ allowlistPath
955
+ });
956
+ }
957
+
958
+ export {
959
+ SCENARIO_MANIFEST_FILE,
960
+ baselinePath,
961
+ normalize,
962
+ readBaseline,
963
+ writeBaseline,
964
+ readScenarioManifest,
965
+ writeScenarioManifest,
966
+ annotate,
967
+ diff,
968
+ formatValue,
969
+ ALLOWLIST_FILE,
970
+ UNREAD_ALLOWLIST_FILE,
971
+ unreadKey,
972
+ coverageExitCode,
973
+ findTsconfig,
974
+ extractCapabilities,
975
+ authoredIds,
976
+ unresolved,
977
+ readInventory,
978
+ staticConfigScope,
979
+ mountScenarios,
980
+ scopeInventory,
981
+ scopeCapabilityIds,
982
+ joinCoverage
983
+ };
984
+ //# sourceMappingURL=chunk-Q5WOLWEW.js.map