@agent-surface/cli 0.9.1 → 0.11.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.
Files changed (38) hide show
  1. package/README.md +64 -23
  2. package/dist/bin.js +75 -28
  3. package/dist/bin.js.map +1 -1
  4. package/dist/check-BYQ34OVQ.js +122 -0
  5. package/dist/check-BYQ34OVQ.js.map +1 -0
  6. package/dist/chunk-DYDSJM7R.js +170 -0
  7. package/dist/chunk-DYDSJM7R.js.map +1 -0
  8. package/dist/chunk-GXXKZTQB.js +553 -0
  9. package/dist/chunk-GXXKZTQB.js.map +1 -0
  10. package/dist/{chunk-S2LM3N6D.js → chunk-QIVOZAWX.js} +14 -12
  11. package/dist/chunk-QIVOZAWX.js.map +1 -0
  12. package/dist/chunk-RXX63JSL.js +298 -0
  13. package/dist/chunk-RXX63JSL.js.map +1 -0
  14. package/dist/collect.js +18 -1
  15. package/dist/collect.js.map +1 -1
  16. package/dist/index.d.ts +126 -1
  17. package/dist/init-LYYVXFEQ.js +141 -0
  18. package/dist/init-LYYVXFEQ.js.map +1 -0
  19. package/dist/ink-P23VKP4H.js +217 -0
  20. package/dist/ink-P23VKP4H.js.map +1 -0
  21. package/dist/inspect-3CBRKTDM.js +108 -0
  22. package/dist/inspect-3CBRKTDM.js.map +1 -0
  23. package/dist/snapshot-DJ22WCT4.js +59 -0
  24. package/dist/snapshot-DJ22WCT4.js.map +1 -0
  25. package/package.json +5 -4
  26. package/dist/check-7Z2VNN5R.js +0 -79
  27. package/dist/check-7Z2VNN5R.js.map +0 -1
  28. package/dist/chunk-KZUR4CAU.js +0 -84
  29. package/dist/chunk-KZUR4CAU.js.map +0 -1
  30. package/dist/chunk-ODUIFFPM.js +0 -104
  31. package/dist/chunk-ODUIFFPM.js.map +0 -1
  32. package/dist/chunk-S2LM3N6D.js.map +0 -1
  33. package/dist/ink-GCWDO4ML.js +0 -122
  34. package/dist/ink-GCWDO4ML.js.map +0 -1
  35. package/dist/inspect-SLYLBZAH.js +0 -204
  36. package/dist/inspect-SLYLBZAH.js.map +0 -1
  37. package/dist/snapshot-3D55FL63.js +0 -36
  38. package/dist/snapshot-3D55FL63.js.map +0 -1
@@ -1,3 +1,11 @@
1
+ // src/contract.ts
2
+ var DEPTHS = ["static", "runtime", "full"];
3
+ function isDepth(value) {
4
+ return typeof value === "string" && DEPTHS.includes(value);
5
+ }
6
+ var UsageError = class extends Error {
7
+ };
8
+
1
9
  // src/load.ts
2
10
  import { existsSync } from "fs";
3
11
  import { dirname, isAbsolute, join, resolve } from "path";
@@ -133,7 +141,7 @@ var cached;
133
141
  async function loadInk() {
134
142
  if (cached !== void 0) return cached;
135
143
  try {
136
- cached = await import("./ink-GCWDO4ML.js");
144
+ cached = await import("./ink-P23VKP4H.js");
137
145
  } catch {
138
146
  cached = null;
139
147
  }
@@ -145,23 +153,17 @@ async function paint(element) {
145
153
  instance.unmount();
146
154
  await instance.waitUntilExit();
147
155
  }
148
- async function transient(element) {
149
- const { render } = await import("ink");
150
- const instance = render(element);
151
- return () => {
152
- instance.clear();
153
- instance.unmount();
154
- };
155
- }
156
156
 
157
157
  export {
158
+ DEPTHS,
159
+ isDepth,
160
+ UsageError,
158
161
  findConfig,
159
162
  createSurfaceRunner,
160
163
  isPlain,
161
164
  write,
162
165
  writeError,
163
166
  loadInk,
164
- paint,
165
- transient
167
+ paint
166
168
  };
167
- //# sourceMappingURL=chunk-S2LM3N6D.js.map
169
+ //# sourceMappingURL=chunk-QIVOZAWX.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/contract.ts","../src/load.ts","../src/output.ts"],"sourcesContent":["/**\n * The vocabulary every layer shares, and nothing else.\n *\n * It is its own module because `bin.ts` needs both of these before it has\n * decided which command to run, and everything else in this package pulls in\n * either the TypeScript compiler or Vite the moment it is imported. A `--help`\n * that boots a TypeScript program to print a paragraph is a `--help` nobody\n * runs twice.\n */\n\nexport const DEPTHS = [\"static\", \"runtime\", \"full\"] as const;\n\n/**\n * How much of the surface a command is asked to compute.\n *\n * A presentation surface has two sources of truth and every command needs some\n * mix of both — the **catalog** this codebase authors, which is static, and the\n * **projection** a mounted scenario surfaces, which is not. Splitting those\n * across separate commands is what let a green `check` sit on top of a route no\n * scenario visits, so the split lives here instead.\n *\n * `static` reads the TypeScript program and mounts nothing — no Vite server, no\n * jsdom, no scenarios. It is the only depth that survives an app which will not\n * mount, and the only one that needs no scenarios to exist yet.\n *\n * `runtime` mounts and skips the program read, for a repository whose tsconfig\n * is wide enough that booting it costs more than the answer is worth.\n *\n * `full` does both and joins them, which is the only depth that can answer\n * *did we author something no scenario reaches*. It is the default because a\n * tool that has to be asked for the complete answer mostly gives the\n * incomplete one.\n */\nexport type Depth = (typeof DEPTHS)[number];\n\nexport function isDepth(value: unknown): value is Depth {\n return typeof value === \"string\" && (DEPTHS as readonly string[]).includes(value);\n}\n\n/**\n * The caller asked for something impossible — as opposed to the app being\n * broken, which is what a mount failure is. Both exit `2`: CI has to tell \"the\n * surface changed\" apart from \"the tool never ran\", and these are both the\n * second one.\n */\nexport class UsageError extends Error {}\n","import { existsSync } from \"node:fs\";\nimport { dirname, isAbsolute, join, resolve } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { createServer, type ViteDevServer } from \"vite\";\nimport { ViteNodeServer } from \"vite-node/server\";\nimport { ViteNodeRunner } from \"vite-node/client\";\nimport { installSourcemapsSupport } from \"vite-node/source-map\";\nimport type { CollectOptions, CollectResult } from \"./collect.js\";\nimport type { SurfaceConfig } from \"./config.js\";\n\nconst CONFIG_NAMES = [\n \"agent-surface.config.tsx\",\n \"agent-surface.config.ts\",\n \"agent-surface.config.mjs\",\n \"agent-surface.config.js\",\n];\n\n/** Walks up from `from` looking for an `agent-surface.config.*`. */\nexport function findConfig(from: string = process.cwd()): string | undefined {\n let dir = resolve(from);\n for (;;) {\n for (const name of CONFIG_NAMES) {\n const candidate = join(dir, name);\n if (existsSync(candidate)) return candidate;\n }\n const parent = dirname(dir);\n if (parent === dir) return undefined;\n dir = parent;\n }\n}\n\n/** `dist/collect.js` when installed; `src/collect.ts` when run from source. */\nfunction collectorPath(): string {\n for (const ext of [\"js\", \"ts\"]) {\n const candidate = fileURLToPath(new URL(`./collect.${ext}`, import.meta.url));\n if (existsSync(candidate)) return candidate;\n }\n throw new Error(\"could not locate the agent-surface collector module\");\n}\n\nexport interface SurfaceRunner {\n config: SurfaceConfig;\n scenarioNames: string[];\n collect(options: CollectOptions): Promise<CollectResult>;\n close(): Promise<void>;\n}\n\n/**\n * Boots a Vite dev server on the app's own config, so the config file and the\n * app modules it imports are transformed and resolved exactly as the app\n * resolves them — its aliases, its plugins, its TSX.\n */\nexport async function createSurfaceRunner(configPath: string): Promise<SurfaceRunner> {\n const absoluteConfig = isAbsolute(configPath) ? configPath : resolve(configPath);\n if (!existsSync(absoluteConfig)) {\n throw new Error(`config not found: ${absoluteConfig}`);\n }\n const root = dirname(absoluteConfig);\n\n let server: ViteDevServer;\n try {\n server = await createServer({\n root,\n logLevel: \"error\",\n // `serve` so plugins behave as they do in dev; nothing is ever served.\n server: { middlewareMode: true, watch: null, fs: { strict: false } },\n optimizeDeps: { noDiscovery: true, include: [] },\n resolve: {\n // Both halves of the graph must agree on these. React because two\n // copies break hooks; core because `explainSurface` finds the registry\n // through a Symbol, which is per-module-instance (see collect.ts).\n dedupe: [\n \"react\",\n \"react-dom\",\n \"@agent-surface/core\",\n \"@agent-surface/react\",\n \"@agent-surface/testing\",\n ],\n },\n });\n } catch (error) {\n throw new Error(\n `could not start Vite for ${root}: ${error instanceof Error ? error.message : String(error)}`,\n );\n }\n\n try {\n await server.pluginContainer.buildStart({});\n } catch {\n // Vite keeps moving this; a plugin that needs buildStart will say so itself.\n }\n\n const nodeServer = new ViteNodeServer(server);\n installSourcemapsSupport({ getSourceMap: (source) => nodeServer.getSourceMap(source) });\n\n const runner = new ViteNodeRunner({\n root: server.config.root,\n base: server.config.base,\n fetchModule: (id) => nodeServer.fetchModule(id),\n resolveId: (id, importer) => nodeServer.resolveId(id, importer),\n });\n\n const close = async (): Promise<void> => {\n await server.close();\n };\n\n try {\n const configModule = (await runner.executeFile(absoluteConfig)) as {\n default?: SurfaceConfig;\n };\n const config = configModule.default;\n if (!config || typeof config.mount !== \"function\") {\n throw new Error(\n `${absoluteConfig} must \\`export default defineSurface({ mount, scenarios })\\``,\n );\n }\n const scenarioNames = Object.keys(config.scenarios ?? {});\n if (scenarioNames.length === 0) {\n throw new Error(`${absoluteConfig} defines no scenarios`);\n }\n\n // Same runner ⇒ same module graph ⇒ the collector shares React and core\n // with the app tree it is about to mount.\n const collector = (await runner.executeFile(collectorPath())) as {\n collect(config: SurfaceConfig, options: CollectOptions): Promise<CollectResult>;\n };\n\n return {\n config,\n scenarioNames,\n collect: async (options) => {\n // Scoped to the mount, never process-wide: `act()` needs it, and Ink\n // renders its own React tree afterwards — with the flag still set,\n // every frame of the CLI's own UI prints React's \"not wrapped in\n // act(...)\" warning at the user.\n const globals = globalThis as Record<string, unknown>;\n const previous = globals[\"IS_REACT_ACT_ENVIRONMENT\"];\n globals[\"IS_REACT_ACT_ENVIRONMENT\"] = true;\n try {\n return await collector.collect(config, options);\n } finally {\n globals[\"IS_REACT_ACT_ENVIRONMENT\"] = previous;\n }\n },\n close,\n };\n } catch (error) {\n await close();\n throw error;\n }\n}\n","import type { ReactElement } from \"react\";\n\nexport interface OutputFlags {\n plain?: boolean;\n json?: boolean;\n}\n\n/**\n * Terminal-aware only when there is a terminal. Piped output, `--plain`, `CI`\n * and `NO_COLOR` all fall back to plain text — a CLI whose output changes shape\n * when redirected is unusable in a build log.\n */\nexport function isPlain(flags: OutputFlags): boolean {\n if (flags.json) return true;\n if (flags.plain) return true;\n if (process.env[\"CI\"]) return true;\n if (process.env[\"NO_COLOR\"]) return true;\n if (process.stdout.isTTY !== true) return true;\n // A TTY that cannot report its width (some CI ptys, `script` on macOS) makes\n // Ink lay out at zero columns and emit one character per line. Plain text is\n // the only honest rendering for a terminal whose size is unknown.\n return !process.stdout.columns;\n}\n\nexport function write(text: string): void {\n process.stdout.write(`${text}\\n`);\n}\n\nexport function writeError(text: string): void {\n process.stderr.write(`${text}\\n`);\n}\n\ntype InkModule = typeof import(\"./render/ink.js\");\n\nlet cached: InkModule | null | undefined;\n\n/**\n * Loads the Ink renderer, or returns `null` when it cannot run here.\n *\n * Two reasons this is lazy rather than a top-level import. It keeps `--plain`\n * and `--json` from paying for a terminal UI they never draw — and Ink drives\n * React through `react-reconciler`, which reads React 19 internals, so a host\n * that pins React 18 globally cannot load it at all. Neither is a reason to\n * fail a command that was about to print text.\n */\nexport async function loadInk(): Promise<InkModule | null> {\n if (cached !== undefined) return cached;\n try {\n cached = await import(\"./render/ink.js\");\n } catch {\n cached = null;\n }\n return cached;\n}\n\n/** Paints an Ink element once and returns when the frame has been flushed. */\nexport async function paint(element: ReactElement): Promise<void> {\n const { render } = await import(\"ink\");\n const instance = render(element);\n instance.unmount();\n await instance.waitUntilExit();\n}\n\n/** A live Ink frame (spinner) that is cleared before the real output lands. */\nexport async function transient(element: ReactElement): Promise<() => void> {\n const { render } = await import(\"ink\");\n const instance = render(element);\n return () => {\n instance.clear();\n instance.unmount();\n };\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,298 @@
1
+ import {
2
+ flatRows
3
+ } from "./chunk-DYDSJM7R.js";
4
+ import {
5
+ authoredIds,
6
+ formatValue,
7
+ unresolved
8
+ } from "./chunk-GXXKZTQB.js";
9
+
10
+ // src/render/plain.ts
11
+ import { relative } from "path";
12
+ var MARK = { expose: "+", disable: "~", hide: "-" };
13
+ var STATE = { expose: "callable", disable: "disabled", hide: "hidden" };
14
+ var NONE = "\u2014";
15
+ function renderTable(headers, rows) {
16
+ const widths = headers.map(
17
+ (header, column) => Math.max(header.length, ...rows.map((row) => (row.cells[column] ?? "").length))
18
+ );
19
+ const line = (cells) => cells.map((cell, column) => column === headers.length - 1 ? cell : cell.padEnd(widths[column])).join(" ").trimEnd();
20
+ const lines = [line(headers)];
21
+ for (const row of rows) {
22
+ lines.push(line(row.cells));
23
+ if (row.note) lines.push(` \u2937 ${row.note}`);
24
+ }
25
+ return lines;
26
+ }
27
+ function section(title, gloss, count) {
28
+ return `${title} \u2014 ${gloss} (${count})`;
29
+ }
30
+ function renderDetailRow(row, lines) {
31
+ const tags = row.tags.length > 0 ? ` [${row.tags.join(", ")}]` : "";
32
+ lines.push(` ${MARK[row.outcome]} ${row.name}${tags}`);
33
+ lines.push(` ${row.description}`);
34
+ if (row.reason) lines.push(` reason: ${row.reason}`);
35
+ if (row.policies) {
36
+ if (row.policies.length === 0) {
37
+ lines.push(" policies: none");
38
+ } else {
39
+ for (const policy of row.policies) {
40
+ const vote = policy.discovery ? policy.discovery.decision === "disable" ? `disable \u2014 ${policy.discovery.reason}` : policy.discovery.decision : "no discovery hook";
41
+ const phases = policy.phases.length > 0 ? policy.phases.join("/") : NONE;
42
+ const flags = [
43
+ policy.threw ? "THREW" : "",
44
+ policy.confirmationEscalation ? "escalates-confirmation" : ""
45
+ ].filter(Boolean).join(", ");
46
+ lines.push(
47
+ ` policy ${policy.name} (${policy.scope}, ${phases}): ${vote}${flags ? ` [${flags}]` : ""}`
48
+ );
49
+ }
50
+ }
51
+ if (row.availability && !row.availability.available) {
52
+ lines.push(
53
+ ` availability: unavailable${row.availability.reason ? ` \u2014 ${row.availability.reason}` : ""}`
54
+ );
55
+ }
56
+ }
57
+ if (row.schemas) {
58
+ if (row.schemas.input !== void 0) {
59
+ lines.push(` input: ${JSON.stringify(row.schemas.input)}`);
60
+ }
61
+ if (row.schemas.output !== void 0) {
62
+ lines.push(` output: ${JSON.stringify(row.schemas.output)}`);
63
+ }
64
+ }
65
+ }
66
+ function renderCountsPlain(view) {
67
+ return `${view.counts.callable} callable, ${view.counts.disabled} visible-disabled, ${view.counts.hidden} hidden` + (view.rejections.length > 0 ? `, ${view.rejections.length} registration${view.rejections.length === 1 ? "" : "s"} rejected` : "");
68
+ }
69
+ function renderHeader(view, lines) {
70
+ lines.push(
71
+ `scenario ${view.scenario}${view.route ? ` route ${view.route}` : ""}${view.scope && view.scope.length > 0 ? ` scope ${view.scope.join(" ")}` : ""}`
72
+ );
73
+ lines.push(renderCountsPlain(view));
74
+ }
75
+ function renderRejections(view, lines) {
76
+ if (view.rejections.length === 0) return;
77
+ lines.push("");
78
+ lines.push(
79
+ section("REJECTED", "the registry refused these during the mount", view.rejections.length)
80
+ );
81
+ for (const rejection of view.rejections) {
82
+ const why = rejection.reason === "duplicate" ? "duplicate \u2014 an earlier registration holds this key" : "guard \u2014 onRegister rejected this registration";
83
+ lines.push(` ! ${rejection.componentType} (${rejection.instanceId}) ${why}`);
84
+ }
85
+ }
86
+ function renderEmpty(view, lines) {
87
+ lines.push("");
88
+ if (view.counts.hidden > 0) {
89
+ lines.push(
90
+ `Nothing is callable here \u2014 all ${view.counts.hidden} registered capabilities were hidden by policy.`
91
+ );
92
+ if (!view.explained) lines.push("Re-run with --explain to see which policy hid them.");
93
+ } else {
94
+ lines.push("Nothing is registered for this scenario \u2014 the agent has no surface here.");
95
+ if (!view.explained) lines.push("Re-run with --explain to see whether a policy hid it.");
96
+ }
97
+ }
98
+ function renderSurfacePlain(view, options = {}) {
99
+ const lines = [];
100
+ renderHeader(view, lines);
101
+ renderRejections(view, lines);
102
+ const rows = flatRows(view);
103
+ if (rows.length === 0) {
104
+ renderEmpty(view, lines);
105
+ return lines.join("\n");
106
+ }
107
+ if (options.detail) {
108
+ for (const group of view.groups.filter((group2) => group2.rows.length > 0)) {
109
+ lines.push("");
110
+ lines.push(`${group.heading} (${group.rows.length})`);
111
+ for (const row of group.rows) renderDetailRow(row, lines);
112
+ }
113
+ return lines.join("\n");
114
+ }
115
+ lines.push("");
116
+ lines.push(
117
+ ...renderTable(
118
+ ["CAPABILITY", "KIND", "EFFECT", "STATE", "FLAGS"],
119
+ rows.map((row) => ({
120
+ cells: [
121
+ row.path,
122
+ row.kind,
123
+ row.effect ?? NONE,
124
+ STATE[row.outcome],
125
+ row.flags.length > 0 ? row.flags.join(" \xB7 ") : NONE
126
+ ],
127
+ ...row.reason ? { note: row.reason } : {}
128
+ }))
129
+ )
130
+ );
131
+ return lines.join("\n");
132
+ }
133
+ function renderCatalogPlain(inventory, options = {}) {
134
+ const lines = [];
135
+ const resolved = inventory.capabilities.filter((c) => c.resolution !== "unresolved");
136
+ const ids = authoredIds(inventory);
137
+ lines.push(
138
+ `${ids.size} authored (upper bound) \xB7 ${resolved.length} call site${resolved.length === 1 ? "" : "s"} across ${inventory.filesAnalyzed} file${inventory.filesAnalyzed === 1 ? "" : "s"} \xB7 domain not analyzed, it comes from the oRPC router (OQ-1)`
139
+ );
140
+ if (inventory.filesOutsideRoot > 0) {
141
+ lines.push(
142
+ `${inventory.filesOutsideRoot} program file${inventory.filesOutsideRoot === 1 ? "" : "s"} outside the config's directory were not analyzed`
143
+ );
144
+ }
145
+ if (!options.standalone) return lines.join("\n");
146
+ const byId = [...resolved].sort((a, b) => a.capabilityId.localeCompare(b.capabilityId));
147
+ if (byId.length > 0) {
148
+ lines.push("");
149
+ lines.push(
150
+ ...renderTable(
151
+ ["CAPABILITY", "KIND", "ORIGIN", "READ"],
152
+ byId.map((capability) => ({
153
+ cells: [
154
+ capability.capabilityId,
155
+ capability.kind,
156
+ `${capability.origin.file}:${capability.origin.line}`,
157
+ capability.resolution
158
+ ],
159
+ ...capability.note ? { note: capability.note } : {}
160
+ }))
161
+ )
162
+ );
163
+ }
164
+ const unread = renderUnread(unresolved(inventory));
165
+ if (unread.length > 0) lines.push("", ...unread);
166
+ return lines.join("\n");
167
+ }
168
+ function renderUnread(entries) {
169
+ if (entries.length === 0) return [];
170
+ const lines = [];
171
+ lines.push(
172
+ section(
173
+ "UNREAD CALL SITES",
174
+ "the catalog is incomplete, so every count above is a floor",
175
+ entries.length
176
+ )
177
+ );
178
+ for (const capability of entries) {
179
+ lines.push(` ? ${capability.origin.file}:${capability.origin.line}`);
180
+ lines.push(` ${capability.note ?? "the extractor could not read this call site"}`);
181
+ }
182
+ return lines;
183
+ }
184
+ function renderCoveragePlain(report) {
185
+ const lines = [];
186
+ if (report.unreached.length > 0) {
187
+ lines.push(
188
+ section("UNREACHED", "authored, and no scenario mounts it", report.unreached.length)
189
+ );
190
+ lines.push(
191
+ ...renderTable(
192
+ ["CAPABILITY", "ORIGIN"],
193
+ report.unreached.map((entry) => ({
194
+ cells: [entry.capabilityId, `${entry.origin.file}:${entry.origin.line}`]
195
+ }))
196
+ )
197
+ );
198
+ lines.push("");
199
+ }
200
+ if (report.undeclared.length > 0) {
201
+ lines.push(
202
+ section(
203
+ "UNDECLARED",
204
+ "present at runtime with no static origin \u2014 a dynamic registration, or a gap here",
205
+ report.undeclared.length
206
+ )
207
+ );
208
+ for (const id of report.undeclared) lines.push(` ${id}`);
209
+ lines.push("");
210
+ }
211
+ if (report.staleAllowlist.length > 0) {
212
+ lines.push(
213
+ section(
214
+ "STALE ALLOWLIST",
215
+ "a scenario reaches these now, so delete them before the list rots",
216
+ report.staleAllowlist.length
217
+ )
218
+ );
219
+ for (const id of report.staleAllowlist) lines.push(` ${id}`);
220
+ lines.push("");
221
+ }
222
+ if (report.unresolved.length > 0) {
223
+ lines.push(...renderUnread(report.unresolved), "");
224
+ }
225
+ lines.push(...renderCoverageSummary(report));
226
+ return lines.join("\n");
227
+ }
228
+ function renderCoverageSummary(report) {
229
+ const qualifiers = [
230
+ `${report.scenarios.length} scenario${report.scenarios.length === 1 ? "" : "s"} (${report.scenarios.join(
231
+ ", "
232
+ )})`
233
+ ];
234
+ if (report.scope && report.scope.length > 0) qualifiers.push(`scope ${report.scope.join(" ")}`);
235
+ const lines = [
236
+ `${report.authored} authored \xB7 ${report.reached} reached \xB7 ${report.unreached.length} unreached \xB7 ${qualifiers.join(" \xB7 ")}`
237
+ ];
238
+ if (report.domainReached.length > 0) {
239
+ lines.push(
240
+ `${report.domainReached.length} domain capabilit${report.domainReached.length === 1 ? "y" : "ies"} reached and held apart \u2014 that plane is the oRPC router's, and this catalog never claimed it`
241
+ );
242
+ }
243
+ if (report.allowed.length > 0) {
244
+ lines.push(
245
+ `${report.allowed.length} unreached capabilit${report.allowed.length === 1 ? "y is" : "ies are"} allowlisted in ${relative(process.cwd(), report.allowlistPath)}`
246
+ );
247
+ }
248
+ if (report.allowlistOutOfScope > 0) {
249
+ lines.push(
250
+ `${report.allowlistOutOfScope} allowlist entr${report.allowlistOutOfScope === 1 ? "y" : "ies"} outside this scope were not judged either way`
251
+ );
252
+ }
253
+ if (report.unreached.length === 0 && report.unresolved.length === 0 && report.staleAllowlist.length === 0) {
254
+ lines.push(
255
+ report.allowed.length > 0 ? "no new surface coverage gaps \u2014 the allowlist still holds the known ones" : "every authored capability is reached by a scenario"
256
+ );
257
+ }
258
+ return lines;
259
+ }
260
+ function renderNoVerdictPlain(failures) {
261
+ return [
262
+ section("NO COVERAGE VERDICT", "a scenario did not mount, so nothing reached anything", failures.length),
263
+ ...failures.flatMap((failure) => [` ${failure.scenario}`, ` ${failure.message}`]),
264
+ "",
265
+ "Every capability those scenarios would have surfaced would be reported unreached,",
266
+ "so no verdict is printed at all. Fix the mount, or name a scenario that works."
267
+ ].join("\n");
268
+ }
269
+ function renderFailuresPlain(failures) {
270
+ return [
271
+ section("DID NOT MOUNT", "these scenarios threw, and were skipped", failures.length),
272
+ ...failures.flatMap((failure) => [` ${failure.scenario}`, ` ${failure.message}`])
273
+ ].join("\n");
274
+ }
275
+ function renderDriftPlain(scenario, entries) {
276
+ const lines = [` ${scenario}: ${entries.length} change${entries.length === 1 ? "" : "s"}`];
277
+ for (const entry of entries) {
278
+ const where = entry.subject ? `${entry.subject} (${entry.path})` : entry.path;
279
+ if (entry.kind === "added") lines.push(` + ${where} ${formatValue(entry.after)}`);
280
+ else if (entry.kind === "removed") lines.push(` - ${where} ${formatValue(entry.before)}`);
281
+ else {
282
+ lines.push(` ~ ${where}`);
283
+ lines.push(` before: ${formatValue(entry.before)}`);
284
+ lines.push(` after: ${formatValue(entry.after)}`);
285
+ }
286
+ }
287
+ return lines.join("\n");
288
+ }
289
+
290
+ export {
291
+ renderSurfacePlain,
292
+ renderCatalogPlain,
293
+ renderCoveragePlain,
294
+ renderNoVerdictPlain,
295
+ renderFailuresPlain,
296
+ renderDriftPlain
297
+ };
298
+ //# sourceMappingURL=chunk-RXX63JSL.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/render/plain.ts"],"sourcesContent":["import { relative } from \"node:path\";\nimport type { CapabilityRow, SurfaceView } from \"./model.js\";\nimport { flatRows } from \"./model.js\";\nimport type { DiffEntry } from \"../baseline.js\";\nimport { formatValue } from \"../baseline.js\";\nimport type { ScenarioFailure } from \"../analysis.js\";\nimport {\n authoredIds,\n unresolved,\n type AuthoredCapability,\n type CapabilityInventory,\n} from \"../extract.js\";\nimport type { CoverageReport } from \"../coverage.js\";\n\n/**\n * The no-colour, no-cursor rendering used when stdout is piped or when\n * `--plain`, `CI` or `NO_COLOR` is set. Same view model as the Ink UI, so the\n * two cannot disagree about what the surface contains.\n */\n\nconst MARK = { expose: \"+\", disable: \"~\", hide: \"-\" } as const;\nconst STATE = { expose: \"callable\", disable: \"disabled\", hide: \"hidden\" } as const;\nconst NONE = \"—\";\n\n/**\n * Column widths come from the *content*, never from `process.stdout.columns`.\n *\n * `AS-CLI-003` requires plain output to be byte-stable across runs, and a table\n * laid out against the terminal it happened to run in is stable only until two\n * people diff the same CI log from different windows. Same rows in, same bytes\n * out, everywhere.\n *\n * The last column is not padded, so no line ever carries trailing whitespace —\n * which some diff tools render and others strip, i.e. another way for identical\n * output to look different.\n */\nfunction renderTable(headers: string[], rows: Array<{ cells: string[]; note?: string }>): string[] {\n const widths = headers.map((header, column) =>\n Math.max(header.length, ...rows.map((row) => (row.cells[column] ?? \"\").length)),\n );\n const line = (cells: string[]): string =>\n cells\n .map((cell, column) => (column === headers.length - 1 ? cell : cell.padEnd(widths[column]!)))\n .join(\" \")\n .trimEnd();\n\n const lines = [line(headers)];\n for (const row of rows) {\n lines.push(line(row.cells));\n // The unavailability reason is prose of unbounded length. A column for it\n // would set the table's width by its longest sentence; a continuation line\n // keeps the grid aligned and puts the reason directly under its capability.\n if (row.note) lines.push(` ⤷ ${row.note}`);\n }\n return lines;\n}\n\n/** `UNREACHED — authored, and no scenario mounts it (1)` */\nfunction section(title: string, gloss: string, count: number): string {\n return `${title} — ${gloss} (${count})`;\n}\n\nfunction renderDetailRow(row: CapabilityRow, lines: string[]): void {\n const tags = row.tags.length > 0 ? ` [${row.tags.join(\", \")}]` : \"\";\n lines.push(` ${MARK[row.outcome]} ${row.name}${tags}`);\n lines.push(` ${row.description}`);\n if (row.reason) lines.push(` reason: ${row.reason}`);\n\n if (row.policies) {\n if (row.policies.length === 0) {\n lines.push(\" policies: none\");\n } else {\n for (const policy of row.policies) {\n const vote = policy.discovery\n ? policy.discovery.decision === \"disable\"\n ? `disable — ${policy.discovery.reason}`\n : policy.discovery.decision\n : \"no discovery hook\";\n const phases = policy.phases.length > 0 ? policy.phases.join(\"/\") : NONE;\n const flags = [\n policy.threw ? \"THREW\" : \"\",\n policy.confirmationEscalation ? \"escalates-confirmation\" : \"\",\n ]\n .filter(Boolean)\n .join(\", \");\n lines.push(\n ` policy ${policy.name} (${policy.scope}, ${phases}): ${vote}${\n flags ? ` [${flags}]` : \"\"\n }`,\n );\n }\n }\n if (row.availability && !row.availability.available) {\n lines.push(\n ` availability: unavailable${\n row.availability.reason ? ` — ${row.availability.reason}` : \"\"\n }`,\n );\n }\n }\n\n if (row.schemas) {\n if (row.schemas.input !== undefined) {\n lines.push(` input: ${JSON.stringify(row.schemas.input)}`);\n }\n if (row.schemas.output !== undefined) {\n lines.push(` output: ${JSON.stringify(row.schemas.output)}`);\n }\n }\n}\n\n/**\n * The counts line, and everything it is relative to (`AS-CLI-007`).\n *\n * `hidden` is printed unconditionally. It is computed on every run — the\n * explanation is always collected — and suppressing it outside `--explain`\n * meant a surface with a policy-hidden half rendered as a complete one. The\n * *attribution* still needs `--explain`; the count and the rows do not.\n */\nexport function renderCountsPlain(view: SurfaceView): string {\n return (\n `${view.counts.callable} callable, ${view.counts.disabled} visible-disabled, ` +\n `${view.counts.hidden} hidden` +\n (view.rejections.length > 0\n ? `, ${view.rejections.length} registration${view.rejections.length === 1 ? \"\" : \"s\"} rejected`\n : \"\")\n );\n}\n\nfunction renderHeader(view: SurfaceView, lines: string[]): void {\n lines.push(\n `scenario ${view.scenario}${view.route ? ` route ${view.route}` : \"\"}${\n view.scope && view.scope.length > 0 ? ` scope ${view.scope.join(\" \")}` : \"\"\n }`,\n );\n lines.push(renderCountsPlain(view));\n}\n\nfunction renderRejections(view: SurfaceView, lines: string[]): void {\n if (view.rejections.length === 0) return;\n lines.push(\"\");\n lines.push(\n section(\"REJECTED\", \"the registry refused these during the mount\", view.rejections.length),\n );\n for (const rejection of view.rejections) {\n const why =\n rejection.reason === \"duplicate\"\n ? \"duplicate — an earlier registration holds this key\"\n : \"guard — onRegister rejected this registration\";\n lines.push(` ! ${rejection.componentType} (${rejection.instanceId}) ${why}`);\n }\n}\n\nfunction renderEmpty(view: SurfaceView, lines: string[]): void {\n lines.push(\"\");\n // \"Nothing is registered\" is only true when nothing was hidden. Saying it\n // over a surface a policy emptied sends the reader to the wrong file.\n if (view.counts.hidden > 0) {\n lines.push(\n `Nothing is callable here — all ${view.counts.hidden} registered capabilities were hidden by policy.`,\n );\n if (!view.explained) lines.push(\"Re-run with --explain to see which policy hid them.\");\n } else {\n lines.push(\"Nothing is registered for this scenario — the agent has no surface here.\");\n if (!view.explained) lines.push(\"Re-run with --explain to see whether a policy hid it.\");\n }\n}\n\nexport interface SurfaceRenderOptions {\n /** The grouped, one-capability-per-paragraph view. Implied by --explain/--schemas. */\n detail?: boolean;\n}\n\n/**\n * One capability per line, aligned. The default, because the question `inspect`\n * is usually asked is *what is on this surface* — which is a scanning question,\n * and prose does not scan.\n *\n * Policy chains and JSON Schemas are multi-line by nature and cannot live in a\n * cell, so `--explain` and `--schemas` fall back to the detail view rather than\n * producing a table with most of the answer missing.\n */\nexport function renderSurfacePlain(\n view: SurfaceView,\n options: SurfaceRenderOptions = {},\n): string {\n const lines: string[] = [];\n renderHeader(view, lines);\n renderRejections(view, lines);\n\n const rows = flatRows(view);\n if (rows.length === 0) {\n renderEmpty(view, lines);\n return lines.join(\"\\n\");\n }\n\n if (options.detail) {\n for (const group of view.groups.filter((group) => group.rows.length > 0)) {\n lines.push(\"\");\n lines.push(`${group.heading} (${group.rows.length})`);\n for (const row of group.rows) renderDetailRow(row, lines);\n }\n return lines.join(\"\\n\");\n }\n\n lines.push(\"\");\n lines.push(\n ...renderTable(\n [\"CAPABILITY\", \"KIND\", \"EFFECT\", \"STATE\", \"FLAGS\"],\n rows.map((row) => ({\n cells: [\n row.path,\n row.kind,\n row.effect ?? NONE,\n STATE[row.outcome],\n row.flags.length > 0 ? row.flags.join(\" · \") : NONE,\n ],\n ...(row.reason ? { note: row.reason } : {}),\n })),\n ),\n );\n return lines.join(\"\\n\");\n}\n\n/**\n * The static catalog (`AS-COVER-001…003`). The summary says \"upper bound\" in so\n * many words: a tsconfig's include globs are wider than what a bundle reaches,\n * so a capability in a component no route renders any more is in here. That is\n * dead code — a different finding, not a false positive — and the reader has to\n * be told which number they are holding.\n */\nexport interface CatalogRenderOptions {\n /**\n * This catalog *is* the command's output, rather than its preamble.\n *\n * True at `--depth static`: there are no scenario tables and no verdict, so\n * the listing and the unread call sites have nowhere else to appear.\n *\n * False at `--depth full`, where the scenario tables below name every\n * capability a scenario reached, the `UNREACHED` section names the ones it did\n * not, and the verdict carries the unread call sites — so printing any of it\n * here is the same information a second time, above the answer instead of in\n * it. Only the summary line survives.\n */\n standalone?: boolean;\n}\n\nexport function renderCatalogPlain(\n inventory: CapabilityInventory,\n options: CatalogRenderOptions = {},\n): string {\n const lines: string[] = [];\n const resolved = inventory.capabilities.filter((c) => c.resolution !== \"unresolved\");\n const ids = authoredIds(inventory);\n\n lines.push(\n `${ids.size} authored (upper bound) · ${resolved.length} call site${\n resolved.length === 1 ? \"\" : \"s\"\n } across ${inventory.filesAnalyzed} file${inventory.filesAnalyzed === 1 ? \"\" : \"s\"}` +\n \" · domain not analyzed, it comes from the oRPC router (OQ-1)\",\n );\n if (inventory.filesOutsideRoot > 0) {\n // Relative, not absolute: plain output is byte-stable across runs\n // (`AS-CLI-003`), and an absolute path makes it machine-specific the moment\n // two people diff a CI log.\n lines.push(\n `${inventory.filesOutsideRoot} program file${\n inventory.filesOutsideRoot === 1 ? \"\" : \"s\"\n } outside the config's directory were not analyzed`,\n );\n }\n\n if (!options.standalone) return lines.join(\"\\n\");\n\n const byId = [...resolved].sort((a, b) => a.capabilityId.localeCompare(b.capabilityId));\n if (byId.length > 0) {\n lines.push(\"\");\n lines.push(\n ...renderTable(\n [\"CAPABILITY\", \"KIND\", \"ORIGIN\", \"READ\"],\n byId.map((capability) => ({\n cells: [\n capability.capabilityId,\n capability.kind,\n `${capability.origin.file}:${capability.origin.line}`,\n capability.resolution,\n ],\n ...(capability.note ? { note: capability.note } : {}),\n })),\n ),\n );\n }\n\n const unread = renderUnread(unresolved(inventory));\n if (unread.length > 0) lines.push(\"\", ...unread);\n return lines.join(\"\\n\");\n}\n\n/**\n * Call sites the extractor could not read. Reported with file and line, never\n * dropped: an inventory that silently omitted what it failed to parse would\n * understate the denominator, and every number built on it would claim a\n * completeness it never had.\n */\nfunction renderUnread(entries: AuthoredCapability[]): string[] {\n if (entries.length === 0) return [];\n const lines: string[] = [];\n lines.push(\n section(\n \"UNREAD CALL SITES\",\n \"the catalog is incomplete, so every count above is a floor\",\n entries.length,\n ),\n );\n for (const capability of entries) {\n lines.push(` ? ${capability.origin.file}:${capability.origin.line}`);\n lines.push(` ${capability.note ?? \"the extractor could not read this call site\"}`);\n }\n return lines;\n}\n\n/**\n * The verdict: authored minus reached (`AS-COVER-004…005`).\n *\n * This is the finding the command surface used to hide behind a fifth command,\n * so it is the last thing printed and the thing a reader stops on.\n */\nexport function renderCoveragePlain(report: CoverageReport): string {\n const lines: string[] = [];\n\n if (report.unreached.length > 0) {\n lines.push(\n section(\"UNREACHED\", \"authored, and no scenario mounts it\", report.unreached.length),\n );\n lines.push(\n ...renderTable(\n [\"CAPABILITY\", \"ORIGIN\"],\n report.unreached.map((entry) => ({\n cells: [entry.capabilityId, `${entry.origin.file}:${entry.origin.line}`],\n })),\n ),\n );\n lines.push(\"\");\n }\n\n if (report.undeclared.length > 0) {\n lines.push(\n section(\n \"UNDECLARED\",\n \"present at runtime with no static origin — a dynamic registration, or a gap here\",\n report.undeclared.length,\n ),\n );\n for (const id of report.undeclared) lines.push(` ${id}`);\n lines.push(\"\");\n }\n\n if (report.staleAllowlist.length > 0) {\n lines.push(\n section(\n \"STALE ALLOWLIST\",\n \"a scenario reaches these now, so delete them before the list rots\",\n report.staleAllowlist.length,\n ),\n );\n for (const id of report.staleAllowlist) lines.push(` ${id}`);\n lines.push(\"\");\n }\n\n if (report.unresolved.length > 0) {\n lines.push(...renderUnread(report.unresolved), \"\");\n }\n\n lines.push(...renderCoverageSummary(report));\n return lines.join(\"\\n\");\n}\n\n/** The one line a reader who stops at the bottom takes away. */\nfunction renderCoverageSummary(report: CoverageReport): string[] {\n const qualifiers = [\n `${report.scenarios.length} scenario${report.scenarios.length === 1 ? \"\" : \"s\"} (${report.scenarios.join(\n \", \",\n )})`,\n ];\n // Every count is relative to the scope, so the scope is printed with them\n // (`AS-CLI-007`) — `10 authored` under a scope is a claim about one prefix of\n // the codebase, not about the codebase.\n if (report.scope && report.scope.length > 0) qualifiers.push(`scope ${report.scope.join(\" \")}`);\n\n const lines = [\n `${report.authored} authored · ${report.reached} reached · ${report.unreached.length} unreached` +\n ` · ${qualifiers.join(\" · \")}`,\n ];\n\n if (report.domainReached.length > 0) {\n lines.push(\n `${report.domainReached.length} domain capabilit${\n report.domainReached.length === 1 ? \"y\" : \"ies\"\n } reached and held apart — that plane is the oRPC router's, and this catalog never claimed it`,\n );\n }\n if (report.allowed.length > 0) {\n lines.push(\n `${report.allowed.length} unreached capabilit${\n report.allowed.length === 1 ? \"y is\" : \"ies are\"\n } allowlisted in ${relative(process.cwd(), report.allowlistPath)}`,\n );\n }\n if (report.allowlistOutOfScope > 0) {\n lines.push(\n `${report.allowlistOutOfScope} allowlist entr${\n report.allowlistOutOfScope === 1 ? \"y\" : \"ies\"\n } outside this scope were not judged either way`,\n );\n }\n\n // Each bucket gets its own remedy. \"Add a scenario, or delete the component\"\n // is the right advice for an unreached capability and useless advice for a\n // call site the extractor could not read.\n if (report.unreached.length === 0 && report.unresolved.length === 0 && report.staleAllowlist.length === 0) {\n lines.push(\n report.allowed.length > 0\n ? \"no new surface coverage gaps — the allowlist still holds the known ones\"\n : \"every authored capability is reached by a scenario\",\n );\n }\n return lines;\n}\n\n/**\n * Why there is no coverage verdict. Never silence: a reader who asked for the\n * complete answer and got a partial one has to be told which part is missing,\n * or the partial one reads as the complete one.\n */\nexport function renderNoVerdictPlain(failures: ScenarioFailure[]): string {\n return [\n section(\"NO COVERAGE VERDICT\", \"a scenario did not mount, so nothing reached anything\", failures.length),\n ...failures.flatMap((failure) => [` ${failure.scenario}`, ` ${failure.message}`]),\n \"\",\n \"Every capability those scenarios would have surfaced would be reported unreached,\",\n \"so no verdict is printed at all. Fix the mount, or name a scenario that works.\",\n ].join(\"\\n\");\n}\n\nexport function renderFailuresPlain(failures: ScenarioFailure[]): string {\n return [\n section(\"DID NOT MOUNT\", \"these scenarios threw, and were skipped\", failures.length),\n ...failures.flatMap((failure) => [` ${failure.scenario}`, ` ${failure.message}`]),\n ].join(\"\\n\");\n}\n\nexport function renderDriftPlain(scenario: string, entries: DiffEntry[]): string {\n const lines = [` ${scenario}: ${entries.length} change${entries.length === 1 ? \"\" : \"s\"}`];\n for (const entry of entries) {\n const where = entry.subject ? `${entry.subject} (${entry.path})` : entry.path;\n if (entry.kind === \"added\") lines.push(` + ${where} ${formatValue(entry.after)}`);\n else if (entry.kind === \"removed\") lines.push(` - ${where} ${formatValue(entry.before)}`);\n else {\n lines.push(` ~ ${where}`);\n lines.push(` before: ${formatValue(entry.before)}`);\n lines.push(` after: ${formatValue(entry.after)}`);\n }\n }\n return lines.join(\"\\n\");\n}\n"],"mappings":"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","names":["group"]}
package/dist/collect.js CHANGED
@@ -20,12 +20,29 @@ async function collect(config, options) {
20
20
  // Inert copies: the live objects are frozen and graph-local, and only
21
21
  // plain JSON may cross back into the CLI process.
22
22
  snapshot: jsonify(mount.mounted.registry.snapshot(ctx)),
23
- explanation: jsonify(explainSurface(mount.mounted.registry, ctx))
23
+ explanation: jsonify(explainSurface(mount.mounted.registry, ctx)),
24
+ // The harness subscribes to the registry when it is constructed, which is
25
+ // before the tree renders — so this is the whole mount, not what happened
26
+ // to still be pending when the render finished.
27
+ rejections: rejectionsFrom(mount.surface.events()),
28
+ ...scope ? { scope } : {}
24
29
  };
25
30
  } finally {
26
31
  mount.surface.dispose();
27
32
  }
28
33
  }
34
+ function rejectionsFrom(events) {
35
+ const rejections = [];
36
+ for (const event of events) {
37
+ if (event.type !== "component-rejected") continue;
38
+ rejections.push({
39
+ componentType: event.componentType,
40
+ instanceId: event.instanceId,
41
+ reason: event.reason
42
+ });
43
+ }
44
+ return rejections;
45
+ }
29
46
  function jsonify(value) {
30
47
  return JSON.parse(JSON.stringify(value));
31
48
  }
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/collect.ts"],"sourcesContent":["/**\n * The collector — the *only* module the CLI executes inside the vite-node\n * graph, and the reason that boundary exists.\n *\n * Two things force it:\n *\n * 1. **One React.** The app's component tree resolves React through the app's\n * own Vite config. If the mount ran in the CLI's Node graph instead, a\n * second React copy would render it and every hook would throw.\n *\n * 2. **One `@agent-surface/core`.** `explainSurface()` reaches the registry\n * through a plain `Symbol` seam, and a symbol is only equal to itself within\n * one module instance. Load core twice and the seam silently misses. So the\n * explanation is computed *here*, beside the registry that owns it.\n *\n * Everything crosses back as plain JSON. Nothing live — no registry, no React\n * element, no policy function — escapes into the CLI process.\n */\nimport { explainSurface, type SurfaceExplanation } from \"@agent-surface/core/explain\";\nimport type { AgentConsumer, AgentSurfaceSnapshot, SnapshotContext } from \"@agent-surface/core\";\nimport type { SurfaceConfig } from \"./config.js\";\nimport { mountScenario } from \"./mount.js\";\n\nexport interface CollectOptions {\n scenario: string;\n consumer?: AgentConsumer;\n scope?: string[];\n}\n\nexport interface CollectResult {\n scenario: string;\n snapshot: AgentSurfaceSnapshot;\n explanation: SurfaceExplanation;\n}\n\nexport async function collect(\n config: SurfaceConfig,\n options: CollectOptions,\n): Promise<CollectResult> {\n const mount = await mountScenario(config, options.scenario, {\n ...(options.consumer ? { consumer: options.consumer } : {}),\n });\n const scope = options.scope ?? config.scope;\n const ctx: SnapshotContext = {\n consumer: mount.consumer,\n includeUnavailable: true,\n ...(scope ? { scope } : {}),\n };\n\n try {\n return {\n scenario: options.scenario,\n // Inert copies: the live objects are frozen and graph-local, and only\n // plain JSON may cross back into the CLI process.\n snapshot: jsonify(mount.mounted.registry.snapshot(ctx)),\n explanation: jsonify(explainSurface(mount.mounted.registry, ctx)),\n };\n } finally {\n mount.surface.dispose();\n }\n}\n\nfunction jsonify<T>(value: T): T {\n return JSON.parse(JSON.stringify(value)) as T;\n}\n"],"mappings":";;;;;AAkBA,SAAS,sBAA+C;AAiBxD,eAAsB,QACpB,QACA,SACwB;AACxB,QAAM,QAAQ,MAAM,cAAc,QAAQ,QAAQ,UAAU;AAAA,IAC1D,GAAI,QAAQ,WAAW,EAAE,UAAU,QAAQ,SAAS,IAAI,CAAC;AAAA,EAC3D,CAAC;AACD,QAAM,QAAQ,QAAQ,SAAS,OAAO;AACtC,QAAM,MAAuB;AAAA,IAC3B,UAAU,MAAM;AAAA,IAChB,oBAAoB;AAAA,IACpB,GAAI,QAAQ,EAAE,MAAM,IAAI,CAAC;AAAA,EAC3B;AAEA,MAAI;AACF,WAAO;AAAA,MACL,UAAU,QAAQ;AAAA;AAAA;AAAA,MAGlB,UAAU,QAAQ,MAAM,QAAQ,SAAS,SAAS,GAAG,CAAC;AAAA,MACtD,aAAa,QAAQ,eAAe,MAAM,QAAQ,UAAU,GAAG,CAAC;AAAA,IAClE;AAAA,EACF,UAAE;AACA,UAAM,QAAQ,QAAQ;AAAA,EACxB;AACF;AAEA,SAAS,QAAW,OAAa;AAC/B,SAAO,KAAK,MAAM,KAAK,UAAU,KAAK,CAAC;AACzC;","names":[]}
1
+ {"version":3,"sources":["../src/collect.ts"],"sourcesContent":["/**\n * The collector — the *only* module the CLI executes inside the vite-node\n * graph, and the reason that boundary exists.\n *\n * Two things force it:\n *\n * 1. **One React.** The app's component tree resolves React through the app's\n * own Vite config. If the mount ran in the CLI's Node graph instead, a\n * second React copy would render it and every hook would throw.\n *\n * 2. **One `@agent-surface/core`.** `explainSurface()` reaches the registry\n * through a plain `Symbol` seam, and a symbol is only equal to itself within\n * one module instance. Load core twice and the seam silently misses. So the\n * explanation is computed *here*, beside the registry that owns it.\n *\n * Everything crosses back as plain JSON. Nothing live — no registry, no React\n * element, no policy function — escapes into the CLI process.\n */\nimport { explainSurface, type SurfaceExplanation } from \"@agent-surface/core/explain\";\nimport type {\n AgentConsumer,\n AgentSurfaceEvent,\n AgentSurfaceSnapshot,\n SnapshotContext,\n} from \"@agent-surface/core\";\nimport type { SurfaceConfig } from \"./config.js\";\nimport { mountScenario } from \"./mount.js\";\n\nexport interface CollectOptions {\n scenario: string;\n consumer?: AgentConsumer;\n scope?: string[];\n}\n\n/**\n * A registration the registry refused while the scenario mounted (`AS-CLI-006`).\n *\n * Rejection is the one failure that is invisible everywhere else. The handle is\n * dead, so the capability never reaches the snapshot; the registration never\n * became active, so `explainSurface()` does not iterate it either. The only\n * diagnostic core emits goes through `devError`, which prints nothing unless\n * the app was built with `environment: \"development\"` — and the config shape\n * this CLI documents builds it with `\"test\"`.\n */\nexport interface RegistrationRejection {\n componentType: string;\n instanceId: string;\n reason: \"duplicate\" | \"guard\";\n}\n\nexport interface CollectResult {\n scenario: string;\n snapshot: AgentSurfaceSnapshot;\n explanation: SurfaceExplanation;\n /** Refused during this mount. Empty on a healthy one. */\n rejections: RegistrationRejection[];\n /** The scope the two projections above were computed under, when one was set. */\n scope?: string[];\n}\n\nexport async function collect(\n config: SurfaceConfig,\n options: CollectOptions,\n): Promise<CollectResult> {\n const mount = await mountScenario(config, options.scenario, {\n ...(options.consumer ? { consumer: options.consumer } : {}),\n });\n const scope = options.scope ?? config.scope;\n const ctx: SnapshotContext = {\n consumer: mount.consumer,\n includeUnavailable: true,\n ...(scope ? { scope } : {}),\n };\n\n try {\n return {\n scenario: options.scenario,\n // Inert copies: the live objects are frozen and graph-local, and only\n // plain JSON may cross back into the CLI process.\n snapshot: jsonify(mount.mounted.registry.snapshot(ctx)),\n explanation: jsonify(explainSurface(mount.mounted.registry, ctx)),\n // The harness subscribes to the registry when it is constructed, which is\n // before the tree renders — so this is the whole mount, not what happened\n // to still be pending when the render finished.\n rejections: rejectionsFrom(mount.surface.events()),\n ...(scope ? { scope } : {}),\n };\n } finally {\n mount.surface.dispose();\n }\n}\n\nfunction rejectionsFrom(events: readonly AgentSurfaceEvent[]): RegistrationRejection[] {\n const rejections: RegistrationRejection[] = [];\n for (const event of events) {\n if (event.type !== \"component-rejected\") continue;\n rejections.push({\n componentType: event.componentType,\n instanceId: event.instanceId,\n reason: event.reason,\n });\n }\n return rejections;\n}\n\nfunction jsonify<T>(value: T): T {\n return JSON.parse(JSON.stringify(value)) as T;\n}\n"],"mappings":";;;;;AAkBA,SAAS,sBAA+C;AA0CxD,eAAsB,QACpB,QACA,SACwB;AACxB,QAAM,QAAQ,MAAM,cAAc,QAAQ,QAAQ,UAAU;AAAA,IAC1D,GAAI,QAAQ,WAAW,EAAE,UAAU,QAAQ,SAAS,IAAI,CAAC;AAAA,EAC3D,CAAC;AACD,QAAM,QAAQ,QAAQ,SAAS,OAAO;AACtC,QAAM,MAAuB;AAAA,IAC3B,UAAU,MAAM;AAAA,IAChB,oBAAoB;AAAA,IACpB,GAAI,QAAQ,EAAE,MAAM,IAAI,CAAC;AAAA,EAC3B;AAEA,MAAI;AACF,WAAO;AAAA,MACL,UAAU,QAAQ;AAAA;AAAA;AAAA,MAGlB,UAAU,QAAQ,MAAM,QAAQ,SAAS,SAAS,GAAG,CAAC;AAAA,MACtD,aAAa,QAAQ,eAAe,MAAM,QAAQ,UAAU,GAAG,CAAC;AAAA;AAAA;AAAA;AAAA,MAIhE,YAAY,eAAe,MAAM,QAAQ,OAAO,CAAC;AAAA,MACjD,GAAI,QAAQ,EAAE,MAAM,IAAI,CAAC;AAAA,IAC3B;AAAA,EACF,UAAE;AACA,UAAM,QAAQ,QAAQ;AAAA,EACxB;AACF;AAEA,SAAS,eAAe,QAA+D;AACrF,QAAM,aAAsC,CAAC;AAC7C,aAAW,SAAS,QAAQ;AAC1B,QAAI,MAAM,SAAS,qBAAsB;AACzC,eAAW,KAAK;AAAA,MACd,eAAe,MAAM;AAAA,MACrB,YAAY,MAAM;AAAA,MAClB,QAAQ,MAAM;AAAA,IAChB,CAAC;AAAA,EACH;AACA,SAAO;AACT;AAEA,SAAS,QAAW,OAAa;AAC/B,SAAO,KAAK,MAAM,KAAK,UAAU,KAAK,CAAC;AACzC;","names":[]}
package/dist/index.d.ts CHANGED
@@ -22,10 +22,135 @@ import 'react';
22
22
  * element, no policy function — escapes into the CLI process.
23
23
  */
24
24
 
25
+ /**
26
+ * A registration the registry refused while the scenario mounted (`AS-CLI-006`).
27
+ *
28
+ * Rejection is the one failure that is invisible everywhere else. The handle is
29
+ * dead, so the capability never reaches the snapshot; the registration never
30
+ * became active, so `explainSurface()` does not iterate it either. The only
31
+ * diagnostic core emits goes through `devError`, which prints nothing unless
32
+ * the app was built with `environment: "development"` — and the config shape
33
+ * this CLI documents builds it with `"test"`.
34
+ */
35
+ interface RegistrationRejection {
36
+ componentType: string;
37
+ instanceId: string;
38
+ reason: "duplicate" | "guard";
39
+ }
25
40
  interface CollectResult {
26
41
  scenario: string;
27
42
  snapshot: AgentSurfaceSnapshot;
28
43
  explanation: SurfaceExplanation;
44
+ /** Refused during this mount. Empty on a healthy one. */
45
+ rejections: RegistrationRejection[];
46
+ /** The scope the two projections above were computed under, when one was set. */
47
+ scope?: string[];
48
+ }
49
+
50
+ interface AuthoredCapability {
51
+ /** Canonical id, instance-independent: `view:devices.table.sort`. */
52
+ capabilityId: string;
53
+ kind: "observation" | "action" | "procedure";
54
+ /** Where a human can go and read it. */
55
+ origin: {
56
+ file: string;
57
+ line: number;
58
+ };
59
+ /** Literals recovered from the call site; absent when not statically known. */
60
+ description?: string;
61
+ effect?: string;
62
+ /**
63
+ * How much of this call site the extractor understood.
64
+ *
65
+ * `static` — identity and metadata both recovered from literals.
66
+ * `partial` — identity resolved, some metadata dynamic. The common case: a
67
+ * spread `instanceId`, or a description built from a template.
68
+ * `unresolved` — identity NOT resolved. Reported, never dropped.
69
+ */
70
+ resolution: "static" | "partial" | "unresolved";
71
+ /** Present on `partial`/`unresolved`: what defeated the extractor. */
72
+ note?: string;
73
+ }
74
+ interface CapabilityInventory {
75
+ capabilities: AuthoredCapability[];
76
+ /** Absolute path to the tsconfig whose file list was analyzed. */
77
+ tsconfig: string;
78
+ /** Directory the analysis was rooted at — the surface config's own. */
79
+ root: string;
80
+ /** Files the program actually walked — the inventory's blast radius. */
81
+ filesAnalyzed: number;
82
+ /**
83
+ * Program files skipped for living outside `root` — workspace packages the
84
+ * app's tsconfig aliases in, typically the library's own source. Reported
85
+ * rather than dropped silently: a boundary nobody can see is a boundary
86
+ * nobody can check.
87
+ */
88
+ filesOutsideRoot: number;
89
+ /**
90
+ * The `domain:` plane is deliberately *not* analyzed here. Those capabilities
91
+ * come from the oRPC router, which is already a static export (OQ-1), and
92
+ * reporting zero of them would read as "there are none" rather than "nobody
93
+ * looked".
94
+ */
95
+ domain: "not-analyzed";
96
+ }
97
+
98
+ /**
99
+ * A committed list of unreached capabilities a repository has decided not to
100
+ * fix yet, each with a reason. Adoption has to ratchet rather than gate: a
101
+ * codebase turning this on with 200 unreached capabilities cannot fix them in
102
+ * one pull request, and a check that can only be adopted big-bang is a check
103
+ * that never gets adopted.
104
+ */
105
+ type CoverageAllowlist = Record<string, string>;
106
+ interface UnreachedCapability {
107
+ capabilityId: string;
108
+ origin: {
109
+ file: string;
110
+ line: number;
111
+ };
112
+ }
113
+ interface CoverageReport {
114
+ /** Distinct capability ids the inventory resolved, within any active scope. */
115
+ authored: number;
116
+ /** How many of them at least one scenario surfaced. */
117
+ reached: number;
118
+ scenarios: string[];
119
+ /**
120
+ * The scope every number here was computed under (`AS-CLI-007`). A scope
121
+ * filters the catalog *and* the mount, so `10 authored` without it on screen
122
+ * reads as a claim about the whole codebase when it is a claim about one
123
+ * prefix of it.
124
+ */
125
+ scope?: string[];
126
+ /**
127
+ * Allowlist entries outside the active scope, which a scoped run cannot
128
+ * judge: not unreached (nothing looked), not stale (nothing reached them).
129
+ * Counted rather than silently dropped, so a scoped run never reads as a
130
+ * verdict on the whole allowlist.
131
+ */
132
+ allowlistOutOfScope: number;
133
+ /** Authored, surfaced by no scenario, and not allowlisted — the finding. */
134
+ unreached: UnreachedCapability[];
135
+ /**
136
+ * Present at runtime with no static origin: a dynamic registration, or a gap
137
+ * in the extractor. `view:` only — see `domainReached`.
138
+ */
139
+ undeclared: string[];
140
+ /**
141
+ * `domain:` capabilities a scenario surfaced. Held apart from `undeclared`
142
+ * because the inventory never claimed to analyze that plane: filing them as
143
+ * "no static origin" would report the design's own stated boundary as a
144
+ * defect, which is the misleading check this whole command rejects.
145
+ */
146
+ domainReached: string[];
147
+ /** Carried forward from the inventory. */
148
+ unresolved: AuthoredCapability[];
149
+ /** Unreached, but listed in the allowlist. */
150
+ allowed: string[];
151
+ /** Listed in the allowlist and reached anyway — the list has rotted. */
152
+ staleAllowlist: string[];
153
+ allowlistPath: string;
29
154
  }
30
155
 
31
- export type { CollectResult };
156
+ export type { AuthoredCapability, CapabilityInventory, CollectResult, CoverageAllowlist, CoverageReport, RegistrationRejection };