@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.
- package/README.md +64 -23
- package/dist/bin.js +75 -28
- package/dist/bin.js.map +1 -1
- package/dist/check-BYQ34OVQ.js +122 -0
- package/dist/check-BYQ34OVQ.js.map +1 -0
- package/dist/chunk-DYDSJM7R.js +170 -0
- package/dist/chunk-DYDSJM7R.js.map +1 -0
- package/dist/chunk-GXXKZTQB.js +553 -0
- package/dist/chunk-GXXKZTQB.js.map +1 -0
- package/dist/{chunk-S2LM3N6D.js → chunk-QIVOZAWX.js} +14 -12
- package/dist/chunk-QIVOZAWX.js.map +1 -0
- package/dist/chunk-RXX63JSL.js +298 -0
- package/dist/chunk-RXX63JSL.js.map +1 -0
- package/dist/collect.js +18 -1
- package/dist/collect.js.map +1 -1
- package/dist/index.d.ts +126 -1
- package/dist/init-LYYVXFEQ.js +141 -0
- package/dist/init-LYYVXFEQ.js.map +1 -0
- package/dist/ink-P23VKP4H.js +217 -0
- package/dist/ink-P23VKP4H.js.map +1 -0
- package/dist/inspect-3CBRKTDM.js +108 -0
- package/dist/inspect-3CBRKTDM.js.map +1 -0
- package/dist/snapshot-DJ22WCT4.js +59 -0
- package/dist/snapshot-DJ22WCT4.js.map +1 -0
- package/package.json +5 -4
- package/dist/check-7Z2VNN5R.js +0 -79
- package/dist/check-7Z2VNN5R.js.map +0 -1
- package/dist/chunk-KZUR4CAU.js +0 -84
- package/dist/chunk-KZUR4CAU.js.map +0 -1
- package/dist/chunk-ODUIFFPM.js +0 -104
- package/dist/chunk-ODUIFFPM.js.map +0 -1
- package/dist/chunk-S2LM3N6D.js.map +0 -1
- package/dist/ink-GCWDO4ML.js +0 -122
- package/dist/ink-GCWDO4ML.js.map +0 -1
- package/dist/inspect-SLYLBZAH.js +0 -204
- package/dist/inspect-SLYLBZAH.js.map +0 -1
- package/dist/snapshot-3D55FL63.js +0 -36
- package/dist/snapshot-3D55FL63.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
# @agent-surface/cli
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The agent surface your app exposes — from a terminal, and in CI. It answers three questions: *what can an agent do here right now*, *did that change without anyone noticing*, and *did we author something no scenario ever reaches*. Part of [agent-surface](https://github.com/Wiseair-srl/agent-surface).
|
|
4
|
+
|
|
5
|
+
It is a developer tool: nothing it prints ever reaches a model.
|
|
4
6
|
|
|
5
7
|
Docs: https://agent-surface-docs.vercel.app/20-cli
|
|
6
8
|
|
|
@@ -37,35 +39,47 @@ Loading goes through vite-node on your own `vite.config.*`, so your aliases, plu
|
|
|
37
39
|
## Use
|
|
38
40
|
|
|
39
41
|
```bash
|
|
40
|
-
agent-surface
|
|
42
|
+
agent-surface init # read the codebase, then scaffold a config
|
|
43
|
+
agent-surface inspect [scenario] # what an agent can reach, and what it cannot
|
|
41
44
|
agent-surface snapshot [scenario] # write/refresh the committed baseline
|
|
42
|
-
agent-surface check [scenario] # exit non-zero
|
|
45
|
+
agent-surface check [scenario] # exit non-zero on drift, or on a capability no scenario reaches
|
|
43
46
|
```
|
|
44
47
|
|
|
45
48
|
Every command covers all scenarios in the config unless you name one. `inspect` prints each in turn:
|
|
46
49
|
|
|
47
50
|
```text
|
|
51
|
+
10 authored (upper bound) · 10 call sites across 21 files · domain not analyzed, it comes from the oRPC router
|
|
52
|
+
|
|
48
53
|
scenario admin route /devices
|
|
49
|
-
9 callable, 2 visible-disabled
|
|
54
|
+
9 callable, 2 visible-disabled, 0 hidden
|
|
55
|
+
|
|
56
|
+
CAPABILITY KIND EFFECT STATE FLAGS
|
|
57
|
+
app.navigation.goTo action navigation callable reversible
|
|
58
|
+
devices.drawer.open action local-state callable reversible
|
|
59
|
+
devices.drawer.close action local-state disabled reversible
|
|
60
|
+
⤷ The drawer is not open
|
|
61
|
+
devices.table.sort action local-state callable idempotent · reversible
|
|
62
|
+
devices.disable procedure destructive disabled confirmation:required · deviceIds bound+locked
|
|
63
|
+
⤷ Select at least one device first
|
|
64
|
+
|
|
65
|
+
10 authored · 10 reached · 0 unreached · 1 scenario (admin)
|
|
66
|
+
```
|
|
50
67
|
|
|
51
|
-
|
|
52
|
-
+ open [local-state, reversible]
|
|
53
|
-
Open the detail drawer for a device
|
|
54
|
-
~ close [local-state, reversible]
|
|
55
|
-
Close the detail drawer
|
|
56
|
-
reason: The drawer is not open
|
|
68
|
+
Every count names what it is relative to — the scenario always, the scope when one is active. A surface is a projection of one mounted context, never "the app".
|
|
57
69
|
|
|
58
|
-
|
|
59
|
-
~ devices.disable [destructive, confirmation:required, deviceIds bound+locked]
|
|
60
|
-
Disable the given devices
|
|
61
|
-
reason: Select at least one device first
|
|
70
|
+
`--detail` swaps the table for one paragraph per capability; `--explain` and `--schemas` imply it.
|
|
62
71
|
|
|
63
|
-
|
|
64
|
-
0 callable, 0 visible-disabled
|
|
72
|
+
### Depth
|
|
65
73
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
74
|
+
A presentation surface has two sources of truth, and `--depth` says how much of each to compute:
|
|
75
|
+
|
|
76
|
+
| | reads the TypeScript program | mounts the scenarios | answers |
|
|
77
|
+
|---|---|---|---|
|
|
78
|
+
| `--depth static` | ✔ | — | what this codebase authors |
|
|
79
|
+
| `--depth runtime` | — | ✔ | what a mounted scenario surfaces |
|
|
80
|
+
| `--depth full` *(default)* | ✔ | ✔ | both, **and the difference between them** |
|
|
81
|
+
|
|
82
|
+
`full` is the default because a tool that has to be asked for the complete answer mostly gives the incomplete one. `static` needs no scenarios and survives an app that will not mount; `runtime` skips the program boot on a repository wide enough to feel it.
|
|
69
83
|
|
|
70
84
|
### Why is my capability missing?
|
|
71
85
|
|
|
@@ -76,15 +90,40 @@ agent-surface inspect anonymous --explain
|
|
|
76
90
|
```
|
|
77
91
|
|
|
78
92
|
```text
|
|
93
|
+
scenario anonymous route /devices
|
|
79
94
|
0 callable, 0 visible-disabled, 11 hidden
|
|
80
95
|
|
|
81
96
|
hidden by policy (absent from the snapshot) (11)
|
|
82
97
|
- set [devices.filters@default]
|
|
83
|
-
|
|
84
|
-
|
|
98
|
+
Update one or both filters; omitted fields are unchanged.
|
|
99
|
+
policy authenticated (registry, discovery/authorize): hide
|
|
85
100
|
```
|
|
86
101
|
|
|
87
|
-
|
|
102
|
+
The hidden capabilities themselves print without the flag — signed out, that scenario shows eleven rows marked `hidden` rather than a bare `0 callable`. `--explain` adds the attribution: every policy in the chain, in the order it runs, with its own vote, the layer it came from, and whether its `onDiscovery` threw. Availability is reported apart from the policy votes — *authority hides, state discloses*, and the two failures must never look alike.
|
|
103
|
+
|
|
104
|
+
### What did we author that no scenario reaches?
|
|
105
|
+
|
|
106
|
+
A route nobody visits never registers, so it is in no snapshot and drifts against no baseline — invisible to a mount by construction. The static half reads the TypeScript program instead, and the difference is a first-class finding in **every** command:
|
|
107
|
+
|
|
108
|
+
```text
|
|
109
|
+
UNREACHED — authored, and no scenario mounts it (1)
|
|
110
|
+
CAPABILITY ORIGIN
|
|
111
|
+
view:cov.unmounted.toCsv Unmounted.tsx:26
|
|
112
|
+
|
|
113
|
+
3 authored · 2 reached · 1 unreached · 1 scenario (default)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
`inspect` reports it and `snapshot` reports it; **`check` fails on it**. It also fails on an unread call site, because the catalog is `unreached`'s denominator and holes in it make that count a floor rather than an answer — pass `--allow-unresolved` to accept that knowingly, which still prints the gap. Adoption ratchets through a committed `.agent-surface/coverage-allow.json`, whose stale entries fail the command.
|
|
117
|
+
|
|
118
|
+
If any scenario fails to mount there is **no verdict at all**: that scenario reached nothing, so everything it would have surfaced would read as unreached. The failed scenarios are named instead, and the static half still prints.
|
|
119
|
+
|
|
120
|
+
### Exit codes
|
|
121
|
+
|
|
122
|
+
**0** clean · **1** a finding · **2** the command could not run.
|
|
123
|
+
|
|
124
|
+
`inspect` never exits `1` — it reports, and `check` gates. CI has to be able to tell "the surface changed" from "the tool never loaded the app", because the second one passing silently is how a gate rots.
|
|
125
|
+
|
|
126
|
+
> Upgrading from 0.10? `capabilities` is now `inspect --depth static`, and `coverage` is folded into `inspect` and `check`. Both were removed rather than aliased; naming either still prints where its answer went.
|
|
88
127
|
|
|
89
128
|
### The scenarios are not a fixture
|
|
90
129
|
|
|
@@ -100,7 +139,9 @@ expect(surface).toExpose("view:devices.filters.set");
|
|
|
100
139
|
|
|
101
140
|
## Notes
|
|
102
141
|
|
|
103
|
-
Requires `@testing-library/react` and `react-dom` (peers) —
|
|
142
|
+
Requires `@testing-library/react` and `react-dom` (peers) — anything that mounts does so in jsdom, so anything needing a real browser is out of reach by construction. `--depth static` and `init` mount nothing at all, and install no DOM.
|
|
143
|
+
|
|
144
|
+
Output falls back to plain text when piped, or under `--plain`, `CI` and `NO_COLOR`. stdout carries the command's output and nothing else; diagnostics go to stderr.
|
|
104
145
|
|
|
105
146
|
Full specification: [docs/20](https://github.com/Wiseair-srl/agent-surface/blob/main/docs/20-cli.md).
|
|
106
147
|
|
package/dist/bin.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
+
DEPTHS,
|
|
3
4
|
findConfig,
|
|
5
|
+
isDepth,
|
|
4
6
|
write,
|
|
5
7
|
writeError
|
|
6
|
-
} from "./chunk-
|
|
8
|
+
} from "./chunk-QIVOZAWX.js";
|
|
7
9
|
|
|
8
10
|
// src/bin.ts
|
|
9
11
|
import { realpathSync } from "fs";
|
|
@@ -35,26 +37,43 @@ function installDom(url = "http://localhost/") {
|
|
|
35
37
|
}
|
|
36
38
|
|
|
37
39
|
// src/bin.ts
|
|
38
|
-
var USAGE = `agent-surface \u2014
|
|
40
|
+
var USAGE = `agent-surface \u2014 the agent surface your app exposes
|
|
39
41
|
|
|
40
42
|
Usage
|
|
41
|
-
agent-surface
|
|
42
|
-
agent-surface
|
|
43
|
-
agent-surface
|
|
43
|
+
agent-surface init read the codebase, then scaffold a config
|
|
44
|
+
agent-surface inspect [scenario] what an agent can reach, and what it cannot
|
|
45
|
+
agent-surface snapshot [scenario] write/refresh the committed baseline
|
|
46
|
+
agent-surface check [scenario] fail on drift, or on a capability no scenario reaches
|
|
44
47
|
|
|
45
48
|
Every command covers all scenarios in the config unless you name one.
|
|
46
49
|
|
|
50
|
+
Depth
|
|
51
|
+
--depth full read the source AND mount the scenarios, and report the gap (default)
|
|
52
|
+
--depth static read the source only \u2014 no Vite, no jsdom, no mount, no scenarios needed
|
|
53
|
+
--depth runtime mount only \u2014 skip the TypeScript program on a repo wide enough to feel it
|
|
54
|
+
|
|
47
55
|
Options
|
|
48
|
-
--config <path>
|
|
49
|
-
--baseline-dir
|
|
50
|
-
--scope <prefix>
|
|
51
|
-
--
|
|
52
|
-
--
|
|
53
|
-
--
|
|
54
|
-
--
|
|
55
|
-
-
|
|
56
|
-
|
|
56
|
+
--config <path> path to agent-surface.config.* (default: nearest, searching upward)
|
|
57
|
+
--baseline-dir where baselines live (default: .agent-surface next to the config)
|
|
58
|
+
--scope <prefix> restrict to a component-type prefix (repeatable)
|
|
59
|
+
--detail one paragraph per capability instead of the table
|
|
60
|
+
--explain name the policies behind every decision (implies --detail)
|
|
61
|
+
--schemas include input/output JSON Schemas (implies --detail)
|
|
62
|
+
--tsconfig <path> tsconfig the source read uses (default: nearest to the config)
|
|
63
|
+
--allow-unresolved check: do not fail on a call site that could not be read
|
|
64
|
+
--yes init: write without asking
|
|
65
|
+
--json emit data instead of a rendered view
|
|
66
|
+
--plain force plain text (implied when piped, or under CI / NO_COLOR)
|
|
67
|
+
-h, --help show this
|
|
68
|
+
-v, --version print the version
|
|
69
|
+
|
|
70
|
+
Exit codes are the contract: 0 clean, 1 a finding, 2 the command could not run.
|
|
57
71
|
`;
|
|
72
|
+
var COMMANDS = ["init", "inspect", "snapshot", "check"];
|
|
73
|
+
var RETIRED = {
|
|
74
|
+
capabilities: "agent-surface inspect --depth static",
|
|
75
|
+
coverage: "agent-surface inspect (or `check`, which now fails on the gap)"
|
|
76
|
+
};
|
|
58
77
|
async function main(argv = process.argv.slice(2)) {
|
|
59
78
|
let parsed;
|
|
60
79
|
try {
|
|
@@ -65,8 +84,13 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
65
84
|
config: { type: "string" },
|
|
66
85
|
"baseline-dir": { type: "string" },
|
|
67
86
|
scope: { type: "string", multiple: true },
|
|
87
|
+
depth: { type: "string", default: "full" },
|
|
88
|
+
detail: { type: "boolean", default: false },
|
|
68
89
|
explain: { type: "boolean", default: false },
|
|
69
90
|
schemas: { type: "boolean", default: false },
|
|
91
|
+
tsconfig: { type: "string" },
|
|
92
|
+
"allow-unresolved": { type: "boolean", default: false },
|
|
93
|
+
yes: { type: "boolean", default: false },
|
|
70
94
|
json: { type: "boolean", default: false },
|
|
71
95
|
plain: { type: "boolean", default: false },
|
|
72
96
|
help: { type: "boolean", short: "h", default: false },
|
|
@@ -92,48 +116,71 @@ async function main(argv = process.argv.slice(2)) {
|
|
|
92
116
|
write(USAGE);
|
|
93
117
|
return 2;
|
|
94
118
|
}
|
|
95
|
-
if (!
|
|
96
|
-
|
|
97
|
-
writeError(USAGE);
|
|
98
|
-
return 2;
|
|
99
|
-
}
|
|
100
|
-
const configPath = values.config ?? findConfig();
|
|
101
|
-
if (!configPath) {
|
|
119
|
+
if (!COMMANDS.includes(command)) {
|
|
120
|
+
const moved = RETIRED[command];
|
|
102
121
|
writeError(
|
|
103
|
-
"
|
|
122
|
+
moved ? `"${command}" was removed in 0.11 \u2014 its answer is now \`${moved}\`. The static catalog and the live projection are one command, so the gap between them is reported rather than left for whoever remembers to look.` : `unknown command "${command}"`
|
|
104
123
|
);
|
|
124
|
+
if (!moved) writeError(USAGE);
|
|
125
|
+
return 2;
|
|
126
|
+
}
|
|
127
|
+
if (!isDepth(values.depth)) {
|
|
128
|
+
writeError(`--depth must be one of ${DEPTHS.join(", ")} \u2014 got "${values.depth}"`);
|
|
105
129
|
return 2;
|
|
106
130
|
}
|
|
107
|
-
|
|
131
|
+
const depth = values.depth;
|
|
108
132
|
try {
|
|
133
|
+
if (command === "init") {
|
|
134
|
+
const { runInit } = await import("./init-LYYVXFEQ.js");
|
|
135
|
+
return await runInit({
|
|
136
|
+
cwd: process.cwd(),
|
|
137
|
+
...values.tsconfig ? { tsconfig: values.tsconfig } : {},
|
|
138
|
+
...values.yes ? { yes: true } : {},
|
|
139
|
+
...values.plain ? { plain: true } : {}
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
const configPath = values.config ?? findConfig();
|
|
143
|
+
if (!configPath) {
|
|
144
|
+
writeError(
|
|
145
|
+
"no agent-surface.config.* found (searched upward from the working directory).\nRun `agent-surface init`, or see https://agent-surface-docs.vercel.app/20-cli"
|
|
146
|
+
);
|
|
147
|
+
return 2;
|
|
148
|
+
}
|
|
149
|
+
if (depth !== "static") installDom();
|
|
109
150
|
const shared = {
|
|
110
151
|
configPath,
|
|
152
|
+
depth,
|
|
111
153
|
...scenario ? { scenario } : {},
|
|
112
154
|
...values.scope ? { scope: values.scope } : {},
|
|
155
|
+
...values.tsconfig ? { tsconfig: values.tsconfig } : {},
|
|
113
156
|
...values.json ? { json: true } : {},
|
|
114
157
|
...values.plain ? { plain: true } : {},
|
|
115
158
|
...values["baseline-dir"] ? { baselineDir: values["baseline-dir"] } : {}
|
|
116
159
|
};
|
|
117
160
|
if (command === "inspect") {
|
|
118
|
-
const { runInspect } = await import("./inspect-
|
|
161
|
+
const { runInspect } = await import("./inspect-3CBRKTDM.js");
|
|
119
162
|
return await runInspect({
|
|
120
163
|
...shared,
|
|
164
|
+
...values.detail ? { detail: true } : {},
|
|
121
165
|
...values.explain ? { explain: true } : {},
|
|
122
166
|
...values.schemas ? { schemas: true } : {}
|
|
123
167
|
});
|
|
124
168
|
}
|
|
125
169
|
if (command === "snapshot") {
|
|
126
|
-
const { runSnapshot } = await import("./snapshot-
|
|
170
|
+
const { runSnapshot } = await import("./snapshot-DJ22WCT4.js");
|
|
127
171
|
return await runSnapshot(shared);
|
|
128
172
|
}
|
|
129
|
-
const { runCheck } = await import("./check-
|
|
130
|
-
return await runCheck(
|
|
173
|
+
const { runCheck } = await import("./check-BYQ34OVQ.js");
|
|
174
|
+
return await runCheck({
|
|
175
|
+
...shared,
|
|
176
|
+
...values["allow-unresolved"] ? { allowUnresolved: true } : {}
|
|
177
|
+
});
|
|
131
178
|
} catch (error) {
|
|
132
179
|
writeError(error instanceof Error ? error.message : String(error));
|
|
133
180
|
if (error instanceof Error && error.stack && process.env["AGENT_SURFACE_DEBUG"]) {
|
|
134
181
|
writeError(error.stack);
|
|
135
182
|
}
|
|
136
|
-
return
|
|
183
|
+
return 2;
|
|
137
184
|
}
|
|
138
185
|
}
|
|
139
186
|
async function readVersion() {
|
package/dist/bin.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/bin.ts","../src/dom.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { realpathSync } from \"node:fs\";\nimport { fileURLToPath } from \"node:url\";\nimport { parseArgs } from \"node:util\";\nimport { installDom } from \"./dom.js\";\nimport { findConfig } from \"./load.js\";\nimport { writeError, write } from \"./output.js\";\n\nconst USAGE = `agent-surface — inspect and check the agent surface your app exposes\n\nUsage\n agent-surface inspect [scenario] what an agent can see right now\n agent-surface snapshot [scenario] write/refresh the committed baseline\n agent-surface check [scenario] fail if the surface drifted from the baseline\n\nEvery command covers all scenarios in the config unless you name one.\n\nOptions\n --config <path> path to agent-surface.config.* (default: nearest, searching upward)\n --baseline-dir where baselines live (default: .agent-surface next to the config)\n --scope <prefix> restrict to a component-type prefix (repeatable)\n --explain name the policies behind every decision, hidden ones included\n --schemas include input/output JSON Schemas\n --json emit data instead of a rendered view\n --plain force plain text (implied when piped, or under CI / NO_COLOR)\n -h, --help show this\n -v, --version print the version\n`;\n\nexport async function main(argv: string[] = process.argv.slice(2)): Promise<number> {\n let parsed;\n try {\n parsed = parseArgs({\n args: argv,\n allowPositionals: true,\n options: {\n config: { type: \"string\" },\n \"baseline-dir\": { type: \"string\" },\n scope: { type: \"string\", multiple: true },\n explain: { type: \"boolean\", default: false },\n schemas: { type: \"boolean\", default: false },\n json: { type: \"boolean\", default: false },\n plain: { type: \"boolean\", default: false },\n help: { type: \"boolean\", short: \"h\", default: false },\n version: { type: \"boolean\", short: \"v\", default: false },\n },\n });\n } catch (error) {\n writeError(error instanceof Error ? error.message : String(error));\n writeError(USAGE);\n return 2;\n }\n\n const { values, positionals } = parsed;\n if (values.help) {\n write(USAGE);\n return 0;\n }\n if (values.version) {\n write(await readVersion());\n return 0;\n }\n\n const [command, scenario] = positionals;\n if (!command) {\n write(USAGE);\n return 2;\n }\n if (![\"inspect\", \"snapshot\", \"check\"].includes(command)) {\n writeError(`unknown command \"${command}\"`);\n writeError(USAGE);\n return 2;\n }\n\n const configPath = values.config ?? findConfig();\n if (!configPath) {\n writeError(\n \"no agent-surface.config.* found (searched upward from the working directory).\\n\" +\n \"Create one that points at your app's composition root — see https://agent-surface-docs.vercel.app/20-cli\",\n );\n return 2;\n }\n\n // A presentation surface needs a DOM to mount into, and react-dom reads these\n // globals at import time — so this must happen before any app module loads.\n // It stays installed for the life of the process, on purpose (see dom.ts).\n installDom();\n try {\n const shared = {\n configPath,\n ...(scenario ? { scenario } : {}),\n ...(values.scope ? { scope: values.scope } : {}),\n ...(values.json ? { json: true } : {}),\n ...(values.plain ? { plain: true } : {}),\n ...(values[\"baseline-dir\"] ? { baselineDir: values[\"baseline-dir\"] } : {}),\n };\n\n if (command === \"inspect\") {\n const { runInspect } = await import(\"./commands/inspect.js\");\n return await runInspect({\n ...shared,\n ...(values.explain ? { explain: true } : {}),\n ...(values.schemas ? { schemas: true } : {}),\n });\n }\n if (command === \"snapshot\") {\n const { runSnapshot } = await import(\"./commands/snapshot.js\");\n return await runSnapshot(shared);\n }\n const { runCheck } = await import(\"./commands/check.js\");\n return await runCheck(shared);\n } catch (error) {\n writeError(error instanceof Error ? error.message : String(error));\n if (error instanceof Error && error.stack && process.env[\"AGENT_SURFACE_DEBUG\"]) {\n writeError(error.stack);\n }\n return 1;\n }\n}\n\nasync function readVersion(): Promise<string> {\n try {\n const { readFileSync } = await import(\"node:fs\");\n const path = fileURLToPath(new URL(\"../package.json\", import.meta.url));\n return (JSON.parse(readFileSync(path, \"utf8\")) as { version: string }).version;\n } catch {\n return \"unknown\";\n }\n}\n\n/**\n * Self-execute only as a binary; importing this module (tests) must not run it.\n * `argv[1]` is compared through `realpathSync` because package managers install\n * the bin as a symlink — comparing the raw path silently never matches, and the\n * CLI exits 0 having done nothing.\n */\nfunction invokedAsBinary(): boolean {\n const entry = process.argv[1];\n if (!entry) return false;\n try {\n return fileURLToPath(import.meta.url) === realpathSync(entry);\n } catch {\n return false;\n }\n}\n\n/**\n * How long a finished command is allowed to keep running before it is treated\n * as wedged. Costs a hung run one extra second; costs a healthy run nothing,\n * because a healthy run has already exited by then.\n */\nconst GRACE_MS = 1000;\n\n/**\n * What this process's own stdin/stdout/stderr are called, depending on where\n * they were pointed: a terminal, a `|`, or a `>`. None of the three holds the\n * event loop open — a clean run exits naturally through all of them — so when\n * something *else* has wedged the command they are still in the handle table,\n * and naming them would send the reader after the one thing that is not the\n * cause. Their own leak would be the CLI's bug to fix, not the app's to hear\n * about.\n */\nconst OWN_STDIO = new Set([\"TTYWrap\", \"PipeWrap\", \"FileWrap\"]);\n\n/** Resource types still holding the loop once the command is provably wedged. */\nfunction heldHandles(): string[] {\n const active = process.getActiveResourcesInfo?.() ?? [];\n return active.filter((resource) => !OWN_STDIO.has(resource));\n}\n\n/**\n * `process.exit()` discards whatever is still buffered on a pipe, so a\n * redirected run could lose its last lines — and redirected runs are the ones\n * that matter (`--json`, CI logs). Drain both streams first, but never wait\n * indefinitely: a reader that has stopped consuming must not turn a forced exit\n * back into the hang it exists to prevent.\n */\nasync function flushOutput(): Promise<void> {\n const drained = Promise.all(\n [process.stdout, process.stderr].map(\n (stream) =>\n new Promise<void>((resolve) => {\n if (stream.writableLength === 0) resolve();\n else stream.write(\"\", () => resolve());\n }),\n ),\n );\n const deadline = new Promise<void>((resolve) => {\n setTimeout(resolve, 2000).unref();\n });\n await Promise.race([drained, deadline]);\n}\n\n/**\n * Ends the process, and says why it had to be ended when that is the case.\n *\n * The mount is an arbitrary React tree, not code written for a one-shot\n * process: a polling interval, a websocket, an animation loop or a data layer's\n * cache timer all keep Node's event loop alive long after the surface has been\n * rendered. Setting `process.exitCode` alone means such a command prints its\n * full, correct output and then appears to hang — with a successful exit code\n * already set, and nothing on screen to explain the wait (`AS-CLI-005`).\n *\n * The detector is the timer itself, not a reading of the handle table. An\n * unref'd timer does not hold the loop open, so a command with nothing left to\n * do exits naturally on `process.exitCode` and this never fires. Its firing is\n * therefore the diagnosis — this run *was* about to hang — and whatever it then\n * finds in the handle table is genuinely the cause. Reading the table eagerly\n * instead would blame the app for the CLI's own teardown: vite's dev server is\n * still closing its socket at the moment the last scenario is rendered, so\n * every healthy run would accuse its own app of leaking a `TCPServerWrap`.\n *\n * Naming the handles is the same move the package already makes for\n * capabilities: the invisible thing becomes inspectable. Exiting anyway is what\n * makes the tool usable unattended.\n */\nfunction exitWhenWedged(code: number): void {\n process.exitCode = code;\n setTimeout(() => {\n const held = heldHandles();\n if (held.length > 0) {\n const kinds = [...new Set(held)].sort().join(\", \");\n writeError(\n `agent-surface: the output above is complete, but ${held.length} handle(s) are still ` +\n `open (${kinds}) — something started during the mount is still running, so this ` +\n `command would have waited instead of exiting. Common causes: a polling interval, a ` +\n `websocket, or a data layer whose cache timer outlives the render. Exiting ${code}.`,\n );\n }\n void flushOutput().then(() => process.exit(code));\n }, GRACE_MS).unref();\n}\n\nif (invokedAsBinary()) {\n main().then(\n (code) => exitWhenWedged(code),\n (error: unknown) => {\n writeError(error instanceof Error ? error.message : String(error));\n exitWhenWedged(1);\n },\n );\n}\n","import { JSDOM } from \"jsdom\";\n\n/**\n * A presentation surface only exists once components mount, and mounting needs\n * a DOM. Vitest gets one from its `jsdom` environment; a plain Node process has\n * to install one itself — *before* anything imports `react-dom`, which reads\n * these globals at module scope.\n *\n * Process-wide on purpose, and permanent: the app tree runs inside the\n * vite-node graph, which shares this realm's globals, and there is deliberately\n * no way to take the DOM back down — see the note on teardown below. Returning\n * nothing is the honest signature; an installer that handed back a disposer\n * doing nothing would read, at every call site, as cleanup that happens.\n */\nexport function installDom(url = \"http://localhost/\"): void {\n const globals = globalThis as Record<string, unknown>;\n if (typeof globals[\"document\"] !== \"undefined\") return;\n\n const dom = new JSDOM(\"<!doctype html><html><body></body></html>\", {\n url,\n pretendToBeVisual: true,\n });\n const { window } = dom;\n\n // Everything jsdom's window defines that this realm does not already have.\n // Skipping existing keys matters: Node's own `fetch`, `URL` and timers are\n // more capable than jsdom's shims, and clobbering them breaks app code.\n for (const key of Object.getOwnPropertyNames(window)) {\n if (key.startsWith(\"_\")) continue;\n if (key in globals) continue;\n const descriptor = Object.getOwnPropertyDescriptor(window, key);\n if (!descriptor) continue;\n Object.defineProperty(globals, key, descriptor);\n }\n\n for (const key of [\"window\", \"document\", \"navigator\"] as const) {\n if (!(key in globals)) {\n Object.defineProperty(globals, key, { value: window[key], configurable: true });\n }\n }\n}\n\n/**\n * There is deliberately no teardown, and the DOM is deliberately process-wide.\n *\n * `react-dom` captures `window`/`document` when it is first imported. Removing\n * the globals — or worse, calling `window.close()` — leaves that captured\n * reference pointing at a dead realm, so the *next* mount in the same process\n * fails in a way that looks nothing like its cause. A CLI invocation ends by\n * exiting (see `exitWhenWedged` in `bin.ts`), so there is nothing to reclaim; only\n * in-process callers (the test suite) run more than one command, and those are\n * exactly the ones this protects.\n */\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/bin.ts","../src/dom.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { realpathSync } from \"node:fs\";\nimport { fileURLToPath } from \"node:url\";\nimport { parseArgs } from \"node:util\";\nimport { DEPTHS, isDepth } from \"./contract.js\";\nimport { installDom } from \"./dom.js\";\nimport { findConfig } from \"./load.js\";\nimport { writeError, write } from \"./output.js\";\n\nconst USAGE = `agent-surface — the agent surface your app exposes\n\nUsage\n agent-surface init read the codebase, then scaffold a config\n agent-surface inspect [scenario] what an agent can reach, and what it cannot\n agent-surface snapshot [scenario] write/refresh the committed baseline\n agent-surface check [scenario] fail on drift, or on a capability no scenario reaches\n\nEvery command covers all scenarios in the config unless you name one.\n\nDepth\n --depth full read the source AND mount the scenarios, and report the gap (default)\n --depth static read the source only — no Vite, no jsdom, no mount, no scenarios needed\n --depth runtime mount only — skip the TypeScript program on a repo wide enough to feel it\n\nOptions\n --config <path> path to agent-surface.config.* (default: nearest, searching upward)\n --baseline-dir where baselines live (default: .agent-surface next to the config)\n --scope <prefix> restrict to a component-type prefix (repeatable)\n --detail one paragraph per capability instead of the table\n --explain name the policies behind every decision (implies --detail)\n --schemas include input/output JSON Schemas (implies --detail)\n --tsconfig <path> tsconfig the source read uses (default: nearest to the config)\n --allow-unresolved check: do not fail on a call site that could not be read\n --yes init: write without asking\n --json emit data instead of a rendered view\n --plain force plain text (implied when piped, or under CI / NO_COLOR)\n -h, --help show this\n -v, --version print the version\n\nExit codes are the contract: 0 clean, 1 a finding, 2 the command could not run.\n`;\n\nconst COMMANDS = [\"init\", \"inspect\", \"snapshot\", \"check\"];\n\n/** Commands that were cut, and where their answer went (0.11.0). */\nconst RETIRED: Record<string, string> = {\n capabilities: \"agent-surface inspect --depth static\",\n coverage: \"agent-surface inspect (or `check`, which now fails on the gap)\",\n};\n\nexport async function main(argv: string[] = process.argv.slice(2)): Promise<number> {\n let parsed;\n try {\n parsed = parseArgs({\n args: argv,\n allowPositionals: true,\n options: {\n config: { type: \"string\" },\n \"baseline-dir\": { type: \"string\" },\n scope: { type: \"string\", multiple: true },\n depth: { type: \"string\", default: \"full\" },\n detail: { type: \"boolean\", default: false },\n explain: { type: \"boolean\", default: false },\n schemas: { type: \"boolean\", default: false },\n tsconfig: { type: \"string\" },\n \"allow-unresolved\": { type: \"boolean\", default: false },\n yes: { type: \"boolean\", default: false },\n json: { type: \"boolean\", default: false },\n plain: { type: \"boolean\", default: false },\n help: { type: \"boolean\", short: \"h\", default: false },\n version: { type: \"boolean\", short: \"v\", default: false },\n },\n });\n } catch (error) {\n writeError(error instanceof Error ? error.message : String(error));\n writeError(USAGE);\n return 2;\n }\n\n const { values, positionals } = parsed;\n if (values.help) {\n write(USAGE);\n return 0;\n }\n if (values.version) {\n write(await readVersion());\n return 0;\n }\n\n const [command, scenario] = positionals;\n if (!command) {\n write(USAGE);\n return 2;\n }\n if (!COMMANDS.includes(command)) {\n const moved = RETIRED[command];\n writeError(\n moved\n ? `\"${command}\" was removed in 0.11 — its answer is now \\`${moved}\\`. ` +\n \"The static catalog and the live projection are one command, so the gap between \" +\n \"them is reported rather than left for whoever remembers to look.\"\n : `unknown command \"${command}\"`,\n );\n if (!moved) writeError(USAGE);\n return 2;\n }\n\n if (!isDepth(values.depth)) {\n writeError(`--depth must be one of ${DEPTHS.join(\", \")} — got \"${values.depth}\"`);\n return 2;\n }\n const depth = values.depth;\n\n try {\n if (command === \"init\") {\n // Nothing to find and nothing to mount: `init` is the command that runs\n // before a config exists.\n const { runInit } = await import(\"./commands/init.js\");\n return await runInit({\n cwd: process.cwd(),\n ...(values.tsconfig ? { tsconfig: values.tsconfig } : {}),\n ...(values.yes ? { yes: true } : {}),\n ...(values.plain ? { plain: true } : {}),\n });\n }\n\n const configPath = values.config ?? findConfig();\n if (!configPath) {\n writeError(\n \"no agent-surface.config.* found (searched upward from the working directory).\\n\" +\n \"Run `agent-surface init`, or see https://agent-surface-docs.vercel.app/20-cli\",\n );\n return 2;\n }\n\n // A presentation surface needs a DOM to mount into, and react-dom reads\n // these globals at import time — so this must happen before any app module\n // loads. It stays installed for the life of the process, on purpose\n // (see dom.ts).\n //\n // `--depth static` is the exception, and deliberately so: it reads the\n // TypeScript program and mounts nothing, so it must not pay for — or be\n // able to be affected by — a DOM it never renders into.\n if (depth !== \"static\") installDom();\n\n const shared = {\n configPath,\n depth,\n ...(scenario ? { scenario } : {}),\n ...(values.scope ? { scope: values.scope } : {}),\n ...(values.tsconfig ? { tsconfig: values.tsconfig } : {}),\n ...(values.json ? { json: true } : {}),\n ...(values.plain ? { plain: true } : {}),\n ...(values[\"baseline-dir\"] ? { baselineDir: values[\"baseline-dir\"] } : {}),\n };\n\n if (command === \"inspect\") {\n const { runInspect } = await import(\"./commands/inspect.js\");\n return await runInspect({\n ...shared,\n ...(values.detail ? { detail: true } : {}),\n ...(values.explain ? { explain: true } : {}),\n ...(values.schemas ? { schemas: true } : {}),\n });\n }\n if (command === \"snapshot\") {\n const { runSnapshot } = await import(\"./commands/snapshot.js\");\n return await runSnapshot(shared);\n }\n const { runCheck } = await import(\"./commands/check.js\");\n return await runCheck({\n ...shared,\n ...(values[\"allow-unresolved\"] ? { allowUnresolved: true } : {}),\n });\n } catch (error) {\n writeError(error instanceof Error ? error.message : String(error));\n if (error instanceof Error && error.stack && process.env[\"AGENT_SURFACE_DEBUG\"]) {\n writeError(error.stack);\n }\n // `2` — the command could not run, as opposed to running and finding\n // something. CI has to tell those apart: a gate that exits 1 both when the\n // surface changed and when the tool never loaded the app is a gate whose\n // green is the only signal worth anything, and whose red says nothing.\n return 2;\n }\n}\n\nasync function readVersion(): Promise<string> {\n try {\n const { readFileSync } = await import(\"node:fs\");\n const path = fileURLToPath(new URL(\"../package.json\", import.meta.url));\n return (JSON.parse(readFileSync(path, \"utf8\")) as { version: string }).version;\n } catch {\n return \"unknown\";\n }\n}\n\n/**\n * Self-execute only as a binary; importing this module (tests) must not run it.\n * `argv[1]` is compared through `realpathSync` because package managers install\n * the bin as a symlink — comparing the raw path silently never matches, and the\n * CLI exits 0 having done nothing.\n */\nfunction invokedAsBinary(): boolean {\n const entry = process.argv[1];\n if (!entry) return false;\n try {\n return fileURLToPath(import.meta.url) === realpathSync(entry);\n } catch {\n return false;\n }\n}\n\n/**\n * How long a finished command is allowed to keep running before it is treated\n * as wedged. Costs a hung run one extra second; costs a healthy run nothing,\n * because a healthy run has already exited by then.\n */\nconst GRACE_MS = 1000;\n\n/**\n * What this process's own stdin/stdout/stderr are called, depending on where\n * they were pointed: a terminal, a `|`, or a `>`. None of the three holds the\n * event loop open — a clean run exits naturally through all of them — so when\n * something *else* has wedged the command they are still in the handle table,\n * and naming them would send the reader after the one thing that is not the\n * cause. Their own leak would be the CLI's bug to fix, not the app's to hear\n * about.\n */\nconst OWN_STDIO = new Set([\"TTYWrap\", \"PipeWrap\", \"FileWrap\"]);\n\n/** Resource types still holding the loop once the command is provably wedged. */\nfunction heldHandles(): string[] {\n const active = process.getActiveResourcesInfo?.() ?? [];\n return active.filter((resource) => !OWN_STDIO.has(resource));\n}\n\n/**\n * `process.exit()` discards whatever is still buffered on a pipe, so a\n * redirected run could lose its last lines — and redirected runs are the ones\n * that matter (`--json`, CI logs). Drain both streams first, but never wait\n * indefinitely: a reader that has stopped consuming must not turn a forced exit\n * back into the hang it exists to prevent.\n */\nasync function flushOutput(): Promise<void> {\n const drained = Promise.all(\n [process.stdout, process.stderr].map(\n (stream) =>\n new Promise<void>((resolve) => {\n if (stream.writableLength === 0) resolve();\n else stream.write(\"\", () => resolve());\n }),\n ),\n );\n const deadline = new Promise<void>((resolve) => {\n setTimeout(resolve, 2000).unref();\n });\n await Promise.race([drained, deadline]);\n}\n\n/**\n * Ends the process, and says why it had to be ended when that is the case.\n *\n * The mount is an arbitrary React tree, not code written for a one-shot\n * process: a polling interval, a websocket, an animation loop or a data layer's\n * cache timer all keep Node's event loop alive long after the surface has been\n * rendered. Setting `process.exitCode` alone means such a command prints its\n * full, correct output and then appears to hang — with a successful exit code\n * already set, and nothing on screen to explain the wait (`AS-CLI-005`).\n *\n * The detector is the timer itself, not a reading of the handle table. An\n * unref'd timer does not hold the loop open, so a command with nothing left to\n * do exits naturally on `process.exitCode` and this never fires. Its firing is\n * therefore the diagnosis — this run *was* about to hang — and whatever it then\n * finds in the handle table is genuinely the cause. Reading the table eagerly\n * instead would blame the app for the CLI's own teardown: vite's dev server is\n * still closing its socket at the moment the last scenario is rendered, so\n * every healthy run would accuse its own app of leaking a `TCPServerWrap`.\n *\n * Naming the handles is the same move the package already makes for\n * capabilities: the invisible thing becomes inspectable. Exiting anyway is what\n * makes the tool usable unattended.\n */\nfunction exitWhenWedged(code: number): void {\n process.exitCode = code;\n setTimeout(() => {\n const held = heldHandles();\n if (held.length > 0) {\n const kinds = [...new Set(held)].sort().join(\", \");\n writeError(\n `agent-surface: the output above is complete, but ${held.length} handle(s) are still ` +\n `open (${kinds}) — something started during the mount is still running, so this ` +\n `command would have waited instead of exiting. Common causes: a polling interval, a ` +\n `websocket, or a data layer whose cache timer outlives the render. Exiting ${code}.`,\n );\n }\n void flushOutput().then(() => process.exit(code));\n }, GRACE_MS).unref();\n}\n\nif (invokedAsBinary()) {\n main().then(\n (code) => exitWhenWedged(code),\n (error: unknown) => {\n writeError(error instanceof Error ? error.message : String(error));\n exitWhenWedged(1);\n },\n );\n}\n","import { JSDOM } from \"jsdom\";\n\n/**\n * A presentation surface only exists once components mount, and mounting needs\n * a DOM. Vitest gets one from its `jsdom` environment; a plain Node process has\n * to install one itself — *before* anything imports `react-dom`, which reads\n * these globals at module scope.\n *\n * Process-wide on purpose, and permanent: the app tree runs inside the\n * vite-node graph, which shares this realm's globals, and there is deliberately\n * no way to take the DOM back down — see the note on teardown below. Returning\n * nothing is the honest signature; an installer that handed back a disposer\n * doing nothing would read, at every call site, as cleanup that happens.\n */\nexport function installDom(url = \"http://localhost/\"): void {\n const globals = globalThis as Record<string, unknown>;\n if (typeof globals[\"document\"] !== \"undefined\") return;\n\n const dom = new JSDOM(\"<!doctype html><html><body></body></html>\", {\n url,\n pretendToBeVisual: true,\n });\n const { window } = dom;\n\n // Everything jsdom's window defines that this realm does not already have.\n // Skipping existing keys matters: Node's own `fetch`, `URL` and timers are\n // more capable than jsdom's shims, and clobbering them breaks app code.\n for (const key of Object.getOwnPropertyNames(window)) {\n if (key.startsWith(\"_\")) continue;\n if (key in globals) continue;\n const descriptor = Object.getOwnPropertyDescriptor(window, key);\n if (!descriptor) continue;\n Object.defineProperty(globals, key, descriptor);\n }\n\n for (const key of [\"window\", \"document\", \"navigator\"] as const) {\n if (!(key in globals)) {\n Object.defineProperty(globals, key, { value: window[key], configurable: true });\n }\n }\n}\n\n/**\n * There is deliberately no teardown, and the DOM is deliberately process-wide.\n *\n * `react-dom` captures `window`/`document` when it is first imported. Removing\n * the globals — or worse, calling `window.close()` — leaves that captured\n * reference pointing at a dead realm, so the *next* mount in the same process\n * fails in a way that looks nothing like its cause. A CLI invocation ends by\n * exiting (see `exitWhenWedged` in `bin.ts`), so there is nothing to reclaim; only\n * in-process callers (the test suite) run more than one command, and those are\n * exactly the ones this protects.\n */\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import {
|
|
2
|
+
renderCoveragePlain,
|
|
3
|
+
renderDriftPlain,
|
|
4
|
+
renderFailuresPlain,
|
|
5
|
+
renderNoVerdictPlain
|
|
6
|
+
} from "./chunk-RXX63JSL.js";
|
|
7
|
+
import "./chunk-DYDSJM7R.js";
|
|
8
|
+
import {
|
|
9
|
+
annotate,
|
|
10
|
+
baselinePath,
|
|
11
|
+
coverageExitCode,
|
|
12
|
+
diff,
|
|
13
|
+
joinCoverage,
|
|
14
|
+
mountScenarios,
|
|
15
|
+
normalize,
|
|
16
|
+
readBaseline,
|
|
17
|
+
readInventory
|
|
18
|
+
} from "./chunk-GXXKZTQB.js";
|
|
19
|
+
import {
|
|
20
|
+
UsageError,
|
|
21
|
+
write,
|
|
22
|
+
writeError
|
|
23
|
+
} from "./chunk-QIVOZAWX.js";
|
|
24
|
+
|
|
25
|
+
// src/commands/check.ts
|
|
26
|
+
import { relative } from "path";
|
|
27
|
+
async function runCheck(options) {
|
|
28
|
+
if (options.depth === "static") {
|
|
29
|
+
throw new UsageError(
|
|
30
|
+
"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."
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
const analysis = {
|
|
34
|
+
configPath: options.configPath,
|
|
35
|
+
depth: options.depth,
|
|
36
|
+
...options.scenario ? { scenario: options.scenario } : {},
|
|
37
|
+
...options.scope ? { scope: options.scope } : {},
|
|
38
|
+
...options.tsconfig ? { tsconfig: options.tsconfig } : {},
|
|
39
|
+
...options.baselineDir ? { baselineDir: options.baselineDir } : {}
|
|
40
|
+
};
|
|
41
|
+
const inventory = readInventory(analysis);
|
|
42
|
+
const runtime = await mountScenarios(analysis);
|
|
43
|
+
if (!runtime) throw new UsageError("check needs a mount, and this depth performs none");
|
|
44
|
+
const drifted = [];
|
|
45
|
+
for (const result of runtime.results) {
|
|
46
|
+
const path = baselinePath(runtime.baselineDir, result.scenario);
|
|
47
|
+
const expected = readBaseline(path);
|
|
48
|
+
if (expected === void 0) {
|
|
49
|
+
drifted.push({ scenario: result.scenario, missingBaseline: true, entries: [] });
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
const actual = normalize(result.snapshot);
|
|
53
|
+
const entries = annotate(diff(expected, actual), actual, expected);
|
|
54
|
+
if (entries.length > 0) drifted.push({ scenario: result.scenario, entries });
|
|
55
|
+
}
|
|
56
|
+
const coverage = joinCoverage(inventory, runtime, analysis);
|
|
57
|
+
const coverageFailed = coverage !== void 0 && coverageExitCode(coverage, { allowUnresolved: options.allowUnresolved === true }) !== 0;
|
|
58
|
+
const couldNotRun = runtime.failures.length > 0;
|
|
59
|
+
const ok = drifted.length === 0 && !coverageFailed && !couldNotRun;
|
|
60
|
+
if (options.json) {
|
|
61
|
+
write(
|
|
62
|
+
JSON.stringify(
|
|
63
|
+
{ ok, drifted, failures: runtime.failures, coverage: coverage ?? null },
|
|
64
|
+
null,
|
|
65
|
+
2
|
|
66
|
+
)
|
|
67
|
+
);
|
|
68
|
+
return couldNotRun ? 2 : ok ? 0 : 1;
|
|
69
|
+
}
|
|
70
|
+
if (coverage) {
|
|
71
|
+
const rendered = renderCoveragePlain(coverage);
|
|
72
|
+
if (coverageFailed) writeError(rendered);
|
|
73
|
+
else write(rendered);
|
|
74
|
+
}
|
|
75
|
+
const missing = drifted.filter((entry) => entry.missingBaseline);
|
|
76
|
+
const changed = drifted.filter((entry) => !entry.missingBaseline);
|
|
77
|
+
if (missing.length > 0) {
|
|
78
|
+
writeError(
|
|
79
|
+
[
|
|
80
|
+
`${coverage ? "\n" : ""}NO BASELINE \u2014 nothing to compare against, which is not the same as a match (${missing.length})`,
|
|
81
|
+
...missing.map(
|
|
82
|
+
(entry) => ` ${entry.scenario}: ${relative(
|
|
83
|
+
process.cwd(),
|
|
84
|
+
baselinePath(runtime.baselineDir, entry.scenario)
|
|
85
|
+
)} does not exist \u2014 run \`agent-surface snapshot\` and commit it`
|
|
86
|
+
)
|
|
87
|
+
].join("\n")
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
if (changed.length > 0) {
|
|
91
|
+
writeError(
|
|
92
|
+
[
|
|
93
|
+
`${coverage || missing.length > 0 ? "\n" : ""}DRIFT \u2014 the surface changed against its baseline (${changed.length})`,
|
|
94
|
+
...changed.map((entry) => renderDriftPlain(entry.scenario, entry.entries)),
|
|
95
|
+
"",
|
|
96
|
+
`surface drift in ${changed.length} scenario${changed.length === 1 ? "" : "s"} \u2014 review the change, then \`agent-surface snapshot\` to accept it`
|
|
97
|
+
].join("\n")
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
if (couldNotRun) {
|
|
101
|
+
writeError(`
|
|
102
|
+
${renderFailuresPlain(runtime.failures)}`);
|
|
103
|
+
if (inventory) writeError(`
|
|
104
|
+
${renderNoVerdictPlain(runtime.failures)}`);
|
|
105
|
+
return 2;
|
|
106
|
+
}
|
|
107
|
+
if (ok) {
|
|
108
|
+
if (coverage) write("");
|
|
109
|
+
write(`surface matches the baseline in ${runtime.scenarios.join(", ")}`);
|
|
110
|
+
if (!coverage) {
|
|
111
|
+
write(
|
|
112
|
+
"that is a statement about these scenarios only \u2014 re-run at --depth full to find capabilities no scenario mounts"
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
return 0;
|
|
116
|
+
}
|
|
117
|
+
return 1;
|
|
118
|
+
}
|
|
119
|
+
export {
|
|
120
|
+
runCheck
|
|
121
|
+
};
|
|
122
|
+
//# sourceMappingURL=check-BYQ34OVQ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/commands/check.ts"],"sourcesContent":["import { relative } from \"node:path\";\nimport {\n UsageError,\n joinCoverage,\n mountScenarios,\n readInventory,\n type AnalysisOptions,\n type Depth,\n} from \"../analysis.js\";\nimport { annotate, baselinePath, diff, normalize, readBaseline, type DiffEntry } from \"../baseline.js\";\nimport { coverageExitCode } from \"../coverage.js\";\nimport {\n renderCoveragePlain,\n renderDriftPlain,\n renderFailuresPlain,\n renderNoVerdictPlain,\n} from \"../render/plain.js\";\nimport { write, writeError } from \"../output.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\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 four:\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 *\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 = normalize(result.snapshot);\n const entries = annotate(diff(expected, actual), actual, expected);\n if (entries.length > 0) drifted.push({ scenario: result.scenario, entries });\n }\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 = drifted.length === 0 && !coverageFailed && !couldNotRun;\n\n if (options.json) {\n write(\n JSON.stringify(\n { ok, drifted, failures: runtime.failures, coverage: coverage ?? null },\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 (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":[]}
|