From d1e180b152f187d57bbbb9bd8eaa5f2658fd0593 Mon Sep 17 00:00:00 2001 From: shuxueshuxue <98064968+shuxueshuxue@users.noreply.github.com> Date: Fri, 11 Sep 2026 03:38:42 -0700 Subject: [PATCH] feat(spexcode-atlas): add SpexCode Atlas plugin One skill, atlas, that reads a repository into a SpexCode spec tree, draws a checked diagram for each part worth one, and hands over the tree as a single browsable page; a whole repository runs as one dynamic workflow, turn by turn where CreateWorkflow is absent. --- assets/spexcode-atlas/icon.png | Bin 0 -> 3474 bytes marketplace.json | 29 ++ .../spexcode-atlas/.zcode-plugin/plugin.json | 24 ++ plugins/spexcode-atlas/README.md | 55 +++ plugins/spexcode-atlas/README_CN.md | 51 +++ plugins/spexcode-atlas/skills/atlas/SKILL.md | 74 ++++ .../spexcode-atlas/skills/atlas/atlas.dwf.ts | 315 ++++++++++++++++++ 7 files changed, 548 insertions(+) create mode 100644 assets/spexcode-atlas/icon.png create mode 100644 plugins/spexcode-atlas/.zcode-plugin/plugin.json create mode 100644 plugins/spexcode-atlas/README.md create mode 100644 plugins/spexcode-atlas/README_CN.md create mode 100644 plugins/spexcode-atlas/skills/atlas/SKILL.md create mode 100644 plugins/spexcode-atlas/skills/atlas/atlas.dwf.ts diff --git a/assets/spexcode-atlas/icon.png b/assets/spexcode-atlas/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..496611bbbbee48d45831e58ae0f0d29c20b064ac GIT binary patch literal 3474 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 literal 0 HcmV?d00001 diff --git a/marketplace.json b/marketplace.json index e96edc0..36ec1b5 100644 --- a/marketplace.json +++ b/marketplace.json @@ -504,6 +504,35 @@ "calendar", "messaging" ] + }, + { + "name": "spexcode-atlas", + "source": "./plugins/spexcode-atlas", + "description": "Read a repository into a SpexCode spec tree, draw an architecture diagram for each part worth one (each checked until it passes), and hand over the whole tree as one browsable page. A whole repository runs as one dynamic workflow.", + "description_i18n": { + "en": "Read a repository into a SpexCode spec tree, draw an architecture diagram for each part worth one (each checked until it passes), and hand over the whole tree as one browsable page. A whole repository runs as one dynamic workflow.", + "zh-CN": "把代码仓库整理成 SpexCode 规格树,给值得配图的部分画架构图(每张图都检查到通过为止),最后交出一个可以浏览的单文件网页。整个仓库的任务作为一个动态工作流运行。" + }, + "version": "0.1.0", + "author": { + "name": "SpexCode", + "url": "https://spexcode.net" + }, + "icon": "https://cdn-zcode.z.ai/zcode/official-plugin/assets/spexcode-atlas/icon.png", + "category": "developer-tools", + "keywords": [ + "spexcode", + "spec", + "architecture", + "diagrams", + "documentation", + "dynamic-workflow" + ], + "displayName": "SpexCode Atlas", + "displayName_i18n": { + "en": "SpexCode Atlas", + "zh-CN": "SpexCode 图集" + } } ] } diff --git a/plugins/spexcode-atlas/.zcode-plugin/plugin.json b/plugins/spexcode-atlas/.zcode-plugin/plugin.json new file mode 100644 index 0000000..a6bc788 --- /dev/null +++ b/plugins/spexcode-atlas/.zcode-plugin/plugin.json @@ -0,0 +1,24 @@ +{ + "name": "spexcode-atlas", + "version": "0.1.0", + "description": "Read a repository into a SpexCode spec tree, draw an architecture diagram for each part worth one (each checked until it passes), and hand over the whole tree as one browsable page. A whole repository runs as one dynamic workflow.", + "description_i18n": { + "en": "Read a repository into a SpexCode spec tree, draw an architecture diagram for each part worth one (each checked until it passes), and hand over the whole tree as one browsable page. A whole repository runs as one dynamic workflow.", + "zh-CN": "把代码仓库整理成 SpexCode 规格树,给值得配图的部分画架构图(每张图都检查到通过为止),最后交出一个可以浏览的单文件网页。整个仓库的任务作为一个动态工作流运行。" + }, + "author": { + "name": "SpexCode", + "url": "https://spexcode.net" + }, + "homepage": "https://spexcode.net", + "repository": "https://github.com/shuxueshuxue/spexcode", + "license": "MIT", + "keywords": [ + "spexcode", + "spec", + "architecture", + "diagrams", + "documentation", + "dynamic-workflow" + ] +} diff --git a/plugins/spexcode-atlas/README.md b/plugins/spexcode-atlas/README.md new file mode 100644 index 0000000..63a2af4 --- /dev/null +++ b/plugins/spexcode-atlas/README.md @@ -0,0 +1,55 @@ +# SpexCode Atlas + +[中文文档](./README_CN.md) + +This plugin gives ZCode one skill, `atlas`, that reads a repository into a [SpexCode](https://spexcode.net) spec tree and +draws its architecture. The result is a `.spec/` folder in the repository, with one `spec.md` per part stating what +that part is for and which file it governs, a diagram beside each node worth one, and a single HTML page that shows +the whole tree and its diagrams and opens straight from disk. + +Maintained by the SpexCode authors, version 0.1.0. + +## What it does + +For a whole repository the skill submits one dynamic workflow (`skills/atlas/atlas.dwf.ts`) with `CreateWorkflow`. +The agent sets its language and phase names and changes nothing else. The run: + +1. surveys the repository and plans its parts; +2. writes the spec for each part in parallel; +3. gates on `spex spec lint` (no errors, 90% coverage) and repairs until it passes; +4. chooses the nodes worth a picture; +5. draws each picture in parallel, each gated on `spex diagram check`; +6. has an independent reader check the top of the tree against the code, each finding re-read by another subagent; +7. commits `.spec/` and publishes the page as the run's `atlas` artifact with a report. + +On a ZCode build without `CreateWorkflow`, the skill does the same job turn by turn and says so. For one node or a +subtree it draws with `spex diagram scaffold` and `spex diagram check` until each diagram passes. + +## Usage + +In a repository, ask for example: + +- "Make a SpexCode atlas of this repository." / "给这个仓库做一份 SpexCode 图集。" +- "Draw a diagram for the session node." + +## Requirements and side effects + +- **Node.js 22 or newer and npm** on PATH. Nothing is installed globally: every SpexCode command runs as + `npx -y -p spexcode@next spex `, which downloads the `spexcode` package from the npm registry into npm's + cache on first use. The page step also downloads `@spexcode/spec-dashboard`. +- **Network:** the npm registry only. No MCP server, no hooks, no remote service, no telemetry. The model is the + one the ZCode session already uses. +- **Files written:** `.spec/` in the current repository (`spec.md` and `diagram.json` files and `.spec/spexcode.json`), + committed to git with `.spec` as the only path, and `spexcode-atlas.html` at the repository root, left + uncommitted. `spex spec lint` also keeps a small history cache (about 8 KB) under `~/.spexcode/projects/`. +- **Commands run:** `spex` subcommands through npx (`spec lint`, `diagram scaffold`, `diagram check`, + `graph --public --html`, `guide`), `git add .spec` and `git commit`, and read-only inspection of the repository. +- **Cost:** the workflow runs many subagents in parallel. On psf/requests (19 source files) it took about two hours + and 43M tokens with GLM-5.2; the runtime lowers and raises its concurrency with the model's rate limits. + +## Source and license + +SpexCode is MIT-licensed: . The diagram renderer (archify) ships inside +the `spexcode` npm package. + +Open a new ZCode session after enabling or updating the plugin so the Skill catalog is refreshed. diff --git a/plugins/spexcode-atlas/README_CN.md b/plugins/spexcode-atlas/README_CN.md new file mode 100644 index 0000000..512aa0e --- /dev/null +++ b/plugins/spexcode-atlas/README_CN.md @@ -0,0 +1,51 @@ +# SpexCode 图集 + +[English](./README.md) + +这个插件给 ZCode 加一个 skill:`atlas`。它把代码仓库整理成 [SpexCode](https://spexcode.net) 规格树并画出架构。 +结果是仓库里的一个 `.spec/` 目录:每个部分一个 `spec.md`,写明这部分做什么、管哪个文件;值得配图的节点旁边有一张图; +另外还有一个单文件 HTML 网页,展示整棵树和所有图,直接从磁盘打开即可。 + +由 SpexCode 作者维护,版本 0.1.0。 + +## 做什么 + +对整个仓库,skill 用 `CreateWorkflow` 提交一个动态工作流(`skills/atlas/atlas.dwf.ts`)。 +agent 只改其中的语言和阶段名,别的不动。运行过程: + +1. 通读仓库,规划各个部分; +2. 并行撰写各部分的规格; +3. 用 `spex spec lint` 把关(0 个错误、覆盖率 90%),不通过就修复; +4. 挑出值得配图的节点; +5. 并行画图,每张图都要通过 `spex diagram check`; +6. 由独立读者对照代码核查规格树的上层,每条发现再交给另一个子代理复核; +7. 提交 `.spec/`,把网页作为这次运行的 `atlas` 产物交出,附一份报告。 + +如果 ZCode 版本里没有 `CreateWorkflow`,skill 会逐步完成同样的工作,并告诉用户走的是这条路。 +只画一个节点或一棵子树时,用 `spex diagram scaffold` 和 `spex diagram check`,每张图检查到通过为止。 + +## 用法 + +在仓库里这样说,例如: + +- “给这个仓库做一份 SpexCode 图集。” / "Make a SpexCode atlas of this repository." +- “给 session 节点画一张图。” + +## 依赖与副作用 + +- **需要 Node.js 22 或更高版本和 npm**。不做全局安装:每条 SpexCode 命令都以 + `npx -y -p spexcode@next spex <命令>` 运行,第一次使用时会从 npm 仓库把 `spexcode` 包下载进 npm 缓存。 + 生成网页那一步还会下载 `@spexcode/spec-dashboard`。 +- **网络:** 只访问 npm 仓库。没有 MCP server、没有 hook、没有远程服务、没有遥测。模型就是当前 ZCode 会话用的模型。 +- **写入的文件:** 当前仓库的 `.spec/`(`spec.md`、`diagram.json` 和 `.spec/spexcode.json`),以 `.spec` 为唯一路径提交到 git; + 仓库根目录的 `spexcode-atlas.html`,不提交。`spex spec lint` 还会在 `~/.spexcode/projects/` 下留一份很小的历史缓存(约 8 KB)。 +- **执行的命令:** 通过 npx 运行的 `spex` 子命令(`spec lint`、`diagram scaffold`、`diagram check`、 + `graph --public --html`、`guide`),`git add .spec` 和 `git commit`,以及对仓库的只读查看。 +- **开销:** 工作流会并行运行很多子代理。在 psf/requests(19 个源文件)上用 GLM-5.2 跑了约两小时、4300 万 token; + 运行时会根据模型限流自动调低、调高并发。 + +## 来源与许可证 + +SpexCode 采用 MIT 许可证:。图表渲染器(archify)随 `spexcode` npm 包一起发布。 + +启用或更新插件后,请开一个新的 ZCode 会话,让 Skill 列表刷新。 diff --git a/plugins/spexcode-atlas/skills/atlas/SKILL.md b/plugins/spexcode-atlas/skills/atlas/SKILL.md new file mode 100644 index 0000000..98eb5c7 --- /dev/null +++ b/plugins/spexcode-atlas/skills/atlas/SKILL.md @@ -0,0 +1,74 @@ +--- +name: atlas +description: "Use when the user wants the atlas of a repository or its spec tree — read the codebase into a SpexCode spec tree, draw its architecture diagrams and hand over a browsable page (提取 .spec、画架构图、做成可浏览网页), draw the atlas, 画规格图, give node X a diagram, diagram this subtree. For a whole repository it runs one dynamic workflow (turn by turn where ZCode has no CreateWorkflow); for a node or subtree it draws with spex diagram scaffold and check until each passes." +--- + +# atlas + +## Before you start + +This skill draws with SpexCode's command line and needs nothing installed or configured on this machine. + +- Run SpexCode through npx: `npx -y -p spexcode@next spex ` (Node 22 or newer). Wherever a step below says + `spex …`, run it that way; a `spex` already on the PATH works the same. +- A diagram draws one node of the repository's spec tree, the `.spec/` folder. If the repository has none, write + only what the drawing needs: `.spec//spec.md` describing the project, and one folder beside it per part + worth a box, each with its own `spec.md` — a `title:` and a `code:` line naming the file it is about in the + frontmatter, a sentence or two below. That is the whole setup: no `spex init`, no hooks, no agent configuration. + `spex guide spec` has the full file format if you need more. +- `spex guide diagram` is the manual for the diagram format and the loop; read it once. + +## In ZCode: the whole repository as one dynamic workflow + +When the job is a whole repository — read it into a spec tree, draw its pictures, hand over a page to browse +(提取 .spec、画架构图、做成可浏览网页) — do not work through it turn by turn: run it as one dynamic workflow. +`${ZCODE_SKILL_DIR}/atlas.dwf.ts` is that workflow, already written and checked by the workflow compiler. + +1. Read the script. Set `LANGUAGE` to the language the user is speaking and rewrite each `phase("...")` name into + that language. Change nothing else. +2. Submit it with the `CreateWorkflow` tool as its `script` — not the legacy `Workflow` tool, not `Agent`. +3. The run surveys the repository and writes the spec for each part in parallel; gates on `spex spec lint` until it + reports no errors and 90% coverage; chooses the nodes worth a picture, draws them in parallel and gates each on + `spex diagram check`; has an independent reader check the top of the tree against the code; commits `.spec/`; + and publishes the page as its `atlas` artifact, with a report beside it. +4. When it finishes, relay the report: coverage, which pictures pass, what was skipped and why, and every claim the + reader found the code does not bear out. If a subagent escalates, answer it, or fix the script and resubmit with + `resume_from` — the `dynamic-workflows` skill has both. + +A repository that already has a `.spec/` tree keeps it: the workflow skips the survey and starts at the gate. For +one node or one subtree the steps below are enough; the workflow is for the whole job. + +If this ZCode has no `CreateWorkflow` tool (dynamic workflows ship in newer builds), do the same job turn by turn +with the steps below: the setup above for a repository without `.spec/`, `spex spec lint` until it reports no +errors, then each picture worth drawing, checked until it passes. Tell the user this is the turn-by-turn path; a +ZCode build with dynamic workflows runs the same job in parallel. + +Draw the spec tree's pictures: one `diagram.json` beside each node's `spec.md` that is worth one. +The format, the rules and the loop for a single diagram live in `spex guide diagram` — read it before drawing. +This skill is the campaign around that loop. + +1. **Scope.** The user names one node, a subtree, or the whole tree. `spex graph` lists the tree; + `spex spec search ` finds a node by what it is about. +2. **Choose what deserves a picture.** A node whose body explains how its children fit together gets an + architecture diagram of those children. A node whose body is a process, a protocol, a data path or a + lifecycle gets that kind instead. Skip leaves with nothing to show, and nodes that already carry a + `diagram.json` unless the user asked for a redraw. Say what you skipped and why. +3. **Draw each node.** Go top-down, one node at a time; if your harness can run sub-agents, give each node to its + own, handing it only that node's context — its body, its children's titles and descriptions, and these steps. + For one node: + - read its `spec.md` and its children's, and choose the kind from what the body spends its words on; + - `spex diagram scaffold ` (add `--type ` for anything but architecture); + - draw: place the boxes, connect what the body says is connected and name each edge by what crosses it, + group with regions, add cards, and write `meta.note` — what was folded, which relation is an inference; + - `spex diagram check `, and repair from its findings until it passes. + Put no numbers on a picture that move on their own — node counts, drift, commit or import counts. +4. **Keep the spec honest.** What drawing reveals about the spec — a claim the code does not bear out, a + relation the body never states — goes into an issue or your report, never into the picture. +5. **Land it.** `spex spec lint`, then commit the diagrams, together with any spec change they belong to. +6. **Report** which nodes got which kind of diagram, which were skipped and why, and anything you filed. + +## Hand over the page + +`npx -y -p spexcode@next -p @spexcode/spec-dashboard@next spex graph --public --html spexcode-atlas.html` writes the whole tree — every body and +every picture — as one self-contained page that opens in any browser, straight from disk. Offer it with the report; +it is a product of the tree, not part of it, so leave it uncommitted. diff --git a/plugins/spexcode-atlas/skills/atlas/atlas.dwf.ts b/plugins/spexcode-atlas/skills/atlas/atlas.dwf.ts new file mode 100644 index 0000000..caca129 --- /dev/null +++ b/plugins/spexcode-atlas/skills/atlas/atlas.dwf.ts @@ -0,0 +1,315 @@ +// SpexCode atlas, as one ZCode dynamic workflow: read this repository into a SpexCode spec tree (.spec/), draw an +// archify diagram for every node worth one, and hand over the whole tree as one browsable page. SpexCode runs +// through npx, so nothing is installed. Submit this script with CreateWorkflow as it stands, after two edits only: +// set LANGUAGE to the language the user is speaking, and rewrite each phase("...") name into that language. + +const LANGUAGE = "English"; +const SPEX = ["-y", "-p", "spexcode@next", "spex"]; +const SPEX_WITH_PAGE = ["-y", "-p", "spexcode@next", "-p", "@spexcode/spec-dashboard@next", "spex"]; +const PAGE = "spexcode-atlas.html"; +const COVERAGE_FLOOR = 90; +const REPAIR_ROUNDS = 4; +const CHECK_RETRIES = 2; +const NPX_TIMEOUT = 900_000; + +interface Part { + /** Lowercase ascii-kebab id for this part's spec node ("http-client"); it becomes a folder name. */ + id: string; + /** The part's title, in the report language. */ + title: string; + /** One sentence: what this part is responsible for. */ + summary: string; + /** Workspace-relative directories or files this part covers. Every governed source file belongs to exactly one part. */ + paths: string[]; +} +interface Survey { + /** Lowercase ascii-kebab id of the project's root spec node, usually the repository's name. */ + project: string; + /** Directories whose source files must each be covered by a spec; "." means the whole repository. */ + governedRoots: string[]; + /** Source file extensions to govern, with the dot: [".py", ".ts"]. */ + sourceExtensions: string[]; + /** Repository-relative globs to leave out of coverage: vendored, generated or build output. Empty when none. */ + excludeGlobs: string[]; + /** The codebase's top-level parts, 3 to 12 of them, together covering every governed source file. */ + parts: Part[]; +} +interface Written { + /** Ids of the spec nodes written, the part's own included. */ + nodes: string[]; + /** One sentence on anything that could not be settled, or "none". */ + notes: string; +} +interface Gate { + /** Governed source files lint counts. */ + governed: number; + /** Percent of governed files some spec covers. */ + coverage: number; + /** Lint errors, "rule: message" (at most 60). */ + errors: string[]; + errorCount: number; + /** Governed files no spec covers yet (at most 200). */ + uncovered: string[]; + uncoveredCount: number; + /** Set when lint produced no readable report: the tail of what it printed instead. */ + failed?: string; +} +interface Pick { + /** Exactly one node id from the list you were given — a folder name, with nothing added to it. */ + id: string; + /** The kind of picture its body calls for. */ + kind: "architecture" | "workflow" | "sequence" | "dataflow" | "lifecycle"; + /** One sentence: why this node deserves this picture. */ + why: string; +} +interface Choice { + picks: Pick[]; + /** Nodes considered and left without a picture, each with the reason. */ + skipped: { id: string; why: string }[]; +} +interface Drawn { + id: string; + kind: string; + /** True when `spex diagram check` passed on the final attempt. */ + passed: boolean; +} +interface Claim { + /** The spec node whose body makes the claim. */ + node: string; + /** The claim, quoted or closely paraphrased. */ + claim: string; + /** What in the code contradicts it or fails to support it, with path:line. */ + evidence: string; +} +interface Reading { + /** At most 8 claims the code does not bear out; empty when every claim checked holds. */ + claims: Claim[]; + /** The nodes that were read. */ + read: string[]; +} +interface Confirmation { + /** True only when you reproduced the problem yourself from the evidence. */ + reproduced: boolean; + /** One sentence: what you checked. */ + note: string; +} + +// The gate reading, computed where the report is produced: lint's JSON grows with the repository, while +// world.run rejects output over 256KB, so the command prints only what the loop branches on. +const GATE = [ + "const { spawnSync } = require('node:child_process')", + "const run = spawnSync('npx', process.argv.slice(1), { encoding: 'utf8', maxBuffer: 1 << 30 })", + "let report", + "try { report = JSON.parse(run.stdout) } catch { console.log(JSON.stringify({ governed: 0, coverage: 0, errors: [], errorCount: 0, uncovered: [], uncoveredCount: 0, failed: String(run.stderr || run.stdout || run.error || '').slice(-3000) })); process.exit(0) }", + "const findings = report.findings || []", + "const governed = (report.sourceFiles || []).length", + "const uncovered = findings.filter((f) => f.rule === 'coverage').map((f) => f.file || f.msg)", + "const errors = findings.filter((f) => f.level === 'error').map((f) => f.rule + ': ' + (f.spec ? f.spec + ': ' : '') + f.msg)", + "console.log(JSON.stringify({ governed, coverage: governed ? Math.round(((governed - uncovered.length) / governed) * 100) : 0, errors: errors.slice(0, 60), errorCount: errors.length, uncovered: uncovered.slice(0, 200), uncoveredCount: uncovered.length }))", +].join("\n"); + +const tail = (text: string) => text.slice(-4000); +// A gate that cannot read lint decides nothing, and no repair round can fix that, so the run stops and says why. +async function readGate(): Promise { + await world.run("git", ["add", "--", ".spec"]); + // `--` ends node's own options; without it node takes npx's `-y` for one of its flags and runs nothing. + const run = await world.run("node", ["-e", GATE, "--", ...SPEX, "spec", "lint", "--json"], { timeoutMs: NPX_TIMEOUT }); + if (run.exitCode !== 0 || !run.stdout.trim()) throw new Error(`The spec gate did not run:\n${tail(run.stderr || run.stdout)}`); + const gate: Gate = JSON.parse(run.stdout); + if (gate.failed) throw new Error(`spex spec lint produced no report:\n${gate.failed}`); + return gate; +} +const passed = (gate: Gate) => gate.errorCount === 0 && gate.governed > 0 && gate.coverage >= COVERAGE_FLOOR; + +const WRITER = + "You write SpexCode spec nodes. A node is a folder under .spec/ holding a spec.md: YAML frontmatter with title, " + + "desc, `code:` listing AT MOST ONE file the node governs, and `related:` listing the other files it covers or " + + "references; then a markdown body that states the part's responsibility, its invariants and how its pieces fit, " + + "as the code stands today — no history, no plans. A child node is a subfolder with its own spec.md. Run " + + "`npx -y -p spexcode@next spex guide spec` once for the full format. Write only under .spec/, never touch source " + + `code, and write every title, desc and body in ${LANGUAGE}; ids, paths and frontmatter keys stay ascii. If an ` + + "instruction cannot be followed, escalate and say so plainly rather than working around it."; +const CARTOGRAPHER = + "You draw one SpexCode node's diagram: a diagram.json beside its spec.md, an archify IR. Run " + + "`npx -y -p spexcode@next spex guide diagram` once for the format, the rules and the loop. Start from " + + "`npx -y -p spexcode@next spex diagram scaffold --type `, connect what the node's body says is " + + "connected and name each edge by what crosses it, group with regions, add cards, and write meta.note — what was " + + "folded, which relation is an inference. Repair from `npx -y -p spexcode@next spex diagram check ` until it " + + "passes. Put no self-moving numbers on the picture (node counts, drift, import counts). Edit only that node's " + + `diagram.json, and write its visible text in ${LANGUAGE}. If the check cannot pass, escalate and say why.`; + +artifact.table("pictures", { + title: "Pictures", + key: "id", + columns: [{ field: "id", label: "Node" }, { field: "kind", label: "Kind" }, { field: "passed", label: "Check passed" }], +}); + +phase("Read the repository and plan its spec tree"); +const npx = await world.run("npx", [...SPEX, "--version"], { timeoutMs: NPX_TIMEOUT }); +if (npx.exitCode !== 0) throw new Error(`SpexCode did not start through npx:\n${tail(npx.stderr || npx.stdout)}`); +const existing = await files.glob(".spec/**/spec.md"); +let project = ""; +if (existing.length === 0) { + const surveyor = agent("Repository surveyor", { tools: "readonly" }); + const survey = await surveyor.ask( + "Read this repository and plan its SpexCode spec tree. Decide which directories hold its source (governedRoots) " + + "and which file extensions count as source, name what should stay out of coverage (vendored, generated, build " + + "output), and divide the codebase into 3 to 12 top-level parts by responsibility, not by file type. Each part " + + "gets an ascii-kebab id, a title, one sentence of summary, and the paths it covers; together the parts cover " + + `every governed source file. Titles and summaries in ${LANGUAGE}.`, + ); + project = survey.project; + const seen = new Set(); + const parts = survey.parts.filter((part) => !seen.has(part.id) && Boolean(seen.add(part.id))); + log(`Planned ${parts.length} parts for ${project}`); + + phase("Write the spec for each part in parallel"); + const lead = agent("Spec lead", WRITER); + await lead.ask( + `Create .spec/spexcode.json with exactly {"lint": {"governedRoots": ${JSON.stringify(survey.governedRoots)}, ` + + `"sourceExtensions": ${JSON.stringify(survey.sourceExtensions)}, "sourceExcludeGlobs": ${JSON.stringify(survey.excludeGlobs)}}}. ` + + `Then write the root node .spec/${project}/spec.md: what this repository is, and how its parts fit together — ` + + `name each part by a [[part-id]] mention. The parts are:\n${JSON.stringify(parts, null, 2)}\n` + + "Do not write the parts' own folders; other writers are doing that now.", + ); + const written = await Promise.all( + parts.map((part) => + agent(`Spec writer: ${part.id}`, WRITER).ask( + `Write the spec nodes for one part of this repository: .spec/${project}/${part.id}/spec.md for the part itself, ` + + "and a child node for each file or cluster of files worth its own statement. Every governed source file under " + + `the part's paths must end up in some node's code: or related: list. The part:\n${JSON.stringify(part, null, 2)}\n` + + "Write only inside that folder.", + ), + ), + ); + report({ step: "written", parts: parts.length, nodes: written.reduce((sum, part) => sum + part.nodes.length, 0) }); +} + +phase("Check the spec tree and repair it until it passes"); +const repairer = agent("Spec repairer", WRITER); +let gate = await readGate(); +for (let round = 1; round <= REPAIR_ROUNDS && !passed(gate); round++) { + log(`Repair round ${round}: ${gate.errorCount} lint errors, ${gate.coverage}% of ${gate.governed} source files covered`); + await repairer.ask( + "The spec tree does not pass SpexCode's gate yet.\n" + + `Lint errors (${gate.errorCount}):\n${gate.errors.join("\n") || "none"}\n` + + `Source files no spec covers (${gate.uncoveredCount}):\n${gate.uncovered.join("\n") || "none"}\n` + + "Fix every error and cover every listed file — in an existing node's related: list when it belongs to that " + + "node, in a new child node when it deserves its own statement.", + ); + gate = await readGate(); +} +report({ step: "gate", passed: passed(gate), coverage: gate.coverage, governed: gate.governed, errors: gate.errorCount }); +const firstCommit = await world.run("git", ["commit", "-m", "spec: SpexCode spec tree", "--", ".spec"]); +log(firstCommit.exitCode === 0 ? "Committed the spec tree" : "Nothing new to commit in .spec"); + +phase("Choose the nodes worth a picture"); +const specs = await files.glob(".spec/**/spec.md"); +// A node's id is its folder's name; a pick that names anything else would send a cartographer after nothing. +const known = new Set(specs.map((path) => path.split("/").slice(-2, -1).join(""))); +const unknownIds = (c: Choice) => c.picks.map((pick) => pick.id).filter((id) => !known.has(id)); +const planner = agent("Atlas planner", { tools: "readonly" }); +let choice = await planner.ask( + "Choose which nodes of this SpexCode spec tree deserve a diagram. A node whose body explains how its children fit " + + "together gets an architecture diagram of those children; a node whose body is a process, a protocol, a data " + + "path or a lifecycle gets that kind instead. Skip leaves with nothing to show and nodes that already have a " + + "diagram.json beside their spec.md. Always include the root node. A node's id is its folder's name. The nodes:\n" + + specs.join("\n"), +); +if (unknownIds(choice).length) { + choice = await planner.ask( + `These picks name no node: ${unknownIds(choice).join(", ")}. A pick's id is exactly one folder name from the ` + + "list, with nothing added to it. Give the whole choice again.", + ); +} +const drawnIds = new Set(); +const picks = choice.picks.filter((pick) => known.has(pick.id) && !drawnIds.has(pick.id) && Boolean(drawnIds.add(pick.id))); +const skipped = [ + ...choice.skipped, + ...unknownIds(choice).map((id) => ({ id, why: "the planner named a node that does not exist, twice" })), +]; +log(`Drawing ${picks.length} pictures, skipping ${skipped.length} nodes`); + +phase("Draw each picture and check it until it passes"); +const drawn = await Promise.all( + picks.map(async (pick) => { + const cartographer = agent(`Cartographer: ${pick.id}`, CARTOGRAPHER); + await cartographer.ask(`Draw node ${pick.id} as a ${pick.kind} diagram. Why this node: ${pick.why}`); + let check = await world.run("npx", [...SPEX, "diagram", "check", pick.id], { timeoutMs: NPX_TIMEOUT }); + for (let attempt = 1; attempt <= CHECK_RETRIES && check.exitCode !== 0; attempt++) { + await cartographer.ask(`The check still fails:\n${tail(check.stdout + check.stderr)}\nRepair the diagram until it passes.`); + check = await world.run("npx", [...SPEX, "diagram", "check", pick.id], { timeoutMs: NPX_TIMEOUT }); + } + const result: Drawn = { id: pick.id, kind: pick.kind, passed: check.exitCode === 0 }; + report(result, "pictures"); + return result; + }), +); + +phase("Have an independent reader check the tree against the code"); +const reader = agent("Independent reader", { tools: "readonly" }); +const reading = await reader.ask( + `Read the root node .spec/${project || ""}/spec.md and each top-level part's spec.md, and ` + + "check what they claim against the code. List the claims the code does not bear out, with the evidence; an empty " + + `list is a fine answer. Write in ${LANGUAGE}.`, +); +const claims = await Promise.all( + reading.claims.map(async (claim, index) => { + const confirmation = await agent(`Confirmer ${index + 1}`, { tools: "readonly" }).ask( + `Reproduce this finding from its evidence alone: read the code it cites.\n${JSON.stringify(claim)}`, + ); + const status: "verified" | "unconfirmed" = confirmation.reproduced ? "verified" : "unconfirmed"; + const finding = { ...claim, status }; + report(finding); + return finding; + }), +); + +phase("Build the browsable page and hand it over"); +await world.run("git", ["add", "--", ".spec"]); +const lastCommit = await world.run("git", ["commit", "-m", "spec: SpexCode atlas diagrams", "--", ".spec"]); +const page = await world.run("npx", [...SPEX_WITH_PAGE, "graph", "--public", "--html", PAGE], { timeoutMs: NPX_TIMEOUT }); +let pageNote = `The page is ${PAGE}; it is not committed.`; +if (page.exitCode === 0) { + await artifact.file("atlas", PAGE, { title: "Spec atlas", description: "The whole spec tree with its pictures, one self-contained page." }); +} else { + pageNote = `The page could not be written:\n${tail(page.stderr || page.stdout)}`; +} +const drawnOk = drawn.filter((d) => d.passed); +const verifiedClaims = claims.filter((c) => c.status === "verified"); +await artifact.markdown( + "report", + [ + `# Spec atlas: ${gate.coverage}% of ${gate.governed} source files covered, ${drawnOk.length} of ${drawn.length} pictures pass`, + "", + `Spec gate: ${passed(gate) ? "passed" : "not passed"} — ${gate.errorCount} lint errors, coverage ${gate.coverage}% (floor ${COVERAGE_FLOOR}%).`, + "", + "## Pictures", + ...drawn.map((d) => `- ${d.id} (${d.kind}): ${d.passed ? "check passes" : "check still fails"}`), + "", + "## Skipped", + ...skipped.map((s) => `- ${s.id}: ${s.why}`), + "", + "## Claims the code does not bear out", + ...(claims.length ? claims.map((c) => `- ${c.node} (${c.status}): ${c.claim} — ${c.evidence}`) : ["- none found"]), + "", + pageNote, + lastCommit.exitCode === 0 || firstCommit.exitCode === 0 ? "The spec tree and its diagrams are committed under .spec/." : "Nothing under .spec/ needed a commit.", + ].join("\n"), + { title: "Atlas report" }, +); +return { + conclusion: + `The spec tree covers ${gate.coverage}% of ${gate.governed} source files with ${gate.errorCount} lint errors; ` + + `${drawnOk.length} of ${drawn.length} diagrams pass their check. ${pageNote}`, + findings: claims.map((c) => ({ where: c.node, what: c.claim, evidence: c.evidence, status: c.status, severity: "medium" as const })), + verified: [ + "spex spec lint --json after every repair round (errors and coverage)", + ...drawn.map((d) => `spex diagram check ${d.id}: ${d.passed ? "passed" : "failed"}`), + `each of ${claims.length} reported claims re-read by a separate subagent (${verifiedClaims.length} reproduced)`, + ], + notCovered: [ + "claims in nodes below the top-level parts were not checked against the code", + ...(passed(gate) ? [] : [`the spec gate did not pass within ${REPAIR_ROUNDS} repair rounds`]), + ], +};