edsger 0.83.0 → 0.85.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.
Files changed (109) hide show
  1. package/README.md +102 -2
  2. package/assets/README.md +29 -10
  3. package/assets/audit/base.yaml +187 -0
  4. package/assets/audit/csharp.yaml +46 -0
  5. package/assets/audit/dart.yaml +46 -0
  6. package/assets/audit/go.yaml +46 -0
  7. package/assets/audit/java.yaml +46 -0
  8. package/assets/audit/python.yaml +49 -0
  9. package/assets/audit/typescript.yaml +51 -0
  10. package/assets/audit-viewer/assets/audit--os0TXXZ.css +1 -0
  11. package/assets/audit-viewer/assets/audit-CSblYELs.js +2 -0
  12. package/assets/audit-viewer/audit.html +31 -0
  13. package/assets/audit-viewer-single.html +32 -0
  14. package/assets/diagrams-viewer/assets/{abnfDiagram-VRR7QNED-BuBTl7CD.js → abnfDiagram-VRR7QNED-CWLpbgGU.js} +1 -1
  15. package/assets/diagrams-viewer/assets/{arc-OMyuAfpK.js → arc-B4VipCr_.js} +1 -1
  16. package/assets/diagrams-viewer/assets/{architectureDiagram-ZJ3FMSHR-BtZuD4Oc.js → architectureDiagram-ZJ3FMSHR-BMDodQXV.js} +1 -1
  17. package/assets/diagrams-viewer/assets/{blockDiagram-677ZJIJ3-BXUV3Kw8.js → blockDiagram-677ZJIJ3-DbHoVVH-.js} +1 -1
  18. package/assets/diagrams-viewer/assets/{c4Diagram-LMCZKHZV-CB6FLms7.js → c4Diagram-LMCZKHZV-CyUEj98S.js} +1 -1
  19. package/assets/diagrams-viewer/assets/channel-DWh7ErxY.js +1 -0
  20. package/assets/diagrams-viewer/assets/{chunk-2Q5K7J3B-mPN4SRuj.js → chunk-2Q5K7J3B-CWu0p0os.js} +1 -1
  21. package/assets/diagrams-viewer/assets/{chunk-32BRIVSS-DVv1-dcH.js → chunk-32BRIVSS-BhVWSCoK.js} +1 -1
  22. package/assets/diagrams-viewer/assets/{chunk-5VM5RSS4-CV1HWp5i.js → chunk-5VM5RSS4-B_2wZBAn.js} +1 -1
  23. package/assets/diagrams-viewer/assets/{chunk-EX3LRPZG-Dka-2CXP.js → chunk-EX3LRPZG-D_MRgtfL.js} +1 -1
  24. package/assets/diagrams-viewer/assets/{chunk-JWPE2WC7-D18DLD3z.js → chunk-JWPE2WC7-dsh1qiy0.js} +1 -1
  25. package/assets/diagrams-viewer/assets/{chunk-MOJQB5TN-DZ8recee.js → chunk-MOJQB5TN-BvLm4a6B.js} +1 -1
  26. package/assets/diagrams-viewer/assets/{chunk-RYQCIY6F-D-T7U7G_.js → chunk-RYQCIY6F-gJyPbIpx.js} +1 -1
  27. package/assets/diagrams-viewer/assets/{chunk-V7JOEXUC-CycRbumY.js → chunk-V7JOEXUC-BMvj0zJM.js} +1 -1
  28. package/assets/diagrams-viewer/assets/{chunk-VR4S4FIN-CgtEJekm.js → chunk-VR4S4FIN-Di2iBUXs.js} +1 -1
  29. package/assets/diagrams-viewer/assets/{chunk-XXDRQBXY-N1fSDz2n.js → chunk-XXDRQBXY-Cfajbd4I.js} +1 -1
  30. package/assets/diagrams-viewer/assets/classDiagram-OUVF2IWQ-winWhRce.js +1 -0
  31. package/assets/diagrams-viewer/assets/classDiagram-v2-EOCWNBFH-winWhRce.js +1 -0
  32. package/assets/diagrams-viewer/assets/{cose-bilkent-JH36ORCC-x2DnNSLL.js → cose-bilkent-JH36ORCC-ClE2EhTL.js} +1 -1
  33. package/assets/diagrams-viewer/assets/{cynefin-VYW2F7L2-CWCJCs-i.js → cynefin-VYW2F7L2-nm5z0Jwc.js} +1 -1
  34. package/assets/diagrams-viewer/assets/{cynefinDiagram-TSTJHNR4-mn39JWqG.js → cynefinDiagram-TSTJHNR4-CGhnRh5V.js} +1 -1
  35. package/assets/diagrams-viewer/assets/{dagre-VKFMJZFB-iOjB5Pax.js → dagre-VKFMJZFB-COlahHKk.js} +1 -1
  36. package/assets/diagrams-viewer/assets/{diagram-FQU43EPY-DIFXEG6K.js → diagram-FQU43EPY-DsUclBK0.js} +1 -1
  37. package/assets/diagrams-viewer/assets/{diagram-G47NLZAW-CUFys0Kk.js → diagram-G47NLZAW-CnjT9SvX.js} +1 -1
  38. package/assets/diagrams-viewer/assets/{diagram-NH7WQ7WH-ByXjnNJR.js → diagram-NH7WQ7WH-DiGoGOyA.js} +1 -1
  39. package/assets/diagrams-viewer/assets/{diagram-OA4YK3LP-B7VCnnms.js → diagram-OA4YK3LP-qi8nz9qe.js} +1 -1
  40. package/assets/diagrams-viewer/assets/{diagram-WEI45ONY-DdmQeP2M.js → diagram-WEI45ONY-BV3PGzwL.js} +1 -1
  41. package/assets/diagrams-viewer/assets/diagrams-8nCc1utO.css +1 -0
  42. package/assets/diagrams-viewer/assets/{diagrams-DFlErBCY.js → diagrams-CF48btj0.js} +100 -100
  43. package/assets/diagrams-viewer/assets/{ebnfDiagram-CCIWWBDH-B54UVMil.js → ebnfDiagram-CCIWWBDH-CtKWe-dh.js} +1 -1
  44. package/assets/diagrams-viewer/assets/{erDiagram-Q63AITRT-zkOgYlyz.js → erDiagram-Q63AITRT-Aajee3Fz.js} +1 -1
  45. package/assets/diagrams-viewer/assets/{flowDiagram-23GEKE2U-CCNIfraz.js → flowDiagram-23GEKE2U-N3Xe2K9t.js} +1 -1
  46. package/assets/diagrams-viewer/assets/{ganttDiagram-NO4QXBWP-DCn2zVSV.js → ganttDiagram-NO4QXBWP-Bjicdg8S.js} +1 -1
  47. package/assets/diagrams-viewer/assets/{gitGraphDiagram-IHSO6WYX-lB6SW9xm.js → gitGraphDiagram-IHSO6WYX-CacIXKIe.js} +1 -1
  48. package/assets/diagrams-viewer/assets/{infoDiagram-FWYZ7A6U-CvGfGSmH.js → infoDiagram-FWYZ7A6U-CVmDapTx.js} +1 -1
  49. package/assets/diagrams-viewer/assets/{ishikawaDiagram-FXEZZL3T-DnwhjgE5.js → ishikawaDiagram-FXEZZL3T-DMJf1YQN.js} +1 -1
  50. package/assets/diagrams-viewer/assets/{journeyDiagram-5HDEW3XC-C8LVhqZM.js → journeyDiagram-5HDEW3XC-Bk_U6MPk.js} +1 -1
  51. package/assets/diagrams-viewer/assets/{kanban-definition-HUTT4EX6-PYOrsIQc.js → kanban-definition-HUTT4EX6-n05wGXiD.js} +1 -1
  52. package/assets/diagrams-viewer/assets/{linear-CIU4B3PA.js → linear-DD_zky65.js} +1 -1
  53. package/assets/diagrams-viewer/assets/{mindmap-definition-LN4V7U3C-B-HjOMSw.js → mindmap-definition-LN4V7U3C-BWt_I9vr.js} +1 -1
  54. package/assets/diagrams-viewer/assets/{pegDiagram-2B236MQR-dAvsUVxy.js → pegDiagram-2B236MQR-CCZTIXBZ.js} +1 -1
  55. package/assets/diagrams-viewer/assets/{pieDiagram-ENE6RG2P-W-6EgmyD.js → pieDiagram-ENE6RG2P-BmIQql1w.js} +1 -1
  56. package/assets/diagrams-viewer/assets/{quadrantDiagram-ABIIQ3AL-SlUlWOO8.js → quadrantDiagram-ABIIQ3AL-BPvOH5QN.js} +1 -1
  57. package/assets/diagrams-viewer/assets/{railroadDiagram-RFXS5EU6-KjtAtTVy.js → railroadDiagram-RFXS5EU6-DpgVbjSl.js} +1 -1
  58. package/assets/diagrams-viewer/assets/{requirementDiagram-TGXJPOKE-DZtlwaER.js → requirementDiagram-TGXJPOKE-CDVpI3MH.js} +1 -1
  59. package/assets/diagrams-viewer/assets/{sankeyDiagram-HTMAVEWB-1Ae4oRc3.js → sankeyDiagram-HTMAVEWB-DU2J23Xy.js} +1 -1
  60. package/assets/diagrams-viewer/assets/{sequenceDiagram-DBY2YBRQ-DU-x0cLP.js → sequenceDiagram-DBY2YBRQ-BIVlCWU4.js} +1 -1
  61. package/assets/diagrams-viewer/assets/{sizeCapture-X5ZJPWSS-Cemh16BZ.js → sizeCapture-X5ZJPWSS-C9KxyKN2.js} +1 -1
  62. package/assets/diagrams-viewer/assets/{stateDiagram-2N3HPSRC-CtfrA6gP.js → stateDiagram-2N3HPSRC-Ba5mRI9d.js} +1 -1
  63. package/assets/diagrams-viewer/assets/stateDiagram-v2-6OUMAXLB-Dw-0OG6b.js +1 -0
  64. package/assets/diagrams-viewer/assets/{swimlanes-5IMT3BWC-BkZY9GJ7.js → swimlanes-5IMT3BWC-aOsD2O80.js} +2 -2
  65. package/assets/diagrams-viewer/assets/swimlanesDiagram-G3AALYLV-KujklZml.js +8 -0
  66. package/assets/diagrams-viewer/assets/{timeline-definition-FHXFAJF6-DikyXYB3.js → timeline-definition-FHXFAJF6-CmYd84Hc.js} +1 -1
  67. package/assets/diagrams-viewer/assets/{vennDiagram-L72KCM5P-C_7veK57.js → vennDiagram-L72KCM5P-CDiMK-z9.js} +1 -1
  68. package/assets/diagrams-viewer/assets/{wardleyDiagram-EHGQE667-Das_YJGv.js → wardleyDiagram-EHGQE667-BKLsv25u.js} +1 -1
  69. package/assets/diagrams-viewer/assets/{xychartDiagram-FW5EYKEG-B5fwef3a.js → xychartDiagram-FW5EYKEG-Bpj6Ub-a.js} +1 -1
  70. package/assets/diagrams-viewer/diagrams.html +19 -2
  71. package/assets/diagrams-viewer-single.html +382 -365
  72. package/assets/manifest.yaml +13 -1
  73. package/assets/mindmap/base.yaml +115 -0
  74. package/assets/mindmap/csharp.yaml +32 -0
  75. package/assets/mindmap/dart.yaml +42 -0
  76. package/assets/mindmap/go.yaml +33 -0
  77. package/assets/mindmap/java.yaml +40 -0
  78. package/assets/mindmap/python.yaml +43 -0
  79. package/assets/mindmap/typescript.yaml +47 -0
  80. package/assets/mindmap-viewer/assets/mindmap-C50Y8qu4.js +4 -0
  81. package/assets/mindmap-viewer/assets/mindmap-D7KMR9EM.css +1 -0
  82. package/assets/mindmap-viewer/mindmap.html +31 -0
  83. package/assets/mindmap-viewer-single.html +34 -0
  84. package/assets/schema/audit.schema.json +66 -0
  85. package/assets/schema/mindmap.schema.json +69 -0
  86. package/assets/schema/testcases.schema.json +69 -0
  87. package/assets/skills/audit/SKILL.md +76 -0
  88. package/assets/skills/mindmap/SKILL.md +69 -0
  89. package/assets/skills/testcases/SKILL.md +73 -0
  90. package/assets/testcases/base.yaml +147 -0
  91. package/assets/testcases-viewer/assets/testcases-DaNbQxip.js +2 -0
  92. package/assets/testcases-viewer/assets/testcases-f2mIbfYU.css +1 -0
  93. package/assets/testcases-viewer/testcases.html +31 -0
  94. package/assets/testcases-viewer-single.html +32 -0
  95. package/assets/viewer/assets/index-BFtGar9N.css +1 -0
  96. package/assets/viewer/assets/index-XOKyyudT.js +3 -0
  97. package/assets/viewer/index.html +19 -2
  98. package/assets/viewer-single.html +21 -4
  99. package/dist/index.js +1961 -564
  100. package/dist/index.js.map +1 -1
  101. package/package.json +2 -2
  102. package/assets/diagrams-viewer/assets/channel-DfIqGUHQ.js +0 -1
  103. package/assets/diagrams-viewer/assets/classDiagram-OUVF2IWQ-D7S3taPH.js +0 -1
  104. package/assets/diagrams-viewer/assets/classDiagram-v2-EOCWNBFH-D7S3taPH.js +0 -1
  105. package/assets/diagrams-viewer/assets/diagrams-hdYg_gGB.css +0 -1
  106. package/assets/diagrams-viewer/assets/stateDiagram-v2-6OUMAXLB-BU13FASH.js +0 -1
  107. package/assets/diagrams-viewer/assets/swimlanesDiagram-G3AALYLV-03GXtgC-.js +0 -8
  108. package/assets/viewer/assets/index-C7hwGkuU.js +0 -3
  109. package/assets/viewer/assets/index-Dhc-7q5o.css +0 -1
package/dist/index.js CHANGED
@@ -4,12 +4,12 @@
4
4
  import { readFileSync as readFileSync4 } from "fs";
5
5
  import { resolve as resolve2 } from "path";
6
6
  import { Command, Option } from "commander";
7
- import pc10 from "picocolors";
7
+ import pc13 from "picocolors";
8
8
 
9
- // src/commands/benchmark.ts
9
+ // src/commands/audit.ts
10
10
  import { appendFileSync, mkdirSync, writeFileSync } from "fs";
11
- import { basename, join as join3 } from "path";
12
- import pc3 from "picocolors";
11
+ import { basename, dirname as dirname2, isAbsolute as isAbsolute2, join as join5 } from "path";
12
+ import pc4 from "picocolors";
13
13
 
14
14
  // src/core/assets.ts
15
15
  import { execFileSync } from "child_process";
@@ -127,7 +127,7 @@ function resolveStandard(kind, opts, config, configDir, forcedLanguages) {
127
127
  const roots = [
128
128
  { path: bundled, label: opts.assetsPath ? `assets:${opts.assetsPath}` : "bundled" }
129
129
  ];
130
- const perKind = kind === "benchmark" ? config.benchmark?.standards : kind === "diagrams" ? config.diagrams?.standards : config.review?.standards;
130
+ const perKind = kind === "benchmark" ? config.benchmark?.standards : kind === "diagrams" ? config.diagrams?.standards : kind === "mindmap" ? config.mindmap?.standards : kind === "testcases" ? config.testcases?.standards : kind === "audit" ? config.audit?.standards : config.review?.standards;
131
131
  const overrideSpec = perKind ?? config.standards;
132
132
  if (overrideSpec && !opts.assetsPath) {
133
133
  roots.push({ path: resolveRootPath(overrideSpec, configDir), label: `config:${overrideSpec}` });
@@ -182,6 +182,24 @@ function mergeLayers(kind, layers) {
182
182
  {}
183
183
  );
184
184
  }
185
+ if (kind === "mindmap") {
186
+ return layers.reduce(
187
+ (acc, layer) => mergeMindmap(acc, layer),
188
+ {}
189
+ );
190
+ }
191
+ if (kind === "testcases") {
192
+ return layers.reduce(
193
+ (acc, layer) => mergeTestcases(acc, layer),
194
+ {}
195
+ );
196
+ }
197
+ if (kind === "audit") {
198
+ return layers.reduce(
199
+ (acc, layer) => mergeAudit(acc, layer),
200
+ {}
201
+ );
202
+ }
185
203
  return layers.reduce(
186
204
  (acc, layer) => mergeReview(acc, layer),
187
205
  {}
@@ -208,6 +226,42 @@ function mergeDiagrams(base, overlay) {
208
226
  groups
209
227
  };
210
228
  }
229
+ function mergeMindmap(base, overlay) {
230
+ const aspects = mergeById(base.aspects ?? [], overlay.aspects ?? [], (b, o) => ({
231
+ ...b,
232
+ ...o
233
+ }));
234
+ return {
235
+ ...base,
236
+ ...overlay,
237
+ // Preserve the base layer's identity rather than the last overlay's.
238
+ id: base.id ?? overlay.id,
239
+ name: base.name ?? overlay.name,
240
+ version: base.version ?? overlay.version,
241
+ kind: "mindmap",
242
+ guidance: overlay.guidance ?? base.guidance,
243
+ maxDepth: overlay.maxDepth ?? base.maxDepth,
244
+ aspects
245
+ };
246
+ }
247
+ function mergeTestcases(base, overlay) {
248
+ const aspects = mergeById(base.aspects ?? [], overlay.aspects ?? [], (b, o) => ({
249
+ ...b,
250
+ ...o
251
+ }));
252
+ return {
253
+ ...base,
254
+ ...overlay,
255
+ // Preserve the base layer's identity rather than the last overlay's.
256
+ id: base.id ?? overlay.id,
257
+ name: base.name ?? overlay.name,
258
+ version: base.version ?? overlay.version,
259
+ kind: "testcases",
260
+ guidance: overlay.guidance ?? base.guidance,
261
+ maxDepth: overlay.maxDepth ?? base.maxDepth,
262
+ aspects
263
+ };
264
+ }
211
265
  function mergeBenchmark(base, overlay) {
212
266
  const categories = mergeById(
213
267
  base.categories ?? [],
@@ -233,6 +287,25 @@ function mergeBenchmark(base, overlay) {
233
287
  categories
234
288
  };
235
289
  }
290
+ function mergeAudit(base, overlay) {
291
+ const dimensions = mergeById(
292
+ base.dimensions ?? [],
293
+ overlay.dimensions ?? [],
294
+ (b, o) => ({ ...b, ...o })
295
+ );
296
+ return {
297
+ ...base,
298
+ ...overlay,
299
+ // Preserve the base layer's identity rather than the last overlay's.
300
+ id: base.id ?? overlay.id,
301
+ name: base.name ?? overlay.name,
302
+ version: base.version ?? overlay.version,
303
+ kind: "audit",
304
+ guidance: overlay.guidance ?? base.guidance,
305
+ policy: { ...base.policy ?? {}, ...overlay.policy ?? {} },
306
+ dimensions
307
+ };
308
+ }
236
309
  function mergeReview(base, overlay) {
237
310
  const focusAreas = mergeById(
238
311
  base.focusAreas ?? [],
@@ -518,6 +591,133 @@ function summarizeTool(name = "tool", input = {}) {
518
591
  }
519
592
  }
520
593
 
594
+ // src/core/dashboard.ts
595
+ import { existsSync as existsSync3, readFileSync as readFileSync2, readdirSync as readdirSync2 } from "fs";
596
+ import { join as join3 } from "path";
597
+ function assembleFrom(dir, latestFile) {
598
+ const latest = readJson(join3(dir, latestFile));
599
+ const history = readJsonl(join3(dir, "history.jsonl"));
600
+ const runs = {};
601
+ const runsDir = join3(dir, "runs");
602
+ if (existsSync3(runsDir)) {
603
+ for (const entry of readdirSync2(runsDir)) {
604
+ const report = readJson(join3(runsDir, entry, latestFile));
605
+ if (report) runs[entry] = report;
606
+ }
607
+ }
608
+ const repo = latest && typeof latest === "object" && "repo" in latest ? latest.repo : void 0;
609
+ return { repo, latest, history, runs };
610
+ }
611
+ function assembleData(benchmarkDir) {
612
+ return assembleFrom(benchmarkDir, "report.json");
613
+ }
614
+ function assembleDiagramData(diagramsDir) {
615
+ return assembleFrom(diagramsDir, "diagrams.json");
616
+ }
617
+ function assembleMindmapData(mindmapDir) {
618
+ return assembleFrom(mindmapDir, "mindmap.json");
619
+ }
620
+ function assembleTestcasesData(testcasesDir) {
621
+ return assembleFrom(testcasesDir, "testcases.json");
622
+ }
623
+ function assembleAuditData(auditDir) {
624
+ return assembleFrom(auditDir, "report.json");
625
+ }
626
+ function hasData(data) {
627
+ return Boolean(data.latest) || data.history.length > 0;
628
+ }
629
+ function findViewerDir(subdir = "viewer", index = "index.html") {
630
+ const dir = join3(findBundledAssetsDir(), subdir);
631
+ if (!existsSync3(join3(dir, index))) {
632
+ throw new Error(
633
+ `Dashboard viewer assets not found (${subdir}/${index}). Reinstall edsger, or run \`npm run sync-viewer\` in a dev checkout.`
634
+ );
635
+ }
636
+ return dir;
637
+ }
638
+ function findDiagramViewerDir() {
639
+ return findViewerDir("diagrams-viewer", "diagrams.html");
640
+ }
641
+ function findMindmapViewerDir() {
642
+ return findViewerDir("mindmap-viewer", "mindmap.html");
643
+ }
644
+ function findTestcasesViewerDir() {
645
+ return findViewerDir("testcases-viewer", "testcases.html");
646
+ }
647
+ function findAuditViewerDir() {
648
+ return findViewerDir("audit-viewer", "audit.html");
649
+ }
650
+ function readSingleTemplate(file) {
651
+ const path = join3(findBundledAssetsDir(), file);
652
+ if (!existsSync3(path)) {
653
+ throw new Error(
654
+ `Single-file viewer template not found (${file}). Reinstall edsger, or run \`npm run sync-viewer\`.`
655
+ );
656
+ }
657
+ return readFileSync2(path, "utf8");
658
+ }
659
+ function readViewerSingleTemplate() {
660
+ return readSingleTemplate("viewer-single.html");
661
+ }
662
+ function readDiagramViewerSingleTemplate() {
663
+ return readSingleTemplate("diagrams-viewer-single.html");
664
+ }
665
+ function readMindmapViewerSingleTemplate() {
666
+ return readSingleTemplate("mindmap-viewer-single.html");
667
+ }
668
+ function readTestcasesViewerSingleTemplate() {
669
+ return readSingleTemplate("testcases-viewer-single.html");
670
+ }
671
+ function readAuditViewerSingleTemplate() {
672
+ return readSingleTemplate("audit-viewer-single.html");
673
+ }
674
+ function renderSingleFileWith(template, data, globalName) {
675
+ const json = JSON.stringify(data).replace(/<\//g, "<\\/");
676
+ const tag = `<script>window.${globalName}=${json}</script>`;
677
+ const placeholder = new RegExp(`<!--[^>]*${globalName}[^>]*-->`);
678
+ if (placeholder.test(template)) return template.replace(placeholder, () => tag);
679
+ const scriptIdx = template.indexOf("<script");
680
+ if (scriptIdx !== -1) return template.slice(0, scriptIdx) + tag + template.slice(scriptIdx);
681
+ const headIdx = template.lastIndexOf("</head>");
682
+ if (headIdx !== -1) return template.slice(0, headIdx) + tag + template.slice(headIdx);
683
+ return tag + template;
684
+ }
685
+ function renderSingleFile(template, data) {
686
+ return renderSingleFileWith(template, data, "__EDSGER_DATA__");
687
+ }
688
+ function renderDiagramsSingleFile(template, data) {
689
+ return renderSingleFileWith(template, data, "__EDSGER_DIAGRAMS__");
690
+ }
691
+ function renderMindmapSingleFile(template, data) {
692
+ return renderSingleFileWith(template, data, "__EDSGER_MINDMAP__");
693
+ }
694
+ function renderTestcasesSingleFile(template, data) {
695
+ return renderSingleFileWith(template, data, "__EDSGER_TESTCASES__");
696
+ }
697
+ function renderAuditSingleFile(template, data) {
698
+ return renderSingleFileWith(template, data, "__EDSGER_AUDIT__");
699
+ }
700
+ function readJson(path) {
701
+ try {
702
+ return JSON.parse(readFileSync2(path, "utf8"));
703
+ } catch {
704
+ return null;
705
+ }
706
+ }
707
+ function readJsonl(path) {
708
+ if (!existsSync3(path)) return [];
709
+ const out = [];
710
+ for (const line of readFileSync2(path, "utf8").split("\n")) {
711
+ const t = line.trim();
712
+ if (!t) continue;
713
+ try {
714
+ out.push(JSON.parse(t));
715
+ } catch {
716
+ }
717
+ }
718
+ return out;
719
+ }
720
+
521
721
  // src/core/git.ts
522
722
  import { execFileSync as execFileSync2 } from "child_process";
523
723
  function tryGit(args, cwd) {
@@ -542,151 +742,220 @@ function gitInfo(cwd) {
542
742
  };
543
743
  }
544
744
 
545
- // src/core/report.ts
546
- function scoreBenchmark(standard, result) {
547
- const scale = standard.scoreScale || 4;
548
- const totalWeight = standard.categories.reduce((s, c) => s + c.weight, 0) || 1;
549
- const tail = (id) => id.slice(id.lastIndexOf("/") + 1);
550
- const byKey = /* @__PURE__ */ new Map();
551
- const byCrit = /* @__PURE__ */ new Map();
552
- for (const cat of result.categories) {
553
- for (const cr of cat.criteria) {
554
- const meta = {
555
- score: cr.score,
556
- justification: cr.justification,
557
- evidence: cr.evidence ?? []
558
- };
559
- byKey.set(`${tail(cat.id)}/${tail(cr.id)}`, meta);
560
- byCrit.set(tail(cr.id), meta);
561
- }
745
+ // src/core/open.ts
746
+ import { spawn } from "child_process";
747
+ function openInBrowser(target) {
748
+ const platform = process.platform;
749
+ const cmd = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
750
+ const args = platform === "win32" ? ["/c", "start", "", target] : [target];
751
+ try {
752
+ spawn(cmd, args, { stdio: "ignore", detached: true }).unref();
753
+ } catch {
562
754
  }
563
- let matched = 0;
564
- const categories = standard.categories.map((cat) => {
565
- const critWeightTotal = cat.criteria.reduce((s, c) => s + (c.weight ?? 1), 0) || 1;
566
- const scored = cat.criteria.map((cr) => {
567
- const meta = byKey.get(`${tail(cat.id)}/${tail(cr.id)}`) ?? byCrit.get(tail(cr.id));
568
- if (meta) matched += 1;
569
- const raw = clamp(meta?.score ?? 0, 0, scale);
570
- return {
571
- id: cr.id,
572
- title: cr.title,
573
- weight: cr.weight ?? 1,
574
- rawScore: raw,
575
- maxScore: scale,
576
- fraction: raw / scale,
577
- justification: meta?.justification ?? "Not assessed.",
578
- evidence: meta?.evidence ?? []
579
- };
755
+ }
756
+
757
+ // src/core/viewer-server.ts
758
+ import { createReadStream, existsSync as existsSync4, statSync as statSync2 } from "fs";
759
+ import { createServer } from "http";
760
+ import { extname, join as join4, normalize } from "path";
761
+ import pc3 from "picocolors";
762
+ var MIME = {
763
+ ".html": "text/html; charset=utf-8",
764
+ ".js": "text/javascript; charset=utf-8",
765
+ ".css": "text/css; charset=utf-8",
766
+ ".json": "application/json; charset=utf-8",
767
+ ".svg": "image/svg+xml",
768
+ ".map": "application/json",
769
+ ".ico": "image/x-icon",
770
+ ".woff2": "font/woff2",
771
+ ".woff": "font/woff"
772
+ };
773
+ async function startViewerServer(opts) {
774
+ const server = createServer((req, res) => {
775
+ const url = (req.url ?? "/").split("?")[0] ?? "/";
776
+ if (url === opts.dataRoute) {
777
+ res.writeHead(200, {
778
+ "content-type": "application/json; charset=utf-8",
779
+ "cache-control": "no-store"
780
+ });
781
+ res.end(JSON.stringify(opts.getData()));
782
+ return;
783
+ }
784
+ const rel = url === "/" ? opts.indexFile : decodeURIComponent(url.replace(/^\/+/, ""));
785
+ const safe = normalize(rel).replace(/^(\.\.(?:[/\\]|$))+/, "");
786
+ let file = join4(opts.viewerDir, safe);
787
+ if (!file.startsWith(opts.viewerDir)) {
788
+ res.writeHead(403);
789
+ res.end("forbidden");
790
+ return;
791
+ }
792
+ if (!existsSync4(file) || statSync2(file).isDirectory())
793
+ file = join4(opts.viewerDir, opts.indexFile);
794
+ res.writeHead(200, { "content-type": MIME[extname(file)] ?? "application/octet-stream" });
795
+ createReadStream(file).pipe(res);
796
+ });
797
+ return new Promise((resolve3, reject) => {
798
+ server.on("error", reject);
799
+ server.listen(opts.port, opts.host, () => {
800
+ const addr = server.address();
801
+ const port = typeof addr === "object" && addr ? addr.port : opts.port;
802
+ const shownHost = opts.host === "0.0.0.0" ? "localhost" : opts.host;
803
+ const url = `http://${shownHost}:${port}/`;
804
+ if (opts.emptyHint) log.warn(opts.emptyHint);
805
+ log.success(`Dashboard at ${pc3.bold(pc3.cyan(url))} ${pc3.dim("(Ctrl-C to stop)")}`);
806
+ if (opts.open) openInBrowser(url);
580
807
  });
581
- const fraction = scored.reduce((s, c) => s + c.fraction * c.weight, 0) / critWeightTotal;
582
- const normWeight = cat.weight / totalWeight * 100;
583
- return {
584
- id: cat.id,
585
- name: cat.name,
586
- weight: round(normWeight, 1),
587
- fraction,
588
- points: round(normWeight * fraction, 1),
589
- criteria: scored
808
+ const shutdown = () => {
809
+ server.close(() => resolve3(0));
590
810
  };
811
+ process.on("SIGINT", shutdown);
812
+ process.on("SIGTERM", shutdown);
591
813
  });
592
- const totalCriteria = standard.categories.reduce((s, c) => s + c.criteria.length, 0);
593
- if (result.categories.length > 0 && matched === 0) {
594
- log.warn(
595
- "The agent returned scores but none matched the rubric's criterion ids \u2014 reporting 0. This is a scoring bug, not a real result."
596
- );
597
- } else if (matched < totalCriteria) {
598
- log.warn(`Only ${matched}/${totalCriteria} criteria were scored; the rest default to 0.`);
599
- }
600
- const overall = round(
601
- categories.reduce((s, c) => s + c.points, 0),
602
- 1
603
- );
814
+ }
815
+
816
+ // src/core/audit-report.ts
817
+ var SEVERITY_ORDER = ["critical", "high", "medium", "low", "info"];
818
+ var DEFAULT_MAX_FINDINGS = 60;
819
+ var SEVERITY_RANK = {
820
+ critical: 4,
821
+ high: 3,
822
+ medium: 2,
823
+ low: 1,
824
+ info: 0
825
+ };
826
+ function organizeAudit(standard, result) {
827
+ const merged = mergeDuplicates(result.findings ?? []);
828
+ merged.sort((a, b) => SEVERITY_RANK[b.severity] - SEVERITY_RANK[a.severity]);
829
+ const cap = Math.max(1, standard.policy?.maxFindings ?? DEFAULT_MAX_FINDINGS);
830
+ const omitted = Math.max(0, merged.length - cap);
831
+ const findings = merged.slice(0, cap);
832
+ const known = new Map(standard.dimensions.map((d) => [d.id, d.title]));
833
+ const groups = standard.dimensions.map((d) => ({
834
+ id: d.id,
835
+ title: d.title,
836
+ findings: []
837
+ }));
838
+ const other = { id: "other", title: "Other", findings: [] };
839
+ const byId = new Map(groups.map((g) => [g.id, g]));
840
+ for (const f of findings) {
841
+ (known.has(f.dimension) ? byId.get(f.dimension) : other).findings.push(f);
842
+ }
843
+ if (other.findings.length) groups.push(other);
844
+ const counts = { critical: 0, high: 0, medium: 0, low: 0, info: 0 };
845
+ for (const f of findings) counts[f.severity]++;
846
+ const quickWins = findings.filter((f) => f.effort === "quick" && f.severity !== "info");
604
847
  return {
605
848
  standardName: standard.name,
606
849
  standardVersion: standard.version,
607
- overall,
608
- grade: gradeFor(overall),
609
- categories,
610
850
  summary: result.summary,
611
- strengths: result.strengths ?? [],
612
- weaknesses: result.weaknesses ?? [],
613
- recommendations: result.recommendations ?? []
851
+ positives: result.positives ?? [],
852
+ findings,
853
+ groups: groups.filter((g) => g.findings.length > 0),
854
+ counts,
855
+ total: findings.length,
856
+ omitted,
857
+ quickWins
614
858
  };
615
859
  }
616
- function gradeFor(score) {
617
- if (score >= 90) return "A";
618
- if (score >= 80) return "B";
619
- if (score >= 70) return "C";
620
- if (score >= 60) return "D";
621
- return "F";
860
+ function mergeDuplicates(findings) {
861
+ const byKey = /* @__PURE__ */ new Map();
862
+ const out = [];
863
+ for (const f of findings) {
864
+ const key = `${f.dimension}\0${f.title.trim().toLowerCase()}`;
865
+ const seen = byKey.get(key);
866
+ if (!seen) {
867
+ const first = { ...f, moreLocations: [] };
868
+ byKey.set(key, first);
869
+ out.push(first);
870
+ continue;
871
+ }
872
+ const [primary, extra] = SEVERITY_RANK[f.severity] > SEVERITY_RANK[seen.severity] ? [{ ...f, moreLocations: seen.moreLocations }, seen] : [seen, f];
873
+ const loc = locationKey(extra);
874
+ const primaryLoc = locationKey(primary);
875
+ if (loc && loc !== primaryLoc && !primary.moreLocations.includes(loc)) {
876
+ primary.moreLocations.push(loc);
877
+ }
878
+ if (primary !== seen) {
879
+ out[out.indexOf(seen)] = primary;
880
+ byKey.set(key, primary);
881
+ }
882
+ }
883
+ return out;
622
884
  }
623
- function renderBenchmarkMarkdown(scored, meta) {
885
+ function hasFindingsAtOrAbove(organized, threshold) {
886
+ return organized.findings.some((f) => SEVERITY_RANK[f.severity] >= SEVERITY_RANK[threshold]);
887
+ }
888
+ function renderAuditMarkdown(organized, meta) {
624
889
  const lines = [];
625
- lines.push(`# Edsger Benchmark Report`);
626
- lines.push("");
627
- lines.push(`**Score: ${scored.overall} / 100 (Grade ${scored.grade})**`);
890
+ lines.push(`# Edsger Audit Report`);
628
891
  lines.push("");
629
- lines.push(`> ${scored.summary}`);
892
+ lines.push(`> ${organized.summary}`);
630
893
  lines.push("");
631
894
  lines.push(`| Field | Value |`);
632
895
  lines.push(`| --- | --- |`);
633
896
  lines.push(`| Repository | \`${meta.repo}\` |`);
634
897
  lines.push(`| Languages | ${meta.languages.join(", ") || "n/a"} |`);
635
- lines.push(`| Standard | ${scored.standardName} v${scored.standardVersion} |`);
898
+ lines.push(`| Standard | ${organized.standardName} v${organized.standardVersion} |`);
636
899
  lines.push(`| Sources | ${meta.layers.join(" \u2192 ")} |`);
637
900
  lines.push(`| Model | ${meta.model} |`);
638
901
  lines.push(`| Generated | ${meta.generatedAt} |`);
639
902
  lines.push("");
640
- lines.push(`## Category scores`);
903
+ lines.push(`## Findings by severity`);
641
904
  lines.push("");
642
- lines.push(`| Category | Weight | Score | Weighted |`);
643
- lines.push(`| --- | ---: | ---: | ---: |`);
644
- for (const c of scored.categories) {
645
- lines.push(`| ${c.name} | ${c.weight}% | ${Math.round(c.fraction * 100)}% | ${c.points} |`);
905
+ lines.push(`| Severity | Count |`);
906
+ lines.push(`| --- | ---: |`);
907
+ for (const s of SEVERITY_ORDER) {
908
+ if (organized.counts[s]) lines.push(`| ${s} | ${organized.counts[s]} |`);
646
909
  }
647
- lines.push(`| **Overall** | **100%** | | **${scored.overall}** |`);
910
+ lines.push(`| **total** | **${organized.total}** |`);
648
911
  lines.push("");
649
- if (scored.strengths.length) {
650
- lines.push(`## Strengths`);
651
- lines.push("");
652
- for (const s of scored.strengths) lines.push(`- ${s}`);
912
+ if (organized.omitted > 0) {
913
+ lines.push(
914
+ `_${organized.omitted} lower-severity finding(s) omitted by the standard's maxFindings policy._`
915
+ );
653
916
  lines.push("");
654
917
  }
655
- if (scored.weaknesses.length) {
656
- lines.push(`## Weaknesses`);
918
+ if (organized.quickWins.length) {
919
+ lines.push(`## Quick wins`);
657
920
  lines.push("");
658
- for (const w of scored.weaknesses) lines.push(`- ${w}`);
921
+ for (const f of organized.quickWins) {
922
+ lines.push(`- **[${f.severity}] ${f.title}** \u2014 ${location(f)}`);
923
+ }
659
924
  lines.push("");
660
925
  }
661
- if (scored.recommendations.length) {
662
- lines.push(`## Recommendations`);
926
+ lines.push(`## Findings by dimension`);
927
+ lines.push("");
928
+ for (const g of organized.groups) {
929
+ lines.push(`### ${g.title} (${g.findings.length})`);
663
930
  lines.push("");
664
- for (const r of scored.recommendations) {
665
- lines.push(`- **[${r.priority}] ${r.title}** \u2014 ${r.detail}`);
931
+ for (const f of g.findings) {
932
+ lines.push(`- **[${f.severity}] ${f.title}** \u2014 ${location(f)} _(effort: ${f.effort})_`);
933
+ lines.push(` - ${f.detail}`);
934
+ if (f.moreLocations.length) {
935
+ lines.push(` - _Also in:_ ${f.moreLocations.map((l) => `\`${l}\``).join(", ")}`);
936
+ }
937
+ if (f.suggestion) lines.push(` - _Fix:_ ${f.suggestion}`);
666
938
  }
667
939
  lines.push("");
668
940
  }
669
- lines.push(`## Detailed findings`);
670
- lines.push("");
671
- for (const c of scored.categories) {
672
- lines.push(`### ${c.name} \u2014 ${Math.round(c.fraction * 100)}%`);
941
+ if (organized.positives.length) {
942
+ lines.push(`## What the repo does well`);
673
943
  lines.push("");
674
- for (const cr of c.criteria) {
675
- lines.push(`- **${cr.title}** (${cr.rawScore}/${cr.maxScore}) \u2014 ${cr.justification}`);
676
- for (const e of cr.evidence) lines.push(` - \`${e}\``);
677
- }
944
+ for (const p of organized.positives) lines.push(`- ${p}`);
678
945
  lines.push("");
679
946
  }
680
947
  lines.push(`---`);
681
948
  lines.push(`<sub>Generated by [edsger](https://www.npmjs.com/package/edsger).</sub>`);
682
949
  return lines.join("\n");
683
950
  }
684
- function clamp(n, lo, hi) {
685
- return Math.max(lo, Math.min(hi, n));
951
+ function location(f) {
952
+ const primary = locationKey(f);
953
+ const more = f.moreLocations.length ? ` (+${f.moreLocations.length} more)` : "";
954
+ return primary ? `\`${primary}\`${more}` : `repo-wide${more}`;
686
955
  }
687
- function round(n, dp) {
688
- const f = 10 ** dp;
689
- return Math.round(n * f) / f;
956
+ function locationKey(f) {
957
+ if (!f.file || f.file === "-") return "";
958
+ return f.line ? `${f.file}:${f.line}` : f.file;
690
959
  }
691
960
 
692
961
  // src/core/schema.ts
@@ -729,6 +998,21 @@ var ReviewResultSchema = z.object({
729
998
  verdict: z.enum(["approve", "request_changes", "comment"]),
730
999
  findings: z.array(ReviewFindingSchema).default([])
731
1000
  });
1001
+ var AuditFindingSchema = z.object({
1002
+ dimension: z.string().describe("Dimension id from the audit standard this finding relates to"),
1003
+ severity: SeveritySchema,
1004
+ file: z.string().describe("Repo-relative file path, or '-' if repo-wide"),
1005
+ line: z.number().nullable().default(null).describe("1-based line number, if applicable"),
1006
+ title: z.string().describe("Short, specific statement of the problem"),
1007
+ detail: z.string().describe("What is wrong and why it is unprofessional, outdated, or risky"),
1008
+ suggestion: z.string().nullable().default(null).describe("Concrete fix or modern replacement"),
1009
+ effort: z.enum(["quick", "moderate", "involved"]).default("moderate").describe("Rough effort to fix: quick (<1h), moderate (a day), involved (multi-day)")
1010
+ });
1011
+ var AuditResultSchema = z.object({
1012
+ summary: z.string().describe("3-5 sentence honest overall assessment of the repository"),
1013
+ findings: z.array(AuditFindingSchema).default([]),
1014
+ positives: z.array(z.string()).default([]).describe("Things the repository genuinely does well (keeps the critique fair)")
1015
+ });
732
1016
  var ResolveResultSchema = z.object({
733
1017
  summary: z.string().describe("What was changed and why"),
734
1018
  changes: z.array(z.object({ file: z.string(), description: z.string() })).default([]),
@@ -757,28 +1041,70 @@ var DiagramsResultSchema = z.object({
757
1041
  diagrams: z.array(GeneratedDiagramSchema).describe("The diagrams selected for and generated from this repository"),
758
1042
  notes: z.string().nullable().default(null).describe("Optional notes, e.g. diagrams intentionally skipped and why")
759
1043
  });
1044
+ var MindmapNodeSchema = z.object({
1045
+ id: z.string().describe("Unique kebab-case node id, e.g. 'core-flows' or 'http-api'"),
1046
+ parentId: z.string().nullable().default(null).describe("id of the parent node; null ONLY for the single root node"),
1047
+ title: z.string().describe("Short outline-bullet title, 2-6 words"),
1048
+ summary: z.string().describe("One or two sentences that are true and specific to this codebase"),
1049
+ kind: z.string().default("topic").describe("Tag: area, module, component, flow, step, data, external, config, or topic"),
1050
+ files: z.array(z.string()).default([]).describe("Repo-relative files this node is grounded in (expected for leaf nodes)")
1051
+ });
1052
+ var MindmapResultSchema = z.object({
1053
+ profile: z.object({
1054
+ summary: z.string().describe("2-4 sentence characterisation of the project"),
1055
+ kinds: z.array(z.string()).default([]).describe("Project kind tags, e.g. cli, rest-api, spa, library, data-pipeline, monorepo"),
1056
+ characteristics: z.array(z.string()).default([]).describe("Notable traits that shaped the map (owns a Postgres DB, monorepo, \u2026)")
1057
+ }).describe("The project profile behind the map's shape"),
1058
+ nodes: z.array(MindmapNodeSchema).describe(
1059
+ "The whole mind map as a flat node list: exactly one root (parentId=null), children listed in reading order after their parent"
1060
+ ),
1061
+ notes: z.string().nullable().default(null).describe("Optional notes, e.g. areas intentionally left out and why")
1062
+ });
1063
+ var TestcasePrioritySchema = z.enum(["critical", "high", "medium", "low"]);
1064
+ var TestcaseNodeSchema = z.object({
1065
+ id: z.string().describe("Unique kebab-case node id, e.g. 'core-flows' or 'login-empty-password'"),
1066
+ parentId: z.string().nullable().default(null).describe("id of the parent node; null ONLY for the single root node"),
1067
+ title: z.string().describe("Short outline-bullet title, 2-8 words"),
1068
+ summary: z.string().describe("One or two sentences: what this area covers, or what this case verifies"),
1069
+ kind: z.string().default("topic").describe("Tag: area, feature, flow, scenario, case, or topic ('case' = executable leaf)"),
1070
+ priority: TestcasePrioritySchema.nullable().default(null).describe("Case priority (critical/high/medium/low); null for grouping nodes"),
1071
+ preconditions: z.array(z.string()).default([]).describe("State a tester must prepare before the steps (cases only)"),
1072
+ steps: z.array(z.string()).default([]).describe("Exact, observable manual actions, in order (cases only)"),
1073
+ expected: z.string().nullable().default(null).describe("The single expected outcome the tester verifies (cases only)"),
1074
+ files: z.array(z.string()).default([]).describe("Repo-relative files implementing the behaviour under test")
1075
+ });
1076
+ var TestcasesResultSchema = z.object({
1077
+ profile: z.object({
1078
+ summary: z.string().describe("2-4 sentence characterisation of the project"),
1079
+ kinds: z.array(z.string()).default([]).describe("Project kind tags, e.g. cli, rest-api, spa, library, data-pipeline, monorepo"),
1080
+ characteristics: z.array(z.string()).default([]).describe("Notable traits that shaped the plan (owns a Postgres DB, monorepo, \u2026)")
1081
+ }).describe("The project profile behind the plan's shape"),
1082
+ nodes: z.array(TestcaseNodeSchema).describe(
1083
+ "The whole test plan as a flat node list: exactly one root (parentId=null), children listed in reading order after their parent, leaves as concrete cases"
1084
+ ),
1085
+ notes: z.string().nullable().default(null).describe("Optional notes, e.g. areas intentionally left untested and why")
1086
+ });
760
1087
 
761
- // src/commands/benchmark.ts
762
- async function runBenchmark(opts) {
1088
+ // src/commands/audit.ts
1089
+ async function runAudit(opts, deps = { agent: runAgent }) {
763
1090
  const { config, path: configPath } = loadConfig(opts.cwd);
764
- const configDir = configPath ? join3(configPath, "..") : opts.cwd;
765
- const resolved = resolveStandard("benchmark", opts, config, configDir);
766
- const standard = resolved.standard;
1091
+ const configDir = configPath ? join5(configPath, "..") : opts.cwd;
1092
+ const resolved = resolveStandard("audit", opts, config, configDir);
1093
+ const standard = narrowDimensions(resolved.standard, opts.only);
767
1094
  log.info(
768
- `Benchmarking ${pc3.bold(basename(opts.cwd))} against ${pc3.bold(standard.name)} (languages: ${resolved.languages.join(", ") || "generic"})`
1095
+ `Auditing ${pc4.bold(basename(opts.cwd))} against ${pc4.bold(standard.name)} (languages: ${resolved.languages.join(", ") || "generic"})`
769
1096
  );
770
1097
  log.debug(`standard layers: ${resolved.layers.join(" \u2192 ")}`);
771
- const prompt = buildPrompt(standard, opts.cwd);
772
- const { result, costUsd, numTurns } = await runAgent({
773
- prompt,
1098
+ const { result, costUsd, numTurns } = await deps.agent({
1099
+ prompt: buildPrompt(standard, opts.cwd, opts.focus),
774
1100
  cwd: opts.cwd,
775
1101
  model: opts.model,
776
1102
  maxTurns: opts.maxTurns,
777
1103
  timeoutMs: opts.timeoutMs,
778
1104
  allowWrite: false,
779
1105
  verbose: opts.verbose,
780
- resultShape: BenchmarkResultSchema.shape,
781
- systemPromptAppend: "You are Edsger, a meticulous staff-level engineer performing an objective repository assessment. Inspect the actual code and configuration before scoring. Be evidence-driven and fair; cite concrete files. Do not modify any files."
1106
+ resultShape: AuditResultSchema.shape,
1107
+ systemPromptAppend: "You are Edsger, an exacting staff-level engineer performing a professionalism audit of an existing repository. Your job is to find what is unprofessional, outdated, fragile, or sloppy \u2014 and to prove it. Every finding must cite real evidence (file, and line where possible) you actually inspected. Be demanding but fair: no invented problems, no vague generalities. Do not modify any files."
782
1108
  });
783
1109
  if (!result) {
784
1110
  log.error(
@@ -786,11 +1112,11 @@ async function runBenchmark(opts) {
786
1112
  );
787
1113
  return 1;
788
1114
  }
789
- const scored = scoreBenchmark(standard, result);
1115
+ const organized = organizeAudit(standard, result);
790
1116
  const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
791
1117
  const runId = generatedAt.replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z");
792
1118
  const git = gitInfo(opts.cwd);
793
- const md = renderBenchmarkMarkdown(scored, {
1119
+ const md = renderAuditMarkdown(organized, {
794
1120
  repo: basename(opts.cwd),
795
1121
  languages: resolved.languages,
796
1122
  layers: resolved.layers,
@@ -798,7 +1124,7 @@ async function runBenchmark(opts) {
798
1124
  model: opts.model
799
1125
  });
800
1126
  const jsonReport = {
801
- schema: "edsger.benchmark/1",
1127
+ schema: "edsger.audit/1",
802
1128
  runId,
803
1129
  generatedAt,
804
1130
  model: opts.model,
@@ -806,13 +1132,14 @@ async function runBenchmark(opts) {
806
1132
  git,
807
1133
  languages: resolved.languages,
808
1134
  standard: { name: standard.name, version: standard.version, layers: resolved.layers },
809
- overall: scored.overall,
810
- grade: scored.grade,
811
- categories: scored.categories,
812
- summary: scored.summary,
813
- strengths: scored.strengths,
814
- weaknesses: scored.weaknesses,
815
- recommendations: scored.recommendations,
1135
+ // Dimension titles in standard order, so viewers can label groups properly.
1136
+ dimensions: standard.dimensions.map((d) => ({ id: d.id, title: d.title })),
1137
+ summary: organized.summary,
1138
+ counts: organized.counts,
1139
+ total: organized.total,
1140
+ omitted: organized.omitted,
1141
+ findings: organized.findings,
1142
+ positives: organized.positives,
816
1143
  costUsd
817
1144
  };
818
1145
  const historyRecord = {
@@ -823,112 +1150,471 @@ async function runBenchmark(opts) {
823
1150
  dirty: git.dirty,
824
1151
  model: opts.model,
825
1152
  standardVersion: standard.version,
826
- overall: scored.overall,
827
- grade: scored.grade,
828
- categories: Object.fromEntries(scored.categories.map((c) => [c.id, c.fraction])),
1153
+ total: organized.total,
1154
+ counts: organized.counts,
829
1155
  costUsd
830
1156
  };
831
1157
  if (!opts.noWrite && !opts.dryRun) {
832
- const baseDir = join3(opts.cwd, opts.out);
1158
+ const baseDir = join5(opts.cwd, opts.out);
833
1159
  const mdOut = md + "\n";
834
1160
  const jsonOut = JSON.stringify(jsonReport, null, 2) + "\n";
835
- const runDir = join3(baseDir, "runs", runId);
1161
+ const runDir = join5(baseDir, "runs", runId);
836
1162
  mkdirSync(runDir, { recursive: true });
837
- writeFileSync(join3(runDir, "report.md"), mdOut, "utf8");
838
- writeFileSync(join3(runDir, "report.json"), jsonOut, "utf8");
839
- writeFileSync(join3(baseDir, "report.md"), mdOut, "utf8");
840
- writeFileSync(join3(baseDir, "report.json"), jsonOut, "utf8");
841
- appendFileSync(join3(baseDir, "history.jsonl"), JSON.stringify(historyRecord) + "\n", "utf8");
1163
+ writeFileSync(join5(runDir, "report.md"), mdOut, "utf8");
1164
+ writeFileSync(join5(runDir, "report.json"), jsonOut, "utf8");
1165
+ writeFileSync(join5(baseDir, "report.md"), mdOut, "utf8");
1166
+ writeFileSync(join5(baseDir, "report.json"), jsonOut, "utf8");
1167
+ appendFileSync(join5(baseDir, "history.jsonl"), JSON.stringify(historyRecord) + "\n", "utf8");
842
1168
  log.success(
843
- `Report written to ${pc3.bold(join3(opts.out, "report.md"))} ${pc3.dim(`(run archived at ${join3(opts.out, "runs", runId)})`)}`
1169
+ `Report written to ${pc4.bold(join5(opts.out, "report.md"))} ${pc4.dim(`(run archived at ${join5(opts.out, "runs", runId)})`)}`
844
1170
  );
845
1171
  }
846
- printSummary(scored, numTurns, costUsd);
1172
+ printSummary(organized, numTurns, costUsd);
847
1173
  if (opts.json) process.stdout.write(JSON.stringify(jsonReport, null, 2) + "\n");
1174
+ if (opts.failOn && hasFindingsAtOrAbove(organized, opts.failOn)) {
1175
+ log.warn(`Findings at or above ${pc4.bold(opts.failOn)} present \u2014 failing (--fail-on).`);
1176
+ return 2;
1177
+ }
848
1178
  return 0;
849
1179
  }
850
- function printSummary(scored, numTurns, costUsd) {
851
- log.heading(`Score: ${scored.overall}/100 (Grade ${scored.grade})`);
852
- for (const c of scored.categories) {
853
- const pct = Math.round(c.fraction * 100);
854
- process.stderr.write(` ${bar(pct)} ${String(pct).padStart(3)}% ${c.name}
1180
+ async function runAuditServe(opts) {
1181
+ const auditDir = join5(opts.cwd, opts.dir);
1182
+ const viewerDir = findAuditViewerDir();
1183
+ return startViewerServer({
1184
+ viewerDir,
1185
+ indexFile: "audit.html",
1186
+ dataRoute: "/edsger-audit.json",
1187
+ getData: () => assembleAuditData(auditDir),
1188
+ port: opts.port,
1189
+ host: opts.host,
1190
+ open: opts.open,
1191
+ emptyHint: hasData(assembleAuditData(auditDir)) ? void 0 : `No audit data in ${opts.dir} yet \u2014 run \`edsger audit\` first.`
1192
+ });
1193
+ }
1194
+ async function runAuditReport(opts) {
1195
+ const auditDir = join5(opts.cwd, opts.dir);
1196
+ const data = assembleAuditData(auditDir);
1197
+ if (!hasData(data)) {
1198
+ log.error(`No audit data in ${opts.dir}. Run \`edsger audit\` first.`);
1199
+ return 1;
1200
+ }
1201
+ const html = renderAuditSingleFile(readAuditViewerSingleTemplate(), data);
1202
+ const out = isAbsolute2(opts.html) ? opts.html : join5(opts.cwd, opts.html);
1203
+ mkdirSync(dirname2(out), { recursive: true });
1204
+ writeFileSync(out, html, "utf8");
1205
+ log.success(
1206
+ `Wrote self-contained audit viewer to ${pc4.bold(opts.html)} ${pc4.dim(`(${Math.round(html.length / 1024)} kB, open in any browser)`)}`
1207
+ );
1208
+ if (opts.open) openInBrowser(out);
1209
+ return 0;
1210
+ }
1211
+ function narrowDimensions(standard, only) {
1212
+ if (!only || only.length === 0) return standard;
1213
+ const valid = new Set(standard.dimensions.map((d) => d.id));
1214
+ const unknown = only.filter((id) => !valid.has(id));
1215
+ if (unknown.length) {
1216
+ throw new Error(
1217
+ `Unknown dimension id(s): ${unknown.join(", ")}. Available: ${[...valid].join(", ")}`
1218
+ );
1219
+ }
1220
+ const keep = new Set(only);
1221
+ return { ...standard, dimensions: standard.dimensions.filter((d) => keep.has(d.id)) };
1222
+ }
1223
+ function printSummary(organized, numTurns, costUsd) {
1224
+ log.heading(`Findings: ${organized.total}`);
1225
+ for (const s of SEVERITY_ORDER) {
1226
+ if (organized.counts[s]) {
1227
+ process.stderr.write(` ${sevColor(s)(s.padEnd(8))} ${organized.counts[s]}
855
1228
  `);
1229
+ }
1230
+ }
1231
+ if (organized.omitted > 0) {
1232
+ log.warn(`${organized.omitted} lower-severity finding(s) omitted (maxFindings policy)`);
1233
+ }
1234
+ if (organized.quickWins.length) {
1235
+ log.step(`${organized.quickWins.length} quick win(s) \u2014 see the report's "Quick wins" section`);
856
1236
  }
857
1237
  log.step(`${numTurns} turns \xB7 ~$${costUsd.toFixed(3)}`);
858
1238
  }
859
- function bar(pct) {
860
- const width = 20;
861
- const filled = Math.round(pct / 100 * width);
862
- const color = pct >= 80 ? pc3.green : pct >= 60 ? pc3.yellow : pc3.red;
863
- return color("\u2588".repeat(filled)) + pc3.dim("\u2591".repeat(width - filled));
1239
+ function sevColor(s) {
1240
+ if (s === "critical" || s === "high") return pc4.red;
1241
+ if (s === "medium") return pc4.yellow;
1242
+ return pc4.dim;
864
1243
  }
865
- function buildPrompt(standard, cwd) {
866
- const rubric = standard.categories.map((cat) => {
867
- const crits = cat.criteria.map((cr) => {
868
- const anchors = cr.rubric ? Object.entries(cr.rubric).map(([k, v]) => ` - ${k}: ${v}`).join("\n") : "";
869
- return ` - id: ${cr.id}
870
- title: ${cr.title}
871
- guidance: ${cr.guidance.trim()}` + (anchors ? `
872
- anchors:
873
- ${anchors}` : "");
874
- }).join("\n");
875
- return `- category: ${cat.id} (${cat.name}, weight ${cat.weight})
876
- ${crits}`;
1244
+ function buildPrompt(standard, cwd, focus) {
1245
+ const maxFindings = Math.max(1, standard.policy?.maxFindings ?? DEFAULT_MAX_FINDINGS);
1246
+ const dims = standard.dimensions.map((d) => {
1247
+ const checklist = (d.checklist ?? []).map((c) => ` - ${c}`).join("\n");
1248
+ return `- id: ${d.id} (${d.title}, default severity ${d.defaultSeverity ?? "medium"})
1249
+ guidance: ${d.guidance.trim()}` + (checklist ? `
1250
+ checklist:
1251
+ ${checklist}` : "");
877
1252
  }).join("\n");
878
1253
  return [
879
- `Assess the repository located at: ${cwd}`,
1254
+ `Audit the repository located at: ${cwd}`,
880
1255
  ``,
881
- `Score it against the rubric below. Each criterion is scored from 0 to ${standard.scoreScale}`,
882
- `(0 = absent/harmful, ${standard.scoreScale} = exemplary), using the level anchors as a guide.`,
1256
+ `This is a deliberate fault-finding pass over the WHOLE repository (not a diff): surface`,
1257
+ `everything that keeps it from being truly professional \u2014 sloppy or amateurish`,
1258
+ `implementations, outdated or deprecated patterns, fragility, and neglected hygiene.`,
1259
+ ...standard.guidance ? [``, `Overall guidance: ${standard.guidance.trim()}`] : [],
1260
+ ...focus ? [``, `Extra focus requested by the user: ${truncate(focus.trim(), 2e3)}`] : [],
883
1261
  ``,
884
1262
  `Method:`,
885
- `1. Explore the repository structure, key source files, configuration, CI, tests, and docs.`,
886
- ` Use Read/Grep/Glob and read-only Bash (e.g. ls, cat, git log). Do NOT modify anything.`,
887
- `2. For each criterion, gather concrete evidence (file paths) before deciding a score.`,
888
- `3. Be objective and consistent. Absence of evidence for a practice means a low score.`,
889
- `4. When finished, call submit_result exactly once with a score for EVERY criterion id below,`,
890
- ` grouped by category. Include a short justification and evidence paths for each.`,
1263
+ ...numbered([
1264
+ `Explore the repository thoroughly: structure, source, configuration, dependencies,
1265
+ CI, tests, and docs. Use Read/Grep/Glob and read-only Bash (ls, cat, git log).
1266
+ Do NOT modify anything.`,
1267
+ `For EVERY dimension below, actively hunt for violations of its guidance and
1268
+ checklist. Only report findings backed by evidence you actually inspected; cite the
1269
+ repo-relative file path and, where it applies, the 1-based line number.`,
1270
+ `For each finding choose a severity (critical/high/medium/low/info; start from the
1271
+ dimension's default and adjust for impact), explain why it matters, give a concrete
1272
+ fix, and estimate the effort (quick/moderate/involved).`,
1273
+ `When the SAME issue recurs across many files, consolidate it into ONE finding and
1274
+ list the other affected locations in its detail \u2014 do not repeat it per file.
1275
+ Report at most ${maxFindings} findings: keep the most severe, fold or drop the rest.`,
1276
+ `Also record what the repo genuinely does well (positives) \u2014 the critique must stay fair.`,
1277
+ `When finished, call submit_result exactly once with the summary, all findings, and
1278
+ the positives.`
1279
+ ]),
891
1280
  ``,
892
- `Rubric:`,
893
- rubric
1281
+ `Dimensions:`,
1282
+ dims
894
1283
  ].join("\n");
895
1284
  }
1285
+ function numbered(steps) {
1286
+ return steps.map((s, i) => {
1287
+ const [first = "", ...rest] = s.split("\n");
1288
+ return [`${i + 1}. ${first}`, ...rest.map((l) => ` ${l}`)].join("\n");
1289
+ });
1290
+ }
1291
+ function truncate(s, max) {
1292
+ return s.length > max ? s.slice(0, max) + " \u2026 [truncated]" : s;
1293
+ }
896
1294
 
897
- // src/commands/pr-review.ts
898
- import pc4 from "picocolors";
1295
+ // src/commands/benchmark.ts
1296
+ import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
1297
+ import { basename as basename2, join as join6 } from "path";
1298
+ import pc5 from "picocolors";
899
1299
 
900
- // src/core/gh.ts
901
- import { execFile } from "child_process";
902
- import { mkdtempSync, rmSync, writeFileSync as writeFileSync2 } from "fs";
903
- import { tmpdir } from "os";
904
- import { join as join4 } from "path";
905
- import { promisify } from "util";
906
- var exec = promisify(execFile);
907
- async function gh(args, cwd) {
908
- const { stdout } = await exec("gh", args, {
909
- cwd,
910
- maxBuffer: 64 * 1024 * 1024,
911
- env: process.env
1300
+ // src/core/report.ts
1301
+ function scoreBenchmark(standard, result) {
1302
+ const scale = standard.scoreScale || 4;
1303
+ const totalWeight = standard.categories.reduce((s, c) => s + c.weight, 0) || 1;
1304
+ const tail = (id) => id.slice(id.lastIndexOf("/") + 1);
1305
+ const byKey = /* @__PURE__ */ new Map();
1306
+ const byCrit = /* @__PURE__ */ new Map();
1307
+ for (const cat of result.categories) {
1308
+ for (const cr of cat.criteria) {
1309
+ const meta = {
1310
+ score: cr.score,
1311
+ justification: cr.justification,
1312
+ evidence: cr.evidence ?? []
1313
+ };
1314
+ byKey.set(`${tail(cat.id)}/${tail(cr.id)}`, meta);
1315
+ byCrit.set(tail(cr.id), meta);
1316
+ }
1317
+ }
1318
+ let matched = 0;
1319
+ const categories = standard.categories.map((cat) => {
1320
+ const critWeightTotal = cat.criteria.reduce((s, c) => s + (c.weight ?? 1), 0) || 1;
1321
+ const scored = cat.criteria.map((cr) => {
1322
+ const meta = byKey.get(`${tail(cat.id)}/${tail(cr.id)}`) ?? byCrit.get(tail(cr.id));
1323
+ if (meta) matched += 1;
1324
+ const raw = clamp(meta?.score ?? 0, 0, scale);
1325
+ return {
1326
+ id: cr.id,
1327
+ title: cr.title,
1328
+ weight: cr.weight ?? 1,
1329
+ rawScore: raw,
1330
+ maxScore: scale,
1331
+ fraction: raw / scale,
1332
+ justification: meta?.justification ?? "Not assessed.",
1333
+ evidence: meta?.evidence ?? []
1334
+ };
1335
+ });
1336
+ const fraction = scored.reduce((s, c) => s + c.fraction * c.weight, 0) / critWeightTotal;
1337
+ const normWeight = cat.weight / totalWeight * 100;
1338
+ return {
1339
+ id: cat.id,
1340
+ name: cat.name,
1341
+ weight: round(normWeight, 1),
1342
+ fraction,
1343
+ points: round(normWeight * fraction, 1),
1344
+ criteria: scored
1345
+ };
912
1346
  });
913
- return stdout;
914
- }
915
- async function ensureGhReady() {
916
- try {
917
- await exec("gh", ["--version"]);
918
- } catch {
919
- throw new Error(
920
- "GitHub CLI (gh) not found. Install it from https://cli.github.com and run `gh auth login`."
1347
+ const totalCriteria = standard.categories.reduce((s, c) => s + c.criteria.length, 0);
1348
+ if (result.categories.length > 0 && matched === 0) {
1349
+ log.warn(
1350
+ "The agent returned scores but none matched the rubric's criterion ids \u2014 reporting 0. This is a scoring bug, not a real result."
921
1351
  );
1352
+ } else if (matched < totalCriteria) {
1353
+ log.warn(`Only ${matched}/${totalCriteria} criteria were scored; the rest default to 0.`);
922
1354
  }
923
- try {
924
- await exec("gh", ["auth", "status"]);
925
- } catch {
926
- throw new Error("Not authenticated with GitHub. Run `gh auth login` first.");
927
- }
928
- }
929
- async function getPrContext(ref, repo, cwd) {
930
- const repoArgs = repo ? ["--repo", repo] : [];
931
- const fields = "number,title,body,author,baseRefName,headRefName,state,url,isCrossRepository,files";
1355
+ const overall = round(
1356
+ categories.reduce((s, c) => s + c.points, 0),
1357
+ 1
1358
+ );
1359
+ return {
1360
+ standardName: standard.name,
1361
+ standardVersion: standard.version,
1362
+ overall,
1363
+ grade: gradeFor(overall),
1364
+ categories,
1365
+ summary: result.summary,
1366
+ strengths: result.strengths ?? [],
1367
+ weaknesses: result.weaknesses ?? [],
1368
+ recommendations: result.recommendations ?? []
1369
+ };
1370
+ }
1371
+ function gradeFor(score) {
1372
+ if (score >= 90) return "A";
1373
+ if (score >= 80) return "B";
1374
+ if (score >= 70) return "C";
1375
+ if (score >= 60) return "D";
1376
+ return "F";
1377
+ }
1378
+ function renderBenchmarkMarkdown(scored, meta) {
1379
+ const lines = [];
1380
+ lines.push(`# Edsger Benchmark Report`);
1381
+ lines.push("");
1382
+ lines.push(`**Score: ${scored.overall} / 100 (Grade ${scored.grade})**`);
1383
+ lines.push("");
1384
+ lines.push(`> ${scored.summary}`);
1385
+ lines.push("");
1386
+ lines.push(`| Field | Value |`);
1387
+ lines.push(`| --- | --- |`);
1388
+ lines.push(`| Repository | \`${meta.repo}\` |`);
1389
+ lines.push(`| Languages | ${meta.languages.join(", ") || "n/a"} |`);
1390
+ lines.push(`| Standard | ${scored.standardName} v${scored.standardVersion} |`);
1391
+ lines.push(`| Sources | ${meta.layers.join(" \u2192 ")} |`);
1392
+ lines.push(`| Model | ${meta.model} |`);
1393
+ lines.push(`| Generated | ${meta.generatedAt} |`);
1394
+ lines.push("");
1395
+ lines.push(`## Category scores`);
1396
+ lines.push("");
1397
+ lines.push(`| Category | Weight | Score | Weighted |`);
1398
+ lines.push(`| --- | ---: | ---: | ---: |`);
1399
+ for (const c of scored.categories) {
1400
+ lines.push(`| ${c.name} | ${c.weight}% | ${Math.round(c.fraction * 100)}% | ${c.points} |`);
1401
+ }
1402
+ lines.push(`| **Overall** | **100%** | | **${scored.overall}** |`);
1403
+ lines.push("");
1404
+ if (scored.strengths.length) {
1405
+ lines.push(`## Strengths`);
1406
+ lines.push("");
1407
+ for (const s of scored.strengths) lines.push(`- ${s}`);
1408
+ lines.push("");
1409
+ }
1410
+ if (scored.weaknesses.length) {
1411
+ lines.push(`## Weaknesses`);
1412
+ lines.push("");
1413
+ for (const w of scored.weaknesses) lines.push(`- ${w}`);
1414
+ lines.push("");
1415
+ }
1416
+ if (scored.recommendations.length) {
1417
+ lines.push(`## Recommendations`);
1418
+ lines.push("");
1419
+ for (const r of scored.recommendations) {
1420
+ lines.push(`- **[${r.priority}] ${r.title}** \u2014 ${r.detail}`);
1421
+ }
1422
+ lines.push("");
1423
+ }
1424
+ lines.push(`## Detailed findings`);
1425
+ lines.push("");
1426
+ for (const c of scored.categories) {
1427
+ lines.push(`### ${c.name} \u2014 ${Math.round(c.fraction * 100)}%`);
1428
+ lines.push("");
1429
+ for (const cr of c.criteria) {
1430
+ lines.push(`- **${cr.title}** (${cr.rawScore}/${cr.maxScore}) \u2014 ${cr.justification}`);
1431
+ for (const e of cr.evidence) lines.push(` - \`${e}\``);
1432
+ }
1433
+ lines.push("");
1434
+ }
1435
+ lines.push(`---`);
1436
+ lines.push(`<sub>Generated by [edsger](https://www.npmjs.com/package/edsger).</sub>`);
1437
+ return lines.join("\n");
1438
+ }
1439
+ function clamp(n, lo, hi) {
1440
+ return Math.max(lo, Math.min(hi, n));
1441
+ }
1442
+ function round(n, dp) {
1443
+ const f = 10 ** dp;
1444
+ return Math.round(n * f) / f;
1445
+ }
1446
+
1447
+ // src/commands/benchmark.ts
1448
+ async function runBenchmark(opts) {
1449
+ const { config, path: configPath } = loadConfig(opts.cwd);
1450
+ const configDir = configPath ? join6(configPath, "..") : opts.cwd;
1451
+ const resolved = resolveStandard("benchmark", opts, config, configDir);
1452
+ const standard = resolved.standard;
1453
+ log.info(
1454
+ `Benchmarking ${pc5.bold(basename2(opts.cwd))} against ${pc5.bold(standard.name)} (languages: ${resolved.languages.join(", ") || "generic"})`
1455
+ );
1456
+ log.debug(`standard layers: ${resolved.layers.join(" \u2192 ")}`);
1457
+ const prompt = buildPrompt2(standard, opts.cwd);
1458
+ const { result, costUsd, numTurns } = await runAgent({
1459
+ prompt,
1460
+ cwd: opts.cwd,
1461
+ model: opts.model,
1462
+ maxTurns: opts.maxTurns,
1463
+ timeoutMs: opts.timeoutMs,
1464
+ allowWrite: false,
1465
+ verbose: opts.verbose,
1466
+ resultShape: BenchmarkResultSchema.shape,
1467
+ systemPromptAppend: "You are Edsger, a meticulous staff-level engineer performing an objective repository assessment. Inspect the actual code and configuration before scoring. Be evidence-driven and fair; cite concrete files. Do not modify any files."
1468
+ });
1469
+ if (!result) {
1470
+ log.error(
1471
+ "The agent did not produce a result. Try increasing --max-turns or check credentials."
1472
+ );
1473
+ return 1;
1474
+ }
1475
+ const scored = scoreBenchmark(standard, result);
1476
+ const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
1477
+ const runId = generatedAt.replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z");
1478
+ const git = gitInfo(opts.cwd);
1479
+ const md = renderBenchmarkMarkdown(scored, {
1480
+ repo: basename2(opts.cwd),
1481
+ languages: resolved.languages,
1482
+ layers: resolved.layers,
1483
+ generatedAt,
1484
+ model: opts.model
1485
+ });
1486
+ const jsonReport = {
1487
+ schema: "edsger.benchmark/1",
1488
+ runId,
1489
+ generatedAt,
1490
+ model: opts.model,
1491
+ repo: basename2(opts.cwd),
1492
+ git,
1493
+ languages: resolved.languages,
1494
+ standard: { name: standard.name, version: standard.version, layers: resolved.layers },
1495
+ overall: scored.overall,
1496
+ grade: scored.grade,
1497
+ categories: scored.categories,
1498
+ summary: scored.summary,
1499
+ strengths: scored.strengths,
1500
+ weaknesses: scored.weaknesses,
1501
+ recommendations: scored.recommendations,
1502
+ costUsd
1503
+ };
1504
+ const historyRecord = {
1505
+ runId,
1506
+ generatedAt,
1507
+ commit: git.commit,
1508
+ branch: git.branch,
1509
+ dirty: git.dirty,
1510
+ model: opts.model,
1511
+ standardVersion: standard.version,
1512
+ overall: scored.overall,
1513
+ grade: scored.grade,
1514
+ categories: Object.fromEntries(scored.categories.map((c) => [c.id, c.fraction])),
1515
+ costUsd
1516
+ };
1517
+ if (!opts.noWrite && !opts.dryRun) {
1518
+ const baseDir = join6(opts.cwd, opts.out);
1519
+ const mdOut = md + "\n";
1520
+ const jsonOut = JSON.stringify(jsonReport, null, 2) + "\n";
1521
+ const runDir = join6(baseDir, "runs", runId);
1522
+ mkdirSync2(runDir, { recursive: true });
1523
+ writeFileSync2(join6(runDir, "report.md"), mdOut, "utf8");
1524
+ writeFileSync2(join6(runDir, "report.json"), jsonOut, "utf8");
1525
+ writeFileSync2(join6(baseDir, "report.md"), mdOut, "utf8");
1526
+ writeFileSync2(join6(baseDir, "report.json"), jsonOut, "utf8");
1527
+ appendFileSync2(join6(baseDir, "history.jsonl"), JSON.stringify(historyRecord) + "\n", "utf8");
1528
+ log.success(
1529
+ `Report written to ${pc5.bold(join6(opts.out, "report.md"))} ${pc5.dim(`(run archived at ${join6(opts.out, "runs", runId)})`)}`
1530
+ );
1531
+ }
1532
+ printSummary2(scored, numTurns, costUsd);
1533
+ if (opts.json) process.stdout.write(JSON.stringify(jsonReport, null, 2) + "\n");
1534
+ return 0;
1535
+ }
1536
+ function printSummary2(scored, numTurns, costUsd) {
1537
+ log.heading(`Score: ${scored.overall}/100 (Grade ${scored.grade})`);
1538
+ for (const c of scored.categories) {
1539
+ const pct = Math.round(c.fraction * 100);
1540
+ process.stderr.write(` ${bar(pct)} ${String(pct).padStart(3)}% ${c.name}
1541
+ `);
1542
+ }
1543
+ log.step(`${numTurns} turns \xB7 ~$${costUsd.toFixed(3)}`);
1544
+ }
1545
+ function bar(pct) {
1546
+ const width = 20;
1547
+ const filled = Math.round(pct / 100 * width);
1548
+ const color = pct >= 80 ? pc5.green : pct >= 60 ? pc5.yellow : pc5.red;
1549
+ return color("\u2588".repeat(filled)) + pc5.dim("\u2591".repeat(width - filled));
1550
+ }
1551
+ function buildPrompt2(standard, cwd) {
1552
+ const rubric = standard.categories.map((cat) => {
1553
+ const crits = cat.criteria.map((cr) => {
1554
+ const anchors = cr.rubric ? Object.entries(cr.rubric).map(([k, v]) => ` - ${k}: ${v}`).join("\n") : "";
1555
+ return ` - id: ${cr.id}
1556
+ title: ${cr.title}
1557
+ guidance: ${cr.guidance.trim()}` + (anchors ? `
1558
+ anchors:
1559
+ ${anchors}` : "");
1560
+ }).join("\n");
1561
+ return `- category: ${cat.id} (${cat.name}, weight ${cat.weight})
1562
+ ${crits}`;
1563
+ }).join("\n");
1564
+ return [
1565
+ `Assess the repository located at: ${cwd}`,
1566
+ ``,
1567
+ `Score it against the rubric below. Each criterion is scored from 0 to ${standard.scoreScale}`,
1568
+ `(0 = absent/harmful, ${standard.scoreScale} = exemplary), using the level anchors as a guide.`,
1569
+ ``,
1570
+ `Method:`,
1571
+ `1. Explore the repository structure, key source files, configuration, CI, tests, and docs.`,
1572
+ ` Use Read/Grep/Glob and read-only Bash (e.g. ls, cat, git log). Do NOT modify anything.`,
1573
+ `2. For each criterion, gather concrete evidence (file paths) before deciding a score.`,
1574
+ `3. Be objective and consistent. Absence of evidence for a practice means a low score.`,
1575
+ `4. When finished, call submit_result exactly once with a score for EVERY criterion id below,`,
1576
+ ` grouped by category. Include a short justification and evidence paths for each.`,
1577
+ ``,
1578
+ `Rubric:`,
1579
+ rubric
1580
+ ].join("\n");
1581
+ }
1582
+
1583
+ // src/commands/pr-review.ts
1584
+ import pc6 from "picocolors";
1585
+
1586
+ // src/core/gh.ts
1587
+ import { execFile } from "child_process";
1588
+ import { mkdtempSync, rmSync, writeFileSync as writeFileSync3 } from "fs";
1589
+ import { tmpdir } from "os";
1590
+ import { join as join7 } from "path";
1591
+ import { promisify } from "util";
1592
+ var exec = promisify(execFile);
1593
+ async function gh(args, cwd) {
1594
+ const { stdout } = await exec("gh", args, {
1595
+ cwd,
1596
+ maxBuffer: 64 * 1024 * 1024,
1597
+ env: process.env
1598
+ });
1599
+ return stdout;
1600
+ }
1601
+ async function ensureGhReady() {
1602
+ try {
1603
+ await exec("gh", ["--version"]);
1604
+ } catch {
1605
+ throw new Error(
1606
+ "GitHub CLI (gh) not found. Install it from https://cli.github.com and run `gh auth login`."
1607
+ );
1608
+ }
1609
+ try {
1610
+ await exec("gh", ["auth", "status"]);
1611
+ } catch {
1612
+ throw new Error("Not authenticated with GitHub. Run `gh auth login` first.");
1613
+ }
1614
+ }
1615
+ async function getPrContext(ref, repo, cwd) {
1616
+ const repoArgs = repo ? ["--repo", repo] : [];
1617
+ const fields = "number,title,body,author,baseRefName,headRefName,state,url,isCrossRepository,files";
932
1618
  const metaRaw = await gh(["pr", "view", ref, ...repoArgs, "--json", fields], cwd);
933
1619
  const meta = JSON.parse(metaRaw);
934
1620
  const diff = await gh(["pr", "diff", ref, ...repoArgs], cwd);
@@ -962,9 +1648,9 @@ async function postReview(ctx, repo, event, body, inline, cwd) {
962
1648
  event,
963
1649
  comments: inline.map((c) => ({ path: c.path, line: c.line, side: "RIGHT", body: c.body }))
964
1650
  };
965
- const dir = mkdtempSync(join4(tmpdir(), "edsger-review-"));
966
- const file = join4(dir, "review.json");
967
- writeFileSync2(file, JSON.stringify(payload), "utf8");
1651
+ const dir = mkdtempSync(join7(tmpdir(), "edsger-review-"));
1652
+ const file = join7(dir, "review.json");
1653
+ writeFileSync3(file, JSON.stringify(payload), "utf8");
968
1654
  try {
969
1655
  await gh(
970
1656
  ["api", "--method", "POST", `repos/${target}/pulls/${ctx.number}/reviews`, "--input", file],
@@ -1005,7 +1691,7 @@ async function getPrFeedback(ctx, repo, cwd) {
1005
1691
  }
1006
1692
 
1007
1693
  // src/commands/pr-review.ts
1008
- var SEVERITY_RANK = {
1694
+ var SEVERITY_RANK2 = {
1009
1695
  critical: 4,
1010
1696
  high: 3,
1011
1697
  medium: 2,
@@ -1014,7 +1700,7 @@ var SEVERITY_RANK = {
1014
1700
  };
1015
1701
  async function runPrReview(opts) {
1016
1702
  await ensureGhReady();
1017
- log.info(`Fetching PR ${pc4.bold(opts.pr)}${opts.repo ? ` from ${opts.repo}` : ""}`);
1703
+ log.info(`Fetching PR ${pc6.bold(opts.pr)}${opts.repo ? ` from ${opts.repo}` : ""}`);
1018
1704
  const ctx = await getPrContext(opts.pr, opts.repo, opts.cwd);
1019
1705
  const { config, path: configPath } = loadConfig(opts.cwd);
1020
1706
  const configDir = configPath ? `${configPath}/..` : opts.cwd;
@@ -1025,10 +1711,10 @@ async function runPrReview(opts) {
1025
1711
  const resolved = resolveStandard("review", opts, config, configDir, fileHint);
1026
1712
  const standard = resolved.standard;
1027
1713
  log.info(
1028
- `Reviewing "${pc4.bold(ctx.title)}" (#${ctx.number}, ${ctx.files.length} files) against ${pc4.bold(standard.name)}`
1714
+ `Reviewing "${pc6.bold(ctx.title)}" (#${ctx.number}, ${ctx.files.length} files) against ${pc6.bold(standard.name)}`
1029
1715
  );
1030
1716
  const { result, costUsd, numTurns } = await runAgent({
1031
- prompt: buildPrompt2(standard, ctx),
1717
+ prompt: buildPrompt3(standard, ctx),
1032
1718
  cwd: opts.cwd,
1033
1719
  model: opts.model,
1034
1720
  maxTurns: opts.maxTurns,
@@ -1059,7 +1745,7 @@ async function runPrReview(opts) {
1059
1745
  await postReview(ctx, opts.repo, event, body, inline, opts.cwd);
1060
1746
  log.success(`Posted review (${event}) to ${ctx.url}`);
1061
1747
  } else {
1062
- log.warn(`Preview only. Re-run with ${pc4.bold("--post")} to submit this review to GitHub.`);
1748
+ log.warn(`Preview only. Re-run with ${pc6.bold("--post")} to submit this review to GitHub.`);
1063
1749
  process.stderr.write("\n" + body + "\n");
1064
1750
  }
1065
1751
  return 0;
@@ -1075,7 +1761,7 @@ function decideEvent(result, standard) {
1075
1761
  function buildInlineComments(result, ctx, standard) {
1076
1762
  const changed = new Set(ctx.files.map((f) => f.path));
1077
1763
  const sorted = [...result.findings].sort(
1078
- (a, b) => SEVERITY_RANK[b.severity] - SEVERITY_RANK[a.severity]
1764
+ (a, b) => SEVERITY_RANK2[b.severity] - SEVERITY_RANK2[a.severity]
1079
1765
  );
1080
1766
  const out = [];
1081
1767
  for (const f of sorted) {
@@ -1124,7 +1810,7 @@ function printPreview(result, event, numTurns, costUsd) {
1124
1810
  log.heading(`Verdict: ${event}`);
1125
1811
  const counts = countBySeverity(result.findings);
1126
1812
  for (const s of ["critical", "high", "medium", "low", "info"]) {
1127
- if (counts[s]) process.stderr.write(` ${sevColor(s)(s.padEnd(8))} ${counts[s]}
1813
+ if (counts[s]) process.stderr.write(` ${sevColor2(s)(s.padEnd(8))} ${counts[s]}
1128
1814
  `);
1129
1815
  }
1130
1816
  log.step(`${numTurns} turns \xB7 ~$${costUsd.toFixed(3)}`);
@@ -1134,15 +1820,15 @@ function countBySeverity(findings) {
1134
1820
  for (const f of findings) c[f.severity]++;
1135
1821
  return c;
1136
1822
  }
1137
- function sevColor(s) {
1138
- if (s === "critical" || s === "high") return pc4.red;
1139
- if (s === "medium") return pc4.yellow;
1140
- return pc4.dim;
1823
+ function sevColor2(s) {
1824
+ if (s === "critical" || s === "high") return pc6.red;
1825
+ if (s === "medium") return pc6.yellow;
1826
+ return pc6.dim;
1141
1827
  }
1142
1828
  function verdictLabel(v) {
1143
1829
  return v === "approve" ? "Approve" : v === "request_changes" ? "Request changes" : "Comment";
1144
1830
  }
1145
- function buildPrompt2(standard, ctx) {
1831
+ function buildPrompt3(standard, ctx) {
1146
1832
  const focus = standard.focusAreas.map((fa) => {
1147
1833
  const checklist = (fa.checklist ?? []).map((c) => ` - ${c}`).join("\n");
1148
1834
  return `- id: ${fa.id} (${fa.title}, default severity ${fa.defaultSeverity ?? "medium"})
@@ -1175,25 +1861,25 @@ ${checklist}` : "");
1175
1861
  ``,
1176
1862
  `Unified diff:`,
1177
1863
  "```diff",
1178
- truncate(ctx.diff, 18e4),
1864
+ truncate2(ctx.diff, 18e4),
1179
1865
  "```"
1180
1866
  ].join("\n");
1181
1867
  }
1182
1868
  function indent(s) {
1183
1869
  return s.split("\n").map((l) => ` ${l}`).join("\n");
1184
1870
  }
1185
- function truncate(s, max) {
1871
+ function truncate2(s, max) {
1186
1872
  return s.length > max ? s.slice(0, max) + "\n... [diff truncated] ..." : s;
1187
1873
  }
1188
1874
 
1189
1875
  // src/commands/pr-resolve.ts
1190
1876
  import { execFile as execFile2 } from "child_process";
1191
1877
  import { promisify as promisify2 } from "util";
1192
- import pc5 from "picocolors";
1878
+ import pc7 from "picocolors";
1193
1879
  var exec2 = promisify2(execFile2);
1194
1880
  async function runPrResolve(opts) {
1195
1881
  await ensureGhReady();
1196
- log.info(`Fetching PR ${pc5.bold(opts.pr)}${opts.repo ? ` from ${opts.repo}` : ""}`);
1882
+ log.info(`Fetching PR ${pc7.bold(opts.pr)}${opts.repo ? ` from ${opts.repo}` : ""}`);
1197
1883
  const ctx = await getPrContext(opts.pr, opts.repo, opts.cwd);
1198
1884
  const feedback = await getPrFeedback(ctx, opts.repo, opts.cwd).catch(() => ({
1199
1885
  reviewComments: [],
@@ -1204,10 +1890,10 @@ async function runPrResolve(opts) {
1204
1890
  await checkoutPr(opts.pr, opts.repo, opts.cwd);
1205
1891
  }
1206
1892
  log.info(
1207
- `Resolving "${pc5.bold(ctx.title)}" (#${ctx.number}) \u2014 ${feedback.reviewComments.length} review comments`
1893
+ `Resolving "${pc7.bold(ctx.title)}" (#${ctx.number}) \u2014 ${feedback.reviewComments.length} review comments`
1208
1894
  );
1209
1895
  const { result, costUsd, numTurns } = await runAgent({
1210
- prompt: buildPrompt3(ctx, feedback, opts.instruction),
1896
+ prompt: buildPrompt4(ctx, feedback, opts.instruction),
1211
1897
  cwd: opts.cwd,
1212
1898
  model: opts.model,
1213
1899
  maxTurns: opts.maxTurns,
@@ -1221,7 +1907,7 @@ async function runPrResolve(opts) {
1221
1907
  log.error("The agent did not produce a result.");
1222
1908
  return 1;
1223
1909
  }
1224
- printSummary2(result, numTurns, costUsd);
1910
+ printSummary3(result, numTurns, costUsd);
1225
1911
  if (opts.json) process.stdout.write(JSON.stringify(result, null, 2) + "\n");
1226
1912
  if (opts.push && !opts.dryRun) {
1227
1913
  const pushed = await commitAndPush(ctx, result, opts.cwd, opts.checkout);
@@ -1229,7 +1915,7 @@ async function runPrResolve(opts) {
1229
1915
  else log.warn("Nothing to commit \u2014 working tree is clean.");
1230
1916
  } else {
1231
1917
  log.warn(
1232
- `Changes left in the working tree. Review them, then commit/push (or re-run with ${pc5.bold("--push")}).`
1918
+ `Changes left in the working tree. Review them, then commit/push (or re-run with ${pc7.bold("--push")}).`
1233
1919
  );
1234
1920
  }
1235
1921
  return 0;
@@ -1266,14 +1952,14 @@ Push manually, or ensure the PR branch was checked out with gh.`
1266
1952
  function errText(err) {
1267
1953
  return err instanceof Error ? err.message.split("\n")[0] ?? "" : String(err);
1268
1954
  }
1269
- function printSummary2(result, numTurns, costUsd) {
1955
+ function printSummary3(result, numTurns, costUsd) {
1270
1956
  log.heading("Resolution summary");
1271
1957
  process.stderr.write(` ${result.summary}
1272
1958
 
1273
1959
  `);
1274
1960
  if (result.changes.length) {
1275
1961
  for (const c of result.changes)
1276
- process.stderr.write(` ${pc5.green("~")} ${c.file} \u2014 ${c.description}
1962
+ process.stderr.write(` ${pc7.green("~")} ${c.file} \u2014 ${c.description}
1277
1963
  `);
1278
1964
  }
1279
1965
  if (result.testsRun) log.step(`verification: ${result.testsRun}`);
@@ -1284,7 +1970,7 @@ function printSummary2(result, numTurns, costUsd) {
1284
1970
  }
1285
1971
  log.step(`${numTurns} turns \xB7 ~$${costUsd.toFixed(3)}`);
1286
1972
  }
1287
- function buildPrompt3(ctx, feedback, instruction) {
1973
+ function buildPrompt4(ctx, feedback, instruction) {
1288
1974
  const reviewComments = feedback.reviewComments.length ? feedback.reviewComments.map((c) => ` - ${c.path}${c.line ? `:${c.line}` : ""} (${c.author}): ${c.body}`).join("\n") : " (none)";
1289
1975
  const issueComments = feedback.issueComments.length ? feedback.issueComments.map((c) => ` - ${c.author}: ${c.body}`).join("\n") : " (none)";
1290
1976
  return [
@@ -1312,175 +1998,9 @@ function buildPrompt3(ctx, feedback, instruction) {
1312
1998
  }
1313
1999
 
1314
2000
  // src/commands/serve.ts
1315
- import { join as join7 } from "path";
1316
-
1317
- // src/core/dashboard.ts
1318
- import { existsSync as existsSync3, readFileSync as readFileSync2, readdirSync as readdirSync2 } from "fs";
1319
- import { join as join5 } from "path";
1320
- function assembleFrom(dir, latestFile) {
1321
- const latest = readJson(join5(dir, latestFile));
1322
- const history = readJsonl(join5(dir, "history.jsonl"));
1323
- const runs = {};
1324
- const runsDir = join5(dir, "runs");
1325
- if (existsSync3(runsDir)) {
1326
- for (const entry of readdirSync2(runsDir)) {
1327
- const report = readJson(join5(runsDir, entry, latestFile));
1328
- if (report) runs[entry] = report;
1329
- }
1330
- }
1331
- const repo = latest && typeof latest === "object" && "repo" in latest ? latest.repo : void 0;
1332
- return { repo, latest, history, runs };
1333
- }
1334
- function assembleData(benchmarkDir) {
1335
- return assembleFrom(benchmarkDir, "report.json");
1336
- }
1337
- function assembleDiagramData(diagramsDir) {
1338
- return assembleFrom(diagramsDir, "diagrams.json");
1339
- }
1340
- function hasData(data) {
1341
- return Boolean(data.latest) || data.history.length > 0;
1342
- }
1343
- function findViewerDir(subdir = "viewer", index = "index.html") {
1344
- const dir = join5(findBundledAssetsDir(), subdir);
1345
- if (!existsSync3(join5(dir, index))) {
1346
- throw new Error(
1347
- `Dashboard viewer assets not found (${subdir}/${index}). Reinstall edsger, or run \`npm run sync-viewer\` in a dev checkout.`
1348
- );
1349
- }
1350
- return dir;
1351
- }
1352
- function findDiagramViewerDir() {
1353
- return findViewerDir("diagrams-viewer", "diagrams.html");
1354
- }
1355
- function readSingleTemplate(file) {
1356
- const path = join5(findBundledAssetsDir(), file);
1357
- if (!existsSync3(path)) {
1358
- throw new Error(
1359
- `Single-file viewer template not found (${file}). Reinstall edsger, or run \`npm run sync-viewer\`.`
1360
- );
1361
- }
1362
- return readFileSync2(path, "utf8");
1363
- }
1364
- function readViewerSingleTemplate() {
1365
- return readSingleTemplate("viewer-single.html");
1366
- }
1367
- function readDiagramViewerSingleTemplate() {
1368
- return readSingleTemplate("diagrams-viewer-single.html");
1369
- }
1370
- function renderSingleFileWith(template, data, globalName) {
1371
- const json = JSON.stringify(data).replace(/<\//g, "<\\/");
1372
- const tag = `<script>window.${globalName}=${json}</script>`;
1373
- const placeholder = new RegExp(`<!--[^>]*${globalName}[^>]*-->`);
1374
- if (placeholder.test(template)) return template.replace(placeholder, () => tag);
1375
- const scriptIdx = template.indexOf("<script");
1376
- if (scriptIdx !== -1) return template.slice(0, scriptIdx) + tag + template.slice(scriptIdx);
1377
- const headIdx = template.lastIndexOf("</head>");
1378
- if (headIdx !== -1) return template.slice(0, headIdx) + tag + template.slice(headIdx);
1379
- return tag + template;
1380
- }
1381
- function renderSingleFile(template, data) {
1382
- return renderSingleFileWith(template, data, "__EDSGER_DATA__");
1383
- }
1384
- function renderDiagramsSingleFile(template, data) {
1385
- return renderSingleFileWith(template, data, "__EDSGER_DIAGRAMS__");
1386
- }
1387
- function readJson(path) {
1388
- try {
1389
- return JSON.parse(readFileSync2(path, "utf8"));
1390
- } catch {
1391
- return null;
1392
- }
1393
- }
1394
- function readJsonl(path) {
1395
- if (!existsSync3(path)) return [];
1396
- const out = [];
1397
- for (const line of readFileSync2(path, "utf8").split("\n")) {
1398
- const t = line.trim();
1399
- if (!t) continue;
1400
- try {
1401
- out.push(JSON.parse(t));
1402
- } catch {
1403
- }
1404
- }
1405
- return out;
1406
- }
1407
-
1408
- // src/core/viewer-server.ts
1409
- import { createReadStream, existsSync as existsSync4, statSync as statSync2 } from "fs";
1410
- import { createServer } from "http";
1411
- import { extname, join as join6, normalize } from "path";
1412
- import pc6 from "picocolors";
1413
-
1414
- // src/core/open.ts
1415
- import { spawn } from "child_process";
1416
- function openInBrowser(target) {
1417
- const platform = process.platform;
1418
- const cmd = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
1419
- const args = platform === "win32" ? ["/c", "start", "", target] : [target];
1420
- try {
1421
- spawn(cmd, args, { stdio: "ignore", detached: true }).unref();
1422
- } catch {
1423
- }
1424
- }
1425
-
1426
- // src/core/viewer-server.ts
1427
- var MIME = {
1428
- ".html": "text/html; charset=utf-8",
1429
- ".js": "text/javascript; charset=utf-8",
1430
- ".css": "text/css; charset=utf-8",
1431
- ".json": "application/json; charset=utf-8",
1432
- ".svg": "image/svg+xml",
1433
- ".map": "application/json",
1434
- ".ico": "image/x-icon",
1435
- ".woff2": "font/woff2",
1436
- ".woff": "font/woff"
1437
- };
1438
- async function startViewerServer(opts) {
1439
- const server = createServer((req, res) => {
1440
- const url = (req.url ?? "/").split("?")[0] ?? "/";
1441
- if (url === opts.dataRoute) {
1442
- res.writeHead(200, {
1443
- "content-type": "application/json; charset=utf-8",
1444
- "cache-control": "no-store"
1445
- });
1446
- res.end(JSON.stringify(opts.getData()));
1447
- return;
1448
- }
1449
- const rel = url === "/" ? opts.indexFile : decodeURIComponent(url.replace(/^\/+/, ""));
1450
- const safe = normalize(rel).replace(/^(\.\.(?:[/\\]|$))+/, "");
1451
- let file = join6(opts.viewerDir, safe);
1452
- if (!file.startsWith(opts.viewerDir)) {
1453
- res.writeHead(403);
1454
- res.end("forbidden");
1455
- return;
1456
- }
1457
- if (!existsSync4(file) || statSync2(file).isDirectory())
1458
- file = join6(opts.viewerDir, opts.indexFile);
1459
- res.writeHead(200, { "content-type": MIME[extname(file)] ?? "application/octet-stream" });
1460
- createReadStream(file).pipe(res);
1461
- });
1462
- return new Promise((resolve3, reject) => {
1463
- server.on("error", reject);
1464
- server.listen(opts.port, opts.host, () => {
1465
- const addr = server.address();
1466
- const port = typeof addr === "object" && addr ? addr.port : opts.port;
1467
- const shownHost = opts.host === "0.0.0.0" ? "localhost" : opts.host;
1468
- const url = `http://${shownHost}:${port}/`;
1469
- if (opts.emptyHint) log.warn(opts.emptyHint);
1470
- log.success(`Dashboard at ${pc6.bold(pc6.cyan(url))} ${pc6.dim("(Ctrl-C to stop)")}`);
1471
- if (opts.open) openInBrowser(url);
1472
- });
1473
- const shutdown = () => {
1474
- server.close(() => resolve3(0));
1475
- };
1476
- process.on("SIGINT", shutdown);
1477
- process.on("SIGTERM", shutdown);
1478
- });
1479
- }
1480
-
1481
- // src/commands/serve.ts
2001
+ import { join as join8 } from "path";
1482
2002
  async function runServe(opts) {
1483
- const benchmarkDir = join7(opts.cwd, opts.dir);
2003
+ const benchmarkDir = join8(opts.cwd, opts.dir);
1484
2004
  const viewerDir = findViewerDir();
1485
2005
  return startViewerServer({
1486
2006
  viewerDir,
@@ -1495,31 +2015,31 @@ async function runServe(opts) {
1495
2015
  }
1496
2016
 
1497
2017
  // src/commands/report.ts
1498
- import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync3 } from "fs";
1499
- import { dirname as dirname2, isAbsolute as isAbsolute2, join as join8 } from "path";
1500
- import pc7 from "picocolors";
2018
+ import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync4 } from "fs";
2019
+ import { dirname as dirname3, isAbsolute as isAbsolute3, join as join9 } from "path";
2020
+ import pc8 from "picocolors";
1501
2021
  async function runReport(opts) {
1502
- const benchmarkDir = join8(opts.cwd, opts.dir);
2022
+ const benchmarkDir = join9(opts.cwd, opts.dir);
1503
2023
  const data = assembleData(benchmarkDir);
1504
2024
  if (!hasData(data)) {
1505
2025
  log.error(`No benchmark data in ${opts.dir}. Run \`edsger benchmark\` first.`);
1506
2026
  return 1;
1507
2027
  }
1508
2028
  const html = renderSingleFile(readViewerSingleTemplate(), data);
1509
- const out = isAbsolute2(opts.html) ? opts.html : join8(opts.cwd, opts.html);
1510
- mkdirSync2(dirname2(out), { recursive: true });
1511
- writeFileSync3(out, html, "utf8");
2029
+ const out = isAbsolute3(opts.html) ? opts.html : join9(opts.cwd, opts.html);
2030
+ mkdirSync3(dirname3(out), { recursive: true });
2031
+ writeFileSync4(out, html, "utf8");
1512
2032
  log.success(
1513
- `Wrote self-contained report to ${pc7.bold(opts.html)} ${pc7.dim(`(${Math.round(html.length / 1024)} kB, open in any browser)`)}`
2033
+ `Wrote self-contained report to ${pc8.bold(opts.html)} ${pc8.dim(`(${Math.round(html.length / 1024)} kB, open in any browser)`)}`
1514
2034
  );
1515
2035
  if (opts.open) openInBrowser(out);
1516
2036
  return 0;
1517
2037
  }
1518
2038
 
1519
2039
  // src/commands/diagram.ts
1520
- import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync3, writeFileSync as writeFileSync4 } from "fs";
1521
- import { basename as basename2, dirname as dirname3, isAbsolute as isAbsolute3, join as join9 } from "path";
1522
- import pc8 from "picocolors";
2040
+ import { appendFileSync as appendFileSync3, mkdirSync as mkdirSync4, writeFileSync as writeFileSync5 } from "fs";
2041
+ import { basename as basename3, dirname as dirname4, isAbsolute as isAbsolute4, join as join10 } from "path";
2042
+ import pc9 from "picocolors";
1523
2043
 
1524
2044
  // src/core/diagrams.ts
1525
2045
  function buildDiagramsPrompt(catalog, cwd, only) {
@@ -1648,52 +2168,750 @@ function validateMermaid(declaredType, mermaid) {
1648
2168
  }
1649
2169
  return warnings;
1650
2170
  }
1651
- function prepareDiagrams(catalog, result) {
1652
- const groupName = new Map(catalog.groups.map((g) => [g.id, g.name]));
1653
- const groupOrder = new Map(catalog.groups.map((g, i) => [g.id, i]));
1654
- const prepared = [];
1655
- const seenIds = /* @__PURE__ */ new Map();
1656
- for (const d of result.diagrams) {
1657
- const mermaid = sanitizeMermaid(d.mermaid ?? "");
1658
- if (!mermaid) {
1659
- log.warn(`Skipping diagram "${d.id}" \u2014 it had no mermaid source.`);
2171
+ function prepareDiagrams(catalog, result) {
2172
+ const groupName = new Map(catalog.groups.map((g) => [g.id, g.name]));
2173
+ const groupOrder = new Map(catalog.groups.map((g, i) => [g.id, i]));
2174
+ const prepared = [];
2175
+ const seenIds = /* @__PURE__ */ new Map();
2176
+ for (const d of result.diagrams) {
2177
+ const mermaid = sanitizeMermaid(d.mermaid ?? "");
2178
+ if (!mermaid) {
2179
+ log.warn(`Skipping diagram "${d.id}" \u2014 it had no mermaid source.`);
2180
+ continue;
2181
+ }
2182
+ const baseId = d.id || inferType(mermaid);
2183
+ const count = seenIds.get(baseId) ?? 0;
2184
+ seenIds.set(baseId, count + 1);
2185
+ const id = count === 0 ? baseId : `${baseId}-${count + 1}`;
2186
+ const type = d.type?.trim() || inferType(mermaid);
2187
+ prepared.push({
2188
+ ...d,
2189
+ id,
2190
+ mermaid,
2191
+ type,
2192
+ evidence: d.evidence ?? [],
2193
+ groupName: groupName.get(d.groupId) ?? "Other",
2194
+ warnings: validateMermaid(type, mermaid)
2195
+ });
2196
+ }
2197
+ prepared.sort((a, b) => {
2198
+ const oa = groupOrder.get(a.groupId) ?? Number.MAX_SAFE_INTEGER;
2199
+ const ob = groupOrder.get(b.groupId) ?? Number.MAX_SAFE_INTEGER;
2200
+ return oa - ob;
2201
+ });
2202
+ if (prepared.length === 0) {
2203
+ log.warn("The agent returned no usable diagrams.");
2204
+ } else {
2205
+ const flagged = prepared.filter((d) => d.warnings.length);
2206
+ if (flagged.length) {
2207
+ log.warn(
2208
+ `${flagged.length}/${prepared.length} diagram(s) have Mermaid warnings \u2014 they will still render if valid, but check: ${flagged.map((d) => d.id).join(", ")}`
2209
+ );
2210
+ }
2211
+ }
2212
+ return { profile: result.profile, diagrams: prepared, notes: result.notes ?? null };
2213
+ }
2214
+ function renderDiagramsMarkdown(prepared, meta) {
2215
+ const lines = [];
2216
+ lines.push(`# Edsger Diagrams`);
2217
+ lines.push("");
2218
+ lines.push(`> ${prepared.profile.summary}`);
2219
+ lines.push("");
2220
+ lines.push(`| Field | Value |`);
2221
+ lines.push(`| --- | --- |`);
2222
+ lines.push(`| Repository | \`${meta.repo}\` |`);
2223
+ lines.push(`| Languages | ${meta.languages.join(", ") || "n/a"} |`);
2224
+ lines.push(`| Project kinds | ${prepared.profile.kinds.join(", ") || "n/a"} |`);
2225
+ lines.push(`| Catalog sources | ${meta.layers.join(" \u2192 ")} |`);
2226
+ lines.push(`| Model | ${meta.model} |`);
2227
+ lines.push(`| Generated | ${meta.generatedAt} |`);
2228
+ lines.push("");
2229
+ if (prepared.profile.characteristics.length) {
2230
+ lines.push(`**Characteristics:** ${prepared.profile.characteristics.join(" \xB7 ")}`);
2231
+ lines.push("");
2232
+ }
2233
+ let currentGroup = null;
2234
+ for (const d of prepared.diagrams) {
2235
+ if (d.groupName !== currentGroup) {
2236
+ currentGroup = d.groupName;
2237
+ lines.push(`## ${currentGroup}`);
2238
+ lines.push("");
2239
+ }
2240
+ lines.push(`### ${d.title}`);
2241
+ lines.push("");
2242
+ lines.push(`${d.description} _(${d.type})_`);
2243
+ lines.push("");
2244
+ if (d.warnings.length) {
2245
+ lines.push(`> \u26A0\uFE0F ${d.warnings.join("; ")}`);
2246
+ lines.push("");
2247
+ }
2248
+ lines.push("```mermaid");
2249
+ lines.push(d.mermaid);
2250
+ lines.push("```");
2251
+ lines.push("");
2252
+ if (d.evidence.length) {
2253
+ lines.push(`<sub>Evidence: ${d.evidence.map((e) => `\`${e}\``).join(", ")}</sub>`);
2254
+ lines.push("");
2255
+ }
2256
+ }
2257
+ if (prepared.notes) {
2258
+ lines.push(`## Notes`);
2259
+ lines.push("");
2260
+ lines.push(prepared.notes);
2261
+ lines.push("");
2262
+ }
2263
+ lines.push(`---`);
2264
+ lines.push(`<sub>Generated by [edsger](https://www.npmjs.com/package/edsger).</sub>`);
2265
+ return lines.join("\n");
2266
+ }
2267
+
2268
+ // src/commands/diagram.ts
2269
+ async function runDiagramGenerate(opts) {
2270
+ const { config, path: configPath } = loadConfig(opts.cwd);
2271
+ const configDir = configPath ? join10(configPath, "..") : opts.cwd;
2272
+ const resolved = resolveStandard("diagrams", opts, config, configDir);
2273
+ const catalog = resolved.standard;
2274
+ log.info(
2275
+ `Diagramming ${pc9.bold(basename3(opts.cwd))} with ${pc9.bold(catalog.name)} (languages: ${resolved.languages.join(", ") || "generic"})`
2276
+ );
2277
+ log.debug(`catalog layers: ${resolved.layers.join(" \u2192 ")}`);
2278
+ const prompt = buildDiagramsPrompt(catalog, opts.cwd, opts.only);
2279
+ const { result, costUsd, numTurns } = await runAgent({
2280
+ prompt,
2281
+ cwd: opts.cwd,
2282
+ model: opts.model,
2283
+ maxTurns: opts.maxTurns,
2284
+ timeoutMs: opts.timeoutMs,
2285
+ allowWrite: false,
2286
+ verbose: opts.verbose,
2287
+ resultShape: DiagramsResultSchema.shape,
2288
+ systemPromptAppend: "You are Edsger, a staff-level software architect. You produce accurate, legible diagrams of a codebase as valid Mermaid, grounded strictly in the code you inspect. Never invent components, tables, or flows that are not in the repository. Do not modify any files."
2289
+ });
2290
+ if (!result) {
2291
+ log.error(
2292
+ "The agent did not produce a result. Try increasing --max-turns or check credentials."
2293
+ );
2294
+ return 1;
2295
+ }
2296
+ const prepared = prepareDiagrams(catalog, result);
2297
+ const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
2298
+ const runId = generatedAt.replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z");
2299
+ const git = gitInfo(opts.cwd);
2300
+ const md = renderDiagramsMarkdown(prepared, {
2301
+ repo: basename3(opts.cwd),
2302
+ languages: resolved.languages,
2303
+ layers: resolved.layers,
2304
+ generatedAt,
2305
+ model: opts.model
2306
+ });
2307
+ const jsonReport = {
2308
+ schema: "edsger.diagrams/1",
2309
+ runId,
2310
+ generatedAt,
2311
+ model: opts.model,
2312
+ repo: basename3(opts.cwd),
2313
+ git,
2314
+ languages: resolved.languages,
2315
+ catalog: { name: catalog.name, version: catalog.version, layers: resolved.layers },
2316
+ profile: prepared.profile,
2317
+ diagrams: prepared.diagrams,
2318
+ notes: prepared.notes,
2319
+ costUsd
2320
+ };
2321
+ const groupCounts = {};
2322
+ for (const d of prepared.diagrams) groupCounts[d.groupId] = (groupCounts[d.groupId] ?? 0) + 1;
2323
+ const historyRecord = {
2324
+ runId,
2325
+ generatedAt,
2326
+ commit: git.commit,
2327
+ branch: git.branch,
2328
+ dirty: git.dirty,
2329
+ model: opts.model,
2330
+ catalogVersion: catalog.version,
2331
+ diagramCount: prepared.diagrams.length,
2332
+ groups: groupCounts,
2333
+ costUsd
2334
+ };
2335
+ if (!opts.noWrite && !opts.dryRun) {
2336
+ const baseDir = join10(opts.cwd, opts.out);
2337
+ const mdOut = md + "\n";
2338
+ const jsonOut = JSON.stringify(jsonReport, null, 2) + "\n";
2339
+ const runDir = join10(baseDir, "runs", runId);
2340
+ mkdirSync4(runDir, { recursive: true });
2341
+ writeFileSync5(join10(runDir, "diagrams.json"), jsonOut, "utf8");
2342
+ writeFileSync5(join10(runDir, "diagrams.md"), mdOut, "utf8");
2343
+ writeFileSync5(join10(baseDir, "diagrams.json"), jsonOut, "utf8");
2344
+ writeFileSync5(join10(baseDir, "diagrams.md"), mdOut, "utf8");
2345
+ appendFileSync3(join10(baseDir, "history.jsonl"), JSON.stringify(historyRecord) + "\n", "utf8");
2346
+ log.success(
2347
+ `Diagrams written to ${pc9.bold(join10(opts.out, "diagrams.md"))} ${pc9.dim(`(run archived at ${join10(opts.out, "runs", runId)})`)}`
2348
+ );
2349
+ }
2350
+ printSummary4(prepared, numTurns, costUsd);
2351
+ if (opts.json) process.stdout.write(JSON.stringify(jsonReport, null, 2) + "\n");
2352
+ return 0;
2353
+ }
2354
+ function printSummary4(prepared, numTurns, costUsd) {
2355
+ log.heading(`${prepared.diagrams.length} diagram(s)`);
2356
+ let currentGroup = null;
2357
+ for (const d of prepared.diagrams) {
2358
+ if (d.groupName !== currentGroup) {
2359
+ currentGroup = d.groupName;
2360
+ process.stderr.write(` ${pc9.bold(currentGroup)}
2361
+ `);
2362
+ }
2363
+ const warn = d.warnings.length ? ` ${pc9.yellow("\u26A0")}` : "";
2364
+ process.stderr.write(` ${pc9.cyan("\u25C6")} ${d.title} ${pc9.dim(`(${d.type})`)}${warn}
2365
+ `);
2366
+ }
2367
+ log.step(`${numTurns} turns \xB7 ~$${costUsd.toFixed(3)}`);
2368
+ }
2369
+ async function runDiagramServe(opts) {
2370
+ const diagramsDir = join10(opts.cwd, opts.dir);
2371
+ const viewerDir = findDiagramViewerDir();
2372
+ return startViewerServer({
2373
+ viewerDir,
2374
+ indexFile: "diagrams.html",
2375
+ dataRoute: "/edsger-diagrams.json",
2376
+ getData: () => assembleDiagramData(diagramsDir),
2377
+ port: opts.port,
2378
+ host: opts.host,
2379
+ open: opts.open,
2380
+ emptyHint: hasData(assembleDiagramData(diagramsDir)) ? void 0 : `No diagram data in ${opts.dir} yet \u2014 run \`edsger diagram\` first.`
2381
+ });
2382
+ }
2383
+ async function runDiagramReport(opts) {
2384
+ const diagramsDir = join10(opts.cwd, opts.dir);
2385
+ const data = assembleDiagramData(diagramsDir);
2386
+ if (!hasData(data)) {
2387
+ log.error(`No diagram data in ${opts.dir}. Run \`edsger diagram\` first.`);
2388
+ return 1;
2389
+ }
2390
+ const html = renderDiagramsSingleFile(readDiagramViewerSingleTemplate(), data);
2391
+ const out = isAbsolute4(opts.html) ? opts.html : join10(opts.cwd, opts.html);
2392
+ mkdirSync4(dirname4(out), { recursive: true });
2393
+ writeFileSync5(out, html, "utf8");
2394
+ log.success(
2395
+ `Wrote self-contained diagram viewer to ${pc9.bold(opts.html)} ${pc9.dim(`(${Math.round(html.length / 1024)} kB, open in any browser)`)}`
2396
+ );
2397
+ if (opts.open) openInBrowser(out);
2398
+ return 0;
2399
+ }
2400
+
2401
+ // src/commands/mindmap.ts
2402
+ import { appendFileSync as appendFileSync4, mkdirSync as mkdirSync5, writeFileSync as writeFileSync6 } from "fs";
2403
+ import { basename as basename4, dirname as dirname5, isAbsolute as isAbsolute5, join as join11 } from "path";
2404
+ import pc10 from "picocolors";
2405
+
2406
+ // src/core/mindmap.ts
2407
+ function buildMindmapPrompt(standard, cwd, focus) {
2408
+ const maxDepth = standard.maxDepth ?? 5;
2409
+ const aspectsText = standard.aspects.map((a) => {
2410
+ const detect = a.detect?.length ? `
2411
+ detect: ${a.detect.join(", ")}` : "";
2412
+ return ` - id: ${a.id}
2413
+ name: ${a.name}
2414
+ appliesWhen: ${a.appliesWhen.trim()}` + detect + `
2415
+ guidance: ${a.guidance.trim()}`;
2416
+ }).join("\n");
2417
+ return [
2418
+ `Build an implementation mind map of the repository at: ${cwd}`,
2419
+ ``,
2420
+ `The mind map is an outline tree (like Workflowy): the root is the project,`,
2421
+ `and the branches explain how the code is actually implemented, deep enough`,
2422
+ `that leaves point at concrete files.`,
2423
+ ...focus ? [
2424
+ ``,
2425
+ `FOCUS: the user asked the map to concentrate on: "${focus}". Give that`,
2426
+ `area most of the depth; keep other branches to a shallow orientation level.`
2427
+ ] : [],
2428
+ ``,
2429
+ `Method:`,
2430
+ `1. Explore the repository (Read/Grep/Glob and read-only Bash like ls, cat,`,
2431
+ ` git log). Characterise the project: what kind is it (service, library, CLI,`,
2432
+ ` SPA, data pipeline, monorepo, \u2026) and how the source is organised.`,
2433
+ `2. You have a list of candidate top-level branches ("aspects") below. Keep only`,
2434
+ ` the ones THIS project's code supports (use appliesWhen/detect), rename them`,
2435
+ ` to the project's own vocabulary where that is clearer, and grow each into a`,
2436
+ ` subtree by following its guidance. Aim for 3-7 first-level branches.`,
2437
+ `3. Tree rules:`,
2438
+ ` - Root node: the project \u2014 its name as the title, a one-line summary of`,
2439
+ ` what it does. Exactly ONE root.`,
2440
+ ` - Every node: a short title (2-6 words, like an outline bullet) and a one-`,
2441
+ ` or-two-sentence summary that says something true and specific about THIS`,
2442
+ ` codebase \u2014 no generic filler.`,
2443
+ ` - Interior nodes group; leaf nodes land on concrete implementation and MUST`,
2444
+ ` cite the repo-relative file(s) they describe in "files".`,
2445
+ ` - Maximum depth ${maxDepth} (root inclusive). Prefer depth where the code is`,
2446
+ ` interesting, brevity where it is boilerplate. Do NOT invent structure that`,
2447
+ ` is not in the code, and do not just mirror the file listing \u2014 explain it.`,
2448
+ `4. Encode the tree as a FLAT node list: each node has a unique kebab-case "id"`,
2449
+ ` and a "parentId" referencing its parent's id (null only for the root). List`,
2450
+ ` children in reading order right after their parent. Tag each node with a`,
2451
+ ` "kind": area, module, component, flow, step, data, external, config, or topic.`,
2452
+ `5. When finished, call submit_result exactly once with the project profile,`,
2453
+ ` the flat node list, and optional notes.`,
2454
+ ``,
2455
+ ...standard.guidance ? [`Overall guidance: ${standard.guidance.trim()}`, ``] : [],
2456
+ `Candidate top-level branches:`,
2457
+ aspectsText
2458
+ ].join("\n");
2459
+ }
2460
+ function slugify(text) {
2461
+ return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60) || "node";
2462
+ }
2463
+ function prepareMindmap(result, fallback) {
2464
+ const warnings = [];
2465
+ const byId = /* @__PURE__ */ new Map();
2466
+ for (const raw of result.nodes) {
2467
+ const node = { ...raw, id: raw.id?.trim() || slugify(raw.title) };
2468
+ if (!node.title?.trim()) {
2469
+ warnings.push(`dropped a node with no title (id "${node.id}")`);
2470
+ continue;
2471
+ }
2472
+ let id = node.id;
2473
+ for (let n = 2; byId.has(id); n++) id = `${node.id}-${n}`;
2474
+ if (id !== node.id) warnings.push(`renamed duplicate node id "${node.id}" to "${id}"`);
2475
+ byId.set(id, { ...node, id });
2476
+ }
2477
+ const parentOf = /* @__PURE__ */ new Map();
2478
+ for (const node of byId.values()) {
2479
+ const p = node.parentId && node.parentId !== node.id ? node.parentId : null;
2480
+ parentOf.set(node.id, p && byId.has(p) ? p : null);
2481
+ if (node.parentId && !byId.has(node.parentId)) {
2482
+ warnings.push(`node "${node.id}" referenced missing parent "${node.parentId}"`);
2483
+ }
2484
+ }
2485
+ for (const id of parentOf.keys()) {
2486
+ const seen = /* @__PURE__ */ new Set([id]);
2487
+ let cur = parentOf.get(id) ?? null;
2488
+ while (cur) {
2489
+ if (seen.has(cur)) {
2490
+ warnings.push(`broke a parent cycle at node "${cur}"`);
2491
+ parentOf.set(cur, null);
2492
+ break;
2493
+ }
2494
+ seen.add(cur);
2495
+ cur = parentOf.get(cur) ?? null;
2496
+ }
2497
+ }
2498
+ const treeOf = /* @__PURE__ */ new Map();
2499
+ for (const node of byId.values()) {
2500
+ treeOf.set(node.id, {
2501
+ id: node.id,
2502
+ title: node.title.trim(),
2503
+ summary: node.summary?.trim() ?? "",
2504
+ kind: node.kind?.trim() || "topic",
2505
+ files: node.files ?? [],
2506
+ children: []
2507
+ });
2508
+ }
2509
+ const roots = [];
2510
+ for (const node of byId.values()) {
2511
+ const tree = treeOf.get(node.id);
2512
+ const parent = parentOf.get(node.id);
2513
+ if (parent) treeOf.get(parent).children.push(tree);
2514
+ else roots.push(tree);
2515
+ }
2516
+ let root;
2517
+ if (roots.length === 1 && roots[0]) {
2518
+ root = roots[0];
2519
+ } else {
2520
+ if (roots.length > 1) warnings.push(`${roots.length} root nodes \u2014 gathered under the project`);
2521
+ let rootId = "root";
2522
+ for (let n = 2; treeOf.has(rootId); n++) rootId = `root-${n}`;
2523
+ root = {
2524
+ id: rootId,
2525
+ title: fallback.repo,
2526
+ summary: result.profile.summary,
2527
+ kind: "area",
2528
+ files: [],
2529
+ children: roots
2530
+ };
2531
+ }
2532
+ const nodeCount = countNodes(root);
2533
+ const depth = treeDepth(root);
2534
+ if (nodeCount <= 1) log.warn("The agent returned an (almost) empty mind map.");
2535
+ for (const w of warnings) log.warn(`mindmap: ${w}`);
2536
+ return { profile: result.profile, root, nodeCount, depth, warnings, notes: result.notes ?? null };
2537
+ }
2538
+ function countNodes(node) {
2539
+ return 1 + node.children.reduce((acc, c) => acc + countNodes(c), 0);
2540
+ }
2541
+ function treeDepth(node) {
2542
+ return 1 + node.children.reduce((acc, c) => Math.max(acc, treeDepth(c)), 0);
2543
+ }
2544
+ function renderMindmapMarkdown(prepared, meta) {
2545
+ const lines = [];
2546
+ lines.push(`# Edsger Mind Map`);
2547
+ lines.push("");
2548
+ lines.push(`> ${prepared.profile.summary}`);
2549
+ lines.push("");
2550
+ lines.push(`| Field | Value |`);
2551
+ lines.push(`| --- | --- |`);
2552
+ lines.push(`| Repository | \`${meta.repo}\` |`);
2553
+ lines.push(`| Languages | ${meta.languages.join(", ") || "n/a"} |`);
2554
+ lines.push(`| Project kinds | ${prepared.profile.kinds.join(", ") || "n/a"} |`);
2555
+ lines.push(`| Nodes | ${prepared.nodeCount} (depth ${prepared.depth}) |`);
2556
+ lines.push(`| Standard sources | ${meta.layers.join(" \u2192 ")} |`);
2557
+ lines.push(`| Model | ${meta.model} |`);
2558
+ lines.push(`| Generated | ${meta.generatedAt} |`);
2559
+ lines.push("");
2560
+ lines.push(`## ${prepared.root.title}`);
2561
+ if (prepared.root.summary && prepared.root.summary !== prepared.profile.summary) {
2562
+ lines.push("");
2563
+ lines.push(prepared.root.summary);
2564
+ }
2565
+ lines.push("");
2566
+ for (const child of prepared.root.children) renderNode(lines, child, 0);
2567
+ if (prepared.notes) {
2568
+ lines.push("");
2569
+ lines.push(`## Notes`);
2570
+ lines.push("");
2571
+ lines.push(prepared.notes);
2572
+ }
2573
+ lines.push("");
2574
+ lines.push(`---`);
2575
+ lines.push(`<sub>Generated by [edsger](https://www.npmjs.com/package/edsger).</sub>`);
2576
+ return lines.join("\n");
2577
+ }
2578
+ function renderNode(lines, node, indent2) {
2579
+ const pad = " ".repeat(indent2);
2580
+ const files = node.files.length ? ` ${node.files.map((f) => `\`${f}\``).join(" ")}` : "";
2581
+ const summary = node.summary ? ` \u2014 ${node.summary}` : "";
2582
+ lines.push(`${pad}- **${node.title}**${summary}${files}`);
2583
+ for (const child of node.children) renderNode(lines, child, indent2 + 1);
2584
+ }
2585
+
2586
+ // src/commands/mindmap.ts
2587
+ async function runMindmapGenerate(opts) {
2588
+ const { config, path: configPath } = loadConfig(opts.cwd);
2589
+ const configDir = configPath ? join11(configPath, "..") : opts.cwd;
2590
+ const resolved = resolveStandard("mindmap", opts, config, configDir);
2591
+ const standard = resolved.standard;
2592
+ log.info(
2593
+ `Mind-mapping ${pc10.bold(basename4(opts.cwd))} with ${pc10.bold(standard.name)} (languages: ${resolved.languages.join(", ") || "generic"})`
2594
+ );
2595
+ log.debug(`standard layers: ${resolved.layers.join(" \u2192 ")}`);
2596
+ const prompt = buildMindmapPrompt(standard, opts.cwd, opts.focus);
2597
+ const { result, costUsd, numTurns } = await runAgent({
2598
+ prompt,
2599
+ cwd: opts.cwd,
2600
+ model: opts.model,
2601
+ maxTurns: opts.maxTurns,
2602
+ timeoutMs: opts.timeoutMs,
2603
+ allowWrite: false,
2604
+ verbose: opts.verbose,
2605
+ resultShape: MindmapResultSchema.shape,
2606
+ systemPromptAppend: "You are Edsger, a staff-level engineer explaining a codebase to a newcomer. You produce a faithful implementation mind map grounded strictly in the code you inspect: every node true and specific, every leaf citing real files. Never invent modules, flows, or files that are not in the repository. Do not modify any files."
2607
+ });
2608
+ if (!result) {
2609
+ log.error(
2610
+ "The agent did not produce a result. Try increasing --max-turns or check credentials."
2611
+ );
2612
+ return 1;
2613
+ }
2614
+ const prepared = prepareMindmap(result, { repo: basename4(opts.cwd) });
2615
+ const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
2616
+ const runId = generatedAt.replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z");
2617
+ const git = gitInfo(opts.cwd);
2618
+ const md = renderMindmapMarkdown(prepared, {
2619
+ repo: basename4(opts.cwd),
2620
+ languages: resolved.languages,
2621
+ layers: resolved.layers,
2622
+ generatedAt,
2623
+ model: opts.model
2624
+ });
2625
+ const jsonReport = {
2626
+ schema: "edsger.mindmap/1",
2627
+ runId,
2628
+ generatedAt,
2629
+ model: opts.model,
2630
+ repo: basename4(opts.cwd),
2631
+ git,
2632
+ languages: resolved.languages,
2633
+ standard: { name: standard.name, version: standard.version, layers: resolved.layers },
2634
+ profile: prepared.profile,
2635
+ root: prepared.root,
2636
+ nodeCount: prepared.nodeCount,
2637
+ depth: prepared.depth,
2638
+ focus: opts.focus ?? null,
2639
+ notes: prepared.notes,
2640
+ costUsd
2641
+ };
2642
+ const historyRecord = {
2643
+ runId,
2644
+ generatedAt,
2645
+ commit: git.commit,
2646
+ branch: git.branch,
2647
+ dirty: git.dirty,
2648
+ model: opts.model,
2649
+ standardVersion: standard.version,
2650
+ nodeCount: prepared.nodeCount,
2651
+ depth: prepared.depth,
2652
+ costUsd
2653
+ };
2654
+ if (!opts.noWrite && !opts.dryRun) {
2655
+ const baseDir = join11(opts.cwd, opts.out);
2656
+ const mdOut = md + "\n";
2657
+ const jsonOut = JSON.stringify(jsonReport, null, 2) + "\n";
2658
+ const runDir = join11(baseDir, "runs", runId);
2659
+ mkdirSync5(runDir, { recursive: true });
2660
+ writeFileSync6(join11(runDir, "mindmap.json"), jsonOut, "utf8");
2661
+ writeFileSync6(join11(runDir, "mindmap.md"), mdOut, "utf8");
2662
+ writeFileSync6(join11(baseDir, "mindmap.json"), jsonOut, "utf8");
2663
+ writeFileSync6(join11(baseDir, "mindmap.md"), mdOut, "utf8");
2664
+ appendFileSync4(join11(baseDir, "history.jsonl"), JSON.stringify(historyRecord) + "\n", "utf8");
2665
+ log.success(
2666
+ `Mind map written to ${pc10.bold(join11(opts.out, "mindmap.md"))} ${pc10.dim(`(run archived at ${join11(opts.out, "runs", runId)})`)}`
2667
+ );
2668
+ }
2669
+ printSummary5(prepared, numTurns, costUsd);
2670
+ if (opts.json) process.stdout.write(JSON.stringify(jsonReport, null, 2) + "\n");
2671
+ return 0;
2672
+ }
2673
+ function printSummary5(prepared, numTurns, costUsd) {
2674
+ log.heading(`${prepared.root.title} \u2014 ${prepared.nodeCount} nodes, depth ${prepared.depth}`);
2675
+ const line = (node, indent2) => {
2676
+ const kids = node.children.length ? pc10.dim(` (${countNodes2(node) - 1})`) : "";
2677
+ process.stderr.write(`${indent2}${pc10.cyan("\u2022")} ${node.title}${kids}
2678
+ `);
2679
+ };
2680
+ for (const branch of prepared.root.children) {
2681
+ line(branch, " ");
2682
+ for (const child of branch.children) line(child, " ");
2683
+ }
2684
+ log.step(`${numTurns} turns \xB7 ~$${costUsd.toFixed(3)}`);
2685
+ }
2686
+ function countNodes2(node) {
2687
+ return 1 + node.children.reduce((acc, c) => acc + countNodes2(c), 0);
2688
+ }
2689
+ async function runMindmapServe(opts) {
2690
+ const mindmapDir = join11(opts.cwd, opts.dir);
2691
+ const viewerDir = findMindmapViewerDir();
2692
+ return startViewerServer({
2693
+ viewerDir,
2694
+ indexFile: "mindmap.html",
2695
+ dataRoute: "/edsger-mindmap.json",
2696
+ getData: () => assembleMindmapData(mindmapDir),
2697
+ port: opts.port,
2698
+ host: opts.host,
2699
+ open: opts.open,
2700
+ emptyHint: hasData(assembleMindmapData(mindmapDir)) ? void 0 : `No mind-map data in ${opts.dir} yet \u2014 run \`edsger mindmap\` first.`
2701
+ });
2702
+ }
2703
+ async function runMindmapReport(opts) {
2704
+ const mindmapDir = join11(opts.cwd, opts.dir);
2705
+ const data = assembleMindmapData(mindmapDir);
2706
+ if (!hasData(data)) {
2707
+ log.error(`No mind-map data in ${opts.dir}. Run \`edsger mindmap\` first.`);
2708
+ return 1;
2709
+ }
2710
+ const html = renderMindmapSingleFile(readMindmapViewerSingleTemplate(), data);
2711
+ const out = isAbsolute5(opts.html) ? opts.html : join11(opts.cwd, opts.html);
2712
+ mkdirSync5(dirname5(out), { recursive: true });
2713
+ writeFileSync6(out, html, "utf8");
2714
+ log.success(
2715
+ `Wrote self-contained mind-map viewer to ${pc10.bold(opts.html)} ${pc10.dim(`(${Math.round(html.length / 1024)} kB, open in any browser)`)}`
2716
+ );
2717
+ if (opts.open) openInBrowser(out);
2718
+ return 0;
2719
+ }
2720
+
2721
+ // src/commands/testcases.ts
2722
+ import { appendFileSync as appendFileSync5, mkdirSync as mkdirSync6, writeFileSync as writeFileSync7 } from "fs";
2723
+ import { basename as basename5, dirname as dirname6, isAbsolute as isAbsolute6, join as join12 } from "path";
2724
+ import pc11 from "picocolors";
2725
+
2726
+ // src/core/testcases.ts
2727
+ var PRIORITY_ORDER = ["critical", "high", "medium", "low"];
2728
+ function isCase(node) {
2729
+ return node.steps.length > 0 || Boolean(node.expected);
2730
+ }
2731
+ function buildTestcasesPrompt(standard, cwd, focus) {
2732
+ const maxDepth = standard.maxDepth ?? 5;
2733
+ const aspectsText = standard.aspects.map((a) => {
2734
+ const detect = a.detect?.length ? `
2735
+ detect: ${a.detect.join(", ")}` : "";
2736
+ return ` - id: ${a.id}
2737
+ name: ${a.name}
2738
+ appliesWhen: ${a.appliesWhen.trim()}` + detect + `
2739
+ guidance: ${a.guidance.trim()}`;
2740
+ }).join("\n");
2741
+ return [
2742
+ `Derive a MANUAL test-case plan for the repository at: ${cwd}`,
2743
+ ``,
2744
+ `The plan is an outline tree (like Workflowy): the root is the project, the`,
2745
+ `branches are the areas a human tester should exercise, and every leaf is a`,
2746
+ `single concrete test case a tester who has never read the code can execute.`,
2747
+ `These are NOT automated code tests \u2014 they describe how a person verifies the`,
2748
+ `software behaves correctly by actually using it.`,
2749
+ ...focus ? [
2750
+ ``,
2751
+ `FOCUS: the user asked the plan to concentrate on: "${focus}". Give that`,
2752
+ `area most of the depth; keep other areas to a shallow orientation level.`
2753
+ ] : [],
2754
+ ``,
2755
+ `Method:`,
2756
+ `1. Explore the repository (Read/Grep/Glob and read-only Bash like ls, cat,`,
2757
+ ` git log). Characterise the project: what kind is it (service, library, CLI,`,
2758
+ ` SPA, data pipeline, monorepo, \u2026), what its real entry points and user-facing`,
2759
+ ` behaviours are, and what failure modes the code actually handles.`,
2760
+ `2. You have a list of candidate top-level test areas ("aspects") below. Keep`,
2761
+ ` only the ones THIS project's code supports (use appliesWhen/detect), rename`,
2762
+ ` them to the project's own vocabulary where that is clearer, and grow each`,
2763
+ ` into a subtree by following its guidance. Aim for 3-7 first-level areas.`,
2764
+ `3. Tree rules:`,
2765
+ ` - Root node: the project \u2014 its name as the title, one line on what a tester`,
2766
+ ` is validating. Exactly ONE root.`,
2767
+ ` - Interior nodes group features/scenarios: short title (2-8 words) plus a`,
2768
+ ` one-sentence summary specific to THIS codebase. Tag them kind: area,`,
2769
+ ` feature, flow, scenario, or topic.`,
2770
+ ` - EVERY leaf is one executable test case, tagged kind: "case", with:`,
2771
+ ` * summary \u2014 what the case verifies and why it matters;`,
2772
+ ` * preconditions \u2014 the state to prepare (install done, env var set, \u2026);`,
2773
+ ` * steps \u2014 exact, observable manual actions in order ("Run \`x --y\`",`,
2774
+ ` "Click Save", "Delete the config file"), each independently checkable;`,
2775
+ ` * expected \u2014 the SINGLE outcome the tester verifies (message, exit`,
2776
+ ` code, UI state, file contents);`,
2777
+ ` * priority \u2014 critical (release blocker if broken), high (core`,
2778
+ ` behaviour), medium (important but recoverable), or low (polish);`,
2779
+ ` * files \u2014 the repo files implementing the behaviour under test.`,
2780
+ ` - Ground every case in behaviour that is actually in the code: real`,
2781
+ ` commands, flags, routes, screens, and error branches. Never invent`,
2782
+ ` behaviour, and never write "check that it works" \u2014 say exactly what to do`,
2783
+ ` and exactly what should happen.`,
2784
+ ` - Maximum depth ${maxDepth} (root inclusive). Prefer depth where the risk`,
2785
+ ` is, brevity where the behaviour is trivial \u2014 a plan a tester can work`,
2786
+ ` through top-to-bottom, not a combinatorial matrix.`,
2787
+ `4. Encode the tree as a FLAT node list: each node has a unique kebab-case "id"`,
2788
+ ` and a "parentId" referencing its parent's id (null only for the root). List`,
2789
+ ` children in reading order right after their parent.`,
2790
+ `5. When finished, call submit_result exactly once with the project profile,`,
2791
+ ` the flat node list, and optional notes.`,
2792
+ ``,
2793
+ ...standard.guidance ? [`Overall guidance: ${standard.guidance.trim()}`, ``] : [],
2794
+ `Candidate top-level test areas:`,
2795
+ aspectsText
2796
+ ].join("\n");
2797
+ }
2798
+ function slugify2(text) {
2799
+ return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60) || "node";
2800
+ }
2801
+ function prepareTestcases(result, fallback) {
2802
+ const warnings = [];
2803
+ const byId = /* @__PURE__ */ new Map();
2804
+ for (const raw of result.nodes) {
2805
+ const node = { ...raw, id: raw.id?.trim() || slugify2(raw.title) };
2806
+ if (!node.title?.trim()) {
2807
+ warnings.push(`dropped a node with no title (id "${node.id}")`);
1660
2808
  continue;
1661
2809
  }
1662
- const baseId = d.id || inferType(mermaid);
1663
- const count = seenIds.get(baseId) ?? 0;
1664
- seenIds.set(baseId, count + 1);
1665
- const id = count === 0 ? baseId : `${baseId}-${count + 1}`;
1666
- const type = d.type?.trim() || inferType(mermaid);
1667
- prepared.push({
1668
- ...d,
1669
- id,
1670
- mermaid,
1671
- type,
1672
- evidence: d.evidence ?? [],
1673
- groupName: groupName.get(d.groupId) ?? "Other",
1674
- warnings: validateMermaid(type, mermaid)
2810
+ let id = node.id;
2811
+ for (let n = 2; byId.has(id); n++) id = `${node.id}-${n}`;
2812
+ if (id !== node.id) warnings.push(`renamed duplicate node id "${node.id}" to "${id}"`);
2813
+ byId.set(id, { ...node, id });
2814
+ }
2815
+ const parentOf = /* @__PURE__ */ new Map();
2816
+ for (const node of byId.values()) {
2817
+ const p = node.parentId && node.parentId !== node.id ? node.parentId : null;
2818
+ parentOf.set(node.id, p && byId.has(p) ? p : null);
2819
+ if (node.parentId && !byId.has(node.parentId)) {
2820
+ warnings.push(`node "${node.id}" referenced missing parent "${node.parentId}"`);
2821
+ }
2822
+ }
2823
+ for (const id of parentOf.keys()) {
2824
+ const seen = /* @__PURE__ */ new Set([id]);
2825
+ let cur = parentOf.get(id) ?? null;
2826
+ while (cur) {
2827
+ if (seen.has(cur)) {
2828
+ warnings.push(`broke a parent cycle at node "${cur}"`);
2829
+ parentOf.set(cur, null);
2830
+ break;
2831
+ }
2832
+ seen.add(cur);
2833
+ cur = parentOf.get(cur) ?? null;
2834
+ }
2835
+ }
2836
+ const treeOf = /* @__PURE__ */ new Map();
2837
+ for (const node of byId.values()) {
2838
+ treeOf.set(node.id, {
2839
+ id: node.id,
2840
+ title: node.title.trim(),
2841
+ summary: node.summary?.trim() ?? "",
2842
+ kind: node.kind?.trim() || "topic",
2843
+ priority: node.priority ?? null,
2844
+ preconditions: node.preconditions ?? [],
2845
+ steps: node.steps ?? [],
2846
+ expected: node.expected?.trim() || null,
2847
+ files: node.files ?? [],
2848
+ children: []
1675
2849
  });
1676
2850
  }
1677
- prepared.sort((a, b) => {
1678
- const oa = groupOrder.get(a.groupId) ?? Number.MAX_SAFE_INTEGER;
1679
- const ob = groupOrder.get(b.groupId) ?? Number.MAX_SAFE_INTEGER;
1680
- return oa - ob;
1681
- });
1682
- if (prepared.length === 0) {
1683
- log.warn("The agent returned no usable diagrams.");
2851
+ const roots = [];
2852
+ for (const node of byId.values()) {
2853
+ const tree = treeOf.get(node.id);
2854
+ const parent = parentOf.get(node.id);
2855
+ if (parent) treeOf.get(parent).children.push(tree);
2856
+ else roots.push(tree);
2857
+ }
2858
+ let root;
2859
+ if (roots.length === 1 && roots[0]) {
2860
+ root = roots[0];
1684
2861
  } else {
1685
- const flagged = prepared.filter((d) => d.warnings.length);
1686
- if (flagged.length) {
1687
- log.warn(
1688
- `${flagged.length}/${prepared.length} diagram(s) have Mermaid warnings \u2014 they will still render if valid, but check: ${flagged.map((d) => d.id).join(", ")}`
1689
- );
2862
+ if (roots.length > 1) warnings.push(`${roots.length} root nodes \u2014 gathered under the project`);
2863
+ let rootId = "root";
2864
+ for (let n = 2; treeOf.has(rootId); n++) rootId = `root-${n}`;
2865
+ root = {
2866
+ id: rootId,
2867
+ title: fallback.repo,
2868
+ summary: result.profile.summary,
2869
+ kind: "area",
2870
+ priority: null,
2871
+ preconditions: [],
2872
+ steps: [],
2873
+ expected: null,
2874
+ files: [],
2875
+ children: roots
2876
+ };
2877
+ }
2878
+ const nodeCount = countNodes3(root);
2879
+ const depth = treeDepth2(root);
2880
+ const priorities = { critical: 0, high: 0, medium: 0, low: 0 };
2881
+ let caseCount = 0;
2882
+ const tally = (node) => {
2883
+ if (isCase(node)) {
2884
+ caseCount++;
2885
+ priorities[node.priority ?? "medium"]++;
1690
2886
  }
2887
+ node.children.forEach(tally);
2888
+ };
2889
+ tally(root);
2890
+ if (nodeCount <= 1) log.warn("The agent returned an (almost) empty test plan.");
2891
+ if (caseCount === 0 && nodeCount > 1) {
2892
+ warnings.push("no leaf node carries steps/expected \u2014 the plan has no executable cases");
1691
2893
  }
1692
- return { profile: result.profile, diagrams: prepared, notes: result.notes ?? null };
2894
+ for (const w of warnings) log.warn(`testcases: ${w}`);
2895
+ return {
2896
+ profile: result.profile,
2897
+ root,
2898
+ nodeCount,
2899
+ caseCount,
2900
+ priorities,
2901
+ depth,
2902
+ warnings,
2903
+ notes: result.notes ?? null
2904
+ };
1693
2905
  }
1694
- function renderDiagramsMarkdown(prepared, meta) {
2906
+ function countNodes3(node) {
2907
+ return 1 + node.children.reduce((acc, c) => acc + countNodes3(c), 0);
2908
+ }
2909
+ function treeDepth2(node) {
2910
+ return 1 + node.children.reduce((acc, c) => Math.max(acc, treeDepth2(c)), 0);
2911
+ }
2912
+ function renderTestcasesMarkdown(prepared, meta) {
1695
2913
  const lines = [];
1696
- lines.push(`# Edsger Diagrams`);
2914
+ lines.push(`# Edsger Manual Test Cases`);
1697
2915
  lines.push("");
1698
2916
  lines.push(`> ${prepared.profile.summary}`);
1699
2917
  lines.push("");
@@ -1702,60 +2920,64 @@ function renderDiagramsMarkdown(prepared, meta) {
1702
2920
  lines.push(`| Repository | \`${meta.repo}\` |`);
1703
2921
  lines.push(`| Languages | ${meta.languages.join(", ") || "n/a"} |`);
1704
2922
  lines.push(`| Project kinds | ${prepared.profile.kinds.join(", ") || "n/a"} |`);
1705
- lines.push(`| Catalog sources | ${meta.layers.join(" \u2192 ")} |`);
2923
+ lines.push(
2924
+ `| Test cases | ${prepared.caseCount} (${PRIORITY_ORDER.map((p) => `${prepared.priorities[p]} ${p}`).join(", ")}) |`
2925
+ );
2926
+ lines.push(`| Nodes | ${prepared.nodeCount} (depth ${prepared.depth}) |`);
2927
+ lines.push(`| Standard sources | ${meta.layers.join(" \u2192 ")} |`);
1706
2928
  lines.push(`| Model | ${meta.model} |`);
1707
2929
  lines.push(`| Generated | ${meta.generatedAt} |`);
1708
2930
  lines.push("");
1709
- if (prepared.profile.characteristics.length) {
1710
- lines.push(`**Characteristics:** ${prepared.profile.characteristics.join(" \xB7 ")}`);
1711
- lines.push("");
1712
- }
1713
- let currentGroup = null;
1714
- for (const d of prepared.diagrams) {
1715
- if (d.groupName !== currentGroup) {
1716
- currentGroup = d.groupName;
1717
- lines.push(`## ${currentGroup}`);
1718
- lines.push("");
1719
- }
1720
- lines.push(`### ${d.title}`);
1721
- lines.push("");
1722
- lines.push(`${d.description} _(${d.type})_`);
1723
- lines.push("");
1724
- if (d.warnings.length) {
1725
- lines.push(`> \u26A0\uFE0F ${d.warnings.join("; ")}`);
1726
- lines.push("");
1727
- }
1728
- lines.push("```mermaid");
1729
- lines.push(d.mermaid);
1730
- lines.push("```");
2931
+ lines.push(`## ${prepared.root.title}`);
2932
+ if (prepared.root.summary && prepared.root.summary !== prepared.profile.summary) {
1731
2933
  lines.push("");
1732
- if (d.evidence.length) {
1733
- lines.push(`<sub>Evidence: ${d.evidence.map((e) => `\`${e}\``).join(", ")}</sub>`);
1734
- lines.push("");
1735
- }
2934
+ lines.push(prepared.root.summary);
1736
2935
  }
2936
+ lines.push("");
2937
+ for (const child of prepared.root.children) renderNode2(lines, child, 0);
1737
2938
  if (prepared.notes) {
2939
+ lines.push("");
1738
2940
  lines.push(`## Notes`);
1739
2941
  lines.push("");
1740
2942
  lines.push(prepared.notes);
1741
- lines.push("");
1742
2943
  }
2944
+ lines.push("");
1743
2945
  lines.push(`---`);
1744
2946
  lines.push(`<sub>Generated by [edsger](https://www.npmjs.com/package/edsger).</sub>`);
1745
2947
  return lines.join("\n");
1746
2948
  }
2949
+ function renderNode2(lines, node, indent2) {
2950
+ const pad = " ".repeat(indent2);
2951
+ const files = node.files.length ? ` ${node.files.map((f) => `\`${f}\``).join(" ")}` : "";
2952
+ const summary = node.summary ? ` \u2014 ${node.summary}` : "";
2953
+ if (isCase(node)) {
2954
+ const prio = node.priority ? ` \`${node.priority}\`` : "";
2955
+ lines.push(`${pad}- [ ] **${node.title}**${prio}${summary}${files}`);
2956
+ const sub = " ".repeat(indent2 + 1);
2957
+ if (node.preconditions.length) {
2958
+ lines.push(`${sub}- Preconditions: ${node.preconditions.join("; ")}`);
2959
+ }
2960
+ for (const [i, step] of node.steps.entries()) {
2961
+ lines.push(`${sub}- Step ${i + 1}: ${step}`);
2962
+ }
2963
+ if (node.expected) lines.push(`${sub}- Expected: ${node.expected}`);
2964
+ } else {
2965
+ lines.push(`${pad}- **${node.title}**${summary}${files}`);
2966
+ }
2967
+ for (const child of node.children) renderNode2(lines, child, indent2 + 1);
2968
+ }
1747
2969
 
1748
- // src/commands/diagram.ts
1749
- async function runDiagramGenerate(opts) {
2970
+ // src/commands/testcases.ts
2971
+ async function runTestcasesGenerate(opts) {
1750
2972
  const { config, path: configPath } = loadConfig(opts.cwd);
1751
- const configDir = configPath ? join9(configPath, "..") : opts.cwd;
1752
- const resolved = resolveStandard("diagrams", opts, config, configDir);
1753
- const catalog = resolved.standard;
2973
+ const configDir = configPath ? join12(configPath, "..") : opts.cwd;
2974
+ const resolved = resolveStandard("testcases", opts, config, configDir);
2975
+ const standard = resolved.standard;
1754
2976
  log.info(
1755
- `Diagramming ${pc8.bold(basename2(opts.cwd))} with ${pc8.bold(catalog.name)} (languages: ${resolved.languages.join(", ") || "generic"})`
2977
+ `Deriving manual test cases for ${pc11.bold(basename5(opts.cwd))} with ${pc11.bold(standard.name)} (languages: ${resolved.languages.join(", ") || "generic"})`
1756
2978
  );
1757
- log.debug(`catalog layers: ${resolved.layers.join(" \u2192 ")}`);
1758
- const prompt = buildDiagramsPrompt(catalog, opts.cwd, opts.only);
2979
+ log.debug(`standard layers: ${resolved.layers.join(" \u2192 ")}`);
2980
+ const prompt = buildTestcasesPrompt(standard, opts.cwd, opts.focus);
1759
2981
  const { result, costUsd, numTurns } = await runAgent({
1760
2982
  prompt,
1761
2983
  cwd: opts.cwd,
@@ -1764,8 +2986,8 @@ async function runDiagramGenerate(opts) {
1764
2986
  timeoutMs: opts.timeoutMs,
1765
2987
  allowWrite: false,
1766
2988
  verbose: opts.verbose,
1767
- resultShape: DiagramsResultSchema.shape,
1768
- systemPromptAppend: "You are Edsger, a staff-level software architect. You produce accurate, legible diagrams of a codebase as valid Mermaid, grounded strictly in the code you inspect. Never invent components, tables, or flows that are not in the repository. Do not modify any files."
2989
+ resultShape: TestcasesResultSchema.shape,
2990
+ systemPromptAppend: "You are Edsger, a staff-level QA engineer writing a manual test plan for a human tester who has never read the code. You derive every test case strictly from behaviour that exists in the repository you inspect: real commands, flags, routes, screens, and error branches, with exact steps and one verifiable expected result each. Never invent behaviour that is not in the code. Do not modify any files."
1769
2991
  });
1770
2992
  if (!result) {
1771
2993
  log.error(
@@ -1773,33 +2995,36 @@ async function runDiagramGenerate(opts) {
1773
2995
  );
1774
2996
  return 1;
1775
2997
  }
1776
- const prepared = prepareDiagrams(catalog, result);
2998
+ const prepared = prepareTestcases(result, { repo: basename5(opts.cwd) });
1777
2999
  const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
1778
3000
  const runId = generatedAt.replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z");
1779
3001
  const git = gitInfo(opts.cwd);
1780
- const md = renderDiagramsMarkdown(prepared, {
1781
- repo: basename2(opts.cwd),
3002
+ const md = renderTestcasesMarkdown(prepared, {
3003
+ repo: basename5(opts.cwd),
1782
3004
  languages: resolved.languages,
1783
3005
  layers: resolved.layers,
1784
3006
  generatedAt,
1785
3007
  model: opts.model
1786
3008
  });
1787
3009
  const jsonReport = {
1788
- schema: "edsger.diagrams/1",
3010
+ schema: "edsger.testcases/1",
1789
3011
  runId,
1790
3012
  generatedAt,
1791
3013
  model: opts.model,
1792
- repo: basename2(opts.cwd),
3014
+ repo: basename5(opts.cwd),
1793
3015
  git,
1794
3016
  languages: resolved.languages,
1795
- catalog: { name: catalog.name, version: catalog.version, layers: resolved.layers },
3017
+ standard: { name: standard.name, version: standard.version, layers: resolved.layers },
1796
3018
  profile: prepared.profile,
1797
- diagrams: prepared.diagrams,
3019
+ root: prepared.root,
3020
+ nodeCount: prepared.nodeCount,
3021
+ caseCount: prepared.caseCount,
3022
+ priorities: prepared.priorities,
3023
+ depth: prepared.depth,
3024
+ focus: opts.focus ?? null,
1798
3025
  notes: prepared.notes,
1799
3026
  costUsd
1800
3027
  };
1801
- const groupCounts = {};
1802
- for (const d of prepared.diagrams) groupCounts[d.groupId] = (groupCounts[d.groupId] ?? 0) + 1;
1803
3028
  const historyRecord = {
1804
3029
  runId,
1805
3030
  generatedAt,
@@ -1807,72 +3032,76 @@ async function runDiagramGenerate(opts) {
1807
3032
  branch: git.branch,
1808
3033
  dirty: git.dirty,
1809
3034
  model: opts.model,
1810
- catalogVersion: catalog.version,
1811
- diagramCount: prepared.diagrams.length,
1812
- groups: groupCounts,
3035
+ standardVersion: standard.version,
3036
+ nodeCount: prepared.nodeCount,
3037
+ caseCount: prepared.caseCount,
3038
+ priorities: prepared.priorities,
3039
+ depth: prepared.depth,
1813
3040
  costUsd
1814
3041
  };
1815
3042
  if (!opts.noWrite && !opts.dryRun) {
1816
- const baseDir = join9(opts.cwd, opts.out);
3043
+ const baseDir = join12(opts.cwd, opts.out);
1817
3044
  const mdOut = md + "\n";
1818
3045
  const jsonOut = JSON.stringify(jsonReport, null, 2) + "\n";
1819
- const runDir = join9(baseDir, "runs", runId);
1820
- mkdirSync3(runDir, { recursive: true });
1821
- writeFileSync4(join9(runDir, "diagrams.json"), jsonOut, "utf8");
1822
- writeFileSync4(join9(runDir, "diagrams.md"), mdOut, "utf8");
1823
- writeFileSync4(join9(baseDir, "diagrams.json"), jsonOut, "utf8");
1824
- writeFileSync4(join9(baseDir, "diagrams.md"), mdOut, "utf8");
1825
- appendFileSync2(join9(baseDir, "history.jsonl"), JSON.stringify(historyRecord) + "\n", "utf8");
3046
+ const runDir = join12(baseDir, "runs", runId);
3047
+ mkdirSync6(runDir, { recursive: true });
3048
+ writeFileSync7(join12(runDir, "testcases.json"), jsonOut, "utf8");
3049
+ writeFileSync7(join12(runDir, "testcases.md"), mdOut, "utf8");
3050
+ writeFileSync7(join12(baseDir, "testcases.json"), jsonOut, "utf8");
3051
+ writeFileSync7(join12(baseDir, "testcases.md"), mdOut, "utf8");
3052
+ appendFileSync5(join12(baseDir, "history.jsonl"), JSON.stringify(historyRecord) + "\n", "utf8");
1826
3053
  log.success(
1827
- `Diagrams written to ${pc8.bold(join9(opts.out, "diagrams.md"))} ${pc8.dim(`(run archived at ${join9(opts.out, "runs", runId)})`)}`
3054
+ `Test plan written to ${pc11.bold(join12(opts.out, "testcases.md"))} ${pc11.dim(`(run archived at ${join12(opts.out, "runs", runId)})`)}`
1828
3055
  );
1829
3056
  }
1830
- printSummary3(prepared, numTurns, costUsd);
3057
+ printSummary6(prepared, numTurns, costUsd);
1831
3058
  if (opts.json) process.stdout.write(JSON.stringify(jsonReport, null, 2) + "\n");
1832
3059
  return 0;
1833
3060
  }
1834
- function printSummary3(prepared, numTurns, costUsd) {
1835
- log.heading(`${prepared.diagrams.length} diagram(s)`);
1836
- let currentGroup = null;
1837
- for (const d of prepared.diagrams) {
1838
- if (d.groupName !== currentGroup) {
1839
- currentGroup = d.groupName;
1840
- process.stderr.write(` ${pc8.bold(currentGroup)}
1841
- `);
1842
- }
1843
- const warn = d.warnings.length ? ` ${pc8.yellow("\u26A0")}` : "";
1844
- process.stderr.write(` ${pc8.cyan("\u25C6")} ${d.title} ${pc8.dim(`(${d.type})`)}${warn}
3061
+ function printSummary6(prepared, numTurns, costUsd) {
3062
+ const tally = PRIORITY_ORDER.filter((p) => prepared.priorities[p] > 0).map((p) => `${prepared.priorities[p]} ${p}`).join(", ");
3063
+ log.heading(
3064
+ `${prepared.root.title} \u2014 ${prepared.caseCount} cases${tally ? ` (${tally})` : ""}, ${prepared.nodeCount} nodes, depth ${prepared.depth}`
3065
+ );
3066
+ const line = (node, indent2) => {
3067
+ const cases = countCases(node);
3068
+ const suffix = cases ? pc11.dim(` (${cases} case${cases === 1 ? "" : "s"})`) : "";
3069
+ process.stderr.write(`${indent2}${pc11.cyan("\u2022")} ${node.title}${suffix}
1845
3070
  `);
1846
- }
3071
+ };
3072
+ for (const branch of prepared.root.children) line(branch, " ");
1847
3073
  log.step(`${numTurns} turns \xB7 ~$${costUsd.toFixed(3)}`);
1848
3074
  }
1849
- async function runDiagramServe(opts) {
1850
- const diagramsDir = join9(opts.cwd, opts.dir);
1851
- const viewerDir = findDiagramViewerDir();
3075
+ function countCases(node) {
3076
+ return (isCase(node) ? 1 : 0) + node.children.reduce((acc, c) => acc + countCases(c), 0);
3077
+ }
3078
+ async function runTestcasesServe(opts) {
3079
+ const testcasesDir = join12(opts.cwd, opts.dir);
3080
+ const viewerDir = findTestcasesViewerDir();
1852
3081
  return startViewerServer({
1853
3082
  viewerDir,
1854
- indexFile: "diagrams.html",
1855
- dataRoute: "/edsger-diagrams.json",
1856
- getData: () => assembleDiagramData(diagramsDir),
3083
+ indexFile: "testcases.html",
3084
+ dataRoute: "/edsger-testcases.json",
3085
+ getData: () => assembleTestcasesData(testcasesDir),
1857
3086
  port: opts.port,
1858
3087
  host: opts.host,
1859
3088
  open: opts.open,
1860
- emptyHint: hasData(assembleDiagramData(diagramsDir)) ? void 0 : `No diagram data in ${opts.dir} yet \u2014 run \`edsger diagram\` first.`
3089
+ emptyHint: hasData(assembleTestcasesData(testcasesDir)) ? void 0 : `No test-case data in ${opts.dir} yet \u2014 run \`edsger testcases\` first.`
1861
3090
  });
1862
3091
  }
1863
- async function runDiagramReport(opts) {
1864
- const diagramsDir = join9(opts.cwd, opts.dir);
1865
- const data = assembleDiagramData(diagramsDir);
3092
+ async function runTestcasesReport(opts) {
3093
+ const testcasesDir = join12(opts.cwd, opts.dir);
3094
+ const data = assembleTestcasesData(testcasesDir);
1866
3095
  if (!hasData(data)) {
1867
- log.error(`No diagram data in ${opts.dir}. Run \`edsger diagram\` first.`);
3096
+ log.error(`No test-case data in ${opts.dir}. Run \`edsger testcases\` first.`);
1868
3097
  return 1;
1869
3098
  }
1870
- const html = renderDiagramsSingleFile(readDiagramViewerSingleTemplate(), data);
1871
- const out = isAbsolute3(opts.html) ? opts.html : join9(opts.cwd, opts.html);
1872
- mkdirSync3(dirname3(out), { recursive: true });
1873
- writeFileSync4(out, html, "utf8");
3099
+ const html = renderTestcasesSingleFile(readTestcasesViewerSingleTemplate(), data);
3100
+ const out = isAbsolute6(opts.html) ? opts.html : join12(opts.cwd, opts.html);
3101
+ mkdirSync6(dirname6(out), { recursive: true });
3102
+ writeFileSync7(out, html, "utf8");
1874
3103
  log.success(
1875
- `Wrote self-contained diagram viewer to ${pc8.bold(opts.html)} ${pc8.dim(`(${Math.round(html.length / 1024)} kB, open in any browser)`)}`
3104
+ `Wrote self-contained test-case viewer to ${pc11.bold(opts.html)} ${pc11.dim(`(${Math.round(html.length / 1024)} kB, open in any browser)`)}`
1876
3105
  );
1877
3106
  if (opts.open) openInBrowser(out);
1878
3107
  return 0;
@@ -1882,22 +3111,22 @@ async function runDiagramReport(opts) {
1882
3111
  import {
1883
3112
  cpSync,
1884
3113
  existsSync as existsSync5,
1885
- mkdirSync as mkdirSync4,
3114
+ mkdirSync as mkdirSync7,
1886
3115
  readFileSync as readFileSync3,
1887
3116
  readdirSync as readdirSync3,
1888
3117
  rmSync as rmSync2,
1889
3118
  statSync as statSync3,
1890
- writeFileSync as writeFileSync5
3119
+ writeFileSync as writeFileSync8
1891
3120
  } from "fs";
1892
3121
  import { homedir as homedir2 } from "os";
1893
- import { join as join10 } from "path";
1894
- import pc9 from "picocolors";
1895
- var MANAGED = ["benchmark", "pr-review", "pr-resolve"];
3122
+ import { join as join13 } from "path";
3123
+ import pc12 from "picocolors";
3124
+ var MANAGED = ["benchmark", "audit", "pr-review", "pr-resolve"];
1896
3125
  function targetSkillsDir(scope) {
1897
- return scope.global ? join10(homedir2(), ".claude", "skills") : join10(scope.cwd, ".claude", "skills");
3126
+ return scope.global ? join13(homedir2(), ".claude", "skills") : join13(scope.cwd, ".claude", "skills");
1898
3127
  }
1899
3128
  function sourceSkillsDir() {
1900
- return join10(findBundledAssetsDir(), "skills");
3129
+ return join13(findBundledAssetsDir(), "skills");
1901
3130
  }
1902
3131
  async function runSkillsInstall(opts) {
1903
3132
  const assetsDir = findBundledAssetsDir();
@@ -1907,20 +3136,20 @@ async function runSkillsInstall(opts) {
1907
3136
  return 1;
1908
3137
  }
1909
3138
  const dest = targetSkillsDir(opts);
1910
- mkdirSync4(dest, { recursive: true });
1911
- const available = readdirSync3(src).filter((n) => existsSync5(join10(src, n, "SKILL.md")));
3139
+ mkdirSync7(dest, { recursive: true });
3140
+ const available = readdirSync3(src).filter((n) => existsSync5(join13(src, n, "SKILL.md")));
1912
3141
  let installed = 0;
1913
3142
  for (const name of available) {
1914
- const outDir = join10(dest, name);
3143
+ const outDir = join13(dest, name);
1915
3144
  if (existsSync5(outDir) && !opts.force) {
1916
3145
  log.warn(`skill "${name}" already exists \u2014 use --force to overwrite`);
1917
3146
  continue;
1918
3147
  }
1919
3148
  rmSync2(outDir, { recursive: true, force: true });
1920
- cpSync(join10(src, name), outDir, { recursive: true });
1921
- const skillFile = join10(outDir, "SKILL.md");
3149
+ cpSync(join13(src, name), outDir, { recursive: true });
3150
+ const skillFile = join13(outDir, "SKILL.md");
1922
3151
  const rewritten = readFileSync3(skillFile, "utf8").split("${CLAUDE_PLUGIN_ROOT}").join(assetsDir);
1923
- writeFileSync5(skillFile, rewritten, "utf8");
3152
+ writeFileSync8(skillFile, rewritten, "utf8");
1924
3153
  installed++;
1925
3154
  log.success(`installed /${name}`);
1926
3155
  }
@@ -1928,17 +3157,17 @@ async function runSkillsInstall(opts) {
1928
3157
  log.info("Nothing installed.");
1929
3158
  return 0;
1930
3159
  }
1931
- log.heading(`Installed ${installed} skill(s) into ${pc9.bold(dest)}`);
3160
+ log.heading(`Installed ${installed} skill(s) into ${pc12.bold(dest)}`);
1932
3161
  log.step(`scope: ${opts.global ? "user (all projects)" : "this project"}`);
1933
3162
  log.step(`standards: ${assetsDir}`);
1934
- log.step("Open Claude Code and run /benchmark, /pr-review, or /pr-resolve.");
3163
+ log.step("Open Claude Code and run /benchmark, /audit, /pr-review, or /pr-resolve.");
1935
3164
  return 0;
1936
3165
  }
1937
3166
  async function runSkillsUninstall(opts) {
1938
3167
  const dest = targetSkillsDir(opts);
1939
3168
  let removed = 0;
1940
3169
  for (const name of MANAGED) {
1941
- const dir = join10(dest, name);
3170
+ const dir = join13(dest, name);
1942
3171
  if (existsSync5(dir)) {
1943
3172
  rmSync2(dir, { recursive: true, force: true });
1944
3173
  removed++;
@@ -1955,13 +3184,13 @@ async function runSkillsList(opts) {
1955
3184
  ]) {
1956
3185
  const dir = targetSkillsDir(scope);
1957
3186
  const present = MANAGED.filter((n) => {
1958
- const p = join10(dir, n, "SKILL.md");
3187
+ const p = join13(dir, n, "SKILL.md");
1959
3188
  return existsSync5(p) && statSync3(p).isFile();
1960
3189
  });
1961
3190
  log.heading(`${label}: ${dir}`);
1962
- if (present.length) for (const n of present) process.stderr.write(` ${pc9.green("\u2713")} /${n}
3191
+ if (present.length) for (const n of present) process.stderr.write(` ${pc12.green("\u2713")} /${n}
1963
3192
  `);
1964
- else process.stderr.write(` ${pc9.dim("(none installed)")}
3193
+ else process.stderr.write(` ${pc12.dim("(none installed)")}
1965
3194
  `);
1966
3195
  }
1967
3196
  return 0;
@@ -2003,7 +3232,7 @@ async function guard(fn) {
2003
3232
  }
2004
3233
  var program = new Command();
2005
3234
  program.name("edsger").description(
2006
- "Agentic CLI to benchmark repositories and review/resolve pull requests\nagainst industrial-grade standards (from edsger-assets, fully overridable)."
3235
+ "Agentic CLI to benchmark and audit repositories and review/resolve pull requests\nagainst industrial-grade standards (from edsger-assets, fully overridable)."
2007
3236
  ).version(VERSION, "-V, --version");
2008
3237
  addGlobalOptions(
2009
3238
  program.command("benchmark").description("Score a repository against a benchmark rubric and write a report").option("--out <dir>", "report output directory (relative to repo)", ".edsger/benchmark").option("--no-write", "do not write report files into the repo").option("--serve", "open the dashboard after benchmarking")
@@ -2017,6 +3246,66 @@ addGlobalOptions(
2017
3246
  return code;
2018
3247
  });
2019
3248
  });
3249
+ var FAIL_ON_SEVERITIES = ["critical", "high", "medium", "low"];
3250
+ var audit = program.command("audit").alias("review").description(
3251
+ "Audit the whole repository: hunt for unprofessional, outdated, or fragile implementations across every dimension and write an organised issue report"
3252
+ );
3253
+ addGlobalOptions(
3254
+ audit.command("generate", { isDefault: true }).description("Audit the repository and write the findings report").option("--out <dir>", "report output directory (relative to repo)", ".edsger/audit").option("--no-write", "do not write report files into the repo").option("--only <ids>", "comma-separated dimension ids to audit (default: all)").option("--focus <text>", "area or concern the audit should concentrate on").addOption(
3255
+ new Option(
3256
+ "--fail-on <severity>",
3257
+ "exit with code 2 if findings exist at or above this severity (CI gate)"
3258
+ ).choices(FAIL_ON_SEVERITIES)
3259
+ ).option("--serve", "open the findings dashboard after auditing")
3260
+ ).action(
3261
+ async (opts) => {
3262
+ await guard(async () => {
3263
+ const g = toGlobal(opts);
3264
+ const code = await runAudit({
3265
+ ...g,
3266
+ out: opts.out,
3267
+ noWrite: opts.write === false,
3268
+ only: opts.only ? opts.only.split(",").map((s) => s.trim()).filter(Boolean) : void 0,
3269
+ focus: opts.focus,
3270
+ failOn: opts.failOn
3271
+ });
3272
+ if (code !== 1 && opts.serve) {
3273
+ const serveCode = await runAuditServe({
3274
+ cwd: g.cwd,
3275
+ dir: opts.out,
3276
+ port: 4580,
3277
+ host: "127.0.0.1",
3278
+ open: true
3279
+ });
3280
+ return code || serveCode;
3281
+ }
3282
+ return code;
3283
+ });
3284
+ }
3285
+ );
3286
+ audit.command("serve").description("Serve the audit findings dashboard locally").option("-C, --cwd <dir>", "repository directory").option("--dir <dir>", "audit output directory (relative to repo)", ".edsger/audit").option("--port <n>", "port to listen on", "4580").option("--host <host>", "host to bind", "127.0.0.1").option("--no-open", "do not open the browser automatically").action(
3287
+ async (opts) => {
3288
+ await guard(
3289
+ () => runAuditServe({
3290
+ cwd: resolve2(opts.cwd ?? process.cwd()),
3291
+ dir: opts.dir,
3292
+ port: Number.parseInt(opts.port, 10) || 4580,
3293
+ host: opts.host,
3294
+ open: opts.open !== false
3295
+ })
3296
+ );
3297
+ }
3298
+ );
3299
+ audit.command("report").description("Export a self-contained HTML findings viewer (data inlined) for sharing").option("-C, --cwd <dir>", "repository directory").option("--dir <dir>", "audit output directory (relative to repo)", ".edsger/audit").option("--html <path>", "output HTML file path", "edsger-audit.html").option("--open", "open the exported file after writing").action(async (opts) => {
3300
+ await guard(
3301
+ () => runAuditReport({
3302
+ cwd: resolve2(opts.cwd ?? process.cwd()),
3303
+ dir: opts.dir,
3304
+ html: opts.html,
3305
+ open: Boolean(opts.open)
3306
+ })
3307
+ );
3308
+ });
2020
3309
  program.command("serve").description("Serve the benchmark dashboard (current, history, trends) locally").option("-C, --cwd <dir>", "repository directory").option("--dir <dir>", "benchmark output directory (relative to repo)", ".edsger/benchmark").option("--port <n>", "port to listen on", "4577").option("--host <host>", "host to bind", "127.0.0.1").option("--no-open", "do not open the browser automatically").action(
2021
3310
  async (opts) => {
2022
3311
  await guard(
@@ -2090,6 +3379,106 @@ diagram.command("report").description("Export a self-contained HTML diagram view
2090
3379
  })
2091
3380
  );
2092
3381
  });
3382
+ var mindmap = program.command("mindmap").description("Generate & browse an implementation mind map (Workflowy-style outline) of a repo");
3383
+ addGlobalOptions(
3384
+ mindmap.command("generate", { isDefault: true }).description("Analyze the repository and generate the implementation mind map").option("--out <dir>", "mind-map output directory (relative to repo)", ".edsger/mindmap").option("--no-write", "do not write mind-map files into the repo").option("--focus <text>", "area or topic the map should concentrate its depth on").option("--serve", "open the mind-map viewer after generating")
3385
+ ).action(
3386
+ async (opts) => {
3387
+ await guard(async () => {
3388
+ const g = toGlobal(opts);
3389
+ const code = await runMindmapGenerate({
3390
+ ...g,
3391
+ out: opts.out,
3392
+ noWrite: opts.write === false,
3393
+ focus: opts.focus
3394
+ });
3395
+ if (code === 0 && opts.serve) {
3396
+ return runMindmapServe({
3397
+ cwd: g.cwd,
3398
+ dir: opts.out,
3399
+ port: 4579,
3400
+ host: "127.0.0.1",
3401
+ open: true
3402
+ });
3403
+ }
3404
+ return code;
3405
+ });
3406
+ }
3407
+ );
3408
+ mindmap.command("serve").description("Serve the mind-map viewer locally").option("-C, --cwd <dir>", "repository directory").option("--dir <dir>", "mind-map output directory (relative to repo)", ".edsger/mindmap").option("--port <n>", "port to listen on", "4579").option("--host <host>", "host to bind", "127.0.0.1").option("--no-open", "do not open the browser automatically").action(
3409
+ async (opts) => {
3410
+ await guard(
3411
+ () => runMindmapServe({
3412
+ cwd: resolve2(opts.cwd ?? process.cwd()),
3413
+ dir: opts.dir,
3414
+ port: Number.parseInt(opts.port, 10) || 4579,
3415
+ host: opts.host,
3416
+ open: opts.open !== false
3417
+ })
3418
+ );
3419
+ }
3420
+ );
3421
+ mindmap.command("report").description("Export a self-contained HTML mind-map viewer (data inlined) for sharing").option("-C, --cwd <dir>", "repository directory").option("--dir <dir>", "mind-map output directory (relative to repo)", ".edsger/mindmap").option("--html <path>", "output HTML file path", "edsger-mindmap.html").option("--open", "open the exported file after writing").action(async (opts) => {
3422
+ await guard(
3423
+ () => runMindmapReport({
3424
+ cwd: resolve2(opts.cwd ?? process.cwd()),
3425
+ dir: opts.dir,
3426
+ html: opts.html,
3427
+ open: Boolean(opts.open)
3428
+ })
3429
+ );
3430
+ });
3431
+ var testcases = program.command("testcases").alias("test-cases").description(
3432
+ "Derive & browse a manual test-case plan (Workflowy-style outline) for a repo \u2014 how a human should test it, not automated code tests"
3433
+ );
3434
+ addGlobalOptions(
3435
+ testcases.command("generate", { isDefault: true }).description("Analyze the repository and derive the manual test-case plan").option("--out <dir>", "test-plan output directory (relative to repo)", ".edsger/testcases").option("--no-write", "do not write test-plan files into the repo").option("--focus <text>", "area or feature the plan should concentrate its depth on").option("--serve", "open the test-case viewer after generating")
3436
+ ).action(
3437
+ async (opts) => {
3438
+ await guard(async () => {
3439
+ const g = toGlobal(opts);
3440
+ const code = await runTestcasesGenerate({
3441
+ ...g,
3442
+ out: opts.out,
3443
+ noWrite: opts.write === false,
3444
+ focus: opts.focus
3445
+ });
3446
+ if (code === 0 && opts.serve) {
3447
+ return runTestcasesServe({
3448
+ cwd: g.cwd,
3449
+ dir: opts.out,
3450
+ port: 4581,
3451
+ host: "127.0.0.1",
3452
+ open: true
3453
+ });
3454
+ }
3455
+ return code;
3456
+ });
3457
+ }
3458
+ );
3459
+ testcases.command("serve").description("Serve the test-case viewer locally").option("-C, --cwd <dir>", "repository directory").option("--dir <dir>", "test-plan output directory (relative to repo)", ".edsger/testcases").option("--port <n>", "port to listen on", "4581").option("--host <host>", "host to bind", "127.0.0.1").option("--no-open", "do not open the browser automatically").action(
3460
+ async (opts) => {
3461
+ await guard(
3462
+ () => runTestcasesServe({
3463
+ cwd: resolve2(opts.cwd ?? process.cwd()),
3464
+ dir: opts.dir,
3465
+ port: Number.parseInt(opts.port, 10) || 4581,
3466
+ host: opts.host,
3467
+ open: opts.open !== false
3468
+ })
3469
+ );
3470
+ }
3471
+ );
3472
+ testcases.command("report").description("Export a self-contained HTML test-case viewer (data inlined) for sharing").option("-C, --cwd <dir>", "repository directory").option("--dir <dir>", "test-plan output directory (relative to repo)", ".edsger/testcases").option("--html <path>", "output HTML file path", "edsger-testcases.html").option("--open", "open the exported file after writing").action(async (opts) => {
3473
+ await guard(
3474
+ () => runTestcasesReport({
3475
+ cwd: resolve2(opts.cwd ?? process.cwd()),
3476
+ dir: opts.dir,
3477
+ html: opts.html,
3478
+ open: Boolean(opts.open)
3479
+ })
3480
+ );
3481
+ });
2093
3482
  addGlobalOptions(
2094
3483
  program.command("pr-review").description("Review a pull request against the review standard (requires gh)").argument("<pr>", "PR number, URL, or branch").option("--repo <owner/repo>", "target repository (defaults to current)").option("--post", "submit the review to GitHub (otherwise preview only)")
2095
3484
  ).action(async (pr, opts) => {
@@ -2113,7 +3502,9 @@ addGlobalOptions(
2113
3502
  );
2114
3503
  }
2115
3504
  );
2116
- var skills = program.command("skills").description("install edsger as Claude Code skills (/benchmark, /pr-review, /pr-resolve)");
3505
+ var skills = program.command("skills").description(
3506
+ "install edsger as Claude Code skills (/benchmark, /audit, /pr-review, /pr-resolve)"
3507
+ );
2117
3508
  skills.command("install").description("install the skills into Claude Code").option("--global", "install for the current user (~/.claude/skills) [default]").option("--project", "install into ./.claude/skills instead").option("-C, --cwd <dir>", "project directory (with --project)").option("--force", "overwrite existing skills").action(async (opts) => {
2118
3509
  await guard(
2119
3510
  () => runSkillsInstall({
@@ -2138,14 +3529,20 @@ program.addHelpText(
2138
3529
  "after",
2139
3530
  `
2140
3531
  Examples:
2141
- ${pc10.dim("$")} edsger benchmark
2142
- ${pc10.dim("$")} edsger benchmark -C ./my-repo --json
2143
- ${pc10.dim("$")} edsger diagram ${pc10.dim("# generate diagrams for the current repo")}
2144
- ${pc10.dim("$")} edsger diagram --serve ${pc10.dim("# \u2026and open them in the browser")}
2145
- ${pc10.dim("$")} edsger diagram report --html diagrams.html
2146
- ${pc10.dim("$")} edsger pr-review 123 --post
2147
- ${pc10.dim("$")} edsger pr-resolve 123 --instruction "fix the failing test" --push
2148
- ${pc10.dim("$")} edsger skills install ${pc10.dim("# enable /benchmark etc. in Claude Code")}
3532
+ ${pc13.dim("$")} edsger benchmark
3533
+ ${pc13.dim("$")} edsger benchmark -C ./my-repo --json
3534
+ ${pc13.dim("$")} edsger audit ${pc13.dim("# nitpick the whole repo, report issues")}
3535
+ ${pc13.dim("$")} edsger audit --fail-on high ${pc13.dim("# CI gate: fail on high/critical findings")}
3536
+ ${pc13.dim("$")} edsger diagram ${pc13.dim("# generate diagrams for the current repo")}
3537
+ ${pc13.dim("$")} edsger diagram --serve ${pc13.dim("# \u2026and open them in the browser")}
3538
+ ${pc13.dim("$")} edsger diagram report --html diagrams.html
3539
+ ${pc13.dim("$")} edsger mindmap ${pc13.dim("# implementation mind map of the repo")}
3540
+ ${pc13.dim("$")} edsger mindmap --serve ${pc13.dim("# \u2026browse it Workflowy-style")}
3541
+ ${pc13.dim("$")} edsger testcases ${pc13.dim("# manual test-case plan for the repo")}
3542
+ ${pc13.dim("$")} edsger testcases --serve ${pc13.dim("# \u2026browse the plan Workflowy-style")}
3543
+ ${pc13.dim("$")} edsger pr-review 123 --post
3544
+ ${pc13.dim("$")} edsger pr-resolve 123 --instruction "fix the failing test" --push
3545
+ ${pc13.dim("$")} edsger skills install ${pc13.dim("# enable /benchmark etc. in Claude Code")}
2149
3546
 
2150
3547
  Auth: the agent uses your Claude Code login (run \`claude\` once to sign in); run \`gh auth login\` for PR commands.
2151
3548
  `