@agent-surface/cli 0.12.0 → 0.13.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 (35) hide show
  1. package/README.md +45 -7
  2. package/dist/bin.js +15 -7
  3. package/dist/bin.js.map +1 -1
  4. package/dist/check-TJIX7NDE.js +241 -0
  5. package/dist/check-TJIX7NDE.js.map +1 -0
  6. package/dist/{chunk-QIVOZAWX.js → chunk-3AJ343NA.js} +12 -3
  7. package/dist/{chunk-QIVOZAWX.js.map → chunk-3AJ343NA.js.map} +1 -1
  8. package/dist/chunk-IALBMW3R.js +278 -0
  9. package/dist/chunk-IALBMW3R.js.map +1 -0
  10. package/dist/chunk-L7GHSC2Z.js +465 -0
  11. package/dist/chunk-L7GHSC2Z.js.map +1 -0
  12. package/dist/{chunk-Q5WOLWEW.js → chunk-UGCLJ5JX.js} +26 -286
  13. package/dist/chunk-UGCLJ5JX.js.map +1 -0
  14. package/dist/chunk-VX6GBEP3.js +539 -0
  15. package/dist/chunk-VX6GBEP3.js.map +1 -0
  16. package/dist/{init-LFQ5R3G7.js → init-6XV64LK2.js} +9 -8
  17. package/dist/{init-LFQ5R3G7.js.map → init-6XV64LK2.js.map} +1 -1
  18. package/dist/{ink-P23VKP4H.js → ink-QR7X7TAC.js} +99 -25
  19. package/dist/ink-QR7X7TAC.js.map +1 -0
  20. package/dist/inspect-NJ4J3MPP.js +242 -0
  21. package/dist/inspect-NJ4J3MPP.js.map +1 -0
  22. package/dist/{snapshot-3E7MVMVG.js → snapshot-ZGTAF2Y5.js} +28 -12
  23. package/dist/snapshot-ZGTAF2Y5.js.map +1 -0
  24. package/package.json +4 -4
  25. package/dist/check-CS4Z3ZK3.js +0 -169
  26. package/dist/check-CS4Z3ZK3.js.map +0 -1
  27. package/dist/chunk-DYDSJM7R.js +0 -170
  28. package/dist/chunk-DYDSJM7R.js.map +0 -1
  29. package/dist/chunk-Q5WOLWEW.js.map +0 -1
  30. package/dist/chunk-TPWRSFK7.js +0 -380
  31. package/dist/chunk-TPWRSFK7.js.map +0 -1
  32. package/dist/ink-P23VKP4H.js.map +0 -1
  33. package/dist/inspect-ELUQ73MZ.js +0 -119
  34. package/dist/inspect-ELUQ73MZ.js.map +0 -1
  35. package/dist/snapshot-3E7MVMVG.js.map +0 -1
@@ -1,9 +1,15 @@
1
1
  import {
2
- renderCoveragePlain,
3
2
  renderFailuresPlain,
3
+ renderSurfaceSummaryPlain,
4
4
  scenarioBaseline
5
- } from "./chunk-TPWRSFK7.js";
6
- import "./chunk-DYDSJM7R.js";
5
+ } from "./chunk-L7GHSC2Z.js";
6
+ import {
7
+ buildView,
8
+ displayPath,
9
+ flatRows,
10
+ scenarioStats,
11
+ trackReach
12
+ } from "./chunk-VX6GBEP3.js";
7
13
  import {
8
14
  baselinePath,
9
15
  joinCoverage,
@@ -11,15 +17,15 @@ import {
11
17
  readInventory,
12
18
  writeBaseline,
13
19
  writeScenarioManifest
14
- } from "./chunk-Q5WOLWEW.js";
20
+ } from "./chunk-IALBMW3R.js";
15
21
  import {
16
22
  UsageError,
17
23
  write,
18
24
  writeError
19
- } from "./chunk-QIVOZAWX.js";
25
+ } from "./chunk-3AJ343NA.js";
26
+ import "./chunk-UGCLJ5JX.js";
20
27
 
21
28
  // src/commands/snapshot.ts
22
- import { relative } from "path";
23
29
  async function runSnapshot(options) {
24
30
  if (options.depth === "static") {
25
31
  throw new UsageError(
@@ -40,14 +46,24 @@ async function runSnapshot(options) {
40
46
  for (const result of runtime.results) {
41
47
  const path = baselinePath(runtime.baselineDir, result.scenario);
42
48
  writeBaseline(path, scenarioBaseline(result));
43
- write(`wrote ${relative(process.cwd(), path)}`);
49
+ write(`wrote ${displayPath(path)}`);
44
50
  }
45
51
  writeScenarioManifest(runtime.baselineDir, runtime.declaredScenarios);
46
- const coverage = joinCoverage(inventory, runtime, analysis);
47
- if (coverage) {
48
- write("");
49
- write(renderCoveragePlain(coverage));
52
+ const reach = /* @__PURE__ */ new Map();
53
+ for (const result of runtime.results) {
54
+ trackReach(reach, flatRows(buildView(result)), result.scenario);
50
55
  }
56
+ const coverage = joinCoverage(inventory, runtime, analysis);
57
+ write("");
58
+ write(
59
+ renderSurfaceSummaryPlain({
60
+ depth: options.depth,
61
+ ...coverage ? { coverage } : {},
62
+ scenarios: runtime.results.map(scenarioStats),
63
+ failures: runtime.failures.length,
64
+ reach
65
+ })
66
+ );
51
67
  if (runtime.failures.length > 0) {
52
68
  writeError(`
53
69
  ${renderFailuresPlain(runtime.failures)}`);
@@ -58,4 +74,4 @@ ${renderFailuresPlain(runtime.failures)}`);
58
74
  export {
59
75
  runSnapshot
60
76
  };
61
- //# sourceMappingURL=snapshot-3E7MVMVG.js.map
77
+ //# sourceMappingURL=snapshot-ZGTAF2Y5.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/commands/snapshot.ts"],"sourcesContent":["import {\n joinCoverage,\n mountScenarios,\n readInventory,\n UsageError,\n type AnalysisOptions,\n type Depth,\n} from \"../analysis.js\";\nimport { baselinePath, writeBaseline, writeScenarioManifest } from \"../baseline.js\";\nimport { scenarioBaseline } from \"../report.js\";\nimport { buildView, flatRows } from \"../render/model.js\";\nimport { renderFailuresPlain, renderSurfaceSummaryPlain } from \"../render/plain.js\";\nimport {\n displayPath,\n scenarioStats,\n trackReach,\n type CapabilityReach,\n} from \"../render/summary.js\";\nimport { write, writeError } from \"../output.js\";\n\nexport interface SnapshotOptions {\n configPath: string;\n depth: Depth;\n scenario?: string;\n scope?: string[];\n tsconfig?: string;\n baselineDir?: string;\n}\n\n/**\n * Writes (or refreshes) the committed baseline `check` compares against.\n *\n * It prints the coverage verdict too. This is the command you run to *accept* a\n * change to the surface, which makes it the last moment before a reviewer sees\n * the diff — and accepting a projection while a capability sits behind a route\n * no scenario visits is exactly the state worth hearing about. It reports;\n * `check` is still the only thing that fails.\n */\nexport async function runSnapshot(options: SnapshotOptions): Promise<number> {\n if (options.depth === \"static\") {\n throw new UsageError(\n \"snapshot --depth static has nothing to write — a baseline is a projection, and \" +\n \"at this depth nothing is mounted.\",\n );\n }\n\n const analysis: AnalysisOptions = {\n configPath: options.configPath,\n depth: options.depth,\n ...(options.scenario ? { scenario: options.scenario } : {}),\n ...(options.scope ? { scope: options.scope } : {}),\n ...(options.tsconfig ? { tsconfig: options.tsconfig } : {}),\n ...(options.baselineDir ? { baselineDir: options.baselineDir } : {}),\n };\n\n const inventory = readInventory(analysis);\n const runtime = await mountScenarios(analysis);\n if (!runtime) throw new UsageError(\"snapshot needs a mount, and this depth performs none\");\n\n for (const result of runtime.results) {\n const path = baselinePath(runtime.baselineDir, result.scenario);\n writeBaseline(path, scenarioBaseline(result));\n write(`wrote ${displayPath(path)}`);\n }\n writeScenarioManifest(runtime.baselineDir, runtime.declaredScenarios);\n\n const reach = new Map<string, CapabilityReach>();\n for (const result of runtime.results) {\n trackReach(reach, flatRows(buildView(result)), result.scenario);\n }\n const coverage = joinCoverage(inventory, runtime, analysis);\n write(\"\");\n write(\n renderSurfaceSummaryPlain({\n depth: options.depth,\n ...(coverage ? { coverage } : {}),\n scenarios: runtime.results.map(scenarioStats),\n failures: runtime.failures.length,\n reach,\n }),\n );\n\n if (runtime.failures.length > 0) {\n // A baseline written for some scenarios and not others is a baseline that\n // will fail `check` for a reason that has nothing to do with the surface.\n writeError(`\\n${renderFailuresPlain(runtime.failures)}`);\n return 2;\n }\n return 0;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAsCA,eAAsB,YAAY,SAA2C;AAC3E,MAAI,QAAQ,UAAU,UAAU;AAC9B,UAAM,IAAI;AAAA,MACR;AAAA,IAEF;AAAA,EACF;AAEA,QAAM,WAA4B;AAAA,IAChC,YAAY,QAAQ;AAAA,IACpB,OAAO,QAAQ;AAAA,IACf,GAAI,QAAQ,WAAW,EAAE,UAAU,QAAQ,SAAS,IAAI,CAAC;AAAA,IACzD,GAAI,QAAQ,QAAQ,EAAE,OAAO,QAAQ,MAAM,IAAI,CAAC;AAAA,IAChD,GAAI,QAAQ,WAAW,EAAE,UAAU,QAAQ,SAAS,IAAI,CAAC;AAAA,IACzD,GAAI,QAAQ,cAAc,EAAE,aAAa,QAAQ,YAAY,IAAI,CAAC;AAAA,EACpE;AAEA,QAAM,YAAY,cAAc,QAAQ;AACxC,QAAM,UAAU,MAAM,eAAe,QAAQ;AAC7C,MAAI,CAAC,QAAS,OAAM,IAAI,WAAW,sDAAsD;AAEzF,aAAW,UAAU,QAAQ,SAAS;AACpC,UAAM,OAAO,aAAa,QAAQ,aAAa,OAAO,QAAQ;AAC9D,kBAAc,MAAM,iBAAiB,MAAM,CAAC;AAC5C,UAAM,SAAS,YAAY,IAAI,CAAC,EAAE;AAAA,EACpC;AACA,wBAAsB,QAAQ,aAAa,QAAQ,iBAAiB;AAEpE,QAAM,QAAQ,oBAAI,IAA6B;AAC/C,aAAW,UAAU,QAAQ,SAAS;AACpC,eAAW,OAAO,SAAS,UAAU,MAAM,CAAC,GAAG,OAAO,QAAQ;AAAA,EAChE;AACA,QAAM,WAAW,aAAa,WAAW,SAAS,QAAQ;AAC1D,QAAM,EAAE;AACR;AAAA,IACE,0BAA0B;AAAA,MACxB,OAAO,QAAQ;AAAA,MACf,GAAI,WAAW,EAAE,SAAS,IAAI,CAAC;AAAA,MAC/B,WAAW,QAAQ,QAAQ,IAAI,aAAa;AAAA,MAC5C,UAAU,QAAQ,SAAS;AAAA,MAC3B;AAAA,IACF,CAAC;AAAA,EACH;AAEA,MAAI,QAAQ,SAAS,SAAS,GAAG;AAG/B,eAAW;AAAA,EAAK,oBAAoB,QAAQ,QAAQ,CAAC,EAAE;AACvD,WAAO;AAAA,EACT;AACA,SAAO;AACT;","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agent-surface/cli",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "description": "Inspect and check the agent surface your app exposes — in the terminal and in CI",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -31,9 +31,9 @@
31
31
  "typescript": "^5.9.2",
32
32
  "vite": "^7.1.3",
33
33
  "vite-node": "^3.2.4",
34
- "@agent-surface/core": "^0.12.0",
35
- "@agent-surface/react": "^0.12.0",
36
- "@agent-surface/testing": "^0.12.0"
34
+ "@agent-surface/react": "^0.13.0",
35
+ "@agent-surface/core": "^0.13.0",
36
+ "@agent-surface/testing": "^0.13.0"
37
37
  },
38
38
  "peerDependencies": {
39
39
  "@testing-library/react": ">=14",
@@ -1,169 +0,0 @@
1
- import {
2
- coverageReport,
3
- renderCoveragePlain,
4
- renderDriftPlain,
5
- renderFailuresPlain,
6
- renderNoVerdictPlain,
7
- scenarioBaseline
8
- } from "./chunk-TPWRSFK7.js";
9
- import "./chunk-DYDSJM7R.js";
10
- import {
11
- ALLOWLIST_FILE,
12
- SCENARIO_MANIFEST_FILE,
13
- UNREAD_ALLOWLIST_FILE,
14
- annotate,
15
- baselinePath,
16
- coverageExitCode,
17
- diff,
18
- joinCoverage,
19
- mountScenarios,
20
- readBaseline,
21
- readInventory,
22
- readScenarioManifest
23
- } from "./chunk-Q5WOLWEW.js";
24
- import {
25
- UsageError,
26
- write,
27
- writeError
28
- } from "./chunk-QIVOZAWX.js";
29
-
30
- // src/commands/check.ts
31
- import { existsSync, readdirSync } from "fs";
32
- import { relative } from "path";
33
- async function runCheck(options) {
34
- if (options.depth === "static") {
35
- throw new UsageError(
36
- "check --depth static has nothing to compare \u2014 a baseline is a projection, and at this depth nothing is mounted. Use --depth runtime for drift alone, or full for both."
37
- );
38
- }
39
- const analysis = {
40
- configPath: options.configPath,
41
- depth: options.depth,
42
- ...options.scenario ? { scenario: options.scenario } : {},
43
- ...options.scope ? { scope: options.scope } : {},
44
- ...options.tsconfig ? { tsconfig: options.tsconfig } : {},
45
- ...options.baselineDir ? { baselineDir: options.baselineDir } : {}
46
- };
47
- const inventory = readInventory(analysis);
48
- const runtime = await mountScenarios(analysis);
49
- if (!runtime) throw new UsageError("check needs a mount, and this depth performs none");
50
- const drifted = [];
51
- for (const result of runtime.results) {
52
- const path = baselinePath(runtime.baselineDir, result.scenario);
53
- const expected = readBaseline(path);
54
- if (expected === void 0) {
55
- drifted.push({ scenario: result.scenario, missingBaseline: true, entries: [] });
56
- continue;
57
- }
58
- const actual = scenarioBaseline(result);
59
- const entries = annotate(diff(expected, actual), actual, expected);
60
- if (entries.length > 0) drifted.push({ scenario: result.scenario, entries });
61
- }
62
- const committedScenarios = readScenarioManifest(runtime.baselineDir);
63
- const declared = [...runtime.declaredScenarios].sort();
64
- const scenarioManifestMismatch = committedScenarios === void 0 || JSON.stringify(committedScenarios) !== JSON.stringify(declared);
65
- const reserved = /* @__PURE__ */ new Set([SCENARIO_MANIFEST_FILE, ALLOWLIST_FILE, UNREAD_ALLOWLIST_FILE]);
66
- const staleBaselineFiles = (existsSync(runtime.baselineDir) ? readdirSync(runtime.baselineDir, { withFileTypes: true }) : []).filter((entry) => entry.isFile() && entry.name.endsWith(".json") && !reserved.has(entry.name)).map((entry) => entry.name.slice(0, -5)).filter((scenario) => !runtime.declaredScenarios.includes(scenario)).sort();
67
- const rejected = runtime.results.filter((result) => result.rejections.length > 0).map((result) => ({ scenario: result.scenario, rejections: result.rejections }));
68
- const coverage = joinCoverage(inventory, runtime, analysis);
69
- const coverageFailed = coverage !== void 0 && coverageExitCode(coverage, { allowUnresolved: options.allowUnresolved === true }) !== 0;
70
- const couldNotRun = runtime.failures.length > 0;
71
- const ok = drifted.length === 0 && !coverageFailed && !couldNotRun && rejected.length === 0 && !scenarioManifestMismatch && staleBaselineFiles.length === 0;
72
- if (options.json) {
73
- write(
74
- JSON.stringify(
75
- {
76
- ok,
77
- drifted,
78
- failures: runtime.failures,
79
- rejected,
80
- scenarioManifest: {
81
- expected: declared,
82
- committed: committedScenarios ?? null,
83
- staleBaselines: staleBaselineFiles
84
- },
85
- coverage: coverageReport(coverage)
86
- },
87
- null,
88
- 2
89
- )
90
- );
91
- return couldNotRun ? 2 : ok ? 0 : 1;
92
- }
93
- if (coverage) {
94
- const rendered = renderCoveragePlain(coverage);
95
- if (coverageFailed) writeError(rendered);
96
- else write(rendered);
97
- }
98
- const missing = drifted.filter((entry) => entry.missingBaseline);
99
- const changed = drifted.filter((entry) => !entry.missingBaseline);
100
- if (rejected.length > 0) {
101
- writeError(
102
- [
103
- `${coverage ? "\n" : ""}REJECTED REGISTRATIONS \u2014 the runtime refused authored surface (${rejected.reduce((sum, entry) => sum + entry.rejections.length, 0)})`,
104
- ...rejected.flatMap(
105
- (entry) => entry.rejections.map(
106
- (rejection) => ` ${entry.scenario}: ${rejection.componentType}@${rejection.instanceId} (${rejection.reason})`
107
- )
108
- )
109
- ].join("\n")
110
- );
111
- }
112
- if (scenarioManifestMismatch || staleBaselineFiles.length > 0) {
113
- const committed = committedScenarios?.join(", ") ?? "missing";
114
- writeError(
115
- [
116
- `${coverage || rejected.length > 0 ? "\n" : ""}SCENARIO DRIFT \u2014 committed baselines do not match config`,
117
- ` config: ${declared.join(", ")}`,
118
- ` manifest: ${committed}`,
119
- ...staleBaselineFiles.length > 0 ? [` stale baselines: ${staleBaselineFiles.join(", ")}`] : [],
120
- " run `agent-surface snapshot` and commit the manifest"
121
- ].join("\n")
122
- );
123
- }
124
- if (missing.length > 0) {
125
- writeError(
126
- [
127
- `${coverage ? "\n" : ""}NO BASELINE \u2014 nothing to compare against, which is not the same as a match (${missing.length})`,
128
- ...missing.map(
129
- (entry) => ` ${entry.scenario}: ${relative(
130
- process.cwd(),
131
- baselinePath(runtime.baselineDir, entry.scenario)
132
- )} does not exist \u2014 run \`agent-surface snapshot\` and commit it`
133
- )
134
- ].join("\n")
135
- );
136
- }
137
- if (changed.length > 0) {
138
- writeError(
139
- [
140
- `${coverage || missing.length > 0 ? "\n" : ""}DRIFT \u2014 the surface changed against its baseline (${changed.length})`,
141
- ...changed.map((entry) => renderDriftPlain(entry.scenario, entry.entries)),
142
- "",
143
- `surface drift in ${changed.length} scenario${changed.length === 1 ? "" : "s"} \u2014 review the change, then \`agent-surface snapshot\` to accept it`
144
- ].join("\n")
145
- );
146
- }
147
- if (couldNotRun) {
148
- writeError(`
149
- ${renderFailuresPlain(runtime.failures)}`);
150
- if (inventory) writeError(`
151
- ${renderNoVerdictPlain(runtime.failures)}`);
152
- return 2;
153
- }
154
- if (ok) {
155
- if (coverage) write("");
156
- write(`surface matches the baseline in ${runtime.scenarios.join(", ")}`);
157
- if (!coverage) {
158
- write(
159
- "that is a statement about these scenarios only \u2014 re-run at --depth full to find capabilities no scenario mounts"
160
- );
161
- }
162
- return 0;
163
- }
164
- return 1;
165
- }
166
- export {
167
- runCheck
168
- };
169
- //# sourceMappingURL=check-CS4Z3ZK3.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/commands/check.ts"],"sourcesContent":["import { existsSync, readdirSync } from \"node:fs\";\nimport { relative } from \"node:path\";\nimport {\n UsageError,\n joinCoverage,\n mountScenarios,\n readInventory,\n type AnalysisOptions,\n type Depth,\n} from \"../analysis.js\";\nimport {\n annotate,\n baselinePath,\n diff,\n readBaseline,\n readScenarioManifest,\n SCENARIO_MANIFEST_FILE,\n type DiffEntry,\n} from \"../baseline.js\";\nimport { ALLOWLIST_FILE, coverageExitCode, UNREAD_ALLOWLIST_FILE } from \"../coverage.js\";\nimport {\n renderCoveragePlain,\n renderDriftPlain,\n renderFailuresPlain,\n renderNoVerdictPlain,\n} from \"../render/plain.js\";\nimport { write, writeError } from \"../output.js\";\nimport { coverageReport, scenarioBaseline } from \"../report.js\";\n\nexport interface CheckOptions {\n configPath: string;\n depth: Depth;\n scenario?: string;\n scope?: string[];\n tsconfig?: string;\n baselineDir?: string;\n allowUnresolved?: boolean;\n json?: boolean;\n plain?: boolean;\n}\n\ninterface ScenarioDrift {\n scenario: string;\n missingBaseline?: boolean;\n entries: DiffEntry[];\n}\n\ninterface RejectedScenario {\n scenario: string;\n rejections: Array<{ componentType: string; instanceId: string; reason: string }>;\n}\n\n/**\n * The gate. The only command in this package that fails on a finding, which is\n * why every finding has to reach it.\n *\n * It used to fail on exactly one class — the projection drifting from its\n * baseline — and print a line telling you that capabilities no scenario mounts\n * were a different command's question. A gate that names the check it is not\n * performing is a gate with a hole in it, and in CI the hole was silent: a\n * whole unreached route sat behind a green tick.\n *\n * So it now fails on every incomplete or changed report:\n *\n * - **drift** — the surface changed against its committed baseline;\n * - **a missing baseline** — nothing to compare, which is not the same as a match;\n * - **an unreached capability** — authored, and no scenario mounts it;\n * - **an unread call site** — the catalog is incomplete, so the third check\n * above is computed over a denominator that is only a floor;\n * - **a rejected registration** — authored surface was refused;\n * - **scenario drift** — config, manifest and baseline files disagree.\n *\n * `.agent-surface/coverage-allow.json` ratchets the third; `--allow-unresolved`\n * accepts the fourth. Both are deliberate, committed decisions rather than\n * flags that quietly widen the gate.\n *\n * Output is always plain, with no rendering framework in its path at all: this\n * is a report that gets pasted into a pull request and read out of a CI log,\n * and neither of those is a terminal.\n */\nexport async function runCheck(options: CheckOptions): Promise<number> {\n if (options.depth === \"static\") {\n throw new UsageError(\n \"check --depth static has nothing to compare — a baseline is a projection, and \" +\n \"at this depth nothing is mounted. Use --depth runtime for drift alone, or full for both.\",\n );\n }\n\n const analysis: AnalysisOptions = {\n configPath: options.configPath,\n depth: options.depth,\n ...(options.scenario ? { scenario: options.scenario } : {}),\n ...(options.scope ? { scope: options.scope } : {}),\n ...(options.tsconfig ? { tsconfig: options.tsconfig } : {}),\n ...(options.baselineDir ? { baselineDir: options.baselineDir } : {}),\n };\n\n const inventory = readInventory(analysis);\n // No streaming here, unlike `inspect`. A report is read top-down and has to\n // lead with its findings, which means every finding has to exist first.\n const runtime = await mountScenarios(analysis);\n if (!runtime) throw new UsageError(\"check needs a mount, and this depth performs none\");\n\n const drifted: ScenarioDrift[] = [];\n for (const result of runtime.results) {\n const path = baselinePath(runtime.baselineDir, result.scenario);\n const expected = readBaseline(path);\n if (expected === undefined) {\n drifted.push({ scenario: result.scenario, missingBaseline: true, entries: [] });\n continue;\n }\n const actual = scenarioBaseline(result);\n const entries = annotate(diff(expected, actual), actual, expected);\n if (entries.length > 0) drifted.push({ scenario: result.scenario, entries });\n }\n\n const committedScenarios = readScenarioManifest(runtime.baselineDir);\n const declared = [...runtime.declaredScenarios].sort();\n const scenarioManifestMismatch =\n committedScenarios === undefined ||\n JSON.stringify(committedScenarios) !== JSON.stringify(declared);\n const reserved = new Set([SCENARIO_MANIFEST_FILE, ALLOWLIST_FILE, UNREAD_ALLOWLIST_FILE]);\n const staleBaselineFiles = (existsSync(runtime.baselineDir)\n ? readdirSync(runtime.baselineDir, { withFileTypes: true })\n : [])\n .filter((entry) => entry.isFile() && entry.name.endsWith(\".json\") && !reserved.has(entry.name))\n .map((entry) => entry.name.slice(0, -5))\n .filter((scenario) => !runtime.declaredScenarios.includes(scenario))\n .sort();\n\n const rejected: RejectedScenario[] = runtime.results\n .filter((result) => result.rejections.length > 0)\n .map((result) => ({ scenario: result.scenario, rejections: result.rejections }));\n\n const coverage = joinCoverage(inventory, runtime, analysis);\n const coverageFailed =\n coverage !== undefined &&\n coverageExitCode(coverage, { allowUnresolved: options.allowUnresolved === true }) !== 0;\n const couldNotRun = runtime.failures.length > 0;\n const ok =\n drifted.length === 0 &&\n !coverageFailed &&\n !couldNotRun &&\n rejected.length === 0 &&\n !scenarioManifestMismatch &&\n staleBaselineFiles.length === 0;\n\n if (options.json) {\n write(\n JSON.stringify(\n {\n ok,\n drifted,\n failures: runtime.failures,\n rejected,\n scenarioManifest: {\n expected: declared,\n committed: committedScenarios ?? null,\n staleBaselines: staleBaselineFiles,\n },\n coverage: coverageReport(coverage),\n },\n null,\n 2,\n ),\n );\n return couldNotRun ? 2 : ok ? 0 : 1;\n }\n\n // The gap leads, because it is the finding this command could not previously\n // make at all. Drift follows, because it is the one it always could.\n if (coverage) {\n const rendered = renderCoveragePlain(coverage);\n if (coverageFailed) writeError(rendered);\n else write(rendered);\n }\n\n // \"No baseline\" is not drift, and filing it under a heading that says the\n // surface changed would be a claim about a comparison that never happened.\n const missing = drifted.filter((entry) => entry.missingBaseline);\n const changed = drifted.filter((entry) => !entry.missingBaseline);\n\n if (rejected.length > 0) {\n writeError(\n [\n `${coverage ? \"\\n\" : \"\"}REJECTED REGISTRATIONS — the runtime refused authored surface (${rejected.reduce((sum, entry) => sum + entry.rejections.length, 0)})`,\n ...rejected.flatMap((entry) =>\n entry.rejections.map(\n (rejection) =>\n ` ${entry.scenario}: ${rejection.componentType}@${rejection.instanceId} (${rejection.reason})`,\n ),\n ),\n ].join(\"\\n\"),\n );\n }\n\n if (scenarioManifestMismatch || staleBaselineFiles.length > 0) {\n const committed = committedScenarios?.join(\", \") ?? \"missing\";\n writeError(\n [\n `${coverage || rejected.length > 0 ? \"\\n\" : \"\"}SCENARIO DRIFT — committed baselines do not match config`,\n ` config: ${declared.join(\", \")}`,\n ` manifest: ${committed}`,\n ...(staleBaselineFiles.length > 0\n ? [` stale baselines: ${staleBaselineFiles.join(\", \")}`]\n : []),\n \" run `agent-surface snapshot` and commit the manifest\",\n ].join(\"\\n\"),\n );\n }\n\n if (missing.length > 0) {\n writeError(\n [\n `${coverage ? \"\\n\" : \"\"}NO BASELINE — nothing to compare against, which is not the same as a match (${missing.length})`,\n ...missing.map(\n (entry) =>\n ` ${entry.scenario}: ${relative(\n process.cwd(),\n baselinePath(runtime.baselineDir, entry.scenario),\n )} does not exist — run \\`agent-surface snapshot\\` and commit it`,\n ),\n ].join(\"\\n\"),\n );\n }\n\n if (changed.length > 0) {\n writeError(\n [\n `${coverage || missing.length > 0 ? \"\\n\" : \"\"}DRIFT — the surface changed against its baseline (${changed.length})`,\n ...changed.map((entry) => renderDriftPlain(entry.scenario, entry.entries)),\n \"\",\n `surface drift in ${changed.length} scenario${\n changed.length === 1 ? \"\" : \"s\"\n } — review the change, then \\`agent-surface snapshot\\` to accept it`,\n ].join(\"\\n\"),\n );\n }\n\n if (couldNotRun) {\n writeError(`\\n${renderFailuresPlain(runtime.failures)}`);\n if (inventory) writeError(`\\n${renderNoVerdictPlain(runtime.failures)}`);\n return 2;\n }\n\n if (ok) {\n if (coverage) write(\"\");\n // Naming them is the point (`AS-CLI-007`). This used to have to add that it\n // was a statement about these scenarios only, and point at another command\n // for the rest; at `--depth full` there is no rest, so the caveat is gone\n // along with the command it pointed at.\n write(`surface matches the baseline in ${runtime.scenarios.join(\", \")}`);\n if (!coverage) {\n write(\n \"that is a statement about these scenarios only — re-run at --depth full to find \" +\n \"capabilities no scenario mounts\",\n );\n }\n return 0;\n }\n return 1;\n}\n"],"mappings":"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","names":[]}
@@ -1,170 +0,0 @@
1
- // src/render/model.ts
2
- function flatRows(view) {
3
- return view.groups.flatMap((group) => group.rows);
4
- }
5
- function pathOf(capabilityId) {
6
- return capabilityId.replace(/^(view|domain):/, "");
7
- }
8
- function leafOf(capabilityId) {
9
- const withoutPlane = pathOf(capabilityId);
10
- const dot = withoutPlane.lastIndexOf(".");
11
- return dot === -1 ? withoutPlane : withoutPlane.slice(dot + 1);
12
- }
13
- function actionFlags(action) {
14
- const flags = [];
15
- if (action.idempotent) flags.push("idempotent");
16
- if (action.reversible) flags.push("reversible");
17
- if (action.confirmation !== "never") flags.push(`confirmation:${action.confirmation}`);
18
- return flags;
19
- }
20
- function procedureFlags(procedure) {
21
- const flags = [];
22
- if (procedure.confirmation !== "never") flags.push(`confirmation:${procedure.confirmation}`);
23
- for (const field of procedure.boundFields) {
24
- flags.push(`${field.path} bound${field.locked ? "+locked" : ""}`);
25
- }
26
- return flags;
27
- }
28
- function explanationIndex(explanation) {
29
- const index = /* @__PURE__ */ new Map();
30
- for (const capability of explanation.capabilities) {
31
- index.set(`${capability.capabilityId}\0${capability.registrationId}`, capability);
32
- }
33
- return index;
34
- }
35
- function buildView(result, options = {}) {
36
- const { snapshot, explanation } = result;
37
- const index = explanationIndex(explanation);
38
- const groups = [];
39
- const counts = { callable: 0, disabled: 0, hidden: 0 };
40
- const enrich = (row, capabilityId, registrationId) => {
41
- const explained = index.get(`${capabilityId}\0${registrationId}`);
42
- if (options.explain && explained) {
43
- row.policies = explained.policies;
44
- row.availability = explained.availability;
45
- }
46
- return row;
47
- };
48
- for (const component of snapshot.components) {
49
- const rows = [];
50
- for (const observation of component.observations) {
51
- rows.push(
52
- enrich(
53
- rowFor(observation, "observation", void 0, [], options, {
54
- input: void 0,
55
- output: observation.outputSchema
56
- }),
57
- observation.capabilityId,
58
- component.registrationId
59
- )
60
- );
61
- }
62
- for (const action of component.actions) {
63
- rows.push(
64
- enrich(
65
- rowFor(action, "action", action.effect, actionFlags(action), options, {
66
- input: action.inputSchema,
67
- output: action.outputSchema
68
- }),
69
- action.capabilityId,
70
- component.registrationId
71
- )
72
- );
73
- }
74
- groups.push({
75
- heading: component.instanceId === "default" ? component.type : `${component.type}@${component.instanceId}`,
76
- rows
77
- });
78
- }
79
- if (snapshot.procedures.length > 0) {
80
- groups.push({
81
- heading: "authoritative (domain)",
82
- rows: snapshot.procedures.map(
83
- (procedure) => enrich(
84
- {
85
- capabilityId: procedure.procedureId,
86
- name: procedure.procedureId.replace(/^domain:/, ""),
87
- path: pathOf(procedure.procedureId),
88
- kind: "procedure",
89
- plane: "domain",
90
- outcome: procedure.available ? "expose" : "disable",
91
- description: procedure.description,
92
- ...procedure.unavailableReason ? { reason: procedure.unavailableReason } : {},
93
- effect: procedure.effect,
94
- flags: procedureFlags(procedure),
95
- tags: [procedure.effect, ...procedureFlags(procedure)],
96
- ...options.schemas ? { schemas: { input: procedure.inputSchema, output: procedure.outputSchema } } : {}
97
- },
98
- procedure.procedureId,
99
- procedure.registrationId
100
- )
101
- )
102
- });
103
- }
104
- const hidden = explanation.capabilities.filter((c) => c.outcome === "hide");
105
- if (hidden.length > 0) {
106
- groups.push({
107
- heading: "hidden by policy (absent from the snapshot)",
108
- rows: hidden.map((capability) => ({
109
- capabilityId: capability.capabilityId,
110
- name: leafOf(capability.capabilityId),
111
- path: pathOf(capability.capabilityId),
112
- kind: capability.kind,
113
- plane: capability.plane,
114
- outcome: "hide",
115
- description: capability.description,
116
- // No reason line, deliberately. The reason a hidden capability carries
117
- // is its *availability* reason — "The drawer is not open" — and printing
118
- // that under a row marked `hidden` says the UI declined when authority
119
- // did. Authority hides, state discloses (D11/D12), and the two must
120
- // never look alike. Why it was hidden is a policy question, which is
121
- // what `--explain` answers.
122
- //
123
- // A hidden capability has no snapshot entry, so there is no effect to
124
- // report — the table prints an em dash rather than inventing one. The
125
- // capability path already carries the component type; only a non-default
126
- // instance adds anything.
127
- flags: capability.component.instanceId === "default" ? [] : [`@${capability.component.instanceId}`],
128
- tags: [`${capability.component.type}@${capability.component.instanceId}`],
129
- ...options.explain ? { policies: capability.policies, availability: capability.availability } : {}
130
- }))
131
- });
132
- }
133
- for (const capability of explanation.capabilities) {
134
- if (capability.outcome === "expose") counts.callable += 1;
135
- else if (capability.outcome === "disable") counts.disabled += 1;
136
- else counts.hidden += 1;
137
- }
138
- return {
139
- scenario: result.scenario,
140
- ...snapshot.route?.path ? { route: snapshot.route.path } : {},
141
- ...result.scope ? { scope: result.scope } : {},
142
- groups,
143
- counts,
144
- rejections: result.rejections ?? [],
145
- explained: options.explain === true
146
- };
147
- }
148
- function rowFor(descriptor, kind, effect, flags, options, schemas) {
149
- return {
150
- capabilityId: descriptor.capabilityId,
151
- name: descriptor.name,
152
- path: pathOf(descriptor.capabilityId),
153
- kind,
154
- plane: "view",
155
- outcome: descriptor.available ? "expose" : "disable",
156
- description: descriptor.description,
157
- ...descriptor.unavailableReason ? { reason: descriptor.unavailableReason } : {},
158
- ...effect ? { effect } : {},
159
- flags,
160
- // The grouped detail view prints one combined list, the way it always has.
161
- tags: effect ? [effect, ...flags] : [kind, ...flags],
162
- ...options.schemas ? { schemas } : {}
163
- };
164
- }
165
-
166
- export {
167
- flatRows,
168
- buildView
169
- };
170
- //# sourceMappingURL=chunk-DYDSJM7R.js.map
@@ -1 +0,0 @@
1
- {"version":3,"sources":["../src/render/model.ts"],"sourcesContent":["import type {\n AgentActionDescriptor,\n AgentObservationDescriptor,\n AgentProcedureDescriptor,\n AgentSurfaceSnapshot,\n} from \"@agent-surface/core\";\nimport type { CapabilityExplanation, SurfaceExplanation } from \"@agent-surface/core/explain\";\nimport type { CollectResult, RegistrationRejection } from \"../collect.js\";\n\n/**\n * One view model, two renderers. The Ink UI and the plain-text fallback both\n * consume this, so `--plain` can never drift into showing something different\n * from what a TTY shows.\n */\nexport interface CapabilityRow {\n capabilityId: string;\n /** Leaf name — the group heading already carries the rest of the id. */\n name: string;\n /**\n * The id minus its plane prefix. The table is flat, so its first column has\n * to carry the whole path; the grouped detail view uses `name`.\n */\n path: string;\n kind: \"observation\" | \"action\" | \"procedure\";\n plane: \"view\" | \"domain\";\n outcome: \"expose\" | \"disable\" | \"hide\";\n description: string;\n reason?: string;\n /** The effect, alone, for the table's own column. Observations have none. */\n effect?: string;\n /** What is left of `tags` once the effect has its own column. */\n flags: string[];\n /** Effect and flags together — what the grouped detail view prints. */\n tags: string[];\n policies?: CapabilityExplanation[\"policies\"];\n availability?: CapabilityExplanation[\"availability\"];\n schemas?: { input?: unknown; output?: unknown };\n}\n\nexport interface CapabilityGroup {\n heading: string;\n rows: CapabilityRow[];\n}\n\nexport interface SurfaceView {\n scenario: string;\n route?: string;\n /**\n * The scope the counts below were computed under (`AS-CLI-007`). A scope\n * filters the snapshot *and* the explanation, so without it on screen the\n * header reads as a statement about the whole surface when it is a statement\n * about one prefix of it.\n */\n scope?: string[];\n groups: CapabilityGroup[];\n counts: { callable: number; disabled: number; hidden: number };\n /** Refused during the mount — absent from both projections (`AS-CLI-006`). */\n rejections: RegistrationRejection[];\n explained: boolean;\n}\n\nexport interface ViewOptions {\n explain?: boolean;\n schemas?: boolean;\n}\n\n/**\n * The table is flat — `groups` exist for the grouped detail view, which is what\n * `--detail`, `--explain` and `--schemas` render. Flattening here rather than in\n * each renderer keeps the two views over one order.\n */\nexport function flatRows(view: SurfaceView): CapabilityRow[] {\n return view.groups.flatMap((group) => group.rows);\n}\n\nfunction pathOf(capabilityId: string): string {\n return capabilityId.replace(/^(view|domain):/, \"\");\n}\n\nfunction leafOf(capabilityId: string): string {\n const withoutPlane = pathOf(capabilityId);\n const dot = withoutPlane.lastIndexOf(\".\");\n return dot === -1 ? withoutPlane : withoutPlane.slice(dot + 1);\n}\n\n/**\n * The effect gets its own table column; everything else is a flag. An\n * observation reads state and has no effect at all, which the table shows as\n * an em dash rather than inventing one.\n */\nfunction actionFlags(action: AgentActionDescriptor): string[] {\n const flags: string[] = [];\n if (action.idempotent) flags.push(\"idempotent\");\n if (action.reversible) flags.push(\"reversible\");\n if (action.confirmation !== \"never\") flags.push(`confirmation:${action.confirmation}`);\n return flags;\n}\n\nfunction procedureFlags(procedure: AgentProcedureDescriptor): string[] {\n const flags: string[] = [];\n if (procedure.confirmation !== \"never\") flags.push(`confirmation:${procedure.confirmation}`);\n for (const field of procedure.boundFields) {\n flags.push(`${field.path} bound${field.locked ? \"+locked\" : \"\"}`);\n }\n return flags;\n}\n\nfunction explanationIndex(explanation: SurfaceExplanation): Map<string, CapabilityExplanation> {\n const index = new Map<string, CapabilityExplanation>();\n for (const capability of explanation.capabilities) {\n // Keyed by id + registration so two instances of one component stay apart.\n index.set(`${capability.capabilityId}\\u0000${capability.registrationId}`, capability);\n }\n return index;\n}\n\nexport function buildView(result: CollectResult, options: ViewOptions = {}): SurfaceView {\n const { snapshot, explanation } = result;\n const index = explanationIndex(explanation);\n const groups: CapabilityGroup[] = [];\n const counts = { callable: 0, disabled: 0, hidden: 0 };\n\n const enrich = (\n row: CapabilityRow,\n capabilityId: string,\n registrationId: string,\n ): CapabilityRow => {\n const explained = index.get(`${capabilityId}\\u0000${registrationId}`);\n if (options.explain && explained) {\n row.policies = explained.policies;\n row.availability = explained.availability;\n }\n return row;\n };\n\n for (const component of snapshot.components) {\n const rows: CapabilityRow[] = [];\n\n for (const observation of component.observations) {\n rows.push(\n enrich(\n rowFor(observation, \"observation\", undefined, [], options, {\n input: undefined,\n output: observation.outputSchema,\n }),\n observation.capabilityId,\n component.registrationId,\n ),\n );\n }\n for (const action of component.actions) {\n rows.push(\n enrich(\n rowFor(action, \"action\", action.effect, actionFlags(action), options, {\n input: action.inputSchema,\n output: action.outputSchema,\n }),\n action.capabilityId,\n component.registrationId,\n ),\n );\n }\n\n groups.push({\n heading:\n component.instanceId === \"default\"\n ? component.type\n : `${component.type}@${component.instanceId}`,\n rows,\n });\n }\n\n if (snapshot.procedures.length > 0) {\n groups.push({\n heading: \"authoritative (domain)\",\n rows: snapshot.procedures.map((procedure) =>\n enrich(\n {\n capabilityId: procedure.procedureId,\n name: procedure.procedureId.replace(/^domain:/, \"\"),\n path: pathOf(procedure.procedureId),\n kind: \"procedure\",\n plane: \"domain\",\n outcome: procedure.available ? \"expose\" : \"disable\",\n description: procedure.description,\n ...(procedure.unavailableReason ? { reason: procedure.unavailableReason } : {}),\n effect: procedure.effect,\n flags: procedureFlags(procedure),\n tags: [procedure.effect, ...procedureFlags(procedure)],\n ...(options.schemas\n ? { schemas: { input: procedure.inputSchema, output: procedure.outputSchema } }\n : {}),\n },\n procedure.procedureId,\n procedure.registrationId,\n ),\n ),\n });\n }\n\n // Hidden capabilities exist only in the explanation — that is the whole point\n // of it. They get their own group so nobody mistakes them for callable.\n //\n // Unconditional, not behind `--explain`, for the reason `AS-CLI-007` moved\n // the hidden *count* out from behind it: signed out, the example app rendered\n // `0 callable, 0 visible-disabled` over eleven perfectly good capabilities\n // that authority had hidden, and a reader who did not know to re-run with a\n // flag read that as an app which annotated nothing. The explanation is\n // collected on every run regardless, so this costs nothing. The policy\n // *attribution* still needs `--explain`; only the rows moved.\n const hidden = explanation.capabilities.filter((c) => c.outcome === \"hide\");\n if (hidden.length > 0) {\n groups.push({\n heading: \"hidden by policy (absent from the snapshot)\",\n rows: hidden.map((capability) => ({\n capabilityId: capability.capabilityId,\n name: leafOf(capability.capabilityId),\n path: pathOf(capability.capabilityId),\n kind: capability.kind,\n plane: capability.plane,\n outcome: \"hide\" as const,\n description: capability.description,\n // No reason line, deliberately. The reason a hidden capability carries\n // is its *availability* reason — \"The drawer is not open\" — and printing\n // that under a row marked `hidden` says the UI declined when authority\n // did. Authority hides, state discloses (D11/D12), and the two must\n // never look alike. Why it was hidden is a policy question, which is\n // what `--explain` answers.\n //\n // A hidden capability has no snapshot entry, so there is no effect to\n // report — the table prints an em dash rather than inventing one. The\n // capability path already carries the component type; only a non-default\n // instance adds anything.\n flags:\n capability.component.instanceId === \"default\"\n ? []\n : [`@${capability.component.instanceId}`],\n tags: [`${capability.component.type}@${capability.component.instanceId}`],\n ...(options.explain\n ? { policies: capability.policies, availability: capability.availability }\n : {}),\n })),\n });\n }\n\n for (const capability of explanation.capabilities) {\n if (capability.outcome === \"expose\") counts.callable += 1;\n else if (capability.outcome === \"disable\") counts.disabled += 1;\n else counts.hidden += 1;\n }\n\n return {\n scenario: result.scenario,\n ...(snapshot.route?.path ? { route: snapshot.route.path } : {}),\n ...(result.scope ? { scope: result.scope } : {}),\n groups,\n counts,\n rejections: result.rejections ?? [],\n explained: options.explain === true,\n };\n}\n\nfunction rowFor(\n descriptor: AgentObservationDescriptor | AgentActionDescriptor,\n kind: \"observation\" | \"action\",\n effect: string | undefined,\n flags: string[],\n options: ViewOptions,\n schemas: { input?: unknown; output?: unknown },\n): CapabilityRow {\n return {\n capabilityId: descriptor.capabilityId,\n name: descriptor.name,\n path: pathOf(descriptor.capabilityId),\n kind,\n plane: \"view\",\n outcome: descriptor.available ? \"expose\" : \"disable\",\n description: descriptor.description,\n ...(descriptor.unavailableReason ? { reason: descriptor.unavailableReason } : {}),\n ...(effect ? { effect } : {}),\n flags,\n // The grouped detail view prints one combined list, the way it always has.\n tags: effect ? [effect, ...flags] : [kind, ...flags],\n ...(options.schemas ? { schemas } : {}),\n };\n}\n"],"mappings":"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","names":[]}