diff --git a/README.md b/README.md index 23f80ae..abd89aa 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,9 @@ node bin/trace.mjs validate examples/strands-forcing.json --json node bin/trace.mjs deliver examples/strands-forcing.json /tmp/loop-trace/demo.html --json # 用浏览器打开 /tmp/loop-trace/demo.html:T 切主题,E 导出 PNG +# 一次渲染全部示例到画廊目录 +node bin/trace.mjs demo --all /tmp/loop-trace/gallery + # 管道友好:输入传 "-" 即可从 stdin 读 IR cat examples/strands-forcing.json | node bin/trace.mjs validate - --json ``` diff --git a/bin/trace.mjs b/bin/trace.mjs index fc056f0..9325d7b 100644 --- a/bin/trace.mjs +++ b/bin/trace.mjs @@ -1,6 +1,6 @@ #!/usr/bin/env node // agent-loop-trace v0.1 — zero-dep validate + deliver, mirrors archify chain. -import { readFileSync, writeFileSync, mkdirSync, renameSync } from "node:fs"; +import { readFileSync, writeFileSync, mkdirSync, renameSync, readdirSync } from "node:fs"; import { createHash } from "node:crypto"; import { dirname, resolve } from "node:path"; @@ -195,12 +195,37 @@ if (cmd === "doctor") { const receipt = { schemaVersion: 1, ok: true, command: "deliver", input: inAbs, output: outAbs, specification: { sha256: sha256Bytes(raw), bytes: raw.length }, artifact: { sha256: sha256Bytes(Buffer.from(html)), bytes: Buffer.byteLength(html) }, validation: { checksPassed: checks.length, checkCount: checks.length, compositionStatus: "pass", errors: 0, warnings: 0 } }; console.log(JSON.stringify(receipt, null, 2)); } else if (cmd === "demo") { - const [dir] = args; + const all = args.includes("--all"); + const [dir] = args.filter((a) => a !== "--all"); + if (!dir) { + console.error("usage: trace.mjs demo [--all]"); + process.exit(2); + } mkdirSync(resolve(dir), { recursive: true }); - const spec = loadJson(resolve(ROOT, "examples/strands-forcing.json")); - const { checks } = validateSpec(spec); - writeFileSync(resolve(dir, "loop-trace-demo.html"), renderHtml(spec, { checks })); - console.log(`Demo ready: ${resolve(dir, "loop-trace-demo.html")}`); + let rendered = 0; + if (all) { + const names = readdirSync(resolve(ROOT, "examples")) + .filter((f) => f.endsWith(".json")) + .sort(); + for (const name of names) { + const spec = loadJson(resolve(ROOT, "examples", name)); + const { checks } = validateSpec(spec); + const out = resolve(dir, `loop-trace-${name.replace(/\.json$/, "")}.html`); + writeFileSync(out, renderHtml(spec, { checks })); + console.log(`Demo ready: ${out}`); + rendered += 1; + } + } else { + const spec = loadJson(resolve(ROOT, "examples/strands-forcing.json")); + const { checks } = validateSpec(spec); + writeFileSync(resolve(dir, "loop-trace-demo.html"), renderHtml(spec, { checks })); + console.log(`Demo ready: ${resolve(dir, "loop-trace-demo.html")}`); + rendered = 1; + } + if (rendered === 0) { + console.error("no example specs found under examples/"); + process.exit(1); + } } else { console.error('usage: trace.mjs [doctor|validate|deliver|demo] ... (input "-" reads the spec from stdin)'); process.exit(2); diff --git a/test/trace.test.mjs b/test/trace.test.mjs index 49b8c1e..a0fc341 100644 --- a/test/trace.test.mjs +++ b/test/trace.test.mjs @@ -91,3 +91,21 @@ test("deliver refuses to render an invalid spec", () => { const receipt = JSON.parse(r.stdout); assert.equal(receipt.ok, false); }); + +test("demo --all renders every example into a gallery dir", () => { + const dir = mkdtempSync(join(tmpdir(), "loop-trace-gallery-")); + const r = run(["demo", "--all", dir]); + assert.equal(r.code, 0); + for (const name of ["strands-forcing", "dspy-reward"]) { + const html = readFileSync(join(dir, `loop-trace-${name}.html`), "utf8"); + assert.match(html, / { + const dir = mkdtempSync(join(tmpdir(), "loop-trace-demo-")); + const r = run(["demo", dir]); + assert.equal(r.code, 0); + const html = readFileSync(join(dir, "loop-trace-demo.html"), "utf8"); + assert.match(html, /