@agent-surface/cli 0.8.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Wiseair S.r.l.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,99 @@
1
+ # @agent-surface/cli
2
+
3
+ Inspect and check the agent surface your app exposes — from a terminal, and in CI. Part of [agent-surface](https://github.com/Wiseair-srl/agent-surface).
4
+
5
+ Docs: https://agent-surface-docs.vercel.app/20-cli
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ pnpm add -D @agent-surface/cli
11
+ ```
12
+
13
+ ## Configure
14
+
15
+ A presentation surface only exists once components mount — it is a projection of *what is mounted × route × host context × consumer × policy × live `when()`*. So there is nothing static to read, and the CLI mounts your app. It does not re-implement it: the config points at the composition root you already have.
16
+
17
+ ```tsx
18
+ // agent-surface.config.tsx
19
+ import { defineSurface } from "@agent-surface/cli";
20
+ import { createApp } from "./src/agent/setup.js"; // already exists
21
+ import { App } from "./src/app/App.js"; // already exists
22
+
23
+ export default defineSurface({
24
+ mount: ({ user }) => {
25
+ const app = createApp({ environment: "test", user });
26
+ return { registry: app.registry, ui: <App app={app} />, app };
27
+ },
28
+ scenarios: {
29
+ admin: { user: { id: "u_admin", permissions: ["devices:write"] } },
30
+ anonymous: { user: null },
31
+ },
32
+ });
33
+ ```
34
+
35
+ Loading goes through vite-node on your own `vite.config.*`, so your aliases, plugins and TSX resolve exactly as they do in dev.
36
+
37
+ ## Use
38
+
39
+ ```bash
40
+ agent-surface inspect [scenario] # what an agent can see right now
41
+ agent-surface snapshot [scenario] # write/refresh the committed baseline
42
+ agent-surface check [scenario] # exit non-zero when the surface drifts
43
+ ```
44
+
45
+ ```text
46
+ scenario admin route /devices
47
+ 9 callable, 2 visible-disabled
48
+
49
+ devices.drawer (3)
50
+ + open [local-state, reversible]
51
+ Open the detail drawer for a device
52
+ ~ close [local-state, reversible]
53
+ Close the detail drawer
54
+ reason: The drawer is not open
55
+
56
+ authoritative (domain) (1)
57
+ ~ devices.disable [destructive, confirmation:required, deviceIds bound+locked]
58
+ Disable the given devices
59
+ reason: Select at least one device first
60
+ ```
61
+
62
+ ### Why is my capability missing?
63
+
64
+ `snapshot()` bakes policy outcomes: a `hide` removes the capability *and* the reason, because the existence of a hidden capability is itself information. Correct at the agent boundary, useless when you are the developer. `--explain` answers it:
65
+
66
+ ```bash
67
+ agent-surface inspect anonymous --explain
68
+ ```
69
+
70
+ ```text
71
+ 0 callable, 0 visible-disabled, 11 hidden
72
+
73
+ hidden by policy (absent from the snapshot) (11)
74
+ - set [devices.filters@default]
75
+ Update one or both filters; omitted fields are unchanged.
76
+ policy authenticated (registry, discovery/authorize): hide
77
+ ```
78
+
79
+ 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.
80
+
81
+ ### The scenarios are not a fixture
82
+
83
+ The same definitions drive your test suite, so "admin on /devices" exists once rather than twice:
84
+
85
+ ```ts
86
+ import config from "../agent-surface.config.js";
87
+ import { mountScenario } from "@agent-surface/cli/vitest";
88
+
89
+ const { surface, app } = await mountScenario(config, "admin");
90
+ expect(surface).toExpose("view:devices.filters.set");
91
+ ```
92
+
93
+ ## Notes
94
+
95
+ Requires `@testing-library/react` and `react-dom` (peers) — it mounts your real tree in jsdom. Anything needing a real browser is out of reach by construction. Output falls back to plain text when piped, or under `--plain`, `CI` and `NO_COLOR`.
96
+
97
+ Full specification: [docs/20](https://github.com/Wiseair-srl/agent-surface/blob/main/docs/20-cli.md).
98
+
99
+ MIT © Wiseair S.r.l.
package/dist/bin.js ADDED
@@ -0,0 +1,174 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ findConfig,
4
+ write,
5
+ writeError
6
+ } from "./chunk-S2LM3N6D.js";
7
+
8
+ // src/bin.ts
9
+ import { realpathSync } from "fs";
10
+ import { fileURLToPath } from "url";
11
+ import { parseArgs } from "util";
12
+
13
+ // src/dom.ts
14
+ import { JSDOM } from "jsdom";
15
+ function installDom(url = "http://localhost/") {
16
+ const globals = globalThis;
17
+ if (typeof globals["document"] !== "undefined") return noop;
18
+ const dom = new JSDOM("<!doctype html><html><body></body></html>", {
19
+ url,
20
+ pretendToBeVisual: true
21
+ });
22
+ const { window } = dom;
23
+ for (const key of Object.getOwnPropertyNames(window)) {
24
+ if (key.startsWith("_")) continue;
25
+ if (key in globals) continue;
26
+ const descriptor = Object.getOwnPropertyDescriptor(window, key);
27
+ if (!descriptor) continue;
28
+ Object.defineProperty(globals, key, descriptor);
29
+ }
30
+ for (const key of ["window", "document", "navigator"]) {
31
+ if (!(key in globals)) {
32
+ Object.defineProperty(globals, key, { value: window[key], configurable: true });
33
+ }
34
+ }
35
+ return noop;
36
+ }
37
+ function noop() {
38
+ }
39
+
40
+ // src/bin.ts
41
+ var USAGE = `agent-surface \u2014 inspect and check the agent surface your app exposes
42
+
43
+ Usage
44
+ agent-surface inspect [scenario] what an agent can see right now
45
+ agent-surface snapshot [scenario] write/refresh the committed baseline
46
+ agent-surface check [scenario] fail if the surface drifted from the baseline
47
+
48
+ Options
49
+ --config <path> path to agent-surface.config.* (default: nearest, searching upward)
50
+ --baseline-dir where baselines live (default: .agent-surface next to the config)
51
+ --scope <prefix> restrict to a component-type prefix (repeatable)
52
+ --explain name the policies behind every decision, hidden ones included
53
+ --schemas include input/output JSON Schemas
54
+ --json emit data instead of a rendered view
55
+ --plain force plain text (implied when piped, or under CI / NO_COLOR)
56
+ -h, --help show this
57
+ -v, --version print the version
58
+ `;
59
+ async function main(argv = process.argv.slice(2)) {
60
+ let parsed;
61
+ try {
62
+ parsed = parseArgs({
63
+ args: argv,
64
+ allowPositionals: true,
65
+ options: {
66
+ config: { type: "string" },
67
+ "baseline-dir": { type: "string" },
68
+ scope: { type: "string", multiple: true },
69
+ explain: { type: "boolean", default: false },
70
+ schemas: { type: "boolean", default: false },
71
+ json: { type: "boolean", default: false },
72
+ plain: { type: "boolean", default: false },
73
+ help: { type: "boolean", short: "h", default: false },
74
+ version: { type: "boolean", short: "v", default: false }
75
+ }
76
+ });
77
+ } catch (error) {
78
+ writeError(error instanceof Error ? error.message : String(error));
79
+ writeError(USAGE);
80
+ return 2;
81
+ }
82
+ const { values, positionals } = parsed;
83
+ if (values.help) {
84
+ write(USAGE);
85
+ return 0;
86
+ }
87
+ if (values.version) {
88
+ write(await readVersion());
89
+ return 0;
90
+ }
91
+ const [command, scenario] = positionals;
92
+ if (!command) {
93
+ write(USAGE);
94
+ return 2;
95
+ }
96
+ if (!["inspect", "snapshot", "check"].includes(command)) {
97
+ writeError(`unknown command "${command}"`);
98
+ writeError(USAGE);
99
+ return 2;
100
+ }
101
+ const configPath = values.config ?? findConfig();
102
+ if (!configPath) {
103
+ writeError(
104
+ "no agent-surface.config.* found (searched upward from the working directory).\nCreate one that points at your app's composition root \u2014 see https://agent-surface-docs.vercel.app/20-cli"
105
+ );
106
+ return 2;
107
+ }
108
+ const uninstallDom = installDom();
109
+ try {
110
+ const shared = {
111
+ configPath,
112
+ ...scenario ? { scenario } : {},
113
+ ...values.scope ? { scope: values.scope } : {},
114
+ ...values.json ? { json: true } : {},
115
+ ...values.plain ? { plain: true } : {},
116
+ ...values["baseline-dir"] ? { baselineDir: values["baseline-dir"] } : {}
117
+ };
118
+ if (command === "inspect") {
119
+ const { runInspect } = await import("./inspect-3AXX3VVK.js");
120
+ return await runInspect({
121
+ ...shared,
122
+ ...values.explain ? { explain: true } : {},
123
+ ...values.schemas ? { schemas: true } : {}
124
+ });
125
+ }
126
+ if (command === "snapshot") {
127
+ const { runSnapshot } = await import("./snapshot-3D55FL63.js");
128
+ return await runSnapshot(shared);
129
+ }
130
+ const { runCheck } = await import("./check-7Z2VNN5R.js");
131
+ return await runCheck(shared);
132
+ } catch (error) {
133
+ writeError(error instanceof Error ? error.message : String(error));
134
+ if (error instanceof Error && error.stack && process.env["AGENT_SURFACE_DEBUG"]) {
135
+ writeError(error.stack);
136
+ }
137
+ return 1;
138
+ } finally {
139
+ uninstallDom();
140
+ }
141
+ }
142
+ async function readVersion() {
143
+ try {
144
+ const { readFileSync } = await import("fs");
145
+ const path = fileURLToPath(new URL("../package.json", import.meta.url));
146
+ return JSON.parse(readFileSync(path, "utf8")).version;
147
+ } catch {
148
+ return "unknown";
149
+ }
150
+ }
151
+ function invokedAsBinary() {
152
+ const entry = process.argv[1];
153
+ if (!entry) return false;
154
+ try {
155
+ return fileURLToPath(import.meta.url) === realpathSync(entry);
156
+ } catch {
157
+ return false;
158
+ }
159
+ }
160
+ if (invokedAsBinary()) {
161
+ main().then(
162
+ (code) => {
163
+ process.exitCode = code;
164
+ },
165
+ (error) => {
166
+ writeError(error instanceof Error ? error.message : String(error));
167
+ process.exitCode = 1;
168
+ }
169
+ );
170
+ }
171
+ export {
172
+ main
173
+ };
174
+ //# sourceMappingURL=bin.js.map
@@ -0,0 +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\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 const uninstallDom = 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 } finally {\n uninstallDom();\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\nif (invokedAsBinary()) {\n main().then(\n (code) => {\n process.exitCode = code;\n },\n (error: unknown) => {\n writeError(error instanceof Error ? error.message : String(error));\n process.exitCode = 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: the app tree runs inside the vite-node graph, which\n * shares this realm's globals.\n */\nexport function installDom(url = \"http://localhost/\"): () => void {\n const globals = globalThis as Record<string, unknown>;\n if (typeof globals[\"document\"] !== \"undefined\") return noop;\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 return noop;\n}\n\n/**\n * Teardown is deliberately a no-op, 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, so there is nothing to reclaim; only in-process callers (the test\n * suite) run more than one command, and those are exactly the ones this\n * protects.\n */\nfunction noop(): void {}\n"],"mappings":";;;;;;;;AACA,SAAS,oBAAoB;AAC7B,SAAS,qBAAqB;AAC9B,SAAS,iBAAiB;;;ACH1B,SAAS,aAAa;AAWf,SAAS,WAAW,MAAM,qBAAiC;AAChE,QAAM,UAAU;AAChB,MAAI,OAAO,QAAQ,UAAU,MAAM,YAAa,QAAO;AAEvD,QAAM,MAAM,IAAI,MAAM,6CAA6C;AAAA,IACjE;AAAA,IACA,mBAAmB;AAAA,EACrB,CAAC;AACD,QAAM,EAAE,OAAO,IAAI;AAKnB,aAAW,OAAO,OAAO,oBAAoB,MAAM,GAAG;AACpD,QAAI,IAAI,WAAW,GAAG,EAAG;AACzB,QAAI,OAAO,QAAS;AACpB,UAAM,aAAa,OAAO,yBAAyB,QAAQ,GAAG;AAC9D,QAAI,CAAC,WAAY;AACjB,WAAO,eAAe,SAAS,KAAK,UAAU;AAAA,EAChD;AAEA,aAAW,OAAO,CAAC,UAAU,YAAY,WAAW,GAAY;AAC9D,QAAI,EAAE,OAAO,UAAU;AACrB,aAAO,eAAe,SAAS,KAAK,EAAE,OAAO,OAAO,GAAG,GAAG,cAAc,KAAK,CAAC;AAAA,IAChF;AAAA,EACF;AAEA,SAAO;AACT;AAaA,SAAS,OAAa;AAAC;;;AD5CvB,IAAM,QAAQ;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAmBd,eAAsB,KAAK,OAAiB,QAAQ,KAAK,MAAM,CAAC,GAAoB;AAClF,MAAI;AACJ,MAAI;AACF,aAAS,UAAU;AAAA,MACjB,MAAM;AAAA,MACN,kBAAkB;AAAA,MAClB,SAAS;AAAA,QACP,QAAQ,EAAE,MAAM,SAAS;AAAA,QACzB,gBAAgB,EAAE,MAAM,SAAS;AAAA,QACjC,OAAO,EAAE,MAAM,UAAU,UAAU,KAAK;AAAA,QACxC,SAAS,EAAE,MAAM,WAAW,SAAS,MAAM;AAAA,QAC3C,SAAS,EAAE,MAAM,WAAW,SAAS,MAAM;AAAA,QAC3C,MAAM,EAAE,MAAM,WAAW,SAAS,MAAM;AAAA,QACxC,OAAO,EAAE,MAAM,WAAW,SAAS,MAAM;AAAA,QACzC,MAAM,EAAE,MAAM,WAAW,OAAO,KAAK,SAAS,MAAM;AAAA,QACpD,SAAS,EAAE,MAAM,WAAW,OAAO,KAAK,SAAS,MAAM;AAAA,MACzD;AAAA,IACF,CAAC;AAAA,EACH,SAAS,OAAO;AACd,eAAW,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,CAAC;AACjE,eAAW,KAAK;AAChB,WAAO;AAAA,EACT;AAEA,QAAM,EAAE,QAAQ,YAAY,IAAI;AAChC,MAAI,OAAO,MAAM;AACf,UAAM,KAAK;AACX,WAAO;AAAA,EACT;AACA,MAAI,OAAO,SAAS;AAClB,UAAM,MAAM,YAAY,CAAC;AACzB,WAAO;AAAA,EACT;AAEA,QAAM,CAAC,SAAS,QAAQ,IAAI;AAC5B,MAAI,CAAC,SAAS;AACZ,UAAM,KAAK;AACX,WAAO;AAAA,EACT;AACA,MAAI,CAAC,CAAC,WAAW,YAAY,OAAO,EAAE,SAAS,OAAO,GAAG;AACvD,eAAW,oBAAoB,OAAO,GAAG;AACzC,eAAW,KAAK;AAChB,WAAO;AAAA,EACT;AAEA,QAAM,aAAa,OAAO,UAAU,WAAW;AAC/C,MAAI,CAAC,YAAY;AACf;AAAA,MACE;AAAA,IAEF;AACA,WAAO;AAAA,EACT;AAIA,QAAM,eAAe,WAAW;AAChC,MAAI;AACF,UAAM,SAAS;AAAA,MACb;AAAA,MACA,GAAI,WAAW,EAAE,SAAS,IAAI,CAAC;AAAA,MAC/B,GAAI,OAAO,QAAQ,EAAE,OAAO,OAAO,MAAM,IAAI,CAAC;AAAA,MAC9C,GAAI,OAAO,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;AAAA,MACpC,GAAI,OAAO,QAAQ,EAAE,OAAO,KAAK,IAAI,CAAC;AAAA,MACtC,GAAI,OAAO,cAAc,IAAI,EAAE,aAAa,OAAO,cAAc,EAAE,IAAI,CAAC;AAAA,IAC1E;AAEA,QAAI,YAAY,WAAW;AACzB,YAAM,EAAE,WAAW,IAAI,MAAM,OAAO,uBAAuB;AAC3D,aAAO,MAAM,WAAW;AAAA,QACtB,GAAG;AAAA,QACH,GAAI,OAAO,UAAU,EAAE,SAAS,KAAK,IAAI,CAAC;AAAA,QAC1C,GAAI,OAAO,UAAU,EAAE,SAAS,KAAK,IAAI,CAAC;AAAA,MAC5C,CAAC;AAAA,IACH;AACA,QAAI,YAAY,YAAY;AAC1B,YAAM,EAAE,YAAY,IAAI,MAAM,OAAO,wBAAwB;AAC7D,aAAO,MAAM,YAAY,MAAM;AAAA,IACjC;AACA,UAAM,EAAE,SAAS,IAAI,MAAM,OAAO,qBAAqB;AACvD,WAAO,MAAM,SAAS,MAAM;AAAA,EAC9B,SAAS,OAAO;AACd,eAAW,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,CAAC;AACjE,QAAI,iBAAiB,SAAS,MAAM,SAAS,QAAQ,IAAI,qBAAqB,GAAG;AAC/E,iBAAW,MAAM,KAAK;AAAA,IACxB;AACA,WAAO;AAAA,EACT,UAAE;AACA,iBAAa;AAAA,EACf;AACF;AAEA,eAAe,cAA+B;AAC5C,MAAI;AACF,UAAM,EAAE,aAAa,IAAI,MAAM,OAAO,IAAS;AAC/C,UAAM,OAAO,cAAc,IAAI,IAAI,mBAAmB,YAAY,GAAG,CAAC;AACtE,WAAQ,KAAK,MAAM,aAAa,MAAM,MAAM,CAAC,EAA0B;AAAA,EACzE,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAQA,SAAS,kBAA2B;AAClC,QAAM,QAAQ,QAAQ,KAAK,CAAC;AAC5B,MAAI,CAAC,MAAO,QAAO;AACnB,MAAI;AACF,WAAO,cAAc,YAAY,GAAG,MAAM,aAAa,KAAK;AAAA,EAC9D,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,IAAI,gBAAgB,GAAG;AACrB,OAAK,EAAE;AAAA,IACL,CAAC,SAAS;AACR,cAAQ,WAAW;AAAA,IACrB;AAAA,IACA,CAAC,UAAmB;AAClB,iBAAW,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,CAAC;AACjE,cAAQ,WAAW;AAAA,IACrB;AAAA,EACF;AACF;","names":[]}
@@ -0,0 +1,79 @@
1
+ import {
2
+ renderDiffPlain
3
+ } from "./chunk-KZUR4CAU.js";
4
+ import {
5
+ createSurfaceRunner,
6
+ isPlain,
7
+ loadInk,
8
+ paint,
9
+ write,
10
+ writeError
11
+ } from "./chunk-S2LM3N6D.js";
12
+ import {
13
+ annotate,
14
+ baselineDirFor,
15
+ baselinePath,
16
+ diff,
17
+ normalize,
18
+ readBaseline
19
+ } from "./chunk-ODUIFFPM.js";
20
+
21
+ // src/commands/check.tsx
22
+ import { relative } from "path";
23
+ import { jsx } from "react/jsx-runtime";
24
+ async function runCheck(options) {
25
+ const runner = await createSurfaceRunner(options.configPath);
26
+ try {
27
+ const scenarios = options.scenario ? [options.scenario] : runner.scenarioNames;
28
+ const dir = baselineDirFor(options.configPath, options.baselineDir ?? runner.config.baselineDir);
29
+ const drifted = [];
30
+ for (const scenario of scenarios) {
31
+ const result = await runner.collect({
32
+ scenario,
33
+ ...options.scope ? { scope: options.scope } : {}
34
+ });
35
+ const path = baselinePath(dir, scenario);
36
+ const expected = readBaseline(path);
37
+ if (expected === void 0) {
38
+ drifted.push({ scenario, missingBaseline: true, entries: [] });
39
+ continue;
40
+ }
41
+ const actual = normalize(result.snapshot);
42
+ const entries = annotate(diff(expected, actual), actual, expected);
43
+ if (entries.length > 0) drifted.push({ scenario, entries });
44
+ }
45
+ if (options.json) {
46
+ write(JSON.stringify({ ok: drifted.length === 0, drifted }, null, 2));
47
+ return drifted.length === 0 ? 0 : 1;
48
+ }
49
+ if (drifted.length === 0) {
50
+ write(`surface matches the baseline (${scenarios.length} scenario${scenarios.length === 1 ? "" : "s"})`);
51
+ return 0;
52
+ }
53
+ const ink = isPlain(options) ? null : await loadInk();
54
+ for (const entry of drifted) {
55
+ if (entry.missingBaseline) {
56
+ writeError(
57
+ `${entry.scenario}: no baseline at ${relative(
58
+ process.cwd(),
59
+ baselinePath(dir, entry.scenario)
60
+ )} \u2014 run \`agent-surface snapshot\` and commit it`
61
+ );
62
+ continue;
63
+ }
64
+ if (ink) await paint(/* @__PURE__ */ jsx(ink.Drift, { scenario: entry.scenario, entries: entry.entries }));
65
+ else write(renderDiffPlain(entry.scenario, entry.entries));
66
+ }
67
+ writeError(
68
+ `
69
+ surface drift in ${drifted.length} scenario${drifted.length === 1 ? "" : "s"} \u2014 review the change, then \`agent-surface snapshot\` to accept it`
70
+ );
71
+ return 1;
72
+ } finally {
73
+ await runner.close();
74
+ }
75
+ }
76
+ export {
77
+ runCheck
78
+ };
79
+ //# sourceMappingURL=check-7Z2VNN5R.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/commands/check.tsx"],"sourcesContent":["import { relative } from \"node:path\";\nimport { createSurfaceRunner } from \"../load.js\";\nimport {\n annotate,\n baselineDirFor,\n baselinePath,\n diff,\n normalize,\n readBaseline,\n type DiffEntry,\n} from \"../baseline.js\";\nimport { renderDiffPlain } from \"../render/plain.js\";\nimport { isPlain, loadInk, paint, write, writeError } from \"../output.js\";\n\nexport interface CheckOptions {\n configPath: string;\n scenario?: string;\n scope?: string[];\n baselineDir?: string;\n json?: boolean;\n plain?: boolean;\n}\n\ninterface ScenarioDrift {\n scenario: string;\n missingBaseline?: boolean;\n entries: DiffEntry[];\n}\n\n/**\n * Compares every scenario against its committed baseline. Exit code is the\n * point: 0 when the surface is what the repo says it is, 1 when it drifted —\n * so CI fails on an unreviewed change to what agents can see.\n */\nexport async function runCheck(options: CheckOptions): Promise<number> {\n const runner = await createSurfaceRunner(options.configPath);\n try {\n const scenarios = options.scenario ? [options.scenario] : runner.scenarioNames;\n const dir = baselineDirFor(options.configPath, options.baselineDir ?? runner.config.baselineDir);\n const drifted: ScenarioDrift[] = [];\n\n for (const scenario of scenarios) {\n const result = await runner.collect({\n scenario,\n ...(options.scope ? { scope: options.scope } : {}),\n });\n const path = baselinePath(dir, scenario);\n const expected = readBaseline(path);\n\n if (expected === undefined) {\n drifted.push({ 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, entries });\n }\n\n if (options.json) {\n write(JSON.stringify({ ok: drifted.length === 0, drifted }, null, 2));\n return drifted.length === 0 ? 0 : 1;\n }\n\n if (drifted.length === 0) {\n write(`surface matches the baseline (${scenarios.length} scenario${scenarios.length === 1 ? \"\" : \"s\"})`);\n return 0;\n }\n\n const ink = isPlain(options) ? null : await loadInk();\n for (const entry of drifted) {\n if (entry.missingBaseline) {\n writeError(\n `${entry.scenario}: no baseline at ${relative(\n process.cwd(),\n baselinePath(dir, entry.scenario),\n )} — run \\`agent-surface snapshot\\` and commit it`,\n );\n continue;\n }\n if (ink) await paint(<ink.Drift scenario={entry.scenario} entries={entry.entries} />);\n else write(renderDiffPlain(entry.scenario, entry.entries));\n }\n writeError(\n `\\nsurface drift in ${drifted.length} scenario${drifted.length === 1 ? \"\" : \"s\"} — review the change, then \\`agent-surface snapshot\\` to accept it`,\n );\n return 1;\n } finally {\n await runner.close();\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;AAAA,SAAS,gBAAgB;AA+EE;AA7C3B,eAAsB,SAAS,SAAwC;AACrE,QAAM,SAAS,MAAM,oBAAoB,QAAQ,UAAU;AAC3D,MAAI;AACF,UAAM,YAAY,QAAQ,WAAW,CAAC,QAAQ,QAAQ,IAAI,OAAO;AACjE,UAAM,MAAM,eAAe,QAAQ,YAAY,QAAQ,eAAe,OAAO,OAAO,WAAW;AAC/F,UAAM,UAA2B,CAAC;AAElC,eAAW,YAAY,WAAW;AAChC,YAAM,SAAS,MAAM,OAAO,QAAQ;AAAA,QAClC;AAAA,QACA,GAAI,QAAQ,QAAQ,EAAE,OAAO,QAAQ,MAAM,IAAI,CAAC;AAAA,MAClD,CAAC;AACD,YAAM,OAAO,aAAa,KAAK,QAAQ;AACvC,YAAM,WAAW,aAAa,IAAI;AAElC,UAAI,aAAa,QAAW;AAC1B,gBAAQ,KAAK,EAAE,UAAU,iBAAiB,MAAM,SAAS,CAAC,EAAE,CAAC;AAC7D;AAAA,MACF;AACA,YAAM,SAAS,UAAU,OAAO,QAAQ;AACxC,YAAM,UAAU,SAAS,KAAK,UAAU,MAAM,GAAG,QAAQ,QAAQ;AACjE,UAAI,QAAQ,SAAS,EAAG,SAAQ,KAAK,EAAE,UAAU,QAAQ,CAAC;AAAA,IAC5D;AAEA,QAAI,QAAQ,MAAM;AAChB,YAAM,KAAK,UAAU,EAAE,IAAI,QAAQ,WAAW,GAAG,QAAQ,GAAG,MAAM,CAAC,CAAC;AACpE,aAAO,QAAQ,WAAW,IAAI,IAAI;AAAA,IACpC;AAEA,QAAI,QAAQ,WAAW,GAAG;AACxB,YAAM,iCAAiC,UAAU,MAAM,YAAY,UAAU,WAAW,IAAI,KAAK,GAAG,GAAG;AACvG,aAAO;AAAA,IACT;AAEA,UAAM,MAAM,QAAQ,OAAO,IAAI,OAAO,MAAM,QAAQ;AACpD,eAAW,SAAS,SAAS;AAC3B,UAAI,MAAM,iBAAiB;AACzB;AAAA,UACE,GAAG,MAAM,QAAQ,oBAAoB;AAAA,YACnC,QAAQ,IAAI;AAAA,YACZ,aAAa,KAAK,MAAM,QAAQ;AAAA,UAClC,CAAC;AAAA,QACH;AACA;AAAA,MACF;AACA,UAAI,IAAK,OAAM,MAAM,oBAAC,IAAI,OAAJ,EAAU,UAAU,MAAM,UAAU,SAAS,MAAM,SAAS,CAAE;AAAA,UAC/E,OAAM,gBAAgB,MAAM,UAAU,MAAM,OAAO,CAAC;AAAA,IAC3D;AACA;AAAA,MACE;AAAA,mBAAsB,QAAQ,MAAM,YAAY,QAAQ,WAAW,IAAI,KAAK,GAAG;AAAA,IACjF;AACA,WAAO;AAAA,EACT,UAAE;AACA,UAAM,OAAO,MAAM;AAAA,EACrB;AACF;","names":[]}
@@ -0,0 +1,29 @@
1
+ // src/mount.ts
2
+ import { act } from "@testing-library/react";
3
+ import { renderAgentSurface } from "@agent-surface/testing/react";
4
+ var DEFAULT_CLI_CONSUMER = { id: "cli", kind: "test" };
5
+ async function mountScenario(config, scenario, options = {}) {
6
+ const props = config.scenarios[scenario];
7
+ if (!props) {
8
+ const known = Object.keys(config.scenarios);
9
+ throw new Error(
10
+ `unknown scenario "${scenario}" \u2014 this config defines ${known.length > 0 ? known.map((name) => `"${name}"`).join(", ") : "none"}`
11
+ );
12
+ }
13
+ const consumer = options.consumer ?? config.consumer ?? DEFAULT_CLI_CONSUMER;
14
+ const mounted = await config.mount({ ...props, scenario });
15
+ const surface = await renderAgentSurface(mounted.ui, {
16
+ registry: mounted.registry,
17
+ consumer
18
+ });
19
+ await act(async () => {
20
+ });
21
+ await config.settle?.(mounted);
22
+ return { scenario, surface, mounted, app: mounted.app, consumer };
23
+ }
24
+
25
+ export {
26
+ DEFAULT_CLI_CONSUMER,
27
+ mountScenario
28
+ };
29
+ //# sourceMappingURL=chunk-A2G4QLX5.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/mount.ts"],"sourcesContent":["import { act } from \"@testing-library/react\";\nimport { renderAgentSurface, type RenderedAgentSurface } from \"@agent-surface/testing/react\";\nimport type { AgentConsumer } from \"@agent-surface/core\";\nimport type { MountResult, ScenarioProps, SurfaceConfig } from \"./config.js\";\n\nexport const DEFAULT_CLI_CONSUMER: AgentConsumer = { id: \"cli\", kind: \"test\" };\n\nexport interface MountScenarioOptions {\n consumer?: AgentConsumer;\n}\n\nexport interface MountedScenario<TApp> {\n scenario: string;\n surface: RenderedAgentSurface;\n mounted: MountResult<TApp>;\n /**\n * Whatever `mount()` returned as `app`. Typed as `TApp` rather than\n * `TApp | undefined` because a config that never sets it infers `TApp` as\n * `unknown`, and forcing a `!` on every test that *does* set it is worse\n * than trusting the config's own return type.\n */\n app: TApp;\n consumer: AgentConsumer;\n}\n\n/**\n * The one mounting path. `agent-surface inspect` and the Vitest helper both\n * come through here, so a scenario cannot behave one way in CI and another in\n * the terminal — which is the entire reason scenarios live in one file.\n */\nexport async function mountScenario<TScenario extends ScenarioProps, TApp>(\n config: SurfaceConfig<TScenario, TApp>,\n scenario: string,\n options: MountScenarioOptions = {},\n): Promise<MountedScenario<TApp>> {\n const props = config.scenarios[scenario];\n if (!props) {\n const known = Object.keys(config.scenarios);\n throw new Error(\n `unknown scenario \"${scenario}\" — this config defines ${\n known.length > 0 ? known.map((name) => `\"${name}\"`).join(\", \") : \"none\"\n }`,\n );\n }\n\n const consumer = options.consumer ?? config.consumer ?? DEFAULT_CLI_CONSUMER;\n const mounted = await config.mount({ ...props, scenario });\n const surface = await renderAgentSurface(mounted.ui, {\n registry: mounted.registry,\n consumer,\n });\n\n // Mount effects have flushed, but whatever the first render *started* has\n // not settled — the initial fetch that fills a table, typically. This flush\n // drains pending microtasks; `settle` covers anything slower.\n await act(async () => {});\n await config.settle?.(mounted);\n\n return { scenario, surface, mounted, app: mounted.app as TApp, consumer };\n}\n"],"mappings":";AAAA,SAAS,WAAW;AACpB,SAAS,0BAAqD;AAIvD,IAAM,uBAAsC,EAAE,IAAI,OAAO,MAAM,OAAO;AAyB7E,eAAsB,cACpB,QACA,UACA,UAAgC,CAAC,GACD;AAChC,QAAM,QAAQ,OAAO,UAAU,QAAQ;AACvC,MAAI,CAAC,OAAO;AACV,UAAM,QAAQ,OAAO,KAAK,OAAO,SAAS;AAC1C,UAAM,IAAI;AAAA,MACR,qBAAqB,QAAQ,gCAC3B,MAAM,SAAS,IAAI,MAAM,IAAI,CAAC,SAAS,IAAI,IAAI,GAAG,EAAE,KAAK,IAAI,IAAI,MACnE;AAAA,IACF;AAAA,EACF;AAEA,QAAM,WAAW,QAAQ,YAAY,OAAO,YAAY;AACxD,QAAM,UAAU,MAAM,OAAO,MAAM,EAAE,GAAG,OAAO,SAAS,CAAC;AACzD,QAAM,UAAU,MAAM,mBAAmB,QAAQ,IAAI;AAAA,IACnD,UAAU,QAAQ;AAAA,IAClB;AAAA,EACF,CAAC;AAKD,QAAM,IAAI,YAAY;AAAA,EAAC,CAAC;AACxB,QAAM,OAAO,SAAS,OAAO;AAE7B,SAAO,EAAE,UAAU,SAAS,SAAS,KAAK,QAAQ,KAAa,SAAS;AAC1E;","names":[]}
@@ -0,0 +1,84 @@
1
+ import {
2
+ formatValue
3
+ } from "./chunk-ODUIFFPM.js";
4
+
5
+ // src/render/plain.ts
6
+ var MARK = { expose: "+", disable: "~", hide: "-" };
7
+ function renderRow(row, lines) {
8
+ const tags = row.tags.length > 0 ? ` [${row.tags.join(", ")}]` : "";
9
+ lines.push(` ${MARK[row.outcome]} ${row.name}${tags}`);
10
+ lines.push(` ${row.description}`);
11
+ if (row.reason) lines.push(` reason: ${row.reason}`);
12
+ if (row.policies) {
13
+ if (row.policies.length === 0) {
14
+ lines.push(" policies: none");
15
+ } else {
16
+ for (const policy of row.policies) {
17
+ const vote = policy.discovery ? policy.discovery.decision === "disable" ? `disable \u2014 ${policy.discovery.reason}` : policy.discovery.decision : "no discovery hook";
18
+ const phases = policy.phases.length > 0 ? policy.phases.join("/") : "\u2014";
19
+ const flags = [
20
+ policy.threw ? "THREW" : "",
21
+ policy.confirmationEscalation ? "escalates-confirmation" : ""
22
+ ].filter(Boolean).join(", ");
23
+ lines.push(
24
+ ` policy ${policy.name} (${policy.scope}, ${phases}): ${vote}${flags ? ` [${flags}]` : ""}`
25
+ );
26
+ }
27
+ }
28
+ if (row.availability && !row.availability.available) {
29
+ lines.push(
30
+ ` availability: unavailable${row.availability.reason ? ` \u2014 ${row.availability.reason}` : ""}`
31
+ );
32
+ }
33
+ }
34
+ if (row.schemas) {
35
+ if (row.schemas.input !== void 0) {
36
+ lines.push(` input: ${JSON.stringify(row.schemas.input)}`);
37
+ }
38
+ if (row.schemas.output !== void 0) {
39
+ lines.push(` output: ${JSON.stringify(row.schemas.output)}`);
40
+ }
41
+ }
42
+ }
43
+ function renderSurfacePlain(view) {
44
+ const lines = [];
45
+ lines.push(`scenario ${view.scenario}${view.route ? ` route ${view.route}` : ""}`);
46
+ lines.push(
47
+ `${view.counts.callable} callable, ${view.counts.disabled} visible-disabled${view.explained ? `, ${view.counts.hidden} hidden` : ""}`
48
+ );
49
+ const populated = view.groups.filter((group) => group.rows.length > 0);
50
+ if (populated.length === 0) {
51
+ lines.push("");
52
+ lines.push("Nothing is registered for this scenario \u2014 the agent has no surface here.");
53
+ if (!view.explained) {
54
+ lines.push("Re-run with --explain to see whether a policy hid it.");
55
+ }
56
+ return lines.join("\n");
57
+ }
58
+ for (const group of populated) {
59
+ lines.push("");
60
+ lines.push(`${group.heading} (${group.rows.length})`);
61
+ for (const row of group.rows) renderRow(row, lines);
62
+ }
63
+ return lines.join("\n");
64
+ }
65
+ function renderDiffPlain(scenario, entries) {
66
+ const lines = [`${scenario}: ${entries.length} change${entries.length === 1 ? "" : "s"}`];
67
+ for (const entry of entries) {
68
+ const where = entry.subject ? `${entry.subject} (${entry.path})` : entry.path;
69
+ if (entry.kind === "added") lines.push(` + ${where} ${formatValue(entry.after)}`);
70
+ else if (entry.kind === "removed") lines.push(` - ${where} ${formatValue(entry.before)}`);
71
+ else {
72
+ lines.push(` ~ ${where}`);
73
+ lines.push(` before: ${formatValue(entry.before)}`);
74
+ lines.push(` after: ${formatValue(entry.after)}`);
75
+ }
76
+ }
77
+ return lines.join("\n");
78
+ }
79
+
80
+ export {
81
+ renderSurfacePlain,
82
+ renderDiffPlain
83
+ };
84
+ //# sourceMappingURL=chunk-KZUR4CAU.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/render/plain.ts"],"sourcesContent":["import type { CapabilityRow, SurfaceView } from \"./model.js\";\nimport type { DiffEntry } from \"../baseline.js\";\nimport { formatValue } from \"../baseline.js\";\n\n/**\n * The no-colour, no-cursor rendering used when stdout is piped or when\n * `--plain`, `CI` or `NO_COLOR` is set. Same view model as the Ink UI, so the\n * two cannot disagree about what the surface contains.\n */\n\nconst MARK = { expose: \"+\", disable: \"~\", hide: \"-\" } as const;\n\nfunction renderRow(row: CapabilityRow, lines: string[]): void {\n const tags = row.tags.length > 0 ? ` [${row.tags.join(\", \")}]` : \"\";\n lines.push(` ${MARK[row.outcome]} ${row.name}${tags}`);\n lines.push(` ${row.description}`);\n if (row.reason) lines.push(` reason: ${row.reason}`);\n\n if (row.policies) {\n if (row.policies.length === 0) {\n lines.push(\" policies: none\");\n } else {\n for (const policy of row.policies) {\n const vote = policy.discovery\n ? policy.discovery.decision === \"disable\"\n ? `disable — ${policy.discovery.reason}`\n : policy.discovery.decision\n : \"no discovery hook\";\n const phases = policy.phases.length > 0 ? policy.phases.join(\"/\") : \"—\";\n const flags = [\n policy.threw ? \"THREW\" : \"\",\n policy.confirmationEscalation ? \"escalates-confirmation\" : \"\",\n ]\n .filter(Boolean)\n .join(\", \");\n lines.push(\n ` policy ${policy.name} (${policy.scope}, ${phases}): ${vote}${\n flags ? ` [${flags}]` : \"\"\n }`,\n );\n }\n }\n if (row.availability && !row.availability.available) {\n lines.push(\n ` availability: unavailable${\n row.availability.reason ? ` — ${row.availability.reason}` : \"\"\n }`,\n );\n }\n }\n\n if (row.schemas) {\n if (row.schemas.input !== undefined) {\n lines.push(` input: ${JSON.stringify(row.schemas.input)}`);\n }\n if (row.schemas.output !== undefined) {\n lines.push(` output: ${JSON.stringify(row.schemas.output)}`);\n }\n }\n}\n\nexport function renderSurfacePlain(view: SurfaceView): string {\n const lines: string[] = [];\n lines.push(`scenario ${view.scenario}${view.route ? ` route ${view.route}` : \"\"}`);\n lines.push(\n `${view.counts.callable} callable, ${view.counts.disabled} visible-disabled${\n view.explained ? `, ${view.counts.hidden} hidden` : \"\"\n }`,\n );\n\n const populated = view.groups.filter((group) => group.rows.length > 0);\n if (populated.length === 0) {\n lines.push(\"\");\n lines.push(\"Nothing is registered for this scenario — the agent has no surface here.\");\n if (!view.explained) {\n lines.push(\"Re-run with --explain to see whether a policy hid it.\");\n }\n return lines.join(\"\\n\");\n }\n\n for (const group of populated) {\n lines.push(\"\");\n lines.push(`${group.heading} (${group.rows.length})`);\n for (const row of group.rows) renderRow(row, lines);\n }\n return lines.join(\"\\n\");\n}\n\nexport function renderDiffPlain(scenario: string, entries: DiffEntry[]): string {\n const lines = [`${scenario}: ${entries.length} change${entries.length === 1 ? \"\" : \"s\"}`];\n for (const entry of entries) {\n const where = entry.subject ? `${entry.subject} (${entry.path})` : entry.path;\n if (entry.kind === \"added\") lines.push(` + ${where} ${formatValue(entry.after)}`);\n else if (entry.kind === \"removed\") lines.push(` - ${where} ${formatValue(entry.before)}`);\n else {\n lines.push(` ~ ${where}`);\n lines.push(` before: ${formatValue(entry.before)}`);\n lines.push(` after: ${formatValue(entry.after)}`);\n }\n }\n return lines.join(\"\\n\");\n}\n"],"mappings":";;;;;AAUA,IAAM,OAAO,EAAE,QAAQ,KAAK,SAAS,KAAK,MAAM,IAAI;AAEpD,SAAS,UAAU,KAAoB,OAAuB;AAC5D,QAAM,OAAO,IAAI,KAAK,SAAS,IAAI,MAAM,IAAI,KAAK,KAAK,IAAI,CAAC,MAAM;AAClE,QAAM,KAAK,KAAK,KAAK,IAAI,OAAO,CAAC,IAAI,IAAI,IAAI,GAAG,IAAI,EAAE;AACtD,QAAM,KAAK,SAAS,IAAI,WAAW,EAAE;AACrC,MAAI,IAAI,OAAQ,OAAM,KAAK,iBAAiB,IAAI,MAAM,EAAE;AAExD,MAAI,IAAI,UAAU;AAChB,QAAI,IAAI,SAAS,WAAW,GAAG;AAC7B,YAAM,KAAK,sBAAsB;AAAA,IACnC,OAAO;AACL,iBAAW,UAAU,IAAI,UAAU;AACjC,cAAM,OAAO,OAAO,YAChB,OAAO,UAAU,aAAa,YAC5B,kBAAa,OAAO,UAAU,MAAM,KACpC,OAAO,UAAU,WACnB;AACJ,cAAM,SAAS,OAAO,OAAO,SAAS,IAAI,OAAO,OAAO,KAAK,GAAG,IAAI;AACpE,cAAM,QAAQ;AAAA,UACZ,OAAO,QAAQ,UAAU;AAAA,UACzB,OAAO,yBAAyB,2BAA2B;AAAA,QAC7D,EACG,OAAO,OAAO,EACd,KAAK,IAAI;AACZ,cAAM;AAAA,UACJ,gBAAgB,OAAO,IAAI,KAAK,OAAO,KAAK,KAAK,MAAM,MAAM,IAAI,GAC/D,QAAQ,KAAK,KAAK,MAAM,EAC1B;AAAA,QACF;AAAA,MACF;AAAA,IACF;AACA,QAAI,IAAI,gBAAgB,CAAC,IAAI,aAAa,WAAW;AACnD,YAAM;AAAA,QACJ,kCACE,IAAI,aAAa,SAAS,WAAM,IAAI,aAAa,MAAM,KAAK,EAC9D;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAEA,MAAI,IAAI,SAAS;AACf,QAAI,IAAI,QAAQ,UAAU,QAAW;AACnC,YAAM,KAAK,gBAAgB,KAAK,UAAU,IAAI,QAAQ,KAAK,CAAC,EAAE;AAAA,IAChE;AACA,QAAI,IAAI,QAAQ,WAAW,QAAW;AACpC,YAAM,KAAK,iBAAiB,KAAK,UAAU,IAAI,QAAQ,MAAM,CAAC,EAAE;AAAA,IAClE;AAAA,EACF;AACF;AAEO,SAAS,mBAAmB,MAA2B;AAC5D,QAAM,QAAkB,CAAC;AACzB,QAAM,KAAK,YAAY,KAAK,QAAQ,GAAG,KAAK,QAAQ,WAAW,KAAK,KAAK,KAAK,EAAE,EAAE;AAClF,QAAM;AAAA,IACJ,GAAG,KAAK,OAAO,QAAQ,cAAc,KAAK,OAAO,QAAQ,oBACvD,KAAK,YAAY,KAAK,KAAK,OAAO,MAAM,YAAY,EACtD;AAAA,EACF;AAEA,QAAM,YAAY,KAAK,OAAO,OAAO,CAAC,UAAU,MAAM,KAAK,SAAS,CAAC;AACrE,MAAI,UAAU,WAAW,GAAG;AAC1B,UAAM,KAAK,EAAE;AACb,UAAM,KAAK,+EAA0E;AACrF,QAAI,CAAC,KAAK,WAAW;AACnB,YAAM,KAAK,uDAAuD;AAAA,IACpE;AACA,WAAO,MAAM,KAAK,IAAI;AAAA,EACxB;AAEA,aAAW,SAAS,WAAW;AAC7B,UAAM,KAAK,EAAE;AACb,UAAM,KAAK,GAAG,MAAM,OAAO,MAAM,MAAM,KAAK,MAAM,GAAG;AACrD,eAAW,OAAO,MAAM,KAAM,WAAU,KAAK,KAAK;AAAA,EACpD;AACA,SAAO,MAAM,KAAK,IAAI;AACxB;AAEO,SAAS,gBAAgB,UAAkB,SAA8B;AAC9E,QAAM,QAAQ,CAAC,GAAG,QAAQ,KAAK,QAAQ,MAAM,UAAU,QAAQ,WAAW,IAAI,KAAK,GAAG,EAAE;AACxF,aAAW,SAAS,SAAS;AAC3B,UAAM,QAAQ,MAAM,UAAU,GAAG,MAAM,OAAO,MAAM,MAAM,IAAI,MAAM,MAAM;AAC1E,QAAI,MAAM,SAAS,QAAS,OAAM,KAAK,OAAO,KAAK,KAAK,YAAY,MAAM,KAAK,CAAC,EAAE;AAAA,aACzE,MAAM,SAAS,UAAW,OAAM,KAAK,OAAO,KAAK,KAAK,YAAY,MAAM,MAAM,CAAC,EAAE;AAAA,SACrF;AACH,YAAM,KAAK,OAAO,KAAK,EAAE;AACzB,YAAM,KAAK,iBAAiB,YAAY,MAAM,MAAM,CAAC,EAAE;AACvD,YAAM,KAAK,iBAAiB,YAAY,MAAM,KAAK,CAAC,EAAE;AAAA,IACxD;AAAA,EACF;AACA,SAAO,MAAM,KAAK,IAAI;AACxB;","names":[]}
@@ -0,0 +1,104 @@
1
+ // src/baseline.ts
2
+ import { mkdirSync, readFileSync, writeFileSync } from "fs";
3
+ import { dirname, join, resolve } from "path";
4
+ import { serializeSurfaceSnapshot } from "@agent-surface/testing";
5
+ var DEFAULT_BASELINE_DIR = ".agent-surface";
6
+ function baselineDirFor(configPath, configured) {
7
+ return resolve(dirname(configPath), configured ?? DEFAULT_BASELINE_DIR);
8
+ }
9
+ function baselinePath(dir, scenario) {
10
+ return join(dir, `${scenario}.json`);
11
+ }
12
+ function normalize(snapshot) {
13
+ return serializeSurfaceSnapshot(snapshot);
14
+ }
15
+ function readBaseline(path) {
16
+ try {
17
+ return JSON.parse(readFileSync(path, "utf8"));
18
+ } catch {
19
+ return void 0;
20
+ }
21
+ }
22
+ function writeBaseline(path, value) {
23
+ mkdirSync(dirname(path), { recursive: true });
24
+ writeFileSync(path, `${JSON.stringify(value, null, 2)}
25
+ `, "utf8");
26
+ }
27
+ var PATH_SEGMENT = /([^.[\]]+)|\[(\d+)\]/g;
28
+ function subjectFor(document, path) {
29
+ let node = document;
30
+ let subject;
31
+ for (const match of path.matchAll(PATH_SEGMENT)) {
32
+ if (typeof node !== "object" || node === null) return subject;
33
+ const record = node;
34
+ const candidate = record["capabilityId"] ?? record["procedureId"];
35
+ if (typeof candidate === "string") subject = candidate;
36
+ const key = match[1] ?? match[2];
37
+ if (key === void 0) return subject;
38
+ node = record[key];
39
+ }
40
+ if (typeof node === "object" && node !== null) {
41
+ const record = node;
42
+ const candidate = record["capabilityId"] ?? record["procedureId"];
43
+ if (typeof candidate === "string") subject = candidate;
44
+ }
45
+ return subject;
46
+ }
47
+ function annotate(entries, after, before) {
48
+ return entries.map((entry) => {
49
+ const subject = subjectFor(after, entry.path) ?? subjectFor(before, entry.path);
50
+ return subject ? { ...entry, subject } : entry;
51
+ });
52
+ }
53
+ function diff(before, after, path = "") {
54
+ if (Object.is(before, after)) return [];
55
+ const bothArrays = Array.isArray(before) && Array.isArray(after);
56
+ const bothObjects = !bothArrays && typeof before === "object" && typeof after === "object" && before !== null && after !== null;
57
+ if (bothArrays) {
58
+ const entries = [];
59
+ const max = Math.max(before.length, after.length);
60
+ for (let i = 0; i < max; i++) {
61
+ const at = `${path}[${i}]`;
62
+ if (i >= before.length) entries.push({ path: at, kind: "added", after: after[i] });
63
+ else if (i >= after.length) entries.push({ path: at, kind: "removed", before: before[i] });
64
+ else entries.push(...diff(before[i], after[i], at));
65
+ }
66
+ return entries;
67
+ }
68
+ if (bothObjects) {
69
+ const entries = [];
70
+ const beforeRecord = before;
71
+ const afterRecord = after;
72
+ const keys = /* @__PURE__ */ new Set([...Object.keys(beforeRecord), ...Object.keys(afterRecord)]);
73
+ for (const key of [...keys].sort()) {
74
+ const at = path ? `${path}.${key}` : key;
75
+ if (!(key in beforeRecord)) {
76
+ entries.push({ path: at, kind: "added", after: afterRecord[key] });
77
+ } else if (!(key in afterRecord)) {
78
+ entries.push({ path: at, kind: "removed", before: beforeRecord[key] });
79
+ } else {
80
+ entries.push(...diff(beforeRecord[key], afterRecord[key], at));
81
+ }
82
+ }
83
+ return entries;
84
+ }
85
+ if (JSON.stringify(before) === JSON.stringify(after)) return [];
86
+ return [{ path: path || "<root>", kind: "changed", before, after }];
87
+ }
88
+ function formatValue(value) {
89
+ if (value === void 0) return "\u2014";
90
+ const text = typeof value === "string" ? value : JSON.stringify(value);
91
+ return text.length > 120 ? `${text.slice(0, 117)}\u2026` : text;
92
+ }
93
+
94
+ export {
95
+ baselineDirFor,
96
+ baselinePath,
97
+ normalize,
98
+ readBaseline,
99
+ writeBaseline,
100
+ annotate,
101
+ diff,
102
+ formatValue
103
+ };
104
+ //# sourceMappingURL=chunk-ODUIFFPM.js.map