opsx-ui 0.1.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 ADDED
@@ -0,0 +1,86 @@
1
+ # opsx-ui
2
+
3
+ A read-only, live-updating **web viewer for everything OpenSpec touches** — a
4
+ browser home for your specs, changes, and proposed deltas that replaces the
5
+ `openspec view` TUI and updates _as agents work_.
6
+
7
+ > opsx-ui only **reads** OpenSpec state. It never writes to, or mutates, any
8
+ > project's `openspec/` directory. Driving agents (propose / apply / archive) is
9
+ > intentionally out of scope for now.
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ npm install -g opsx-ui
15
+ ```
16
+
17
+ Requires the [`openspec`](https://github.com/Fission-AI/OpenSpec) CLI **>= 1.5.0**
18
+ on your PATH.
19
+
20
+ ## Usage
21
+
22
+ ```bash
23
+ opsx-ui # open the OpenSpec project in the current directory
24
+ opsx-ui ./some/project # open a specific project
25
+ opsx-ui -g # global mode: skip cwd, show the project picker
26
+ ```
27
+
28
+ Options:
29
+
30
+ | Flag | Meaning |
31
+ | ---------------- | -------------------------------------------------------- |
32
+ | `-g, --global` | Skip the current directory and open the project picker. |
33
+ | `-p, --port <n>` | Preferred port (auto-increments if taken). Default 4573. |
34
+ | `--poll` | Use filesystem polling (WSL / network mounts). |
35
+ | `--no-open` | Don't open a browser automatically. |
36
+
37
+ If the current directory has no `openspec/`, opsx-ui offers to point you at
38
+ another project or a registered store. Running it in a project with `openspec/`
39
+ opens straight to the **change board**.
40
+
41
+ ## What you get
42
+
43
+ - **Change board** — every active change as a card with task progress and
44
+ validation health, updating live.
45
+ - **Activity feed** — a reverse-chronological rail of what just changed on disk,
46
+ so you can watch an agent's edits land in real time.
47
+ - **Spec browser** — drill into capabilities, requirements, and scenarios.
48
+ - **Spec diff** — a change's proposed deltas beside the current spec, grouped and
49
+ colored by operation (ADDED / MODIFIED / REMOVED).
50
+
51
+ ## Architecture
52
+
53
+ A global CLI boots a local Node server (Express + WebSocket) that reads OpenSpec
54
+ state and streams it to a React/Vite UI.
55
+
56
+ ```
57
+ Browser (React/Vite) ◀── WebSocket (live snapshot + activity) ──▶ Node server
58
+
59
+ chokidar watch(openspec/) │
60
+
61
+ OpenSpecSource (port)
62
+ └ CliOpenSpecSource → `openspec … --json`
63
+ ```
64
+
65
+ All OpenSpec reads go through a single `OpenSpecSource` interface that returns
66
+ typed view-models — the UI never sees raw CLI output. The CLI adapter is the one
67
+ swappable seam, so a future OpenSpec library or HTTP API drops in without
68
+ touching the server or UI.
69
+
70
+ ## Develop
71
+
72
+ ```bash
73
+ npm install
74
+ npm run dev:server # local API + watcher (port 4573)
75
+ npm run dev:web # Vite dev server (port 5273, proxies /api and /ws)
76
+ npm test # unit tests (mappers, source port, read-only guarantee)
77
+ npm run build # bundle web + compile server into dist/
78
+ ```
79
+
80
+ ## Scope
81
+
82
+ v1 renders the diff as operation-grouped delta cards beside the current spec
83
+ (no token-level inline highlighting) because OpenSpec's delta JSON does not
84
+ expose the `### Requirement:` header used as identity. Paired, highlighted
85
+ before→after diffing is a planned follow-up and slots in behind the same
86
+ `OpenSpecSource` seam.
@@ -0,0 +1,69 @@
1
+ #!/usr/bin/env node
2
+ import { Command } from "commander";
3
+ import open from "open";
4
+ import path from "node:path";
5
+ import { ViewerServer } from "../server/viewer-server.js";
6
+ import { ProjectResolver } from "../server/project-resolver.js";
7
+ const DEFAULT_PORT = 4573;
8
+ async function main() {
9
+ const program = new Command();
10
+ program
11
+ .name("opsx-ui")
12
+ .description("A read-only, live web viewer for everything OpenSpec touches.")
13
+ .argument("[dir]", "project directory to open (defaults to current)")
14
+ .option("-g, --global", "skip current directory; open the project picker")
15
+ .option("-p, --port <port>", "preferred port", String(DEFAULT_PORT))
16
+ .option("--poll", "use filesystem polling (WSL/network mounts)")
17
+ .option("--no-open", "do not open a browser automatically")
18
+ .action(async (dir, options) => {
19
+ const cwd = process.cwd();
20
+ const initialProject = resolveInitialProject(cwd, dir, options.global);
21
+ const server = new ViewerServer({
22
+ cwd,
23
+ initialProject,
24
+ usePolling: options.poll,
25
+ });
26
+ const port = await server.start(Number(options.port) || DEFAULT_PORT);
27
+ const url = `http://localhost:${port}`;
28
+ printBanner(url, initialProject, cwd, Boolean(options.global));
29
+ if (options.open)
30
+ await open(url).catch(() => undefined);
31
+ const shutdown = async () => {
32
+ await server.stop();
33
+ process.exit(0);
34
+ };
35
+ process.on("SIGINT", shutdown);
36
+ process.on("SIGTERM", shutdown);
37
+ });
38
+ await program.parseAsync(process.argv);
39
+ }
40
+ /** Decide what (if anything) to open at launch (viewer-cli spec). */
41
+ function resolveInitialProject(cwd, dir, global) {
42
+ if (global)
43
+ return null; // `-g`: never auto-open; show the picker.
44
+ if (dir) {
45
+ const target = path.resolve(cwd, dir);
46
+ return ProjectResolver.hasOpenSpec(target)
47
+ ? ProjectResolver.forDirectory(target, "picked")
48
+ : null;
49
+ }
50
+ return ProjectResolver.fromCwd(cwd); // null → UI offers init / pick.
51
+ }
52
+ function printBanner(url, project, cwd, global) {
53
+ console.log(`\n opsx-ui → ${url}`);
54
+ if (project) {
55
+ console.log(` project → ${project.name} (${project.root})`);
56
+ }
57
+ else if (global) {
58
+ console.log(` global mode: choose a project in the browser`);
59
+ }
60
+ else {
61
+ console.log(` no openspec/ in ${cwd} — initialize or pick one in the browser`);
62
+ }
63
+ console.log("");
64
+ }
65
+ main().catch((error) => {
66
+ console.error(error);
67
+ process.exit(1);
68
+ });
69
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/cli/index.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,IAAI,MAAM,MAAM,CAAC;AACxB,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAC1D,OAAO,EAAE,eAAe,EAAE,MAAM,+BAA+B,CAAC;AAGhE,MAAM,YAAY,GAAG,IAAI,CAAC;AAS1B,KAAK,UAAU,IAAI;IACjB,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;IAC9B,OAAO;SACJ,IAAI,CAAC,SAAS,CAAC;SACf,WAAW,CACV,+DAA+D,CAChE;SACA,QAAQ,CAAC,OAAO,EAAE,iDAAiD,CAAC;SACpE,MAAM,CAAC,cAAc,EAAE,iDAAiD,CAAC;SACzE,MAAM,CAAC,mBAAmB,EAAE,gBAAgB,EAAE,MAAM,CAAC,YAAY,CAAC,CAAC;SACnE,MAAM,CAAC,QAAQ,EAAE,6CAA6C,CAAC;SAC/D,MAAM,CAAC,WAAW,EAAE,qCAAqC,CAAC;SAC1D,MAAM,CAAC,KAAK,EAAE,GAAuB,EAAE,OAAmB,EAAE,EAAE;QAC7D,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,EAAE,CAAC;QAC1B,MAAM,cAAc,GAAG,qBAAqB,CAAC,GAAG,EAAE,GAAG,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;QAEvE,MAAM,MAAM,GAAG,IAAI,YAAY,CAAC;YAC9B,GAAG;YACH,cAAc;YACd,UAAU,EAAE,OAAO,CAAC,IAAI;SACzB,CAAC,CAAC;QAEH,MAAM,IAAI,GAAG,MAAM,MAAM,CAAC,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,IAAI,YAAY,CAAC,CAAC;QACtE,MAAM,GAAG,GAAG,oBAAoB,IAAI,EAAE,CAAC;QAEvC,WAAW,CAAC,GAAG,EAAE,cAAc,EAAE,GAAG,EAAE,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC;QAC/D,IAAI,OAAO,CAAC,IAAI;YAAE,MAAM,IAAI,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,SAAS,CAAC,CAAC;QAEzD,MAAM,QAAQ,GAAG,KAAK,IAAI,EAAE;YAC1B,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;YACpB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAClB,CAAC,CAAC;QACF,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAC;QAC/B,OAAO,CAAC,EAAE,CAAC,SAAS,EAAE,QAAQ,CAAC,CAAC;IAClC,CAAC,CAAC,CAAC;IAEL,MAAM,OAAO,CAAC,UAAU,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;AACzC,CAAC;AAED,qEAAqE;AACrE,SAAS,qBAAqB,CAC5B,GAAW,EACX,GAAuB,EACvB,MAA2B;IAE3B,IAAI,MAAM;QAAE,OAAO,IAAI,CAAC,CAAC,0CAA0C;IACnE,IAAI,GAAG,EAAE,CAAC;QACR,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;QACtC,OAAO,eAAe,CAAC,WAAW,CAAC,MAAM,CAAC;YACxC,CAAC,CAAC,eAAe,CAAC,YAAY,CAAC,MAAM,EAAE,QAAQ,CAAC;YAChD,CAAC,CAAC,IAAI,CAAC;IACX,CAAC;IACD,OAAO,eAAe,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,gCAAgC;AACvE,CAAC;AAED,SAAS,WAAW,CAClB,GAAW,EACX,OAA2B,EAC3B,GAAW,EACX,MAAe;IAEf,OAAO,CAAC,GAAG,CAAC,mBAAmB,GAAG,EAAE,CAAC,CAAC;IACtC,IAAI,OAAO,EAAE,CAAC;QACZ,OAAO,CAAC,GAAG,CAAC,iBAAiB,OAAO,CAAC,IAAI,KAAK,OAAO,CAAC,IAAI,GAAG,CAAC,CAAC;IACjE,CAAC;SAAM,IAAI,MAAM,EAAE,CAAC;QAClB,OAAO,CAAC,GAAG,CAAC,gDAAgD,CAAC,CAAC;IAChE,CAAC;SAAM,CAAC;QACN,OAAO,CAAC,GAAG,CACT,qBAAqB,GAAG,0CAA0C,CACnE,CAAC;IACJ,CAAC;IACD,OAAO,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC;AAClB,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE;IACrB,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IACrB,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;AAClB,CAAC,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=archive-source.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"archive-source.js","sourceRoot":"","sources":["../../src/core/archive-source.ts"],"names":[],"mappings":""}
@@ -0,0 +1,75 @@
1
+ import { mapChangeList, mapDeltas, mapSpec, mapSpecList, mapStatus, mapValidation, } from "./mappers.js";
2
+ import { OpenSpecCliError, UnsupportedCliError } from "./errors.js";
3
+ const MIN_CLI_VERSION = "1.5.0";
4
+ /**
5
+ * Read-only OpenSpecSource backed by the `openspec` CLI (design D2/D3).
6
+ * Composes an injected CliRunner with the pure mappers; contains no rendering
7
+ * or filesystem-mutating logic.
8
+ */
9
+ export class CliOpenSpecSource {
10
+ runner;
11
+ options;
12
+ constructor(runner, options = {}) {
13
+ this.runner = runner;
14
+ this.options = options;
15
+ }
16
+ /** Append `--store <id>` to store-scoped commands when a store is active. */
17
+ scoped(args) {
18
+ return this.options.storeId
19
+ ? [...args, "--store", this.options.storeId]
20
+ : args;
21
+ }
22
+ async json(args, map) {
23
+ const stdout = await this.runner.run(args);
24
+ try {
25
+ return map(JSON.parse(stdout));
26
+ }
27
+ catch (error) {
28
+ throw new OpenSpecCliError(`Could not parse JSON from openspec ${args.join(" ")}: ${error.message}`, args);
29
+ }
30
+ }
31
+ async version() {
32
+ const out = (await this.runner.run(["--version"])).trim();
33
+ const found = out.split(/\s+/).pop() ?? out;
34
+ if (compareSemver(found, MIN_CLI_VERSION) < 0) {
35
+ throw new UnsupportedCliError(found, MIN_CLI_VERSION);
36
+ }
37
+ return found;
38
+ }
39
+ listChanges() {
40
+ return this.json(this.scoped(["list", "--json"]), mapChangeList);
41
+ }
42
+ listSpecs() {
43
+ return this.json(this.scoped(["list", "--specs", "--json"]), mapSpecList);
44
+ }
45
+ getSpec(specId) {
46
+ return this.json(this.scoped(["show", specId, "--json", "--type", "spec"]), mapSpec);
47
+ }
48
+ getDeltas(changeId) {
49
+ return this.json(this.scoped(["show", changeId, "--json", "--deltas-only"]), mapDeltas);
50
+ }
51
+ getStatus(changeId) {
52
+ return this.json(this.scoped(["status", "--change", changeId, "--json"]), mapStatus);
53
+ }
54
+ validateChanges() {
55
+ return this.json(this.scoped(["validate", "--changes", "--json"]), mapValidation);
56
+ }
57
+ }
58
+ /** Minimal semver compare (major.minor.patch). Returns -1 / 0 / 1. */
59
+ export function compareSemver(a, b) {
60
+ const pa = a
61
+ .replace(/^v/, "")
62
+ .split(".")
63
+ .map((n) => parseInt(n, 10) || 0);
64
+ const pb = b
65
+ .replace(/^v/, "")
66
+ .split(".")
67
+ .map((n) => parseInt(n, 10) || 0);
68
+ for (let i = 0; i < 3; i += 1) {
69
+ const diff = (pa[i] ?? 0) - (pb[i] ?? 0);
70
+ if (diff !== 0)
71
+ return Math.sign(diff);
72
+ }
73
+ return 0;
74
+ }
75
+ //# sourceMappingURL=cli-openspec-source.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli-openspec-source.js","sourceRoot":"","sources":["../../src/core/cli-openspec-source.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,29 @@
1
+ import { execFile } from "node:child_process";
2
+ import { OpenSpecCliError, OpenSpecNotFoundError } from "./errors.js";
3
+ /** Production runner: spawns the real `openspec` binary. */
4
+ export class ExecCliRunner {
5
+ cwd;
6
+ bin;
7
+ maxBuffer;
8
+ constructor(options) {
9
+ this.cwd = options.cwd;
10
+ this.bin = options.bin ?? "openspec";
11
+ this.maxBuffer = options.maxBuffer ?? 32 * 1024 * 1024;
12
+ }
13
+ run(args) {
14
+ return new Promise((resolve, reject) => {
15
+ execFile(this.bin, [...args, "--no-color"], { cwd: this.cwd, maxBuffer: this.maxBuffer }, (error, stdout, stderr) => {
16
+ if (error) {
17
+ if (error.code === "ENOENT") {
18
+ reject(new OpenSpecNotFoundError());
19
+ return;
20
+ }
21
+ reject(new OpenSpecCliError(`openspec ${args.join(" ")} failed: ${error.message}`, args, stderr));
22
+ return;
23
+ }
24
+ resolve(stdout);
25
+ });
26
+ });
27
+ }
28
+ }
29
+ //# sourceMappingURL=cli-runner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"cli-runner.js","sourceRoot":"","sources":["../../src/core/cli-runner.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAC9C,OAAO,EAAE,gBAAgB,EAAE,qBAAqB,EAAE,MAAM,aAAa,CAAC;AAoBtE,4DAA4D;AAC5D,MAAM,OAAO,aAAa;IACP,GAAG,CAAS;IACZ,GAAG,CAAS;IACZ,SAAS,CAAS;IAEnC,YAAY,OAA6B;QACvC,IAAI,CAAC,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC;QACvB,IAAI,CAAC,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,UAAU,CAAC;QACrC,IAAI,CAAC,SAAS,GAAG,OAAO,CAAC,SAAS,IAAI,EAAE,GAAG,IAAI,GAAG,IAAI,CAAC;IACzD,CAAC;IAED,GAAG,CAAC,IAAc;QAChB,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YACrC,QAAQ,CACN,IAAI,CAAC,GAAG,EACR,CAAC,GAAG,IAAI,EAAE,YAAY,CAAC,EACvB,EAAE,GAAG,EAAE,IAAI,CAAC,GAAG,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE,EAC5C,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,EAAE;gBACxB,IAAI,KAAK,EAAE,CAAC;oBACV,IAAK,KAA+B,CAAC,IAAI,KAAK,QAAQ,EAAE,CAAC;wBACvD,MAAM,CAAC,IAAI,qBAAqB,EAAE,CAAC,CAAC;wBACpC,OAAO;oBACT,CAAC;oBACD,MAAM,CACJ,IAAI,gBAAgB,CAClB,YAAY,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,YAAY,KAAK,CAAC,OAAO,EAAE,EACrD,IAAI,EACJ,MAAM,CACP,CACF,CAAC;oBACF,OAAO;gBACT,CAAC;gBACD,OAAO,CAAC,MAAM,CAAC,CAAC;YAClB,CAAC,CACF,CAAC;QACJ,CAAC,CAAC,CAAC;IACL,CAAC;CACF"}
@@ -0,0 +1,30 @@
1
+ /** Raised when the `openspec` executable cannot be found on the machine. */
2
+ export class OpenSpecNotFoundError extends Error {
3
+ constructor(message = "The `openspec` CLI was not found on PATH.") {
4
+ super(message);
5
+ this.name = "OpenSpecNotFoundError";
6
+ }
7
+ }
8
+ /** Raised when the installed `openspec` CLI is too old to be supported. */
9
+ export class UnsupportedCliError extends Error {
10
+ found;
11
+ required;
12
+ constructor(found, required) {
13
+ super(`Unsupported openspec CLI ${found}; opsx-ui requires >= ${required}.`);
14
+ this.found = found;
15
+ this.required = required;
16
+ this.name = "UnsupportedCliError";
17
+ }
18
+ }
19
+ /** Raised when an `openspec` invocation exits non-zero or emits bad JSON. */
20
+ export class OpenSpecCliError extends Error {
21
+ args;
22
+ stderr;
23
+ constructor(message, args, stderr) {
24
+ super(message);
25
+ this.args = args;
26
+ this.stderr = stderr;
27
+ this.name = "OpenSpecCliError";
28
+ }
29
+ }
30
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/core/errors.ts"],"names":[],"mappings":"AAAA,4EAA4E;AAC5E,MAAM,OAAO,qBAAsB,SAAQ,KAAK;IAC9C,YAAY,OAAO,GAAG,2CAA2C;QAC/D,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,uBAAuB,CAAC;IACtC,CAAC;CACF;AAED,2EAA2E;AAC3E,MAAM,OAAO,mBAAoB,SAAQ,KAAK;IAE1B;IACA;IAFlB,YACkB,KAAa,EACb,QAAgB;QAEhC,KAAK,CACH,4BAA4B,KAAK,yBAAyB,QAAQ,GAAG,CACtE,CAAC;QALc,UAAK,GAAL,KAAK,CAAQ;QACb,aAAQ,GAAR,QAAQ,CAAQ;QAKhC,IAAI,CAAC,IAAI,GAAG,qBAAqB,CAAC;IACpC,CAAC;CACF;AAED,6EAA6E;AAC7E,MAAM,OAAO,gBAAiB,SAAQ,KAAK;IAGvB;IACA;IAHlB,YACE,OAAe,EACC,IAAc,EACd,MAAe;QAE/B,KAAK,CAAC,OAAO,CAAC,CAAC;QAHC,SAAI,GAAJ,IAAI,CAAU;QACd,WAAM,GAAN,MAAM,CAAS;QAG/B,IAAI,CAAC,IAAI,GAAG,kBAAkB,CAAC;IACjC,CAAC;CACF"}
@@ -0,0 +1,107 @@
1
+ import { readFile, readdir, stat } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { OpenSpecCliError } from "./errors.js";
4
+ const ARTIFACT_FILES = ["proposal", "design", "tasks"];
5
+ const DATE_PREFIX = /^(\d{4}-\d{2}-\d{2})-(.+)$/;
6
+ /**
7
+ * Reads OpenSpec state directly from disk for the things the CLI can't provide:
8
+ * raw markdown and the archive tree (design D1/D6). Strictly read-only, and it
9
+ * refuses paths that escape the project's `openspec/` directory.
10
+ */
11
+ export class FilesystemSource {
12
+ openspecDir;
13
+ archiveDir;
14
+ constructor(projectRoot) {
15
+ this.openspecDir = path.join(projectRoot, "openspec");
16
+ this.archiveDir = path.join(this.openspecDir, "changes", "archive");
17
+ }
18
+ async listArchived() {
19
+ const dirs = await this.safeReaddir(this.archiveDir);
20
+ const summaries = [];
21
+ for (const entry of dirs) {
22
+ const full = path.join(this.archiveDir, entry);
23
+ const info = await stat(full).catch(() => null);
24
+ if (!info?.isDirectory())
25
+ continue;
26
+ const tasks = await this.readTaskProgress(path.join(full, "tasks.md"));
27
+ const match = DATE_PREFIX.exec(entry);
28
+ summaries.push({
29
+ id: entry,
30
+ name: match ? match[2] : entry,
31
+ archivedDate: match ? match[1] : null,
32
+ tasks,
33
+ mtime: info.mtimeMs,
34
+ });
35
+ }
36
+ return summaries
37
+ .sort((a, b) => (a.id < b.id ? 1 : a.id > b.id ? -1 : b.mtime - a.mtime))
38
+ .map(({ mtime: _mtime, ...summary }) => summary);
39
+ }
40
+ async getArchivedChange(id) {
41
+ const dir = this.resolveArchiveEntry(id);
42
+ const [summary] = (await this.listArchived()).filter((s) => s.id === id);
43
+ if (!summary) {
44
+ throw new OpenSpecCliError(`Archived change not found: ${id}`, [
45
+ "archive",
46
+ id,
47
+ ]);
48
+ }
49
+ const artifacts = [];
50
+ for (const artifact of ARTIFACT_FILES) {
51
+ const content = await this.readIfExists(path.join(dir, `${artifact}.md`));
52
+ if (content !== null)
53
+ artifacts.push({ id: artifact, content });
54
+ }
55
+ const deltas = await this.readArchivedDeltas(path.join(dir, "specs"));
56
+ return { ...summary, artifacts, deltas };
57
+ }
58
+ async getRawDocument(relativePath) {
59
+ const resolved = this.resolveWithin(this.openspecDir, relativePath);
60
+ const content = await readFile(resolved, "utf8");
61
+ return { path: relativePath, content };
62
+ }
63
+ // --- helpers ---
64
+ async readArchivedDeltas(specsDir) {
65
+ const capabilities = await this.safeReaddir(specsDir);
66
+ const deltas = [];
67
+ for (const cap of capabilities) {
68
+ const content = await this.readIfExists(path.join(specsDir, cap, "spec.md"));
69
+ if (content !== null)
70
+ deltas.push({ spec: cap, content });
71
+ }
72
+ return deltas;
73
+ }
74
+ async readTaskProgress(tasksPath) {
75
+ const content = await this.readIfExists(tasksPath);
76
+ if (content === null)
77
+ return { completed: 0, total: 0 };
78
+ const done = (content.match(/^\s*- \[x\]/gim) ?? []).length;
79
+ const todo = (content.match(/^\s*- \[ \]/gim) ?? []).length;
80
+ return { completed: done, total: done + todo };
81
+ }
82
+ async safeReaddir(dir) {
83
+ return readdir(dir).catch(() => []);
84
+ }
85
+ async readIfExists(file) {
86
+ return readFile(file, "utf8").catch(() => null);
87
+ }
88
+ /** Resolve an archive entry id, guarding against traversal. */
89
+ resolveArchiveEntry(id) {
90
+ if (id.includes("/") || id.includes("\\") || id.includes("..")) {
91
+ throw new OpenSpecCliError(`Invalid archive id: ${id}`, ["archive", id]);
92
+ }
93
+ return path.join(this.archiveDir, id);
94
+ }
95
+ /** Resolve `relativePath` and ensure it stays under `base`. */
96
+ resolveWithin(base, relativePath) {
97
+ const resolved = path.resolve(base, relativePath);
98
+ const rel = path.relative(base, resolved);
99
+ if (rel.startsWith("..") || path.isAbsolute(rel)) {
100
+ throw new OpenSpecCliError(`Path escapes openspec/: ${relativePath}`, [
101
+ relativePath,
102
+ ]);
103
+ }
104
+ return resolved;
105
+ }
106
+ }
107
+ //# sourceMappingURL=filesystem-source.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"filesystem-source.js","sourceRoot":"","sources":["../../src/core/filesystem-source.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAC3D,OAAO,IAAI,MAAM,WAAW,CAAC;AAU7B,OAAO,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AAE/C,MAAM,cAAc,GAAG,CAAC,UAAU,EAAE,QAAQ,EAAE,OAAO,CAAU,CAAC;AAChE,MAAM,WAAW,GAAG,4BAA4B,CAAC;AAEjD;;;;GAIG;AACH,MAAM,OAAO,gBAAgB;IACV,WAAW,CAAS;IACpB,UAAU,CAAS;IAEpC,YAAY,WAAmB;QAC7B,IAAI,CAAC,WAAW,GAAG,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,UAAU,CAAC,CAAC;QACtD,IAAI,CAAC,UAAU,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,EAAE,SAAS,EAAE,SAAS,CAAC,CAAC;IACtE,CAAC;IAED,KAAK,CAAC,YAAY;QAChB,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC;QACrD,MAAM,SAAS,GAAkD,EAAE,CAAC;QAEpE,KAAK,MAAM,KAAK,IAAI,IAAI,EAAE,CAAC;YACzB,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,KAAK,CAAC,CAAC;YAC/C,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC;YAChD,IAAI,CAAC,IAAI,EAAE,WAAW,EAAE;gBAAE,SAAS;YAEnC,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC;YACvE,MAAM,KAAK,GAAG,WAAW,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACtC,SAAS,CAAC,IAAI,CAAC;gBACb,EAAE,EAAE,KAAK;gBACT,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK;gBAC9B,YAAY,EAAE,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI;gBACrC,KAAK;gBACL,KAAK,EAAE,IAAI,CAAC,OAAO;aACpB,CAAC,CAAC;QACL,CAAC;QAED,OAAO,SAAS;aACb,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC;aACxE,GAAG,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,OAAO,EAAE,EAAE,EAAE,CAAC,OAAO,CAAC,CAAC;IACrD,CAAC;IAED,KAAK,CAAC,iBAAiB,CAAC,EAAU;QAChC,MAAM,GAAG,GAAG,IAAI,CAAC,mBAAmB,CAAC,EAAE,CAAC,CAAC;QACzC,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,IAAI,CAAC,YAAY,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC;QACzE,IAAI,CAAC,OAAO,EAAE,CAAC;YACb,MAAM,IAAI,gBAAgB,CAAC,8BAA8B,EAAE,EAAE,EAAE;gBAC7D,SAAS;gBACT,EAAE;aACH,CAAC,CAAC;QACL,CAAC;QAED,MAAM,SAAS,GAAuB,EAAE,CAAC;QACzC,KAAK,MAAM,QAAQ,IAAI,cAAc,EAAE,CAAC;YACtC,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,QAAQ,KAAK,CAAC,CAAC,CAAC;YAC1E,IAAI,OAAO,KAAK,IAAI;gBAAE,SAAS,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC,CAAC;QAClE,CAAC;QAED,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,kBAAkB,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC,CAAC;QACtE,OAAO,EAAE,GAAG,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,CAAC;IAC3C,CAAC;IAED,KAAK,CAAC,cAAc,CAAC,YAAoB;QACvC,MAAM,QAAQ,GAAG,IAAI,CAAC,aAAa,CAAC,IAAI,CAAC,WAAW,EAAE,YAAY,CAAC,CAAC;QACpE,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;QACjD,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,OAAO,EAAE,CAAC;IACzC,CAAC;IAED,kBAAkB;IAEV,KAAK,CAAC,kBAAkB,CAAC,QAAgB;QAC/C,MAAM,YAAY,GAAG,MAAM,IAAI,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC;QACtD,MAAM,MAAM,GAAoB,EAAE,CAAC;QACnC,KAAK,MAAM,GAAG,IAAI,YAAY,EAAE,CAAC;YAC/B,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,YAAY,CACrC,IAAI,CAAC,IAAI,CAAC,QAAQ,EAAE,GAAG,EAAE,SAAS,CAAC,CACpC,CAAC;YACF,IAAI,OAAO,KAAK,IAAI;gBAAE,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,GAAG,EAAE,OAAO,EAAE,CAAC,CAAC;QAC5D,CAAC;QACD,OAAO,MAAM,CAAC;IAChB,CAAC;IAEO,KAAK,CAAC,gBAAgB,CAAC,SAAiB;QAC9C,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,YAAY,CAAC,SAAS,CAAC,CAAC;QACnD,IAAI,OAAO,KAAK,IAAI;YAAE,OAAO,EAAE,SAAS,EAAE,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,CAAC;QACxD,MAAM,IAAI,GAAG,CAAC,OAAO,CAAC,KAAK,CAAC,gBAAgB,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC;QAC5D,MAAM,IAAI,GAAG,CAAC,OAAO,CAAC,KAAK,CAAC,gBAAgB,CAAC,IAAI,EAAE,CAAC,CAAC,MAAM,CAAC;QAC5D,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,GAAG,IAAI,EAAE,CAAC;IACjD,CAAC;IAEO,KAAK,CAAC,WAAW,CAAC,GAAW;QACnC,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;IACtC,CAAC;IAEO,KAAK,CAAC,YAAY,CAAC,IAAY;QACrC,OAAO,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC;IAClD,CAAC;IAED,+DAA+D;IACvD,mBAAmB,CAAC,EAAU;QACpC,IAAI,EAAE,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,QAAQ,CAAC,IAAI,CAAC,EAAE,CAAC;YAC/D,MAAM,IAAI,gBAAgB,CAAC,uBAAuB,EAAE,EAAE,EAAE,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC,CAAC;QAC3E,CAAC;QACD,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,EAAE,EAAE,CAAC,CAAC;IACxC,CAAC;IAED,+DAA+D;IACvD,aAAa,CAAC,IAAY,EAAE,YAAoB;QACtD,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,IAAI,EAAE,YAAY,CAAC,CAAC;QAClD,MAAM,GAAG,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;QAC1C,IAAI,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC;YACjD,MAAM,IAAI,gBAAgB,CAAC,2BAA2B,YAAY,EAAE,EAAE;gBACpE,YAAY;aACb,CAAC,CAAC;QACL,CAAC;QACD,OAAO,QAAQ,CAAC;IAClB,CAAC;CACF"}
@@ -0,0 +1,9 @@
1
+ export * from "./errors.js";
2
+ export * from "./cli-runner.js";
3
+ export * from "./openspec-source.js";
4
+ export * from "./cli-openspec-source.js";
5
+ export * from "./archive-source.js";
6
+ export * from "./filesystem-source.js";
7
+ export * from "./snapshot-service.js";
8
+ export * as mappers from "./mappers.js";
9
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/core/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,iBAAiB,CAAC;AAChC,cAAc,sBAAsB,CAAC;AACrC,cAAc,0BAA0B,CAAC;AACzC,cAAc,qBAAqB,CAAC;AACpC,cAAc,wBAAwB,CAAC;AACvC,cAAc,uBAAuB,CAAC;AACtC,OAAO,KAAK,OAAO,MAAM,cAAc,CAAC"}
@@ -0,0 +1,129 @@
1
+ function asArray(value) {
2
+ return Array.isArray(value) ? value : [];
3
+ }
4
+ function asString(value, fallback = "") {
5
+ return typeof value === "string" ? value : fallback;
6
+ }
7
+ function asNumber(value, fallback = 0) {
8
+ return typeof value === "number" && Number.isFinite(value) ? value : fallback;
9
+ }
10
+ function mapScenario(raw) {
11
+ const obj = (raw ?? {});
12
+ return { rawText: asString(obj.rawText) };
13
+ }
14
+ function mapRequirement(raw) {
15
+ const obj = (raw ?? {});
16
+ return {
17
+ name: typeof obj.name === "string" ? obj.name : null,
18
+ text: asString(obj.text),
19
+ scenarios: asArray(obj.scenarios).map(mapScenario),
20
+ };
21
+ }
22
+ export function mapChangeList(raw) {
23
+ const obj = (raw ?? {});
24
+ return asArray(obj.changes).map((entry) => {
25
+ const c = (entry ?? {});
26
+ const completed = asNumber(c.completedTasks);
27
+ const total = asNumber(c.totalTasks);
28
+ return {
29
+ name: asString(c.name),
30
+ status: asString(c.status, "in-progress"),
31
+ tasks: { completed, total },
32
+ validation: "unknown",
33
+ lastModified: typeof c.lastModified === "string" ? c.lastModified : null,
34
+ };
35
+ });
36
+ }
37
+ export function mapSpecList(raw) {
38
+ const obj = (raw ?? {});
39
+ return asArray(obj.specs).map((entry) => {
40
+ const s = (entry ?? {});
41
+ const id = asString(s.id);
42
+ return {
43
+ id,
44
+ title: asString(s.title, id),
45
+ requirementCount: asNumber(s.requirementCount),
46
+ };
47
+ });
48
+ }
49
+ export function mapSpec(raw) {
50
+ const obj = (raw ?? {});
51
+ const id = asString(obj.id);
52
+ return {
53
+ id,
54
+ title: asString(obj.title, id),
55
+ overview: typeof obj.overview === "string" ? obj.overview : null,
56
+ requirements: asArray(obj.requirements).map(mapRequirement),
57
+ };
58
+ }
59
+ const KNOWN_OPERATIONS = [
60
+ "ADDED",
61
+ "MODIFIED",
62
+ "REMOVED",
63
+ "RENAMED",
64
+ ];
65
+ function mapOperation(value) {
66
+ const upper = asString(value).toUpperCase();
67
+ return (KNOWN_OPERATIONS.find((op) => op === upper) ??
68
+ "MODIFIED");
69
+ }
70
+ export function mapDeltas(raw) {
71
+ const obj = (raw ?? {});
72
+ const deltas = asArray(obj.deltas).map((entry) => {
73
+ const d = (entry ?? {});
74
+ return {
75
+ spec: asString(d.spec),
76
+ operation: mapOperation(d.operation),
77
+ description: asString(d.description),
78
+ requirement: d.requirement && typeof d.requirement === "object"
79
+ ? mapRequirement(d.requirement)
80
+ : null,
81
+ };
82
+ });
83
+ return {
84
+ changeId: asString(obj.id),
85
+ title: asString(obj.title, asString(obj.id)),
86
+ deltaCount: asNumber(obj.deltaCount, deltas.length),
87
+ deltas,
88
+ };
89
+ }
90
+ export function mapStatus(raw) {
91
+ const obj = (raw ?? {});
92
+ return {
93
+ changeName: asString(obj.changeName),
94
+ schema: asString(obj.schemaName),
95
+ isComplete: obj.isComplete === true,
96
+ applyRequires: asArray(obj.applyRequires).map((x) => asString(x)),
97
+ artifacts: asArray(obj.artifacts).map((entry) => {
98
+ const a = (entry ?? {});
99
+ return { id: asString(a.id), status: asString(a.status) };
100
+ }),
101
+ };
102
+ }
103
+ function mapIssue(raw) {
104
+ if (typeof raw === "string")
105
+ return { level: "error", message: raw };
106
+ const obj = (raw ?? {});
107
+ return {
108
+ level: asString(obj.level ?? obj.severity, "error"),
109
+ message: asString(obj.message ?? obj.text, "Validation issue"),
110
+ };
111
+ }
112
+ export function mapValidation(raw) {
113
+ const obj = (raw ?? {});
114
+ return asArray(obj.items).map((entry) => {
115
+ const i = (entry ?? {});
116
+ return {
117
+ id: asString(i.id),
118
+ valid: i.valid === true,
119
+ issues: asArray(i.issues).map(mapIssue),
120
+ };
121
+ });
122
+ }
123
+ /** Fold a validation result into the board status a card renders. */
124
+ export function toValidationStatus(view) {
125
+ if (!view)
126
+ return "unknown";
127
+ return view.valid ? "valid" : "invalid";
128
+ }
129
+ //# sourceMappingURL=mappers.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mappers.js","sourceRoot":"","sources":["../../src/core/mappers.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=openspec-source.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"openspec-source.js","sourceRoot":"","sources":["../../src/core/openspec-source.ts"],"names":[],"mappings":""}
@@ -0,0 +1,36 @@
1
+ import { toValidationStatus } from "./mappers.js";
2
+ /**
3
+ * Composes the board-level snapshot from independent source calls (design
4
+ * D3/D1). Merges per-change validation health into each card and includes the
5
+ * archived-change list from the filesystem source. Pure orchestration — no CLI
6
+ * or filesystem knowledge of its own, so it is unit-testable against mocks.
7
+ */
8
+ export class SnapshotService {
9
+ source;
10
+ archive;
11
+ constructor(source, archive) {
12
+ this.source = source;
13
+ this.archive = archive;
14
+ }
15
+ async build(project) {
16
+ const [changes, specs, validations, archived] = await Promise.all([
17
+ this.source.listChanges(),
18
+ this.source.listSpecs(),
19
+ this.source.validateChanges().catch(() => []),
20
+ this.archive.listArchived().catch(() => []),
21
+ ]);
22
+ const validationById = new Map(validations.map((v) => [v.id, v]));
23
+ const withHealth = changes.map((change) => ({
24
+ ...change,
25
+ validation: toValidationStatus(validationById.get(change.name)),
26
+ }));
27
+ return {
28
+ project,
29
+ changes: withHealth,
30
+ specs,
31
+ archived,
32
+ generatedAt: new Date().toISOString(),
33
+ };
34
+ }
35
+ }
36
+ //# sourceMappingURL=snapshot-service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"snapshot-service.js","sourceRoot":"","sources":["../../src/core/snapshot-service.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAElD;;;;;GAKG;AACH,MAAM,OAAO,eAAe;IAEP;IACA;IAFnB,YACmB,MAAsB,EACtB,OAAsB;QADtB,WAAM,GAAN,MAAM,CAAgB;QACtB,YAAO,GAAP,OAAO,CAAe;IACtC,CAAC;IAEJ,KAAK,CAAC,KAAK,CAAC,OAAoB;QAC9B,MAAM,CAAC,OAAO,EAAE,KAAK,EAAE,WAAW,EAAE,QAAQ,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;YAChE,IAAI,CAAC,MAAM,CAAC,WAAW,EAAE;YACzB,IAAI,CAAC,MAAM,CAAC,SAAS,EAAE;YACvB,IAAI,CAAC,MAAM,CAAC,eAAe,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC;YAC7C,IAAI,CAAC,OAAO,CAAC,YAAY,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC;SAC5C,CAAC,CAAC;QAEH,MAAM,cAAc,GAAG,IAAI,GAAG,CAAC,WAAW,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;QAClE,MAAM,UAAU,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;YAC1C,GAAG,MAAM;YACT,UAAU,EAAE,kBAAkB,CAAC,cAAc,CAAC,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;SAChE,CAAC,CAAC,CAAC;QAEJ,OAAO;YACL,OAAO;YACP,OAAO,EAAE,UAAU;YACnB,KAAK;YACL,QAAQ;YACR,WAAW,EAAE,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE;SACtC,CAAC;IACJ,CAAC;CACF"}