@agent-surface/cli 0.13.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bin.js +41 -14
- package/dist/bin.js.map +1 -1
- package/dist/{check-TJIX7NDE.js → check-VWRYNYLN.js} +66 -48
- package/dist/check-VWRYNYLN.js.map +1 -0
- package/dist/chunk-A5UCBF7D.js +246 -0
- package/dist/chunk-A5UCBF7D.js.map +1 -0
- package/dist/chunk-GYYWHZPM.js +532 -0
- package/dist/chunk-GYYWHZPM.js.map +1 -0
- package/dist/chunk-NFK3XWWH.js +1475 -0
- package/dist/chunk-NFK3XWWH.js.map +1 -0
- package/dist/chunk-Y2LSPEVK.js +61 -0
- package/dist/chunk-Y2LSPEVK.js.map +1 -0
- package/dist/index.d.ts +3 -2
- package/dist/{init-6XV64LK2.js → init-BVZR6CRS.js} +62 -40
- package/dist/init-BVZR6CRS.js.map +1 -0
- package/dist/{ink-QR7X7TAC.js → ink-ZCQ26EY4.js} +71 -17
- package/dist/ink-ZCQ26EY4.js.map +1 -0
- package/dist/{inspect-NJ4J3MPP.js → inspect-3YFPCYQJ.js} +88 -104
- package/dist/inspect-3YFPCYQJ.js.map +1 -0
- package/dist/snapshot-5QKLZKZI.js +130 -0
- package/dist/snapshot-5QKLZKZI.js.map +1 -0
- package/package.json +4 -4
- package/dist/check-TJIX7NDE.js.map +0 -1
- package/dist/chunk-3AJ343NA.js +0 -178
- package/dist/chunk-3AJ343NA.js.map +0 -1
- package/dist/chunk-IALBMW3R.js +0 -278
- package/dist/chunk-IALBMW3R.js.map +0 -1
- package/dist/chunk-L7GHSC2Z.js +0 -465
- package/dist/chunk-L7GHSC2Z.js.map +0 -1
- package/dist/chunk-UGCLJ5JX.js +0 -724
- package/dist/chunk-UGCLJ5JX.js.map +0 -1
- package/dist/chunk-VX6GBEP3.js +0 -539
- package/dist/chunk-VX6GBEP3.js.map +0 -1
- package/dist/init-6XV64LK2.js.map +0 -1
- package/dist/ink-QR7X7TAC.js.map +0 -1
- package/dist/inspect-NJ4J3MPP.js.map +0 -1
- package/dist/snapshot-ZGTAF2Y5.js +0 -77
- package/dist/snapshot-ZGTAF2Y5.js.map +0 -1
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import {
|
|
2
|
+
UsageError,
|
|
3
|
+
baselineDirFor,
|
|
4
|
+
createSurfaceRunner,
|
|
5
|
+
isPlain,
|
|
6
|
+
loadInk,
|
|
7
|
+
paint,
|
|
8
|
+
renderPartPlain,
|
|
9
|
+
transient,
|
|
10
|
+
write
|
|
11
|
+
} from "./chunk-GYYWHZPM.js";
|
|
12
|
+
import {
|
|
13
|
+
LABEL_WIDTH,
|
|
14
|
+
allowlistPathFor,
|
|
15
|
+
authoredIds,
|
|
16
|
+
buildCoverageReport,
|
|
17
|
+
extractCapabilities,
|
|
18
|
+
readAllowlist,
|
|
19
|
+
readLiteralConfigScope,
|
|
20
|
+
reportGrid,
|
|
21
|
+
unreadAllowlistPathFor,
|
|
22
|
+
unresolved
|
|
23
|
+
} from "./chunk-NFK3XWWH.js";
|
|
24
|
+
|
|
25
|
+
// src/analysis.ts
|
|
26
|
+
import { dirname } from "path";
|
|
27
|
+
import { matchesScope } from "@agent-surface/core/explain";
|
|
28
|
+
function readInventory(options) {
|
|
29
|
+
if (options.depth === "runtime") return void 0;
|
|
30
|
+
return extractCapabilities({
|
|
31
|
+
root: dirname(options.configPath),
|
|
32
|
+
...options.tsconfig ? { tsconfig: options.tsconfig } : {}
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
function staticConfigScope(options) {
|
|
36
|
+
return options.scope ?? readLiteralConfigScope(options.configPath);
|
|
37
|
+
}
|
|
38
|
+
function describeThrown(error) {
|
|
39
|
+
if (error instanceof Error) {
|
|
40
|
+
const message = error.message.trim();
|
|
41
|
+
if (message) return message;
|
|
42
|
+
const name = error.name.trim() || error.constructor.name || "Error";
|
|
43
|
+
return `${name} (no message)`;
|
|
44
|
+
}
|
|
45
|
+
const rendered = String(error).trim();
|
|
46
|
+
return rendered || "Unknown thrown value (empty)";
|
|
47
|
+
}
|
|
48
|
+
function nonEmptyStringProperty(error, key) {
|
|
49
|
+
if (!error || typeof error !== "object" && typeof error !== "function") return void 0;
|
|
50
|
+
const value = error[key];
|
|
51
|
+
return typeof value === "string" && value.trim() ? value.trim() : void 0;
|
|
52
|
+
}
|
|
53
|
+
function scenarioFailure(scenario, error) {
|
|
54
|
+
const message = describeThrown(error);
|
|
55
|
+
const stack = nonEmptyStringProperty(error, "stack");
|
|
56
|
+
const componentStack = nonEmptyStringProperty(error, "componentStack");
|
|
57
|
+
const cause = error instanceof Error ? error.cause : void 0;
|
|
58
|
+
const causeMessage = cause !== void 0 && cause !== error ? describeThrown(cause) : void 0;
|
|
59
|
+
return {
|
|
60
|
+
scenario,
|
|
61
|
+
message,
|
|
62
|
+
...stack ? { stack } : {},
|
|
63
|
+
...componentStack ? { componentStack } : {},
|
|
64
|
+
...causeMessage && causeMessage !== message ? { cause: causeMessage } : {}
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
async function mountScenarios(options, hooks = {}) {
|
|
68
|
+
if (options.depth === "static") return void 0;
|
|
69
|
+
const runner = await createSurfaceRunner(options.configPath);
|
|
70
|
+
try {
|
|
71
|
+
if (options.scenario && !runner.scenarioNames.includes(options.scenario)) {
|
|
72
|
+
throw new UsageError(
|
|
73
|
+
`unknown scenario "${options.scenario}" \u2014 this config defines ` + runner.scenarioNames.map((name) => `"${name}"`).join(", ")
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
const scenarios = options.scenario ? [options.scenario] : runner.scenarioNames;
|
|
77
|
+
const effectiveScope = options.scope ?? runner.config.scope;
|
|
78
|
+
const results = [];
|
|
79
|
+
const failures = [];
|
|
80
|
+
const plan = {
|
|
81
|
+
scenarios,
|
|
82
|
+
declaredScenarios: runner.scenarioNames,
|
|
83
|
+
baselineDir: baselineDirFor(
|
|
84
|
+
options.configPath,
|
|
85
|
+
options.baselineDir ?? runner.config.baselineDir
|
|
86
|
+
),
|
|
87
|
+
...effectiveScope ? { scope: effectiveScope } : {},
|
|
88
|
+
domainCapabilities: Object.keys(runner.config.manifest?.tools ?? {}).map((path) => `domain:${path}`).sort(),
|
|
89
|
+
domainManifestConfigured: runner.config.manifest !== void 0
|
|
90
|
+
};
|
|
91
|
+
await hooks.onPlan?.(plan);
|
|
92
|
+
for (const scenario of scenarios) {
|
|
93
|
+
let result;
|
|
94
|
+
try {
|
|
95
|
+
result = await runner.collect({
|
|
96
|
+
scenario,
|
|
97
|
+
...options.scope ? { scope: options.scope } : {}
|
|
98
|
+
});
|
|
99
|
+
} catch (error) {
|
|
100
|
+
failures.push(scenarioFailure(scenario, error));
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
results.push(result);
|
|
104
|
+
await hooks.onEach?.(result);
|
|
105
|
+
}
|
|
106
|
+
return { ...plan, results, failures };
|
|
107
|
+
} finally {
|
|
108
|
+
await runner.close();
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function componentTypeOf(capabilityId) {
|
|
112
|
+
const withoutPlane = capabilityId.replace(/^(view|domain):/, "");
|
|
113
|
+
const dot = withoutPlane.lastIndexOf(".");
|
|
114
|
+
return dot === -1 ? withoutPlane : withoutPlane.slice(0, dot);
|
|
115
|
+
}
|
|
116
|
+
function scopeInventory(inventory, scope) {
|
|
117
|
+
if (!inventory || !scope) return inventory;
|
|
118
|
+
return {
|
|
119
|
+
...inventory,
|
|
120
|
+
capabilities: inventory.capabilities.filter(
|
|
121
|
+
(capability) => capability.resolution === "unresolved" || matchesScope(componentTypeOf(capability.capabilityId), scope)
|
|
122
|
+
)
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function scopeCapabilityIds(ids, scope) {
|
|
126
|
+
return scope ? ids.filter((id) => matchesScope(componentTypeOf(id), scope)) : ids;
|
|
127
|
+
}
|
|
128
|
+
function joinCoverage(inventory, runtime, options) {
|
|
129
|
+
if (!inventory || !runtime) return void 0;
|
|
130
|
+
if (runtime.failures.length > 0) return void 0;
|
|
131
|
+
const effectiveScope = options.scope ?? runtime.scope;
|
|
132
|
+
const inScope = (capabilityId) => matchesScope(componentTypeOf(capabilityId), effectiveScope);
|
|
133
|
+
const origins = /* @__PURE__ */ new Map();
|
|
134
|
+
for (const capability of inventory.capabilities) {
|
|
135
|
+
if (!origins.has(capability.capabilityId)) {
|
|
136
|
+
origins.set(capability.capabilityId, capability.origin);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const authored = new Set([...authoredIds(inventory)].filter(inScope));
|
|
140
|
+
for (const capabilityId of runtime.domainCapabilities) {
|
|
141
|
+
if (inScope(capabilityId)) authored.add(capabilityId);
|
|
142
|
+
if (!origins.has(capabilityId)) {
|
|
143
|
+
origins.set(capabilityId, { file: "oRPC manifest", line: 0 });
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
const reachedIds = /* @__PURE__ */ new Set();
|
|
147
|
+
for (const result of runtime.results) {
|
|
148
|
+
for (const capability of result.explanation.capabilities) {
|
|
149
|
+
reachedIds.add(capability.capabilityId);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const allowlistPath = allowlistPathFor(runtime.baselineDir);
|
|
153
|
+
const wholeAllowlist = readAllowlist(allowlistPath);
|
|
154
|
+
const allowlist = Object.fromEntries(
|
|
155
|
+
Object.entries(wholeAllowlist).filter(([id]) => inScope(id))
|
|
156
|
+
);
|
|
157
|
+
const unreadAllowlistPath = unreadAllowlistPathFor(runtime.baselineDir);
|
|
158
|
+
return buildCoverageReport({
|
|
159
|
+
unreadAllowlist: readAllowlist(unreadAllowlistPath, "file#reason#site"),
|
|
160
|
+
unreadAllowlistPath,
|
|
161
|
+
domainAuthoritative: runtime.domainManifestConfigured,
|
|
162
|
+
authored,
|
|
163
|
+
origins,
|
|
164
|
+
reachedIds,
|
|
165
|
+
scenarios: runtime.scenarios,
|
|
166
|
+
...effectiveScope ? { scope: effectiveScope } : {},
|
|
167
|
+
unresolved: unresolved(inventory),
|
|
168
|
+
allowlist,
|
|
169
|
+
allowlistOutOfScope: Object.keys(wholeAllowlist).length - Object.keys(allowlist).length,
|
|
170
|
+
allowlistPath
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// src/render/present.tsx
|
|
175
|
+
import { jsx } from "react/jsx-runtime";
|
|
176
|
+
var ReportPresenter = class {
|
|
177
|
+
/** `null` on a stream rendering plain text — piped, CI, `--plain`, React 18. */
|
|
178
|
+
#out;
|
|
179
|
+
#err;
|
|
180
|
+
/** Sticky, and only ever wider: a grid that shrank mid-report is two grids. */
|
|
181
|
+
#labelWidth = LABEL_WIDTH;
|
|
182
|
+
#wrote = false;
|
|
183
|
+
/**
|
|
184
|
+
* Whether the last part was written as plain text. Ink's own frame supplies
|
|
185
|
+
* the blank line after it; plain text has to be asked for one before the next
|
|
186
|
+
* part. Tracking which drew last is what keeps the spacing identical when a
|
|
187
|
+
* drawn report sends a finding to a redirected stderr.
|
|
188
|
+
*/
|
|
189
|
+
#plainLast = false;
|
|
190
|
+
#stop;
|
|
191
|
+
constructor(out, err) {
|
|
192
|
+
this.#out = out;
|
|
193
|
+
this.#err = err;
|
|
194
|
+
}
|
|
195
|
+
async emit(...parts) {
|
|
196
|
+
for (const part of parts) await this.#draw(part);
|
|
197
|
+
}
|
|
198
|
+
async wait(label) {
|
|
199
|
+
this.settle();
|
|
200
|
+
const ink = this.#out;
|
|
201
|
+
if (ink) this.#stop = await transient(/* @__PURE__ */ jsx(ink.Loading, { label }));
|
|
202
|
+
}
|
|
203
|
+
settle() {
|
|
204
|
+
this.#stop?.();
|
|
205
|
+
this.#stop = void 0;
|
|
206
|
+
}
|
|
207
|
+
async #draw(part) {
|
|
208
|
+
this.settle();
|
|
209
|
+
const stream = part.stream ?? "out";
|
|
210
|
+
if (part.kind === "blocks") {
|
|
211
|
+
this.#labelWidth = Math.max(this.#labelWidth, reportGrid(part.blocks, LABEL_WIDTH).label);
|
|
212
|
+
}
|
|
213
|
+
const separate = this.#wrote && this.#plainLast;
|
|
214
|
+
const ink = stream === "err" ? this.#err : this.#out;
|
|
215
|
+
if (ink) {
|
|
216
|
+
if (separate) write("", stream);
|
|
217
|
+
await paint(/* @__PURE__ */ jsx(ink.Part, { part, labelWidth: this.#labelWidth }), stream);
|
|
218
|
+
} else {
|
|
219
|
+
const text = renderPartPlain(part, this.#labelWidth);
|
|
220
|
+
if (text.length === 0) return;
|
|
221
|
+
write(separate ? `
|
|
222
|
+
${text}` : text, stream);
|
|
223
|
+
}
|
|
224
|
+
this.#plainLast = ink === null;
|
|
225
|
+
this.#wrote = true;
|
|
226
|
+
}
|
|
227
|
+
};
|
|
228
|
+
async function createPresenter(flags) {
|
|
229
|
+
const drawn = !isPlain(flags, "out") || !isPlain(flags, "err");
|
|
230
|
+
const ink = drawn ? await loadInk() : null;
|
|
231
|
+
return new ReportPresenter(
|
|
232
|
+
ink && !isPlain(flags, "out") ? ink : null,
|
|
233
|
+
ink && !isPlain(flags, "err") ? ink : null
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export {
|
|
238
|
+
readInventory,
|
|
239
|
+
staticConfigScope,
|
|
240
|
+
mountScenarios,
|
|
241
|
+
scopeInventory,
|
|
242
|
+
scopeCapabilityIds,
|
|
243
|
+
joinCoverage,
|
|
244
|
+
createPresenter
|
|
245
|
+
};
|
|
246
|
+
//# sourceMappingURL=chunk-A5UCBF7D.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/analysis.ts","../src/render/present.tsx"],"sourcesContent":["/**\n * The two halves of the surface, and the join between them.\n *\n * A presentation surface has two sources of truth, and every command needs\n * some mix of both:\n *\n * - the **catalog** — what this codebase authors. Static: `type` is a string\n * literal, capability names are object keys, so `view:devices.table.sort` is\n * fully determined by source text ([`extract.ts`](./extract.ts)).\n * - the **projection** — what a mounted scenario actually surfaces, after\n * availability, policy and binding have had their say ([`collect.ts`](./collect.ts)).\n *\n * Splitting those across separate commands is what let a green `check` sit on\n * top of a route no scenario visits. So the split lives here, behind a `depth`\n * dial, and the commands compose the same three steps in whatever order their\n * output needs: `inspect` streams and closes with the verdict, `check` collects\n * and leads with it.\n */\nimport { dirname } from \"node:path\";\nimport { matchesScope } from \"@agent-surface/core/explain\";\nimport { baselineDirFor } from \"./baseline.js\";\nimport { UsageError, type Depth } from \"./contract.js\";\nimport type { CollectResult } from \"./collect.js\";\nimport {\n allowlistPathFor,\n buildCoverageReport,\n readAllowlist,\n unreadAllowlistPathFor,\n type CoverageReport,\n} from \"./coverage.js\";\nimport {\n authoredIds,\n extractCapabilities,\n readLiteralConfigScope,\n unresolved,\n type CapabilityInventory,\n} from \"./extract.js\";\nimport { createSurfaceRunner } from \"./load.js\";\n\nexport type { Depth } from \"./contract.js\";\nexport { UsageError } from \"./contract.js\";\n\nexport interface AnalysisOptions {\n configPath: string;\n depth: Depth;\n scenario?: string;\n scope?: string[];\n tsconfig?: string;\n baselineDir?: string;\n}\n\n/**\n * The static half. `undefined` at `--depth runtime`, which is the caller\n * saying it does not want this computed rather than it having failed.\n */\nexport function readInventory(options: AnalysisOptions): CapabilityInventory | undefined {\n if (options.depth === \"runtime\") return undefined;\n return extractCapabilities({\n root: dirname(options.configPath),\n ...(options.tsconfig ? { tsconfig: options.tsconfig } : {}),\n });\n}\n\nexport function staticConfigScope(options: AnalysisOptions): string[] | undefined {\n return options.scope ?? readLiteralConfigScope(options.configPath);\n}\n\n/** A scenario the config declares whose mount threw. Named, never swallowed. */\nexport interface ScenarioFailure {\n scenario: string;\n message: string;\n stack?: string;\n componentStack?: string;\n cause?: string;\n}\n\nfunction describeThrown(error: unknown): string {\n if (error instanceof Error) {\n const message = error.message.trim();\n if (message) return message;\n const name = error.name.trim() || error.constructor.name || \"Error\";\n return `${name} (no message)`;\n }\n const rendered = String(error).trim();\n return rendered || \"Unknown thrown value (empty)\";\n}\n\nfunction nonEmptyStringProperty(error: unknown, key: string): string | undefined {\n if (!error || (typeof error !== \"object\" && typeof error !== \"function\")) return undefined;\n const value = (error as Record<string, unknown>)[key];\n return typeof value === \"string\" && value.trim() ? value.trim() : undefined;\n}\n\nfunction scenarioFailure(scenario: string, error: unknown): ScenarioFailure {\n const message = describeThrown(error);\n const stack = nonEmptyStringProperty(error, \"stack\");\n const componentStack = nonEmptyStringProperty(error, \"componentStack\");\n const cause = error instanceof Error ? error.cause : undefined;\n const causeMessage = cause !== undefined && cause !== error ? describeThrown(cause) : undefined;\n return {\n scenario,\n message,\n ...(stack ? { stack } : {}),\n ...(componentStack ? { componentStack } : {}),\n ...(causeMessage && causeMessage !== message ? { cause: causeMessage } : {}),\n };\n}\n\n/**\n * Everything knowable once the config has loaded and before the first mount:\n * which scenarios will run, under which scope, against which manifest.\n *\n * Split out so a command can *say* what it is about to measure. The mounts are\n * the slow half — on a real app, seconds of them — and a report that opens with\n * its qualifiers only after they finish spends that time showing nothing and\n * then asks the reader to re-read the numbers above.\n */\nexport interface RuntimePlan {\n /** Scenarios selected for this run, in config order. */\n scenarios: string[];\n /** Every scenario declared by the config, even when one was selected. */\n declaredScenarios: string[];\n baselineDir: string;\n /** CLI scope wins; otherwise the config scope is effective everywhere. */\n scope?: string[];\n /** Authoritative domain capability ids from the configured oRPC manifest. */\n domainCapabilities: string[];\n domainManifestConfigured: boolean;\n}\n\nexport interface RuntimeAnalysis extends RuntimePlan {\n /** The ones that mounted. */\n results: CollectResult[];\n failures: ScenarioFailure[];\n}\n\nexport interface MountHooks {\n /** Called once, after the config loads and before the first mount. */\n onPlan?: (plan: RuntimePlan) => void | Promise<void>;\n /** Called as each scenario finishes, so a command can print as it goes. */\n onEach?: (result: CollectResult) => void | Promise<void>;\n}\n\n/**\n * The runtime half. `undefined` at `--depth static`.\n *\n * `onEach` is awaited as each scenario finishes, so a command can print as it\n * goes instead of after the last mount — a config with ten scenarios is a long\n * time to look at nothing.\n *\n * A scenario that throws is recorded and the run continues. Before this was\n * one command, `capabilities` was the only thing that still worked on an app\n * that would not mount; merging the commands would have thrown that away if a\n * single bad scenario could abort the run.\n */\nexport async function mountScenarios(\n options: AnalysisOptions,\n hooks: MountHooks = {},\n): Promise<RuntimeAnalysis | undefined> {\n if (options.depth === \"static\") return undefined;\n\n const runner = await createSurfaceRunner(options.configPath);\n try {\n if (options.scenario && !runner.scenarioNames.includes(options.scenario)) {\n throw new UsageError(\n `unknown scenario \"${options.scenario}\" — this config defines ` +\n runner.scenarioNames.map((name) => `\"${name}\"`).join(\", \"),\n );\n }\n const scenarios = options.scenario ? [options.scenario] : runner.scenarioNames;\n const effectiveScope = options.scope ?? runner.config.scope;\n const results: CollectResult[] = [];\n const failures: ScenarioFailure[] = [];\n const plan: RuntimePlan = {\n scenarios,\n declaredScenarios: runner.scenarioNames,\n baselineDir: baselineDirFor(\n options.configPath,\n options.baselineDir ?? runner.config.baselineDir,\n ),\n ...(effectiveScope ? { scope: effectiveScope } : {}),\n domainCapabilities: Object.keys(runner.config.manifest?.tools ?? {})\n .map((path) => `domain:${path}`)\n .sort(),\n domainManifestConfigured: runner.config.manifest !== undefined,\n };\n await hooks.onPlan?.(plan);\n\n for (const scenario of scenarios) {\n let result: CollectResult;\n try {\n result = await runner.collect({\n scenario,\n ...(options.scope ? { scope: options.scope } : {}),\n });\n } catch (error) {\n failures.push(scenarioFailure(scenario, error));\n continue;\n }\n results.push(result);\n await hooks.onEach?.(result);\n }\n\n return { ...plan, results, failures };\n } finally {\n await runner.close();\n }\n}\n\n/**\n * The component type a capability id belongs to: `view:devices.table.sort` →\n * `devices.table`. Capability names are object keys and cannot contain a dot,\n * so the last one is always the boundary; component types can and do.\n */\nfunction componentTypeOf(capabilityId: string): string {\n const withoutPlane = capabilityId.replace(/^(view|domain):/, \"\");\n const dot = withoutPlane.lastIndexOf(\".\");\n return dot === -1 ? withoutPlane : withoutPlane.slice(0, dot);\n}\n\nexport function scopeInventory(\n inventory: CapabilityInventory | undefined,\n scope: string[] | undefined,\n): CapabilityInventory | undefined {\n if (!inventory || !scope) return inventory;\n return {\n ...inventory,\n capabilities: inventory.capabilities.filter(\n (capability) =>\n capability.resolution === \"unresolved\" ||\n matchesScope(componentTypeOf(capability.capabilityId), scope),\n ),\n };\n}\n\nexport function scopeCapabilityIds(ids: string[], scope: string[] | undefined): string[] {\n return scope ? ids.filter((id) => matchesScope(componentTypeOf(id), scope)) : ids;\n}\n\n/**\n * Authored minus reached (`AS-COVER-004…005`).\n *\n * Two ways this returns `undefined`, and neither is \"no gaps\":\n *\n * - a half was not computed, because the depth did not ask for it;\n * - **a scenario failed to mount.** That scenario reached nothing, so every\n * capability it would have surfaced would be reported as one no scenario\n * reaches. A coverage verdict computed over a partial run is precisely the\n * misleading check this package refuses to emit, so there is no verdict\n * until every scenario mounted. The renderer says which of the two it was.\n */\nexport function joinCoverage(\n inventory: CapabilityInventory | undefined,\n runtime: RuntimeAnalysis | undefined,\n options: AnalysisOptions,\n): CoverageReport | undefined {\n if (!inventory || !runtime) return undefined;\n if (runtime.failures.length > 0) return undefined;\n\n // A scope filters the mount, so it has to filter the catalog by the same\n // predicate — core's own, not a second copy of it. Without this, `--scope\n // devices` reported every `app.navigation` capability as unreached, with the\n // words \"no scenario mounts it\" over two that both scenarios mount.\n const effectiveScope = options.scope ?? runtime.scope;\n const inScope = (capabilityId: string): boolean =>\n matchesScope(componentTypeOf(capabilityId), effectiveScope);\n\n const origins = new Map<string, { file: string; line: number }>();\n for (const capability of inventory.capabilities) {\n if (!origins.has(capability.capabilityId)) {\n origins.set(capability.capabilityId, capability.origin);\n }\n }\n\n const authored = new Set([...authoredIds(inventory)].filter(inScope));\n for (const capabilityId of runtime.domainCapabilities) {\n if (inScope(capabilityId)) authored.add(capabilityId);\n if (!origins.has(capabilityId)) {\n origins.set(capabilityId, { file: \"oRPC manifest\", line: 0 });\n }\n }\n const reachedIds = new Set<string>();\n for (const result of runtime.results) {\n for (const capability of result.explanation.capabilities) {\n reachedIds.add(capability.capabilityId);\n }\n }\n\n // The allowlist is a statement about the whole catalog, and a scoped run has\n // only looked at part of it. Judging an out-of-scope entry either way would\n // be wrong in both directions — it is not an unreached capability this run\n // waved through, and it is not a stale entry either, because nothing here\n // reached it.\n const allowlistPath = allowlistPathFor(runtime.baselineDir);\n const wholeAllowlist = readAllowlist(allowlistPath);\n const allowlist = Object.fromEntries(\n Object.entries(wholeAllowlist).filter(([id]) => inScope(id)),\n );\n\n // Not scope-filtered, deliberately: an unread call site has no capability id,\n // so there is no component type to test a scope prefix against. A scoped run\n // simply reports the same unread sites as an unscoped one.\n const unreadAllowlistPath = unreadAllowlistPathFor(runtime.baselineDir);\n\n return buildCoverageReport({\n unreadAllowlist: readAllowlist(unreadAllowlistPath, \"file#reason#site\"),\n unreadAllowlistPath,\n domainAuthoritative: runtime.domainManifestConfigured,\n authored,\n origins,\n reachedIds,\n scenarios: runtime.scenarios,\n ...(effectiveScope ? { scope: effectiveScope } : {}),\n unresolved: unresolved(inventory),\n allowlist,\n allowlistOutOfScope: Object.keys(wholeAllowlist).length - Object.keys(allowlist).length,\n allowlistPath,\n });\n}\n","/**\n * The presenter: one report, one look, whichever command produced it.\n *\n * Every human-facing command builds [`ReportPart`s](./summary.ts) and hands them\n * here. Nothing else in this package decides between the terminal UI and plain\n * text, waits on a spinner, or writes a blank line — which is the point. That\n * choice used to be made at each call site, so `check` and `snapshot` never drew\n * a terminal UI at all, `inspect` printed its static catalog and its mount\n * failures as raw text in the middle of a rendered report, and the same block\n * carried a different text column in each of them.\n *\n * Three invariants it exists to keep:\n *\n * - **One renderer per stream, decided once.** Ink when that stream is a\n * terminal, plain text otherwise (`AS-CLI-003`), for every part of the run.\n * - **One blank line between parts**, on both paths. Ink ends each painted frame\n * with a newline and plain text has to be asked for one, so the callers stop\n * sprinkling `\\n` and getting it wrong in one of the two.\n * - **One label grid.** The text column belongs to the report, not to the block,\n * and it can only ever widen — so `Config`, `Capabilities` and `Coverage` line\n * up whether they were printed a second or a minute apart.\n */\nimport { isPlain, loadInk, paint, transient, write, type OutputFlags } from \"../output.js\";\nimport { renderPartPlain } from \"./plain.js\";\nimport { LABEL_WIDTH, reportGrid, type ReportPart, type ReportStream } from \"./summary.js\";\n\nexport interface Presenter {\n /** Draws parts in order, one blank line between them. */\n emit(...parts: ReportPart[]): Promise<void>;\n /** Names the slow thing currently happening. A no-op in plain mode. */\n wait(label: string): Promise<void>;\n /** Clears any spinner. Called for you before anything is drawn. */\n settle(): void;\n}\n\ntype InkModule = NonNullable<Awaited<ReturnType<typeof loadInk>>>;\n\nclass ReportPresenter implements Presenter {\n /** `null` on a stream rendering plain text — piped, CI, `--plain`, React 18. */\n readonly #out: InkModule | null;\n readonly #err: InkModule | null;\n /** Sticky, and only ever wider: a grid that shrank mid-report is two grids. */\n #labelWidth = LABEL_WIDTH;\n #wrote = false;\n /**\n * Whether the last part was written as plain text. Ink's own frame supplies\n * the blank line after it; plain text has to be asked for one before the next\n * part. Tracking which drew last is what keeps the spacing identical when a\n * drawn report sends a finding to a redirected stderr.\n */\n #plainLast = false;\n #stop: (() => void) | undefined;\n\n constructor(out: InkModule | null, err: InkModule | null) {\n this.#out = out;\n this.#err = err;\n }\n\n async emit(...parts: ReportPart[]): Promise<void> {\n for (const part of parts) await this.#draw(part);\n }\n\n async wait(label: string): Promise<void> {\n this.settle();\n // Nothing transient is ever written in plain mode: `AS-CLI-003` wants\n // byte-stable output, and a spinner is neither.\n const ink = this.#out;\n if (ink) this.#stop = await transient(<ink.Loading label={label} />);\n }\n\n settle(): void {\n this.#stop?.();\n this.#stop = undefined;\n }\n\n async #draw(part: ReportPart): Promise<void> {\n // A spinner is a live frame: anything written under it lands in the region\n // Ink is about to erase.\n this.settle();\n const stream: ReportStream = part.stream ?? \"out\";\n if (part.kind === \"blocks\") {\n this.#labelWidth = Math.max(this.#labelWidth, reportGrid(part.blocks, LABEL_WIDTH).label);\n }\n const separate = this.#wrote && this.#plainLast;\n const ink = stream === \"err\" ? this.#err : this.#out;\n\n if (ink) {\n if (separate) write(\"\", stream);\n await paint(<ink.Part part={part} labelWidth={this.#labelWidth} />, stream);\n } else {\n const text = renderPartPlain(part, this.#labelWidth);\n if (text.length === 0) return;\n write(separate ? `\\n${text}` : text, stream);\n }\n this.#plainLast = ink === null;\n this.#wrote = true;\n }\n}\n\n/**\n * The presenter for this run.\n *\n * `--json` resolves to the plain path and its spinner to nothing, so a command\n * emitting data rather than a report simply has no parts to emit — it never has\n * to ask which renderer it is talking to.\n */\nexport async function createPresenter(flags: OutputFlags): Promise<Presenter> {\n const drawn = !isPlain(flags, \"out\") || !isPlain(flags, \"err\");\n // `null` when Ink cannot run here (React 18 host), which is a fallback to\n // plain text rather than a failed command — see loadInk().\n const ink = drawn ? await loadInk() : null;\n return new ReportPresenter(\n ink && !isPlain(flags, \"out\") ? ink : null,\n ink && !isPlain(flags, \"err\") ? ink : null,\n );\n}\n"],"mappings":"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","names":[]}
|