edsger 0.82.0 → 0.84.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 (113) hide show
  1. package/README.md +101 -2
  2. package/assets/README.md +31 -18
  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/base.yaml +351 -0
  15. package/assets/diagrams/csharp.yaml +48 -0
  16. package/assets/diagrams/dart.yaml +41 -0
  17. package/assets/diagrams/go.yaml +44 -0
  18. package/assets/diagrams/java.yaml +43 -0
  19. package/assets/diagrams/python.yaml +41 -0
  20. package/assets/diagrams/typescript.yaml +45 -0
  21. package/assets/diagrams-viewer/assets/abnfDiagram-VRR7QNED-CWLpbgGU.js +1 -0
  22. package/assets/diagrams-viewer/assets/arc-B4VipCr_.js +1 -0
  23. package/assets/diagrams-viewer/assets/architectureDiagram-ZJ3FMSHR-BMDodQXV.js +36 -0
  24. package/assets/diagrams-viewer/assets/blockDiagram-677ZJIJ3-DbHoVVH-.js +132 -0
  25. package/assets/diagrams-viewer/assets/c4Diagram-LMCZKHZV-CyUEj98S.js +10 -0
  26. package/assets/diagrams-viewer/assets/channel-DWh7ErxY.js +1 -0
  27. package/assets/diagrams-viewer/assets/chunk-2Q5K7J3B-CWu0p0os.js +1 -0
  28. package/assets/diagrams-viewer/assets/chunk-32BRIVSS-BhVWSCoK.js +1 -0
  29. package/assets/diagrams-viewer/assets/chunk-5VM5RSS4-B_2wZBAn.js +15 -0
  30. package/assets/diagrams-viewer/assets/chunk-EX3LRPZG-D_MRgtfL.js +231 -0
  31. package/assets/diagrams-viewer/assets/chunk-JWPE2WC7-dsh1qiy0.js +1 -0
  32. package/assets/diagrams-viewer/assets/chunk-MOJQB5TN-BvLm4a6B.js +88 -0
  33. package/assets/diagrams-viewer/assets/chunk-RYQCIY6F-gJyPbIpx.js +1 -0
  34. package/assets/diagrams-viewer/assets/chunk-V7JOEXUC-BMvj0zJM.js +206 -0
  35. package/assets/diagrams-viewer/assets/chunk-VR4S4FIN-Di2iBUXs.js +1 -0
  36. package/assets/diagrams-viewer/assets/chunk-XXDRQBXY-Cfajbd4I.js +1 -0
  37. package/assets/diagrams-viewer/assets/classDiagram-OUVF2IWQ-winWhRce.js +1 -0
  38. package/assets/diagrams-viewer/assets/classDiagram-v2-EOCWNBFH-winWhRce.js +1 -0
  39. package/assets/diagrams-viewer/assets/cose-bilkent-JH36ORCC-ClE2EhTL.js +1 -0
  40. package/assets/diagrams-viewer/assets/cynefin-VYW2F7L2-nm5z0Jwc.js +178 -0
  41. package/assets/diagrams-viewer/assets/cynefinDiagram-TSTJHNR4-CGhnRh5V.js +62 -0
  42. package/assets/diagrams-viewer/assets/cytoscape.esm-CUqq0XTU.js +331 -0
  43. package/assets/diagrams-viewer/assets/dagre-VKFMJZFB-COlahHKk.js +4 -0
  44. package/assets/diagrams-viewer/assets/defaultLocale-DX6XiGOO.js +1 -0
  45. package/assets/diagrams-viewer/assets/diagram-FQU43EPY-DsUclBK0.js +3 -0
  46. package/assets/diagrams-viewer/assets/diagram-G47NLZAW-CnjT9SvX.js +24 -0
  47. package/assets/diagrams-viewer/assets/diagram-NH7WQ7WH-DiGoGOyA.js +24 -0
  48. package/assets/diagrams-viewer/assets/diagram-OA4YK3LP-qi8nz9qe.js +30 -0
  49. package/assets/diagrams-viewer/assets/diagram-WEI45ONY-BV3PGzwL.js +41 -0
  50. package/assets/diagrams-viewer/assets/diagrams-8nCc1utO.css +1 -0
  51. package/assets/diagrams-viewer/assets/diagrams-CF48btj0.js +314 -0
  52. package/assets/diagrams-viewer/assets/ebnfDiagram-CCIWWBDH-CtKWe-dh.js +1 -0
  53. package/assets/diagrams-viewer/assets/erDiagram-Q63AITRT-Aajee3Fz.js +85 -0
  54. package/assets/diagrams-viewer/assets/flowDiagram-23GEKE2U-N3Xe2K9t.js +156 -0
  55. package/assets/diagrams-viewer/assets/ganttDiagram-NO4QXBWP-Bjicdg8S.js +292 -0
  56. package/assets/diagrams-viewer/assets/gitGraphDiagram-IHSO6WYX-CacIXKIe.js +106 -0
  57. package/assets/diagrams-viewer/assets/graph-C2PyVmSI.js +1 -0
  58. package/assets/diagrams-viewer/assets/infoDiagram-FWYZ7A6U-CVmDapTx.js +2 -0
  59. package/assets/diagrams-viewer/assets/init-Gi6I4Gst.js +1 -0
  60. package/assets/diagrams-viewer/assets/ishikawaDiagram-FXEZZL3T-DMJf1YQN.js +70 -0
  61. package/assets/diagrams-viewer/assets/journeyDiagram-5HDEW3XC-Bk_U6MPk.js +139 -0
  62. package/assets/diagrams-viewer/assets/kanban-definition-HUTT4EX6-n05wGXiD.js +89 -0
  63. package/assets/diagrams-viewer/assets/katex-C5jXJg4s.js +257 -0
  64. package/assets/diagrams-viewer/assets/layout-HEHzrzvT.js +1 -0
  65. package/assets/diagrams-viewer/assets/linear-DD_zky65.js +1 -0
  66. package/assets/diagrams-viewer/assets/map-BQHxRNoK.js +1 -0
  67. package/assets/diagrams-viewer/assets/mindmap-definition-LN4V7U3C-BWt_I9vr.js +96 -0
  68. package/assets/diagrams-viewer/assets/ordinal-Cboi1Yqb.js +1 -0
  69. package/assets/diagrams-viewer/assets/pegDiagram-2B236MQR-CCZTIXBZ.js +1 -0
  70. package/assets/diagrams-viewer/assets/pieDiagram-ENE6RG2P-BmIQql1w.js +39 -0
  71. package/assets/diagrams-viewer/assets/quadrantDiagram-ABIIQ3AL-BPvOH5QN.js +7 -0
  72. package/assets/diagrams-viewer/assets/railroadDiagram-RFXS5EU6-DpgVbjSl.js +1 -0
  73. package/assets/diagrams-viewer/assets/requirementDiagram-TGXJPOKE-CDVpI3MH.js +84 -0
  74. package/assets/diagrams-viewer/assets/sankeyDiagram-HTMAVEWB-DU2J23Xy.js +40 -0
  75. package/assets/diagrams-viewer/assets/sequenceDiagram-DBY2YBRQ-BIVlCWU4.js +162 -0
  76. package/assets/diagrams-viewer/assets/sizeCapture-X5ZJPWSS-C9KxyKN2.js +1 -0
  77. package/assets/diagrams-viewer/assets/stateDiagram-2N3HPSRC-Ba5mRI9d.js +1 -0
  78. package/assets/diagrams-viewer/assets/stateDiagram-v2-6OUMAXLB-Dw-0OG6b.js +1 -0
  79. package/assets/diagrams-viewer/assets/swimlanes-5IMT3BWC-aOsD2O80.js +2 -0
  80. package/assets/diagrams-viewer/assets/swimlanesDiagram-G3AALYLV-KujklZml.js +8 -0
  81. package/assets/diagrams-viewer/assets/timeline-definition-FHXFAJF6-CmYd84Hc.js +120 -0
  82. package/assets/diagrams-viewer/assets/vennDiagram-L72KCM5P-CDiMK-z9.js +34 -0
  83. package/assets/diagrams-viewer/assets/wardleyDiagram-EHGQE667-BKLsv25u.js +78 -0
  84. package/assets/diagrams-viewer/assets/xychartDiagram-FW5EYKEG-Bpj6Ub-a.js +7 -0
  85. package/assets/diagrams-viewer/diagrams.html +31 -0
  86. package/assets/diagrams-viewer-single.html +3593 -0
  87. package/assets/manifest.yaml +13 -1
  88. package/assets/mindmap/base.yaml +115 -0
  89. package/assets/mindmap/csharp.yaml +32 -0
  90. package/assets/mindmap/dart.yaml +42 -0
  91. package/assets/mindmap/go.yaml +33 -0
  92. package/assets/mindmap/java.yaml +40 -0
  93. package/assets/mindmap/python.yaml +43 -0
  94. package/assets/mindmap/typescript.yaml +47 -0
  95. package/assets/mindmap-viewer/assets/mindmap-D7KMR9EM.css +1 -0
  96. package/assets/mindmap-viewer/assets/mindmap-Di5RM-4N.js +4 -0
  97. package/assets/mindmap-viewer/mindmap.html +31 -0
  98. package/assets/mindmap-viewer-single.html +34 -0
  99. package/assets/schema/audit.schema.json +66 -0
  100. package/assets/schema/diagrams.schema.json +100 -0
  101. package/assets/schema/mindmap.schema.json +69 -0
  102. package/assets/skills/audit/SKILL.md +76 -0
  103. package/assets/skills/diagram/SKILL.md +70 -0
  104. package/assets/skills/mindmap/SKILL.md +69 -0
  105. package/assets/viewer/assets/index-BFtGar9N.css +1 -0
  106. package/assets/viewer/assets/index-XOKyyudT.js +3 -0
  107. package/assets/viewer/index.html +19 -2
  108. package/assets/viewer-single.html +21 -4
  109. package/dist/index.js +1720 -318
  110. package/dist/index.js.map +1 -1
  111. package/package.json +2 -2
  112. package/assets/viewer/assets/index-C7hwGkuU.js +0 -3
  113. 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 pc9 from "picocolors";
7
+ import pc12 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,8 @@ 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 overrideSpec = (kind === "benchmark" ? config.benchmark?.standards : config.review?.standards) ?? config.standards;
130
+ const perKind = kind === "benchmark" ? config.benchmark?.standards : kind === "diagrams" ? config.diagrams?.standards : kind === "mindmap" ? config.mindmap?.standards : kind === "audit" ? config.audit?.standards : config.review?.standards;
131
+ const overrideSpec = perKind ?? config.standards;
131
132
  if (overrideSpec && !opts.assetsPath) {
132
133
  roots.push({ path: resolveRootPath(overrideSpec, configDir), label: `config:${overrideSpec}` });
133
134
  }
@@ -175,11 +176,68 @@ function mergeLayers(kind, layers) {
175
176
  {}
176
177
  );
177
178
  }
179
+ if (kind === "diagrams") {
180
+ return layers.reduce(
181
+ (acc, layer) => mergeDiagrams(acc, layer),
182
+ {}
183
+ );
184
+ }
185
+ if (kind === "mindmap") {
186
+ return layers.reduce(
187
+ (acc, layer) => mergeMindmap(acc, layer),
188
+ {}
189
+ );
190
+ }
191
+ if (kind === "audit") {
192
+ return layers.reduce(
193
+ (acc, layer) => mergeAudit(acc, layer),
194
+ {}
195
+ );
196
+ }
178
197
  return layers.reduce(
179
198
  (acc, layer) => mergeReview(acc, layer),
180
199
  {}
181
200
  );
182
201
  }
202
+ function mergeDiagrams(base, overlay) {
203
+ const groups = mergeById(base.groups ?? [], overlay.groups ?? [], (b, o) => ({
204
+ ...b,
205
+ ...o,
206
+ // Diagrams with the same id are overridden; new ids are appended.
207
+ diagrams: mergeById(b.diagrams ?? [], o.diagrams ?? [], (bd, od) => ({
208
+ ...bd,
209
+ ...od
210
+ }))
211
+ }));
212
+ return {
213
+ ...base,
214
+ ...overlay,
215
+ // Preserve the base layer's identity rather than the last overlay's.
216
+ id: base.id ?? overlay.id,
217
+ name: base.name ?? overlay.name,
218
+ version: base.version ?? overlay.version,
219
+ kind: "diagrams",
220
+ groups
221
+ };
222
+ }
223
+ function mergeMindmap(base, overlay) {
224
+ const aspects = mergeById(base.aspects ?? [], overlay.aspects ?? [], (b, o) => ({
225
+ ...b,
226
+ ...o
227
+ }));
228
+ return {
229
+ ...base,
230
+ ...overlay,
231
+ // Preserve the base layer's identity rather than the last overlay's.
232
+ id: base.id ?? overlay.id,
233
+ name: base.name ?? overlay.name,
234
+ version: base.version ?? overlay.version,
235
+ kind: "mindmap",
236
+ guidance: overlay.guidance ?? base.guidance,
237
+ maxDepth: overlay.maxDepth ?? base.maxDepth,
238
+ aspects
239
+ };
240
+ }
183
241
  function mergeBenchmark(base, overlay) {
184
242
  const categories = mergeById(
185
243
  base.categories ?? [],
@@ -205,6 +263,25 @@ function mergeBenchmark(base, overlay) {
205
263
  categories
206
264
  };
207
265
  }
266
+ function mergeAudit(base, overlay) {
267
+ const dimensions = mergeById(
268
+ base.dimensions ?? [],
269
+ overlay.dimensions ?? [],
270
+ (b, o) => ({ ...b, ...o })
271
+ );
272
+ return {
273
+ ...base,
274
+ ...overlay,
275
+ // Preserve the base layer's identity rather than the last overlay's.
276
+ id: base.id ?? overlay.id,
277
+ name: base.name ?? overlay.name,
278
+ version: base.version ?? overlay.version,
279
+ kind: "audit",
280
+ guidance: overlay.guidance ?? base.guidance,
281
+ policy: { ...base.policy ?? {}, ...overlay.policy ?? {} },
282
+ dimensions
283
+ };
284
+ }
208
285
  function mergeReview(base, overlay) {
209
286
  const focusAreas = mergeById(
210
287
  base.focusAreas ?? [],
@@ -490,6 +567,121 @@ function summarizeTool(name = "tool", input = {}) {
490
567
  }
491
568
  }
492
569
 
570
+ // src/core/dashboard.ts
571
+ import { existsSync as existsSync3, readFileSync as readFileSync2, readdirSync as readdirSync2 } from "fs";
572
+ import { join as join3 } from "path";
573
+ function assembleFrom(dir, latestFile) {
574
+ const latest = readJson(join3(dir, latestFile));
575
+ const history = readJsonl(join3(dir, "history.jsonl"));
576
+ const runs = {};
577
+ const runsDir = join3(dir, "runs");
578
+ if (existsSync3(runsDir)) {
579
+ for (const entry of readdirSync2(runsDir)) {
580
+ const report = readJson(join3(runsDir, entry, latestFile));
581
+ if (report) runs[entry] = report;
582
+ }
583
+ }
584
+ const repo = latest && typeof latest === "object" && "repo" in latest ? latest.repo : void 0;
585
+ return { repo, latest, history, runs };
586
+ }
587
+ function assembleData(benchmarkDir) {
588
+ return assembleFrom(benchmarkDir, "report.json");
589
+ }
590
+ function assembleDiagramData(diagramsDir) {
591
+ return assembleFrom(diagramsDir, "diagrams.json");
592
+ }
593
+ function assembleMindmapData(mindmapDir) {
594
+ return assembleFrom(mindmapDir, "mindmap.json");
595
+ }
596
+ function assembleAuditData(auditDir) {
597
+ return assembleFrom(auditDir, "report.json");
598
+ }
599
+ function hasData(data) {
600
+ return Boolean(data.latest) || data.history.length > 0;
601
+ }
602
+ function findViewerDir(subdir = "viewer", index = "index.html") {
603
+ const dir = join3(findBundledAssetsDir(), subdir);
604
+ if (!existsSync3(join3(dir, index))) {
605
+ throw new Error(
606
+ `Dashboard viewer assets not found (${subdir}/${index}). Reinstall edsger, or run \`npm run sync-viewer\` in a dev checkout.`
607
+ );
608
+ }
609
+ return dir;
610
+ }
611
+ function findDiagramViewerDir() {
612
+ return findViewerDir("diagrams-viewer", "diagrams.html");
613
+ }
614
+ function findMindmapViewerDir() {
615
+ return findViewerDir("mindmap-viewer", "mindmap.html");
616
+ }
617
+ function findAuditViewerDir() {
618
+ return findViewerDir("audit-viewer", "audit.html");
619
+ }
620
+ function readSingleTemplate(file) {
621
+ const path = join3(findBundledAssetsDir(), file);
622
+ if (!existsSync3(path)) {
623
+ throw new Error(
624
+ `Single-file viewer template not found (${file}). Reinstall edsger, or run \`npm run sync-viewer\`.`
625
+ );
626
+ }
627
+ return readFileSync2(path, "utf8");
628
+ }
629
+ function readViewerSingleTemplate() {
630
+ return readSingleTemplate("viewer-single.html");
631
+ }
632
+ function readDiagramViewerSingleTemplate() {
633
+ return readSingleTemplate("diagrams-viewer-single.html");
634
+ }
635
+ function readMindmapViewerSingleTemplate() {
636
+ return readSingleTemplate("mindmap-viewer-single.html");
637
+ }
638
+ function readAuditViewerSingleTemplate() {
639
+ return readSingleTemplate("audit-viewer-single.html");
640
+ }
641
+ function renderSingleFileWith(template, data, globalName) {
642
+ const json = JSON.stringify(data).replace(/<\//g, "<\\/");
643
+ const tag = `<script>window.${globalName}=${json}</script>`;
644
+ const placeholder = new RegExp(`<!--[^>]*${globalName}[^>]*-->`);
645
+ if (placeholder.test(template)) return template.replace(placeholder, () => tag);
646
+ const scriptIdx = template.indexOf("<script");
647
+ if (scriptIdx !== -1) return template.slice(0, scriptIdx) + tag + template.slice(scriptIdx);
648
+ const headIdx = template.lastIndexOf("</head>");
649
+ if (headIdx !== -1) return template.slice(0, headIdx) + tag + template.slice(headIdx);
650
+ return tag + template;
651
+ }
652
+ function renderSingleFile(template, data) {
653
+ return renderSingleFileWith(template, data, "__EDSGER_DATA__");
654
+ }
655
+ function renderDiagramsSingleFile(template, data) {
656
+ return renderSingleFileWith(template, data, "__EDSGER_DIAGRAMS__");
657
+ }
658
+ function renderMindmapSingleFile(template, data) {
659
+ return renderSingleFileWith(template, data, "__EDSGER_MINDMAP__");
660
+ }
661
+ function renderAuditSingleFile(template, data) {
662
+ return renderSingleFileWith(template, data, "__EDSGER_AUDIT__");
663
+ }
664
+ function readJson(path) {
665
+ try {
666
+ return JSON.parse(readFileSync2(path, "utf8"));
667
+ } catch {
668
+ return null;
669
+ }
670
+ }
671
+ function readJsonl(path) {
672
+ if (!existsSync3(path)) return [];
673
+ const out = [];
674
+ for (const line of readFileSync2(path, "utf8").split("\n")) {
675
+ const t = line.trim();
676
+ if (!t) continue;
677
+ try {
678
+ out.push(JSON.parse(t));
679
+ } catch {
680
+ }
681
+ }
682
+ return out;
683
+ }
684
+
493
685
  // src/core/git.ts
494
686
  import { execFileSync as execFileSync2 } from "child_process";
495
687
  function tryGit(args, cwd) {
@@ -514,151 +706,220 @@ function gitInfo(cwd) {
514
706
  };
515
707
  }
516
708
 
517
- // src/core/report.ts
518
- function scoreBenchmark(standard, result) {
519
- const scale = standard.scoreScale || 4;
520
- const totalWeight = standard.categories.reduce((s, c) => s + c.weight, 0) || 1;
521
- const tail = (id) => id.slice(id.lastIndexOf("/") + 1);
522
- const byKey = /* @__PURE__ */ new Map();
523
- const byCrit = /* @__PURE__ */ new Map();
524
- for (const cat of result.categories) {
525
- for (const cr of cat.criteria) {
526
- const meta = {
527
- score: cr.score,
528
- justification: cr.justification,
529
- evidence: cr.evidence ?? []
530
- };
531
- byKey.set(`${tail(cat.id)}/${tail(cr.id)}`, meta);
532
- byCrit.set(tail(cr.id), meta);
533
- }
709
+ // src/core/open.ts
710
+ import { spawn } from "child_process";
711
+ function openInBrowser(target) {
712
+ const platform = process.platform;
713
+ const cmd = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
714
+ const args = platform === "win32" ? ["/c", "start", "", target] : [target];
715
+ try {
716
+ spawn(cmd, args, { stdio: "ignore", detached: true }).unref();
717
+ } catch {
534
718
  }
535
- let matched = 0;
536
- const categories = standard.categories.map((cat) => {
537
- const critWeightTotal = cat.criteria.reduce((s, c) => s + (c.weight ?? 1), 0) || 1;
538
- const scored = cat.criteria.map((cr) => {
539
- const meta = byKey.get(`${tail(cat.id)}/${tail(cr.id)}`) ?? byCrit.get(tail(cr.id));
540
- if (meta) matched += 1;
541
- const raw = clamp(meta?.score ?? 0, 0, scale);
542
- return {
543
- id: cr.id,
544
- title: cr.title,
545
- weight: cr.weight ?? 1,
546
- rawScore: raw,
547
- maxScore: scale,
548
- fraction: raw / scale,
549
- justification: meta?.justification ?? "Not assessed.",
550
- evidence: meta?.evidence ?? []
551
- };
719
+ }
720
+
721
+ // src/core/viewer-server.ts
722
+ import { createReadStream, existsSync as existsSync4, statSync as statSync2 } from "fs";
723
+ import { createServer } from "http";
724
+ import { extname, join as join4, normalize } from "path";
725
+ import pc3 from "picocolors";
726
+ var MIME = {
727
+ ".html": "text/html; charset=utf-8",
728
+ ".js": "text/javascript; charset=utf-8",
729
+ ".css": "text/css; charset=utf-8",
730
+ ".json": "application/json; charset=utf-8",
731
+ ".svg": "image/svg+xml",
732
+ ".map": "application/json",
733
+ ".ico": "image/x-icon",
734
+ ".woff2": "font/woff2",
735
+ ".woff": "font/woff"
736
+ };
737
+ async function startViewerServer(opts) {
738
+ const server = createServer((req, res) => {
739
+ const url = (req.url ?? "/").split("?")[0] ?? "/";
740
+ if (url === opts.dataRoute) {
741
+ res.writeHead(200, {
742
+ "content-type": "application/json; charset=utf-8",
743
+ "cache-control": "no-store"
744
+ });
745
+ res.end(JSON.stringify(opts.getData()));
746
+ return;
747
+ }
748
+ const rel = url === "/" ? opts.indexFile : decodeURIComponent(url.replace(/^\/+/, ""));
749
+ const safe = normalize(rel).replace(/^(\.\.(?:[/\\]|$))+/, "");
750
+ let file = join4(opts.viewerDir, safe);
751
+ if (!file.startsWith(opts.viewerDir)) {
752
+ res.writeHead(403);
753
+ res.end("forbidden");
754
+ return;
755
+ }
756
+ if (!existsSync4(file) || statSync2(file).isDirectory())
757
+ file = join4(opts.viewerDir, opts.indexFile);
758
+ res.writeHead(200, { "content-type": MIME[extname(file)] ?? "application/octet-stream" });
759
+ createReadStream(file).pipe(res);
760
+ });
761
+ return new Promise((resolve3, reject) => {
762
+ server.on("error", reject);
763
+ server.listen(opts.port, opts.host, () => {
764
+ const addr = server.address();
765
+ const port = typeof addr === "object" && addr ? addr.port : opts.port;
766
+ const shownHost = opts.host === "0.0.0.0" ? "localhost" : opts.host;
767
+ const url = `http://${shownHost}:${port}/`;
768
+ if (opts.emptyHint) log.warn(opts.emptyHint);
769
+ log.success(`Dashboard at ${pc3.bold(pc3.cyan(url))} ${pc3.dim("(Ctrl-C to stop)")}`);
770
+ if (opts.open) openInBrowser(url);
552
771
  });
553
- const fraction = scored.reduce((s, c) => s + c.fraction * c.weight, 0) / critWeightTotal;
554
- const normWeight = cat.weight / totalWeight * 100;
555
- return {
556
- id: cat.id,
557
- name: cat.name,
558
- weight: round(normWeight, 1),
559
- fraction,
560
- points: round(normWeight * fraction, 1),
561
- criteria: scored
772
+ const shutdown = () => {
773
+ server.close(() => resolve3(0));
562
774
  };
775
+ process.on("SIGINT", shutdown);
776
+ process.on("SIGTERM", shutdown);
563
777
  });
564
- const totalCriteria = standard.categories.reduce((s, c) => s + c.criteria.length, 0);
565
- if (result.categories.length > 0 && matched === 0) {
566
- log.warn(
567
- "The agent returned scores but none matched the rubric's criterion ids \u2014 reporting 0. This is a scoring bug, not a real result."
568
- );
569
- } else if (matched < totalCriteria) {
570
- log.warn(`Only ${matched}/${totalCriteria} criteria were scored; the rest default to 0.`);
778
+ }
779
+
780
+ // src/core/audit-report.ts
781
+ var SEVERITY_ORDER = ["critical", "high", "medium", "low", "info"];
782
+ var DEFAULT_MAX_FINDINGS = 60;
783
+ var SEVERITY_RANK = {
784
+ critical: 4,
785
+ high: 3,
786
+ medium: 2,
787
+ low: 1,
788
+ info: 0
789
+ };
790
+ function organizeAudit(standard, result) {
791
+ const merged = mergeDuplicates(result.findings ?? []);
792
+ merged.sort((a, b) => SEVERITY_RANK[b.severity] - SEVERITY_RANK[a.severity]);
793
+ const cap = Math.max(1, standard.policy?.maxFindings ?? DEFAULT_MAX_FINDINGS);
794
+ const omitted = Math.max(0, merged.length - cap);
795
+ const findings = merged.slice(0, cap);
796
+ const known = new Map(standard.dimensions.map((d) => [d.id, d.title]));
797
+ const groups = standard.dimensions.map((d) => ({
798
+ id: d.id,
799
+ title: d.title,
800
+ findings: []
801
+ }));
802
+ const other = { id: "other", title: "Other", findings: [] };
803
+ const byId = new Map(groups.map((g) => [g.id, g]));
804
+ for (const f of findings) {
805
+ (known.has(f.dimension) ? byId.get(f.dimension) : other).findings.push(f);
571
806
  }
572
- const overall = round(
573
- categories.reduce((s, c) => s + c.points, 0),
574
- 1
575
- );
807
+ if (other.findings.length) groups.push(other);
808
+ const counts = { critical: 0, high: 0, medium: 0, low: 0, info: 0 };
809
+ for (const f of findings) counts[f.severity]++;
810
+ const quickWins = findings.filter((f) => f.effort === "quick" && f.severity !== "info");
576
811
  return {
577
812
  standardName: standard.name,
578
813
  standardVersion: standard.version,
579
- overall,
580
- grade: gradeFor(overall),
581
- categories,
582
814
  summary: result.summary,
583
- strengths: result.strengths ?? [],
584
- weaknesses: result.weaknesses ?? [],
585
- recommendations: result.recommendations ?? []
815
+ positives: result.positives ?? [],
816
+ findings,
817
+ groups: groups.filter((g) => g.findings.length > 0),
818
+ counts,
819
+ total: findings.length,
820
+ omitted,
821
+ quickWins
586
822
  };
587
823
  }
588
- function gradeFor(score) {
589
- if (score >= 90) return "A";
590
- if (score >= 80) return "B";
591
- if (score >= 70) return "C";
592
- if (score >= 60) return "D";
593
- return "F";
824
+ function mergeDuplicates(findings) {
825
+ const byKey = /* @__PURE__ */ new Map();
826
+ const out = [];
827
+ for (const f of findings) {
828
+ const key = `${f.dimension}\0${f.title.trim().toLowerCase()}`;
829
+ const seen = byKey.get(key);
830
+ if (!seen) {
831
+ const first = { ...f, moreLocations: [] };
832
+ byKey.set(key, first);
833
+ out.push(first);
834
+ continue;
835
+ }
836
+ const [primary, extra] = SEVERITY_RANK[f.severity] > SEVERITY_RANK[seen.severity] ? [{ ...f, moreLocations: seen.moreLocations }, seen] : [seen, f];
837
+ const loc = locationKey(extra);
838
+ const primaryLoc = locationKey(primary);
839
+ if (loc && loc !== primaryLoc && !primary.moreLocations.includes(loc)) {
840
+ primary.moreLocations.push(loc);
841
+ }
842
+ if (primary !== seen) {
843
+ out[out.indexOf(seen)] = primary;
844
+ byKey.set(key, primary);
845
+ }
846
+ }
847
+ return out;
594
848
  }
595
- function renderBenchmarkMarkdown(scored, meta) {
849
+ function hasFindingsAtOrAbove(organized, threshold) {
850
+ return organized.findings.some((f) => SEVERITY_RANK[f.severity] >= SEVERITY_RANK[threshold]);
851
+ }
852
+ function renderAuditMarkdown(organized, meta) {
596
853
  const lines = [];
597
- lines.push(`# Edsger Benchmark Report`);
598
- lines.push("");
599
- lines.push(`**Score: ${scored.overall} / 100 (Grade ${scored.grade})**`);
854
+ lines.push(`# Edsger Audit Report`);
600
855
  lines.push("");
601
- lines.push(`> ${scored.summary}`);
856
+ lines.push(`> ${organized.summary}`);
602
857
  lines.push("");
603
858
  lines.push(`| Field | Value |`);
604
859
  lines.push(`| --- | --- |`);
605
860
  lines.push(`| Repository | \`${meta.repo}\` |`);
606
861
  lines.push(`| Languages | ${meta.languages.join(", ") || "n/a"} |`);
607
- lines.push(`| Standard | ${scored.standardName} v${scored.standardVersion} |`);
862
+ lines.push(`| Standard | ${organized.standardName} v${organized.standardVersion} |`);
608
863
  lines.push(`| Sources | ${meta.layers.join(" \u2192 ")} |`);
609
864
  lines.push(`| Model | ${meta.model} |`);
610
865
  lines.push(`| Generated | ${meta.generatedAt} |`);
611
866
  lines.push("");
612
- lines.push(`## Category scores`);
867
+ lines.push(`## Findings by severity`);
613
868
  lines.push("");
614
- lines.push(`| Category | Weight | Score | Weighted |`);
615
- lines.push(`| --- | ---: | ---: | ---: |`);
616
- for (const c of scored.categories) {
617
- lines.push(`| ${c.name} | ${c.weight}% | ${Math.round(c.fraction * 100)}% | ${c.points} |`);
869
+ lines.push(`| Severity | Count |`);
870
+ lines.push(`| --- | ---: |`);
871
+ for (const s of SEVERITY_ORDER) {
872
+ if (organized.counts[s]) lines.push(`| ${s} | ${organized.counts[s]} |`);
618
873
  }
619
- lines.push(`| **Overall** | **100%** | | **${scored.overall}** |`);
874
+ lines.push(`| **total** | **${organized.total}** |`);
620
875
  lines.push("");
621
- if (scored.strengths.length) {
622
- lines.push(`## Strengths`);
623
- lines.push("");
624
- for (const s of scored.strengths) lines.push(`- ${s}`);
876
+ if (organized.omitted > 0) {
877
+ lines.push(
878
+ `_${organized.omitted} lower-severity finding(s) omitted by the standard's maxFindings policy._`
879
+ );
625
880
  lines.push("");
626
881
  }
627
- if (scored.weaknesses.length) {
628
- lines.push(`## Weaknesses`);
882
+ if (organized.quickWins.length) {
883
+ lines.push(`## Quick wins`);
629
884
  lines.push("");
630
- for (const w of scored.weaknesses) lines.push(`- ${w}`);
885
+ for (const f of organized.quickWins) {
886
+ lines.push(`- **[${f.severity}] ${f.title}** \u2014 ${location(f)}`);
887
+ }
631
888
  lines.push("");
632
889
  }
633
- if (scored.recommendations.length) {
634
- lines.push(`## Recommendations`);
890
+ lines.push(`## Findings by dimension`);
891
+ lines.push("");
892
+ for (const g of organized.groups) {
893
+ lines.push(`### ${g.title} (${g.findings.length})`);
635
894
  lines.push("");
636
- for (const r of scored.recommendations) {
637
- lines.push(`- **[${r.priority}] ${r.title}** \u2014 ${r.detail}`);
895
+ for (const f of g.findings) {
896
+ lines.push(`- **[${f.severity}] ${f.title}** \u2014 ${location(f)} _(effort: ${f.effort})_`);
897
+ lines.push(` - ${f.detail}`);
898
+ if (f.moreLocations.length) {
899
+ lines.push(` - _Also in:_ ${f.moreLocations.map((l) => `\`${l}\``).join(", ")}`);
900
+ }
901
+ if (f.suggestion) lines.push(` - _Fix:_ ${f.suggestion}`);
638
902
  }
639
903
  lines.push("");
640
904
  }
641
- lines.push(`## Detailed findings`);
642
- lines.push("");
643
- for (const c of scored.categories) {
644
- lines.push(`### ${c.name} \u2014 ${Math.round(c.fraction * 100)}%`);
905
+ if (organized.positives.length) {
906
+ lines.push(`## What the repo does well`);
645
907
  lines.push("");
646
- for (const cr of c.criteria) {
647
- lines.push(`- **${cr.title}** (${cr.rawScore}/${cr.maxScore}) \u2014 ${cr.justification}`);
648
- for (const e of cr.evidence) lines.push(` - \`${e}\``);
649
- }
908
+ for (const p of organized.positives) lines.push(`- ${p}`);
650
909
  lines.push("");
651
910
  }
652
911
  lines.push(`---`);
653
912
  lines.push(`<sub>Generated by [edsger](https://www.npmjs.com/package/edsger).</sub>`);
654
913
  return lines.join("\n");
655
914
  }
656
- function clamp(n, lo, hi) {
657
- return Math.max(lo, Math.min(hi, n));
915
+ function location(f) {
916
+ const primary = locationKey(f);
917
+ const more = f.moreLocations.length ? ` (+${f.moreLocations.length} more)` : "";
918
+ return primary ? `\`${primary}\`${more}` : `repo-wide${more}`;
658
919
  }
659
- function round(n, dp) {
660
- const f = 10 ** dp;
661
- return Math.round(n * f) / f;
920
+ function locationKey(f) {
921
+ if (!f.file || f.file === "-") return "";
922
+ return f.line ? `${f.file}:${f.line}` : f.file;
662
923
  }
663
924
 
664
925
  // src/core/schema.ts
@@ -701,24 +962,439 @@ var ReviewResultSchema = z.object({
701
962
  verdict: z.enum(["approve", "request_changes", "comment"]),
702
963
  findings: z.array(ReviewFindingSchema).default([])
703
964
  });
965
+ var AuditFindingSchema = z.object({
966
+ dimension: z.string().describe("Dimension id from the audit standard this finding relates to"),
967
+ severity: SeveritySchema,
968
+ file: z.string().describe("Repo-relative file path, or '-' if repo-wide"),
969
+ line: z.number().nullable().default(null).describe("1-based line number, if applicable"),
970
+ title: z.string().describe("Short, specific statement of the problem"),
971
+ detail: z.string().describe("What is wrong and why it is unprofessional, outdated, or risky"),
972
+ suggestion: z.string().nullable().default(null).describe("Concrete fix or modern replacement"),
973
+ effort: z.enum(["quick", "moderate", "involved"]).default("moderate").describe("Rough effort to fix: quick (<1h), moderate (a day), involved (multi-day)")
974
+ });
975
+ var AuditResultSchema = z.object({
976
+ summary: z.string().describe("3-5 sentence honest overall assessment of the repository"),
977
+ findings: z.array(AuditFindingSchema).default([]),
978
+ positives: z.array(z.string()).default([]).describe("Things the repository genuinely does well (keeps the critique fair)")
979
+ });
704
980
  var ResolveResultSchema = z.object({
705
981
  summary: z.string().describe("What was changed and why"),
706
982
  changes: z.array(z.object({ file: z.string(), description: z.string() })).default([]),
707
983
  testsRun: z.string().nullable().default(null).describe("Command used to verify, and its outcome"),
708
984
  remaining: z.array(z.string()).default([]).describe("Anything still unresolved")
709
985
  });
986
+ var GeneratedDiagramSchema = z.object({
987
+ id: z.string().describe("Diagram id from the catalog, or a descriptive slug if bespoke"),
988
+ groupId: z.string().describe("Catalog group id this diagram belongs to (e.g. architecture)"),
989
+ title: z.string().describe("Human-readable diagram title"),
990
+ type: z.string().describe(
991
+ "Mermaid diagram type used, e.g. flowchart, erDiagram, sequenceDiagram, classDiagram"
992
+ ),
993
+ mermaid: z.string().describe("Valid Mermaid source for the diagram (renders in HTML/Markdown)"),
994
+ description: z.string().describe("One or two sentences: what the diagram shows and why it applies to this repo"),
995
+ evidence: z.array(z.string()).default([]).describe("Repo files this diagram was derived from")
996
+ });
997
+ var DiagramsResultSchema = z.object({
998
+ profile: z.object({
999
+ summary: z.string().describe("2-4 sentence characterisation of the project"),
1000
+ kinds: z.array(z.string()).default([]).describe("Project kind tags, e.g. cli, rest-api, spa, library, data-pipeline, monorepo"),
1001
+ characteristics: z.array(z.string()).default([]).describe(
1002
+ "Notable traits that drove diagram selection (owns a Postgres DB, deploys to k8s, \u2026)"
1003
+ )
1004
+ }).describe("The project profile that justifies which diagrams were chosen"),
1005
+ diagrams: z.array(GeneratedDiagramSchema).describe("The diagrams selected for and generated from this repository"),
1006
+ notes: z.string().nullable().default(null).describe("Optional notes, e.g. diagrams intentionally skipped and why")
1007
+ });
1008
+ var MindmapNodeSchema = z.object({
1009
+ id: z.string().describe("Unique kebab-case node id, e.g. 'core-flows' or 'http-api'"),
1010
+ parentId: z.string().nullable().default(null).describe("id of the parent node; null ONLY for the single root node"),
1011
+ title: z.string().describe("Short outline-bullet title, 2-6 words"),
1012
+ summary: z.string().describe("One or two sentences that are true and specific to this codebase"),
1013
+ kind: z.string().default("topic").describe("Tag: area, module, component, flow, step, data, external, config, or topic"),
1014
+ files: z.array(z.string()).default([]).describe("Repo-relative files this node is grounded in (expected for leaf nodes)")
1015
+ });
1016
+ var MindmapResultSchema = z.object({
1017
+ profile: z.object({
1018
+ summary: z.string().describe("2-4 sentence characterisation of the project"),
1019
+ kinds: z.array(z.string()).default([]).describe("Project kind tags, e.g. cli, rest-api, spa, library, data-pipeline, monorepo"),
1020
+ characteristics: z.array(z.string()).default([]).describe("Notable traits that shaped the map (owns a Postgres DB, monorepo, \u2026)")
1021
+ }).describe("The project profile behind the map's shape"),
1022
+ nodes: z.array(MindmapNodeSchema).describe(
1023
+ "The whole mind map as a flat node list: exactly one root (parentId=null), children listed in reading order after their parent"
1024
+ ),
1025
+ notes: z.string().nullable().default(null).describe("Optional notes, e.g. areas intentionally left out and why")
1026
+ });
1027
+
1028
+ // src/commands/audit.ts
1029
+ async function runAudit(opts, deps = { agent: runAgent }) {
1030
+ const { config, path: configPath } = loadConfig(opts.cwd);
1031
+ const configDir = configPath ? join5(configPath, "..") : opts.cwd;
1032
+ const resolved = resolveStandard("audit", opts, config, configDir);
1033
+ const standard = narrowDimensions(resolved.standard, opts.only);
1034
+ log.info(
1035
+ `Auditing ${pc4.bold(basename(opts.cwd))} against ${pc4.bold(standard.name)} (languages: ${resolved.languages.join(", ") || "generic"})`
1036
+ );
1037
+ log.debug(`standard layers: ${resolved.layers.join(" \u2192 ")}`);
1038
+ const { result, costUsd, numTurns } = await deps.agent({
1039
+ prompt: buildPrompt(standard, opts.cwd, opts.focus),
1040
+ cwd: opts.cwd,
1041
+ model: opts.model,
1042
+ maxTurns: opts.maxTurns,
1043
+ timeoutMs: opts.timeoutMs,
1044
+ allowWrite: false,
1045
+ verbose: opts.verbose,
1046
+ resultShape: AuditResultSchema.shape,
1047
+ 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."
1048
+ });
1049
+ if (!result) {
1050
+ log.error(
1051
+ "The agent did not produce a result. Try increasing --max-turns or check credentials."
1052
+ );
1053
+ return 1;
1054
+ }
1055
+ const organized = organizeAudit(standard, result);
1056
+ const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
1057
+ const runId = generatedAt.replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z");
1058
+ const git = gitInfo(opts.cwd);
1059
+ const md = renderAuditMarkdown(organized, {
1060
+ repo: basename(opts.cwd),
1061
+ languages: resolved.languages,
1062
+ layers: resolved.layers,
1063
+ generatedAt,
1064
+ model: opts.model
1065
+ });
1066
+ const jsonReport = {
1067
+ schema: "edsger.audit/1",
1068
+ runId,
1069
+ generatedAt,
1070
+ model: opts.model,
1071
+ repo: basename(opts.cwd),
1072
+ git,
1073
+ languages: resolved.languages,
1074
+ standard: { name: standard.name, version: standard.version, layers: resolved.layers },
1075
+ // Dimension titles in standard order, so viewers can label groups properly.
1076
+ dimensions: standard.dimensions.map((d) => ({ id: d.id, title: d.title })),
1077
+ summary: organized.summary,
1078
+ counts: organized.counts,
1079
+ total: organized.total,
1080
+ omitted: organized.omitted,
1081
+ findings: organized.findings,
1082
+ positives: organized.positives,
1083
+ costUsd
1084
+ };
1085
+ const historyRecord = {
1086
+ runId,
1087
+ generatedAt,
1088
+ commit: git.commit,
1089
+ branch: git.branch,
1090
+ dirty: git.dirty,
1091
+ model: opts.model,
1092
+ standardVersion: standard.version,
1093
+ total: organized.total,
1094
+ counts: organized.counts,
1095
+ costUsd
1096
+ };
1097
+ if (!opts.noWrite && !opts.dryRun) {
1098
+ const baseDir = join5(opts.cwd, opts.out);
1099
+ const mdOut = md + "\n";
1100
+ const jsonOut = JSON.stringify(jsonReport, null, 2) + "\n";
1101
+ const runDir = join5(baseDir, "runs", runId);
1102
+ mkdirSync(runDir, { recursive: true });
1103
+ writeFileSync(join5(runDir, "report.md"), mdOut, "utf8");
1104
+ writeFileSync(join5(runDir, "report.json"), jsonOut, "utf8");
1105
+ writeFileSync(join5(baseDir, "report.md"), mdOut, "utf8");
1106
+ writeFileSync(join5(baseDir, "report.json"), jsonOut, "utf8");
1107
+ appendFileSync(join5(baseDir, "history.jsonl"), JSON.stringify(historyRecord) + "\n", "utf8");
1108
+ log.success(
1109
+ `Report written to ${pc4.bold(join5(opts.out, "report.md"))} ${pc4.dim(`(run archived at ${join5(opts.out, "runs", runId)})`)}`
1110
+ );
1111
+ }
1112
+ printSummary(organized, numTurns, costUsd);
1113
+ if (opts.json) process.stdout.write(JSON.stringify(jsonReport, null, 2) + "\n");
1114
+ if (opts.failOn && hasFindingsAtOrAbove(organized, opts.failOn)) {
1115
+ log.warn(`Findings at or above ${pc4.bold(opts.failOn)} present \u2014 failing (--fail-on).`);
1116
+ return 2;
1117
+ }
1118
+ return 0;
1119
+ }
1120
+ async function runAuditServe(opts) {
1121
+ const auditDir = join5(opts.cwd, opts.dir);
1122
+ const viewerDir = findAuditViewerDir();
1123
+ return startViewerServer({
1124
+ viewerDir,
1125
+ indexFile: "audit.html",
1126
+ dataRoute: "/edsger-audit.json",
1127
+ getData: () => assembleAuditData(auditDir),
1128
+ port: opts.port,
1129
+ host: opts.host,
1130
+ open: opts.open,
1131
+ emptyHint: hasData(assembleAuditData(auditDir)) ? void 0 : `No audit data in ${opts.dir} yet \u2014 run \`edsger audit\` first.`
1132
+ });
1133
+ }
1134
+ async function runAuditReport(opts) {
1135
+ const auditDir = join5(opts.cwd, opts.dir);
1136
+ const data = assembleAuditData(auditDir);
1137
+ if (!hasData(data)) {
1138
+ log.error(`No audit data in ${opts.dir}. Run \`edsger audit\` first.`);
1139
+ return 1;
1140
+ }
1141
+ const html = renderAuditSingleFile(readAuditViewerSingleTemplate(), data);
1142
+ const out = isAbsolute2(opts.html) ? opts.html : join5(opts.cwd, opts.html);
1143
+ mkdirSync(dirname2(out), { recursive: true });
1144
+ writeFileSync(out, html, "utf8");
1145
+ log.success(
1146
+ `Wrote self-contained audit viewer to ${pc4.bold(opts.html)} ${pc4.dim(`(${Math.round(html.length / 1024)} kB, open in any browser)`)}`
1147
+ );
1148
+ if (opts.open) openInBrowser(out);
1149
+ return 0;
1150
+ }
1151
+ function narrowDimensions(standard, only) {
1152
+ if (!only || only.length === 0) return standard;
1153
+ const valid = new Set(standard.dimensions.map((d) => d.id));
1154
+ const unknown = only.filter((id) => !valid.has(id));
1155
+ if (unknown.length) {
1156
+ throw new Error(
1157
+ `Unknown dimension id(s): ${unknown.join(", ")}. Available: ${[...valid].join(", ")}`
1158
+ );
1159
+ }
1160
+ const keep = new Set(only);
1161
+ return { ...standard, dimensions: standard.dimensions.filter((d) => keep.has(d.id)) };
1162
+ }
1163
+ function printSummary(organized, numTurns, costUsd) {
1164
+ log.heading(`Findings: ${organized.total}`);
1165
+ for (const s of SEVERITY_ORDER) {
1166
+ if (organized.counts[s]) {
1167
+ process.stderr.write(` ${sevColor(s)(s.padEnd(8))} ${organized.counts[s]}
1168
+ `);
1169
+ }
1170
+ }
1171
+ if (organized.omitted > 0) {
1172
+ log.warn(`${organized.omitted} lower-severity finding(s) omitted (maxFindings policy)`);
1173
+ }
1174
+ if (organized.quickWins.length) {
1175
+ log.step(`${organized.quickWins.length} quick win(s) \u2014 see the report's "Quick wins" section`);
1176
+ }
1177
+ log.step(`${numTurns} turns \xB7 ~$${costUsd.toFixed(3)}`);
1178
+ }
1179
+ function sevColor(s) {
1180
+ if (s === "critical" || s === "high") return pc4.red;
1181
+ if (s === "medium") return pc4.yellow;
1182
+ return pc4.dim;
1183
+ }
1184
+ function buildPrompt(standard, cwd, focus) {
1185
+ const maxFindings = Math.max(1, standard.policy?.maxFindings ?? DEFAULT_MAX_FINDINGS);
1186
+ const dims = standard.dimensions.map((d) => {
1187
+ const checklist = (d.checklist ?? []).map((c) => ` - ${c}`).join("\n");
1188
+ return `- id: ${d.id} (${d.title}, default severity ${d.defaultSeverity ?? "medium"})
1189
+ guidance: ${d.guidance.trim()}` + (checklist ? `
1190
+ checklist:
1191
+ ${checklist}` : "");
1192
+ }).join("\n");
1193
+ return [
1194
+ `Audit the repository located at: ${cwd}`,
1195
+ ``,
1196
+ `This is a deliberate fault-finding pass over the WHOLE repository (not a diff): surface`,
1197
+ `everything that keeps it from being truly professional \u2014 sloppy or amateurish`,
1198
+ `implementations, outdated or deprecated patterns, fragility, and neglected hygiene.`,
1199
+ ...standard.guidance ? [``, `Overall guidance: ${standard.guidance.trim()}`] : [],
1200
+ ...focus ? [``, `Extra focus requested by the user: ${truncate(focus.trim(), 2e3)}`] : [],
1201
+ ``,
1202
+ `Method:`,
1203
+ ...numbered([
1204
+ `Explore the repository thoroughly: structure, source, configuration, dependencies,
1205
+ CI, tests, and docs. Use Read/Grep/Glob and read-only Bash (ls, cat, git log).
1206
+ Do NOT modify anything.`,
1207
+ `For EVERY dimension below, actively hunt for violations of its guidance and
1208
+ checklist. Only report findings backed by evidence you actually inspected; cite the
1209
+ repo-relative file path and, where it applies, the 1-based line number.`,
1210
+ `For each finding choose a severity (critical/high/medium/low/info; start from the
1211
+ dimension's default and adjust for impact), explain why it matters, give a concrete
1212
+ fix, and estimate the effort (quick/moderate/involved).`,
1213
+ `When the SAME issue recurs across many files, consolidate it into ONE finding and
1214
+ list the other affected locations in its detail \u2014 do not repeat it per file.
1215
+ Report at most ${maxFindings} findings: keep the most severe, fold or drop the rest.`,
1216
+ `Also record what the repo genuinely does well (positives) \u2014 the critique must stay fair.`,
1217
+ `When finished, call submit_result exactly once with the summary, all findings, and
1218
+ the positives.`
1219
+ ]),
1220
+ ``,
1221
+ `Dimensions:`,
1222
+ dims
1223
+ ].join("\n");
1224
+ }
1225
+ function numbered(steps) {
1226
+ return steps.map((s, i) => {
1227
+ const [first = "", ...rest] = s.split("\n");
1228
+ return [`${i + 1}. ${first}`, ...rest.map((l) => ` ${l}`)].join("\n");
1229
+ });
1230
+ }
1231
+ function truncate(s, max) {
1232
+ return s.length > max ? s.slice(0, max) + " \u2026 [truncated]" : s;
1233
+ }
1234
+
1235
+ // src/commands/benchmark.ts
1236
+ import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync2, writeFileSync as writeFileSync2 } from "fs";
1237
+ import { basename as basename2, join as join6 } from "path";
1238
+ import pc5 from "picocolors";
1239
+
1240
+ // src/core/report.ts
1241
+ function scoreBenchmark(standard, result) {
1242
+ const scale = standard.scoreScale || 4;
1243
+ const totalWeight = standard.categories.reduce((s, c) => s + c.weight, 0) || 1;
1244
+ const tail = (id) => id.slice(id.lastIndexOf("/") + 1);
1245
+ const byKey = /* @__PURE__ */ new Map();
1246
+ const byCrit = /* @__PURE__ */ new Map();
1247
+ for (const cat of result.categories) {
1248
+ for (const cr of cat.criteria) {
1249
+ const meta = {
1250
+ score: cr.score,
1251
+ justification: cr.justification,
1252
+ evidence: cr.evidence ?? []
1253
+ };
1254
+ byKey.set(`${tail(cat.id)}/${tail(cr.id)}`, meta);
1255
+ byCrit.set(tail(cr.id), meta);
1256
+ }
1257
+ }
1258
+ let matched = 0;
1259
+ const categories = standard.categories.map((cat) => {
1260
+ const critWeightTotal = cat.criteria.reduce((s, c) => s + (c.weight ?? 1), 0) || 1;
1261
+ const scored = cat.criteria.map((cr) => {
1262
+ const meta = byKey.get(`${tail(cat.id)}/${tail(cr.id)}`) ?? byCrit.get(tail(cr.id));
1263
+ if (meta) matched += 1;
1264
+ const raw = clamp(meta?.score ?? 0, 0, scale);
1265
+ return {
1266
+ id: cr.id,
1267
+ title: cr.title,
1268
+ weight: cr.weight ?? 1,
1269
+ rawScore: raw,
1270
+ maxScore: scale,
1271
+ fraction: raw / scale,
1272
+ justification: meta?.justification ?? "Not assessed.",
1273
+ evidence: meta?.evidence ?? []
1274
+ };
1275
+ });
1276
+ const fraction = scored.reduce((s, c) => s + c.fraction * c.weight, 0) / critWeightTotal;
1277
+ const normWeight = cat.weight / totalWeight * 100;
1278
+ return {
1279
+ id: cat.id,
1280
+ name: cat.name,
1281
+ weight: round(normWeight, 1),
1282
+ fraction,
1283
+ points: round(normWeight * fraction, 1),
1284
+ criteria: scored
1285
+ };
1286
+ });
1287
+ const totalCriteria = standard.categories.reduce((s, c) => s + c.criteria.length, 0);
1288
+ if (result.categories.length > 0 && matched === 0) {
1289
+ log.warn(
1290
+ "The agent returned scores but none matched the rubric's criterion ids \u2014 reporting 0. This is a scoring bug, not a real result."
1291
+ );
1292
+ } else if (matched < totalCriteria) {
1293
+ log.warn(`Only ${matched}/${totalCriteria} criteria were scored; the rest default to 0.`);
1294
+ }
1295
+ const overall = round(
1296
+ categories.reduce((s, c) => s + c.points, 0),
1297
+ 1
1298
+ );
1299
+ return {
1300
+ standardName: standard.name,
1301
+ standardVersion: standard.version,
1302
+ overall,
1303
+ grade: gradeFor(overall),
1304
+ categories,
1305
+ summary: result.summary,
1306
+ strengths: result.strengths ?? [],
1307
+ weaknesses: result.weaknesses ?? [],
1308
+ recommendations: result.recommendations ?? []
1309
+ };
1310
+ }
1311
+ function gradeFor(score) {
1312
+ if (score >= 90) return "A";
1313
+ if (score >= 80) return "B";
1314
+ if (score >= 70) return "C";
1315
+ if (score >= 60) return "D";
1316
+ return "F";
1317
+ }
1318
+ function renderBenchmarkMarkdown(scored, meta) {
1319
+ const lines = [];
1320
+ lines.push(`# Edsger Benchmark Report`);
1321
+ lines.push("");
1322
+ lines.push(`**Score: ${scored.overall} / 100 (Grade ${scored.grade})**`);
1323
+ lines.push("");
1324
+ lines.push(`> ${scored.summary}`);
1325
+ lines.push("");
1326
+ lines.push(`| Field | Value |`);
1327
+ lines.push(`| --- | --- |`);
1328
+ lines.push(`| Repository | \`${meta.repo}\` |`);
1329
+ lines.push(`| Languages | ${meta.languages.join(", ") || "n/a"} |`);
1330
+ lines.push(`| Standard | ${scored.standardName} v${scored.standardVersion} |`);
1331
+ lines.push(`| Sources | ${meta.layers.join(" \u2192 ")} |`);
1332
+ lines.push(`| Model | ${meta.model} |`);
1333
+ lines.push(`| Generated | ${meta.generatedAt} |`);
1334
+ lines.push("");
1335
+ lines.push(`## Category scores`);
1336
+ lines.push("");
1337
+ lines.push(`| Category | Weight | Score | Weighted |`);
1338
+ lines.push(`| --- | ---: | ---: | ---: |`);
1339
+ for (const c of scored.categories) {
1340
+ lines.push(`| ${c.name} | ${c.weight}% | ${Math.round(c.fraction * 100)}% | ${c.points} |`);
1341
+ }
1342
+ lines.push(`| **Overall** | **100%** | | **${scored.overall}** |`);
1343
+ lines.push("");
1344
+ if (scored.strengths.length) {
1345
+ lines.push(`## Strengths`);
1346
+ lines.push("");
1347
+ for (const s of scored.strengths) lines.push(`- ${s}`);
1348
+ lines.push("");
1349
+ }
1350
+ if (scored.weaknesses.length) {
1351
+ lines.push(`## Weaknesses`);
1352
+ lines.push("");
1353
+ for (const w of scored.weaknesses) lines.push(`- ${w}`);
1354
+ lines.push("");
1355
+ }
1356
+ if (scored.recommendations.length) {
1357
+ lines.push(`## Recommendations`);
1358
+ lines.push("");
1359
+ for (const r of scored.recommendations) {
1360
+ lines.push(`- **[${r.priority}] ${r.title}** \u2014 ${r.detail}`);
1361
+ }
1362
+ lines.push("");
1363
+ }
1364
+ lines.push(`## Detailed findings`);
1365
+ lines.push("");
1366
+ for (const c of scored.categories) {
1367
+ lines.push(`### ${c.name} \u2014 ${Math.round(c.fraction * 100)}%`);
1368
+ lines.push("");
1369
+ for (const cr of c.criteria) {
1370
+ lines.push(`- **${cr.title}** (${cr.rawScore}/${cr.maxScore}) \u2014 ${cr.justification}`);
1371
+ for (const e of cr.evidence) lines.push(` - \`${e}\``);
1372
+ }
1373
+ lines.push("");
1374
+ }
1375
+ lines.push(`---`);
1376
+ lines.push(`<sub>Generated by [edsger](https://www.npmjs.com/package/edsger).</sub>`);
1377
+ return lines.join("\n");
1378
+ }
1379
+ function clamp(n, lo, hi) {
1380
+ return Math.max(lo, Math.min(hi, n));
1381
+ }
1382
+ function round(n, dp) {
1383
+ const f = 10 ** dp;
1384
+ return Math.round(n * f) / f;
1385
+ }
710
1386
 
711
1387
  // src/commands/benchmark.ts
712
1388
  async function runBenchmark(opts) {
713
1389
  const { config, path: configPath } = loadConfig(opts.cwd);
714
- const configDir = configPath ? join3(configPath, "..") : opts.cwd;
1390
+ const configDir = configPath ? join6(configPath, "..") : opts.cwd;
715
1391
  const resolved = resolveStandard("benchmark", opts, config, configDir);
716
1392
  const standard = resolved.standard;
717
1393
  log.info(
718
- `Benchmarking ${pc3.bold(basename(opts.cwd))} against ${pc3.bold(standard.name)} (languages: ${resolved.languages.join(", ") || "generic"})`
1394
+ `Benchmarking ${pc5.bold(basename2(opts.cwd))} against ${pc5.bold(standard.name)} (languages: ${resolved.languages.join(", ") || "generic"})`
719
1395
  );
720
1396
  log.debug(`standard layers: ${resolved.layers.join(" \u2192 ")}`);
721
- const prompt = buildPrompt(standard, opts.cwd);
1397
+ const prompt = buildPrompt2(standard, opts.cwd);
722
1398
  const { result, costUsd, numTurns } = await runAgent({
723
1399
  prompt,
724
1400
  cwd: opts.cwd,
@@ -741,7 +1417,7 @@ async function runBenchmark(opts) {
741
1417
  const runId = generatedAt.replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z");
742
1418
  const git = gitInfo(opts.cwd);
743
1419
  const md = renderBenchmarkMarkdown(scored, {
744
- repo: basename(opts.cwd),
1420
+ repo: basename2(opts.cwd),
745
1421
  languages: resolved.languages,
746
1422
  layers: resolved.layers,
747
1423
  generatedAt,
@@ -752,7 +1428,7 @@ async function runBenchmark(opts) {
752
1428
  runId,
753
1429
  generatedAt,
754
1430
  model: opts.model,
755
- repo: basename(opts.cwd),
1431
+ repo: basename2(opts.cwd),
756
1432
  git,
757
1433
  languages: resolved.languages,
758
1434
  standard: { name: standard.name, version: standard.version, layers: resolved.layers },
@@ -779,25 +1455,25 @@ async function runBenchmark(opts) {
779
1455
  costUsd
780
1456
  };
781
1457
  if (!opts.noWrite && !opts.dryRun) {
782
- const baseDir = join3(opts.cwd, opts.out);
1458
+ const baseDir = join6(opts.cwd, opts.out);
783
1459
  const mdOut = md + "\n";
784
1460
  const jsonOut = JSON.stringify(jsonReport, null, 2) + "\n";
785
- const runDir = join3(baseDir, "runs", runId);
786
- mkdirSync(runDir, { recursive: true });
787
- writeFileSync(join3(runDir, "report.md"), mdOut, "utf8");
788
- writeFileSync(join3(runDir, "report.json"), jsonOut, "utf8");
789
- writeFileSync(join3(baseDir, "report.md"), mdOut, "utf8");
790
- writeFileSync(join3(baseDir, "report.json"), jsonOut, "utf8");
791
- appendFileSync(join3(baseDir, "history.jsonl"), JSON.stringify(historyRecord) + "\n", "utf8");
1461
+ const runDir = join6(baseDir, "runs", runId);
1462
+ mkdirSync2(runDir, { recursive: true });
1463
+ writeFileSync2(join6(runDir, "report.md"), mdOut, "utf8");
1464
+ writeFileSync2(join6(runDir, "report.json"), jsonOut, "utf8");
1465
+ writeFileSync2(join6(baseDir, "report.md"), mdOut, "utf8");
1466
+ writeFileSync2(join6(baseDir, "report.json"), jsonOut, "utf8");
1467
+ appendFileSync2(join6(baseDir, "history.jsonl"), JSON.stringify(historyRecord) + "\n", "utf8");
792
1468
  log.success(
793
- `Report written to ${pc3.bold(join3(opts.out, "report.md"))} ${pc3.dim(`(run archived at ${join3(opts.out, "runs", runId)})`)}`
1469
+ `Report written to ${pc5.bold(join6(opts.out, "report.md"))} ${pc5.dim(`(run archived at ${join6(opts.out, "runs", runId)})`)}`
794
1470
  );
795
1471
  }
796
- printSummary(scored, numTurns, costUsd);
1472
+ printSummary2(scored, numTurns, costUsd);
797
1473
  if (opts.json) process.stdout.write(JSON.stringify(jsonReport, null, 2) + "\n");
798
1474
  return 0;
799
1475
  }
800
- function printSummary(scored, numTurns, costUsd) {
1476
+ function printSummary2(scored, numTurns, costUsd) {
801
1477
  log.heading(`Score: ${scored.overall}/100 (Grade ${scored.grade})`);
802
1478
  for (const c of scored.categories) {
803
1479
  const pct = Math.round(c.fraction * 100);
@@ -809,10 +1485,10 @@ function printSummary(scored, numTurns, costUsd) {
809
1485
  function bar(pct) {
810
1486
  const width = 20;
811
1487
  const filled = Math.round(pct / 100 * width);
812
- const color = pct >= 80 ? pc3.green : pct >= 60 ? pc3.yellow : pc3.red;
813
- return color("\u2588".repeat(filled)) + pc3.dim("\u2591".repeat(width - filled));
1488
+ const color = pct >= 80 ? pc5.green : pct >= 60 ? pc5.yellow : pc5.red;
1489
+ return color("\u2588".repeat(filled)) + pc5.dim("\u2591".repeat(width - filled));
814
1490
  }
815
- function buildPrompt(standard, cwd) {
1491
+ function buildPrompt2(standard, cwd) {
816
1492
  const rubric = standard.categories.map((cat) => {
817
1493
  const crits = cat.criteria.map((cr) => {
818
1494
  const anchors = cr.rubric ? Object.entries(cr.rubric).map(([k, v]) => ` - ${k}: ${v}`).join("\n") : "";
@@ -845,13 +1521,13 @@ ${crits}`;
845
1521
  }
846
1522
 
847
1523
  // src/commands/pr-review.ts
848
- import pc4 from "picocolors";
1524
+ import pc6 from "picocolors";
849
1525
 
850
1526
  // src/core/gh.ts
851
1527
  import { execFile } from "child_process";
852
- import { mkdtempSync, rmSync, writeFileSync as writeFileSync2 } from "fs";
1528
+ import { mkdtempSync, rmSync, writeFileSync as writeFileSync3 } from "fs";
853
1529
  import { tmpdir } from "os";
854
- import { join as join4 } from "path";
1530
+ import { join as join7 } from "path";
855
1531
  import { promisify } from "util";
856
1532
  var exec = promisify(execFile);
857
1533
  async function gh(args, cwd) {
@@ -912,9 +1588,9 @@ async function postReview(ctx, repo, event, body, inline, cwd) {
912
1588
  event,
913
1589
  comments: inline.map((c) => ({ path: c.path, line: c.line, side: "RIGHT", body: c.body }))
914
1590
  };
915
- const dir = mkdtempSync(join4(tmpdir(), "edsger-review-"));
916
- const file = join4(dir, "review.json");
917
- writeFileSync2(file, JSON.stringify(payload), "utf8");
1591
+ const dir = mkdtempSync(join7(tmpdir(), "edsger-review-"));
1592
+ const file = join7(dir, "review.json");
1593
+ writeFileSync3(file, JSON.stringify(payload), "utf8");
918
1594
  try {
919
1595
  await gh(
920
1596
  ["api", "--method", "POST", `repos/${target}/pulls/${ctx.number}/reviews`, "--input", file],
@@ -955,7 +1631,7 @@ async function getPrFeedback(ctx, repo, cwd) {
955
1631
  }
956
1632
 
957
1633
  // src/commands/pr-review.ts
958
- var SEVERITY_RANK = {
1634
+ var SEVERITY_RANK2 = {
959
1635
  critical: 4,
960
1636
  high: 3,
961
1637
  medium: 2,
@@ -964,7 +1640,7 @@ var SEVERITY_RANK = {
964
1640
  };
965
1641
  async function runPrReview(opts) {
966
1642
  await ensureGhReady();
967
- log.info(`Fetching PR ${pc4.bold(opts.pr)}${opts.repo ? ` from ${opts.repo}` : ""}`);
1643
+ log.info(`Fetching PR ${pc6.bold(opts.pr)}${opts.repo ? ` from ${opts.repo}` : ""}`);
968
1644
  const ctx = await getPrContext(opts.pr, opts.repo, opts.cwd);
969
1645
  const { config, path: configPath } = loadConfig(opts.cwd);
970
1646
  const configDir = configPath ? `${configPath}/..` : opts.cwd;
@@ -975,10 +1651,10 @@ async function runPrReview(opts) {
975
1651
  const resolved = resolveStandard("review", opts, config, configDir, fileHint);
976
1652
  const standard = resolved.standard;
977
1653
  log.info(
978
- `Reviewing "${pc4.bold(ctx.title)}" (#${ctx.number}, ${ctx.files.length} files) against ${pc4.bold(standard.name)}`
1654
+ `Reviewing "${pc6.bold(ctx.title)}" (#${ctx.number}, ${ctx.files.length} files) against ${pc6.bold(standard.name)}`
979
1655
  );
980
1656
  const { result, costUsd, numTurns } = await runAgent({
981
- prompt: buildPrompt2(standard, ctx),
1657
+ prompt: buildPrompt3(standard, ctx),
982
1658
  cwd: opts.cwd,
983
1659
  model: opts.model,
984
1660
  maxTurns: opts.maxTurns,
@@ -1009,7 +1685,7 @@ async function runPrReview(opts) {
1009
1685
  await postReview(ctx, opts.repo, event, body, inline, opts.cwd);
1010
1686
  log.success(`Posted review (${event}) to ${ctx.url}`);
1011
1687
  } else {
1012
- log.warn(`Preview only. Re-run with ${pc4.bold("--post")} to submit this review to GitHub.`);
1688
+ log.warn(`Preview only. Re-run with ${pc6.bold("--post")} to submit this review to GitHub.`);
1013
1689
  process.stderr.write("\n" + body + "\n");
1014
1690
  }
1015
1691
  return 0;
@@ -1025,7 +1701,7 @@ function decideEvent(result, standard) {
1025
1701
  function buildInlineComments(result, ctx, standard) {
1026
1702
  const changed = new Set(ctx.files.map((f) => f.path));
1027
1703
  const sorted = [...result.findings].sort(
1028
- (a, b) => SEVERITY_RANK[b.severity] - SEVERITY_RANK[a.severity]
1704
+ (a, b) => SEVERITY_RANK2[b.severity] - SEVERITY_RANK2[a.severity]
1029
1705
  );
1030
1706
  const out = [];
1031
1707
  for (const f of sorted) {
@@ -1074,7 +1750,7 @@ function printPreview(result, event, numTurns, costUsd) {
1074
1750
  log.heading(`Verdict: ${event}`);
1075
1751
  const counts = countBySeverity(result.findings);
1076
1752
  for (const s of ["critical", "high", "medium", "low", "info"]) {
1077
- if (counts[s]) process.stderr.write(` ${sevColor(s)(s.padEnd(8))} ${counts[s]}
1753
+ if (counts[s]) process.stderr.write(` ${sevColor2(s)(s.padEnd(8))} ${counts[s]}
1078
1754
  `);
1079
1755
  }
1080
1756
  log.step(`${numTurns} turns \xB7 ~$${costUsd.toFixed(3)}`);
@@ -1084,15 +1760,15 @@ function countBySeverity(findings) {
1084
1760
  for (const f of findings) c[f.severity]++;
1085
1761
  return c;
1086
1762
  }
1087
- function sevColor(s) {
1088
- if (s === "critical" || s === "high") return pc4.red;
1089
- if (s === "medium") return pc4.yellow;
1090
- return pc4.dim;
1763
+ function sevColor2(s) {
1764
+ if (s === "critical" || s === "high") return pc6.red;
1765
+ if (s === "medium") return pc6.yellow;
1766
+ return pc6.dim;
1091
1767
  }
1092
1768
  function verdictLabel(v) {
1093
1769
  return v === "approve" ? "Approve" : v === "request_changes" ? "Request changes" : "Comment";
1094
1770
  }
1095
- function buildPrompt2(standard, ctx) {
1771
+ function buildPrompt3(standard, ctx) {
1096
1772
  const focus = standard.focusAreas.map((fa) => {
1097
1773
  const checklist = (fa.checklist ?? []).map((c) => ` - ${c}`).join("\n");
1098
1774
  return `- id: ${fa.id} (${fa.title}, default severity ${fa.defaultSeverity ?? "medium"})
@@ -1125,25 +1801,25 @@ ${checklist}` : "");
1125
1801
  ``,
1126
1802
  `Unified diff:`,
1127
1803
  "```diff",
1128
- truncate(ctx.diff, 18e4),
1804
+ truncate2(ctx.diff, 18e4),
1129
1805
  "```"
1130
1806
  ].join("\n");
1131
1807
  }
1132
1808
  function indent(s) {
1133
1809
  return s.split("\n").map((l) => ` ${l}`).join("\n");
1134
1810
  }
1135
- function truncate(s, max) {
1811
+ function truncate2(s, max) {
1136
1812
  return s.length > max ? s.slice(0, max) + "\n... [diff truncated] ..." : s;
1137
1813
  }
1138
1814
 
1139
1815
  // src/commands/pr-resolve.ts
1140
1816
  import { execFile as execFile2 } from "child_process";
1141
1817
  import { promisify as promisify2 } from "util";
1142
- import pc5 from "picocolors";
1818
+ import pc7 from "picocolors";
1143
1819
  var exec2 = promisify2(execFile2);
1144
1820
  async function runPrResolve(opts) {
1145
1821
  await ensureGhReady();
1146
- log.info(`Fetching PR ${pc5.bold(opts.pr)}${opts.repo ? ` from ${opts.repo}` : ""}`);
1822
+ log.info(`Fetching PR ${pc7.bold(opts.pr)}${opts.repo ? ` from ${opts.repo}` : ""}`);
1147
1823
  const ctx = await getPrContext(opts.pr, opts.repo, opts.cwd);
1148
1824
  const feedback = await getPrFeedback(ctx, opts.repo, opts.cwd).catch(() => ({
1149
1825
  reviewComments: [],
@@ -1154,10 +1830,10 @@ async function runPrResolve(opts) {
1154
1830
  await checkoutPr(opts.pr, opts.repo, opts.cwd);
1155
1831
  }
1156
1832
  log.info(
1157
- `Resolving "${pc5.bold(ctx.title)}" (#${ctx.number}) \u2014 ${feedback.reviewComments.length} review comments`
1833
+ `Resolving "${pc7.bold(ctx.title)}" (#${ctx.number}) \u2014 ${feedback.reviewComments.length} review comments`
1158
1834
  );
1159
1835
  const { result, costUsd, numTurns } = await runAgent({
1160
- prompt: buildPrompt3(ctx, feedback, opts.instruction),
1836
+ prompt: buildPrompt4(ctx, feedback, opts.instruction),
1161
1837
  cwd: opts.cwd,
1162
1838
  model: opts.model,
1163
1839
  maxTurns: opts.maxTurns,
@@ -1171,7 +1847,7 @@ async function runPrResolve(opts) {
1171
1847
  log.error("The agent did not produce a result.");
1172
1848
  return 1;
1173
1849
  }
1174
- printSummary2(result, numTurns, costUsd);
1850
+ printSummary3(result, numTurns, costUsd);
1175
1851
  if (opts.json) process.stdout.write(JSON.stringify(result, null, 2) + "\n");
1176
1852
  if (opts.push && !opts.dryRun) {
1177
1853
  const pushed = await commitAndPush(ctx, result, opts.cwd, opts.checkout);
@@ -1179,7 +1855,7 @@ async function runPrResolve(opts) {
1179
1855
  else log.warn("Nothing to commit \u2014 working tree is clean.");
1180
1856
  } else {
1181
1857
  log.warn(
1182
- `Changes left in the working tree. Review them, then commit/push (or re-run with ${pc5.bold("--push")}).`
1858
+ `Changes left in the working tree. Review them, then commit/push (or re-run with ${pc7.bold("--push")}).`
1183
1859
  );
1184
1860
  }
1185
1861
  return 0;
@@ -1216,14 +1892,14 @@ Push manually, or ensure the PR branch was checked out with gh.`
1216
1892
  function errText(err) {
1217
1893
  return err instanceof Error ? err.message.split("\n")[0] ?? "" : String(err);
1218
1894
  }
1219
- function printSummary2(result, numTurns, costUsd) {
1895
+ function printSummary3(result, numTurns, costUsd) {
1220
1896
  log.heading("Resolution summary");
1221
1897
  process.stderr.write(` ${result.summary}
1222
1898
 
1223
1899
  `);
1224
1900
  if (result.changes.length) {
1225
1901
  for (const c of result.changes)
1226
- process.stderr.write(` ${pc5.green("~")} ${c.file} \u2014 ${c.description}
1902
+ process.stderr.write(` ${pc7.green("~")} ${c.file} \u2014 ${c.description}
1227
1903
  `);
1228
1904
  }
1229
1905
  if (result.testsRun) log.step(`verification: ${result.testsRun}`);
@@ -1234,7 +1910,7 @@ function printSummary2(result, numTurns, costUsd) {
1234
1910
  }
1235
1911
  log.step(`${numTurns} turns \xB7 ~$${costUsd.toFixed(3)}`);
1236
1912
  }
1237
- function buildPrompt3(ctx, feedback, instruction) {
1913
+ function buildPrompt4(ctx, feedback, instruction) {
1238
1914
  const reviewComments = feedback.reviewComments.length ? feedback.reviewComments.map((c) => ` - ${c.path}${c.line ? `:${c.line}` : ""} (${c.author}): ${c.body}`).join("\n") : " (none)";
1239
1915
  const issueComments = feedback.issueComments.length ? feedback.issueComments.map((c) => ` - ${c.author}: ${c.body}`).join("\n") : " (none)";
1240
1916
  return [
@@ -1262,163 +1938,721 @@ function buildPrompt3(ctx, feedback, instruction) {
1262
1938
  }
1263
1939
 
1264
1940
  // src/commands/serve.ts
1265
- import { createReadStream, existsSync as existsSync4, statSync as statSync2 } from "fs";
1266
- import { createServer } from "http";
1267
- import { extname, join as join6, normalize } from "path";
1268
- import pc6 from "picocolors";
1941
+ import { join as join8 } from "path";
1942
+ async function runServe(opts) {
1943
+ const benchmarkDir = join8(opts.cwd, opts.dir);
1944
+ const viewerDir = findViewerDir();
1945
+ return startViewerServer({
1946
+ viewerDir,
1947
+ indexFile: "index.html",
1948
+ dataRoute: "/edsger-data.json",
1949
+ getData: () => assembleData(benchmarkDir),
1950
+ port: opts.port,
1951
+ host: opts.host,
1952
+ open: opts.open,
1953
+ emptyHint: hasData(assembleData(benchmarkDir)) ? void 0 : `No benchmark data in ${opts.dir} yet \u2014 run \`edsger benchmark\` first.`
1954
+ });
1955
+ }
1269
1956
 
1270
- // src/core/dashboard.ts
1271
- import { existsSync as existsSync3, readFileSync as readFileSync2, readdirSync as readdirSync2 } from "fs";
1272
- import { join as join5 } from "path";
1273
- function assembleData(benchmarkDir) {
1274
- const latest = readJson(join5(benchmarkDir, "report.json"));
1275
- const history = readJsonl(join5(benchmarkDir, "history.jsonl"));
1276
- const runs = {};
1277
- const runsDir = join5(benchmarkDir, "runs");
1278
- if (existsSync3(runsDir)) {
1279
- for (const entry of readdirSync2(runsDir)) {
1280
- const report = readJson(join5(runsDir, entry, "report.json"));
1281
- if (report) runs[entry] = report;
1957
+ // src/commands/report.ts
1958
+ import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync4 } from "fs";
1959
+ import { dirname as dirname3, isAbsolute as isAbsolute3, join as join9 } from "path";
1960
+ import pc8 from "picocolors";
1961
+ async function runReport(opts) {
1962
+ const benchmarkDir = join9(opts.cwd, opts.dir);
1963
+ const data = assembleData(benchmarkDir);
1964
+ if (!hasData(data)) {
1965
+ log.error(`No benchmark data in ${opts.dir}. Run \`edsger benchmark\` first.`);
1966
+ return 1;
1967
+ }
1968
+ const html = renderSingleFile(readViewerSingleTemplate(), data);
1969
+ const out = isAbsolute3(opts.html) ? opts.html : join9(opts.cwd, opts.html);
1970
+ mkdirSync3(dirname3(out), { recursive: true });
1971
+ writeFileSync4(out, html, "utf8");
1972
+ log.success(
1973
+ `Wrote self-contained report to ${pc8.bold(opts.html)} ${pc8.dim(`(${Math.round(html.length / 1024)} kB, open in any browser)`)}`
1974
+ );
1975
+ if (opts.open) openInBrowser(out);
1976
+ return 0;
1977
+ }
1978
+
1979
+ // src/commands/diagram.ts
1980
+ import { appendFileSync as appendFileSync3, mkdirSync as mkdirSync4, writeFileSync as writeFileSync5 } from "fs";
1981
+ import { basename as basename3, dirname as dirname4, isAbsolute as isAbsolute4, join as join10 } from "path";
1982
+ import pc9 from "picocolors";
1983
+
1984
+ // src/core/diagrams.ts
1985
+ function buildDiagramsPrompt(catalog, cwd, only) {
1986
+ const onlySet = only && only.length ? new Set(only.map((s) => s.trim())) : null;
1987
+ const catalogText = catalog.groups.map((group) => {
1988
+ const diagrams = group.diagrams.filter((d) => !onlySet || onlySet.has(d.id)).map((d) => {
1989
+ const detect = d.detect?.length ? `
1990
+ detect: ${d.detect.join(", ")}` : "";
1991
+ return ` - id: ${d.id}
1992
+ title: ${d.title}
1993
+ mermaidType: ${d.type}
1994
+ appliesWhen: ${d.appliesWhen.trim()}` + detect + `
1995
+ guidance: ${d.guidance.trim()}`;
1996
+ }).join("\n");
1997
+ if (!diagrams) return "";
1998
+ return `- group: ${group.id} (${group.name})
1999
+ ${diagrams}`;
2000
+ }).filter(Boolean).join("\n");
2001
+ return [
2002
+ `Produce a set of professional software diagrams for the repository at: ${cwd}`,
2003
+ ``,
2004
+ `You have a catalog of candidate diagrams below. Your job is to pick the ones`,
2005
+ `that genuinely fit THIS project and generate each as valid Mermaid.`,
2006
+ ``,
2007
+ `Method:`,
2008
+ `1. Explore the repository (Read/Grep/Glob and read-only Bash like ls, cat, git log).`,
2009
+ ` Characterise the project: what kind is it (service, library, CLI, SPA, data`,
2010
+ ` pipeline, monorepo, \u2026), what does it integrate with, does it own a database,`,
2011
+ ` how is it deployed?`,
2012
+ `2. For each catalog diagram, use its appliesWhen/detect to decide whether it adds`,
2013
+ ` value for this repo. Only include diagrams the evidence supports. A library`,
2014
+ ` needs different diagrams than a microservice. Do NOT invent structure that is`,
2015
+ ` not in the code. Aim for the ${onlySet ? "requested" : "5-9 most valuable"} diagrams.`,
2016
+ `3. For each selected diagram, follow its guidance and emit VALID Mermaid of the`,
2017
+ ` given mermaidType, grounded in real files. Rules for the mermaid field:`,
2018
+ ` - Put ONLY the Mermaid source (starting with the diagram keyword, e.g.`,
2019
+ ` "flowchart TD" or "erDiagram"). Do NOT wrap it in \`\`\`mermaid fences and`,
2020
+ ` do NOT add any prose/markdown outside the diagram.`,
2021
+ ` - It MUST parse as-is. Use simple alphanumeric node/participant ids and put`,
2022
+ ` human text in quoted labels, e.g. A["Order Service"] not A[Order Service].`,
2023
+ ` Quote any label containing spaces, punctuation, parentheses or slashes.`,
2024
+ ` - Avoid Mermaid reserved words as bare node ids (especially "end" in`,
2025
+ ` flowcharts \u2014 write End or "end" instead).`,
2026
+ ` - Set "type" to the exact keyword the source starts with.`,
2027
+ ` - Keep each diagram to roughly one screen and legible.`,
2028
+ ` - Prefer faithful over exhaustive; record the files you used as evidence.`,
2029
+ `4. When finished, call submit_result exactly once with the project profile and`,
2030
+ ` the diagrams (each with id, groupId, title, type, mermaid, description, evidence).`,
2031
+ ``,
2032
+ `Diagram catalog:`,
2033
+ catalogText
2034
+ ].join("\n");
2035
+ }
2036
+ function sanitizeMermaid(src) {
2037
+ let s = src.trim();
2038
+ const fence = s.match(/^```(?:mermaid)?\s*\n([\s\S]*?)\n```$/);
2039
+ if (fence && fence[1] !== void 0) s = fence[1].trim();
2040
+ return s;
2041
+ }
2042
+ var KNOWN_MERMAID_TYPES = /* @__PURE__ */ new Set([
2043
+ "flowchart",
2044
+ "graph",
2045
+ "sequenceDiagram",
2046
+ "classDiagram",
2047
+ "stateDiagram",
2048
+ "stateDiagram-v2",
2049
+ "erDiagram",
2050
+ "journey",
2051
+ "gantt",
2052
+ "gitGraph",
2053
+ "mindmap",
2054
+ "timeline",
2055
+ "requirementDiagram",
2056
+ "quadrantChart",
2057
+ "pie",
2058
+ "sankey-beta",
2059
+ "xychart-beta",
2060
+ "block-beta",
2061
+ "packet-beta",
2062
+ "kanban",
2063
+ "zenuml",
2064
+ "C4Context",
2065
+ "C4Container",
2066
+ "C4Component",
2067
+ "C4Deployment",
2068
+ "C4Dynamic",
2069
+ "architecture-beta"
2070
+ ]);
2071
+ function firstKeyword(mermaid) {
2072
+ for (const raw of mermaid.split("\n")) {
2073
+ const line = raw.trim();
2074
+ if (!line || line.startsWith("%%")) continue;
2075
+ return line.split(/[\s({]/)[0] ?? line;
2076
+ }
2077
+ return "";
2078
+ }
2079
+ function normaliseType(keyword) {
2080
+ const k = keyword.trim();
2081
+ if (k === "graph") return "flowchart";
2082
+ if (k === "stateDiagram") return "stateDiagram-v2";
2083
+ return k;
2084
+ }
2085
+ function inferType(mermaid) {
2086
+ return firstKeyword(mermaid) || "flowchart";
2087
+ }
2088
+ function validateMermaid(declaredType, mermaid) {
2089
+ const warnings = [];
2090
+ if (mermaid.includes("```")) warnings.push("source still contains a ``` fence");
2091
+ const keyword = firstKeyword(mermaid);
2092
+ if (!keyword) {
2093
+ warnings.push("no diagram content");
2094
+ return warnings;
2095
+ }
2096
+ if (!KNOWN_MERMAID_TYPES.has(keyword)) {
2097
+ warnings.push(`unrecognised diagram keyword "${keyword}"`);
2098
+ }
2099
+ const body = mermaid.split("\n").filter((l) => {
2100
+ const t = l.trim();
2101
+ return t && !t.startsWith("%%");
2102
+ });
2103
+ if (body.length < 2) warnings.push("header only \u2014 the diagram has no body");
2104
+ if (declaredType && KNOWN_MERMAID_TYPES.has(keyword)) {
2105
+ if (normaliseType(declaredType) !== normaliseType(keyword)) {
2106
+ warnings.push(`declared type "${declaredType}" but source starts with "${keyword}"`);
2107
+ }
2108
+ }
2109
+ return warnings;
2110
+ }
2111
+ function prepareDiagrams(catalog, result) {
2112
+ const groupName = new Map(catalog.groups.map((g) => [g.id, g.name]));
2113
+ const groupOrder = new Map(catalog.groups.map((g, i) => [g.id, i]));
2114
+ const prepared = [];
2115
+ const seenIds = /* @__PURE__ */ new Map();
2116
+ for (const d of result.diagrams) {
2117
+ const mermaid = sanitizeMermaid(d.mermaid ?? "");
2118
+ if (!mermaid) {
2119
+ log.warn(`Skipping diagram "${d.id}" \u2014 it had no mermaid source.`);
2120
+ continue;
2121
+ }
2122
+ const baseId = d.id || inferType(mermaid);
2123
+ const count = seenIds.get(baseId) ?? 0;
2124
+ seenIds.set(baseId, count + 1);
2125
+ const id = count === 0 ? baseId : `${baseId}-${count + 1}`;
2126
+ const type = d.type?.trim() || inferType(mermaid);
2127
+ prepared.push({
2128
+ ...d,
2129
+ id,
2130
+ mermaid,
2131
+ type,
2132
+ evidence: d.evidence ?? [],
2133
+ groupName: groupName.get(d.groupId) ?? "Other",
2134
+ warnings: validateMermaid(type, mermaid)
2135
+ });
2136
+ }
2137
+ prepared.sort((a, b) => {
2138
+ const oa = groupOrder.get(a.groupId) ?? Number.MAX_SAFE_INTEGER;
2139
+ const ob = groupOrder.get(b.groupId) ?? Number.MAX_SAFE_INTEGER;
2140
+ return oa - ob;
2141
+ });
2142
+ if (prepared.length === 0) {
2143
+ log.warn("The agent returned no usable diagrams.");
2144
+ } else {
2145
+ const flagged = prepared.filter((d) => d.warnings.length);
2146
+ if (flagged.length) {
2147
+ log.warn(
2148
+ `${flagged.length}/${prepared.length} diagram(s) have Mermaid warnings \u2014 they will still render if valid, but check: ${flagged.map((d) => d.id).join(", ")}`
2149
+ );
2150
+ }
2151
+ }
2152
+ return { profile: result.profile, diagrams: prepared, notes: result.notes ?? null };
2153
+ }
2154
+ function renderDiagramsMarkdown(prepared, meta) {
2155
+ const lines = [];
2156
+ lines.push(`# Edsger Diagrams`);
2157
+ lines.push("");
2158
+ lines.push(`> ${prepared.profile.summary}`);
2159
+ lines.push("");
2160
+ lines.push(`| Field | Value |`);
2161
+ lines.push(`| --- | --- |`);
2162
+ lines.push(`| Repository | \`${meta.repo}\` |`);
2163
+ lines.push(`| Languages | ${meta.languages.join(", ") || "n/a"} |`);
2164
+ lines.push(`| Project kinds | ${prepared.profile.kinds.join(", ") || "n/a"} |`);
2165
+ lines.push(`| Catalog sources | ${meta.layers.join(" \u2192 ")} |`);
2166
+ lines.push(`| Model | ${meta.model} |`);
2167
+ lines.push(`| Generated | ${meta.generatedAt} |`);
2168
+ lines.push("");
2169
+ if (prepared.profile.characteristics.length) {
2170
+ lines.push(`**Characteristics:** ${prepared.profile.characteristics.join(" \xB7 ")}`);
2171
+ lines.push("");
2172
+ }
2173
+ let currentGroup = null;
2174
+ for (const d of prepared.diagrams) {
2175
+ if (d.groupName !== currentGroup) {
2176
+ currentGroup = d.groupName;
2177
+ lines.push(`## ${currentGroup}`);
2178
+ lines.push("");
2179
+ }
2180
+ lines.push(`### ${d.title}`);
2181
+ lines.push("");
2182
+ lines.push(`${d.description} _(${d.type})_`);
2183
+ lines.push("");
2184
+ if (d.warnings.length) {
2185
+ lines.push(`> \u26A0\uFE0F ${d.warnings.join("; ")}`);
2186
+ lines.push("");
1282
2187
  }
2188
+ lines.push("```mermaid");
2189
+ lines.push(d.mermaid);
2190
+ lines.push("```");
2191
+ lines.push("");
2192
+ if (d.evidence.length) {
2193
+ lines.push(`<sub>Evidence: ${d.evidence.map((e) => `\`${e}\``).join(", ")}</sub>`);
2194
+ lines.push("");
2195
+ }
2196
+ }
2197
+ if (prepared.notes) {
2198
+ lines.push(`## Notes`);
2199
+ lines.push("");
2200
+ lines.push(prepared.notes);
2201
+ lines.push("");
2202
+ }
2203
+ lines.push(`---`);
2204
+ lines.push(`<sub>Generated by [edsger](https://www.npmjs.com/package/edsger).</sub>`);
2205
+ return lines.join("\n");
2206
+ }
2207
+
2208
+ // src/commands/diagram.ts
2209
+ async function runDiagramGenerate(opts) {
2210
+ const { config, path: configPath } = loadConfig(opts.cwd);
2211
+ const configDir = configPath ? join10(configPath, "..") : opts.cwd;
2212
+ const resolved = resolveStandard("diagrams", opts, config, configDir);
2213
+ const catalog = resolved.standard;
2214
+ log.info(
2215
+ `Diagramming ${pc9.bold(basename3(opts.cwd))} with ${pc9.bold(catalog.name)} (languages: ${resolved.languages.join(", ") || "generic"})`
2216
+ );
2217
+ log.debug(`catalog layers: ${resolved.layers.join(" \u2192 ")}`);
2218
+ const prompt = buildDiagramsPrompt(catalog, opts.cwd, opts.only);
2219
+ const { result, costUsd, numTurns } = await runAgent({
2220
+ prompt,
2221
+ cwd: opts.cwd,
2222
+ model: opts.model,
2223
+ maxTurns: opts.maxTurns,
2224
+ timeoutMs: opts.timeoutMs,
2225
+ allowWrite: false,
2226
+ verbose: opts.verbose,
2227
+ resultShape: DiagramsResultSchema.shape,
2228
+ 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."
2229
+ });
2230
+ if (!result) {
2231
+ log.error(
2232
+ "The agent did not produce a result. Try increasing --max-turns or check credentials."
2233
+ );
2234
+ return 1;
2235
+ }
2236
+ const prepared = prepareDiagrams(catalog, result);
2237
+ const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
2238
+ const runId = generatedAt.replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z");
2239
+ const git = gitInfo(opts.cwd);
2240
+ const md = renderDiagramsMarkdown(prepared, {
2241
+ repo: basename3(opts.cwd),
2242
+ languages: resolved.languages,
2243
+ layers: resolved.layers,
2244
+ generatedAt,
2245
+ model: opts.model
2246
+ });
2247
+ const jsonReport = {
2248
+ schema: "edsger.diagrams/1",
2249
+ runId,
2250
+ generatedAt,
2251
+ model: opts.model,
2252
+ repo: basename3(opts.cwd),
2253
+ git,
2254
+ languages: resolved.languages,
2255
+ catalog: { name: catalog.name, version: catalog.version, layers: resolved.layers },
2256
+ profile: prepared.profile,
2257
+ diagrams: prepared.diagrams,
2258
+ notes: prepared.notes,
2259
+ costUsd
2260
+ };
2261
+ const groupCounts = {};
2262
+ for (const d of prepared.diagrams) groupCounts[d.groupId] = (groupCounts[d.groupId] ?? 0) + 1;
2263
+ const historyRecord = {
2264
+ runId,
2265
+ generatedAt,
2266
+ commit: git.commit,
2267
+ branch: git.branch,
2268
+ dirty: git.dirty,
2269
+ model: opts.model,
2270
+ catalogVersion: catalog.version,
2271
+ diagramCount: prepared.diagrams.length,
2272
+ groups: groupCounts,
2273
+ costUsd
2274
+ };
2275
+ if (!opts.noWrite && !opts.dryRun) {
2276
+ const baseDir = join10(opts.cwd, opts.out);
2277
+ const mdOut = md + "\n";
2278
+ const jsonOut = JSON.stringify(jsonReport, null, 2) + "\n";
2279
+ const runDir = join10(baseDir, "runs", runId);
2280
+ mkdirSync4(runDir, { recursive: true });
2281
+ writeFileSync5(join10(runDir, "diagrams.json"), jsonOut, "utf8");
2282
+ writeFileSync5(join10(runDir, "diagrams.md"), mdOut, "utf8");
2283
+ writeFileSync5(join10(baseDir, "diagrams.json"), jsonOut, "utf8");
2284
+ writeFileSync5(join10(baseDir, "diagrams.md"), mdOut, "utf8");
2285
+ appendFileSync3(join10(baseDir, "history.jsonl"), JSON.stringify(historyRecord) + "\n", "utf8");
2286
+ log.success(
2287
+ `Diagrams written to ${pc9.bold(join10(opts.out, "diagrams.md"))} ${pc9.dim(`(run archived at ${join10(opts.out, "runs", runId)})`)}`
2288
+ );
2289
+ }
2290
+ printSummary4(prepared, numTurns, costUsd);
2291
+ if (opts.json) process.stdout.write(JSON.stringify(jsonReport, null, 2) + "\n");
2292
+ return 0;
2293
+ }
2294
+ function printSummary4(prepared, numTurns, costUsd) {
2295
+ log.heading(`${prepared.diagrams.length} diagram(s)`);
2296
+ let currentGroup = null;
2297
+ for (const d of prepared.diagrams) {
2298
+ if (d.groupName !== currentGroup) {
2299
+ currentGroup = d.groupName;
2300
+ process.stderr.write(` ${pc9.bold(currentGroup)}
2301
+ `);
2302
+ }
2303
+ const warn = d.warnings.length ? ` ${pc9.yellow("\u26A0")}` : "";
2304
+ process.stderr.write(` ${pc9.cyan("\u25C6")} ${d.title} ${pc9.dim(`(${d.type})`)}${warn}
2305
+ `);
2306
+ }
2307
+ log.step(`${numTurns} turns \xB7 ~$${costUsd.toFixed(3)}`);
2308
+ }
2309
+ async function runDiagramServe(opts) {
2310
+ const diagramsDir = join10(opts.cwd, opts.dir);
2311
+ const viewerDir = findDiagramViewerDir();
2312
+ return startViewerServer({
2313
+ viewerDir,
2314
+ indexFile: "diagrams.html",
2315
+ dataRoute: "/edsger-diagrams.json",
2316
+ getData: () => assembleDiagramData(diagramsDir),
2317
+ port: opts.port,
2318
+ host: opts.host,
2319
+ open: opts.open,
2320
+ emptyHint: hasData(assembleDiagramData(diagramsDir)) ? void 0 : `No diagram data in ${opts.dir} yet \u2014 run \`edsger diagram\` first.`
2321
+ });
2322
+ }
2323
+ async function runDiagramReport(opts) {
2324
+ const diagramsDir = join10(opts.cwd, opts.dir);
2325
+ const data = assembleDiagramData(diagramsDir);
2326
+ if (!hasData(data)) {
2327
+ log.error(`No diagram data in ${opts.dir}. Run \`edsger diagram\` first.`);
2328
+ return 1;
2329
+ }
2330
+ const html = renderDiagramsSingleFile(readDiagramViewerSingleTemplate(), data);
2331
+ const out = isAbsolute4(opts.html) ? opts.html : join10(opts.cwd, opts.html);
2332
+ mkdirSync4(dirname4(out), { recursive: true });
2333
+ writeFileSync5(out, html, "utf8");
2334
+ log.success(
2335
+ `Wrote self-contained diagram viewer to ${pc9.bold(opts.html)} ${pc9.dim(`(${Math.round(html.length / 1024)} kB, open in any browser)`)}`
2336
+ );
2337
+ if (opts.open) openInBrowser(out);
2338
+ return 0;
2339
+ }
2340
+
2341
+ // src/commands/mindmap.ts
2342
+ import { appendFileSync as appendFileSync4, mkdirSync as mkdirSync5, writeFileSync as writeFileSync6 } from "fs";
2343
+ import { basename as basename4, dirname as dirname5, isAbsolute as isAbsolute5, join as join11 } from "path";
2344
+ import pc10 from "picocolors";
2345
+
2346
+ // src/core/mindmap.ts
2347
+ function buildMindmapPrompt(standard, cwd, focus) {
2348
+ const maxDepth = standard.maxDepth ?? 5;
2349
+ const aspectsText = standard.aspects.map((a) => {
2350
+ const detect = a.detect?.length ? `
2351
+ detect: ${a.detect.join(", ")}` : "";
2352
+ return ` - id: ${a.id}
2353
+ name: ${a.name}
2354
+ appliesWhen: ${a.appliesWhen.trim()}` + detect + `
2355
+ guidance: ${a.guidance.trim()}`;
2356
+ }).join("\n");
2357
+ return [
2358
+ `Build an implementation mind map of the repository at: ${cwd}`,
2359
+ ``,
2360
+ `The mind map is an outline tree (like Workflowy): the root is the project,`,
2361
+ `and the branches explain how the code is actually implemented, deep enough`,
2362
+ `that leaves point at concrete files.`,
2363
+ ...focus ? [
2364
+ ``,
2365
+ `FOCUS: the user asked the map to concentrate on: "${focus}". Give that`,
2366
+ `area most of the depth; keep other branches to a shallow orientation level.`
2367
+ ] : [],
2368
+ ``,
2369
+ `Method:`,
2370
+ `1. Explore the repository (Read/Grep/Glob and read-only Bash like ls, cat,`,
2371
+ ` git log). Characterise the project: what kind is it (service, library, CLI,`,
2372
+ ` SPA, data pipeline, monorepo, \u2026) and how the source is organised.`,
2373
+ `2. You have a list of candidate top-level branches ("aspects") below. Keep only`,
2374
+ ` the ones THIS project's code supports (use appliesWhen/detect), rename them`,
2375
+ ` to the project's own vocabulary where that is clearer, and grow each into a`,
2376
+ ` subtree by following its guidance. Aim for 3-7 first-level branches.`,
2377
+ `3. Tree rules:`,
2378
+ ` - Root node: the project \u2014 its name as the title, a one-line summary of`,
2379
+ ` what it does. Exactly ONE root.`,
2380
+ ` - Every node: a short title (2-6 words, like an outline bullet) and a one-`,
2381
+ ` or-two-sentence summary that says something true and specific about THIS`,
2382
+ ` codebase \u2014 no generic filler.`,
2383
+ ` - Interior nodes group; leaf nodes land on concrete implementation and MUST`,
2384
+ ` cite the repo-relative file(s) they describe in "files".`,
2385
+ ` - Maximum depth ${maxDepth} (root inclusive). Prefer depth where the code is`,
2386
+ ` interesting, brevity where it is boilerplate. Do NOT invent structure that`,
2387
+ ` is not in the code, and do not just mirror the file listing \u2014 explain it.`,
2388
+ `4. Encode the tree as a FLAT node list: each node has a unique kebab-case "id"`,
2389
+ ` and a "parentId" referencing its parent's id (null only for the root). List`,
2390
+ ` children in reading order right after their parent. Tag each node with a`,
2391
+ ` "kind": area, module, component, flow, step, data, external, config, or topic.`,
2392
+ `5. When finished, call submit_result exactly once with the project profile,`,
2393
+ ` the flat node list, and optional notes.`,
2394
+ ``,
2395
+ ...standard.guidance ? [`Overall guidance: ${standard.guidance.trim()}`, ``] : [],
2396
+ `Candidate top-level branches:`,
2397
+ aspectsText
2398
+ ].join("\n");
2399
+ }
2400
+ function slugify(text) {
2401
+ return text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60) || "node";
2402
+ }
2403
+ function prepareMindmap(result, fallback) {
2404
+ const warnings = [];
2405
+ const byId = /* @__PURE__ */ new Map();
2406
+ for (const raw of result.nodes) {
2407
+ const node = { ...raw, id: raw.id?.trim() || slugify(raw.title) };
2408
+ if (!node.title?.trim()) {
2409
+ warnings.push(`dropped a node with no title (id "${node.id}")`);
2410
+ continue;
2411
+ }
2412
+ let id = node.id;
2413
+ for (let n = 2; byId.has(id); n++) id = `${node.id}-${n}`;
2414
+ if (id !== node.id) warnings.push(`renamed duplicate node id "${node.id}" to "${id}"`);
2415
+ byId.set(id, { ...node, id });
2416
+ }
2417
+ const parentOf = /* @__PURE__ */ new Map();
2418
+ for (const node of byId.values()) {
2419
+ const p = node.parentId && node.parentId !== node.id ? node.parentId : null;
2420
+ parentOf.set(node.id, p && byId.has(p) ? p : null);
2421
+ if (node.parentId && !byId.has(node.parentId)) {
2422
+ warnings.push(`node "${node.id}" referenced missing parent "${node.parentId}"`);
2423
+ }
2424
+ }
2425
+ for (const id of parentOf.keys()) {
2426
+ const seen = /* @__PURE__ */ new Set([id]);
2427
+ let cur = parentOf.get(id) ?? null;
2428
+ while (cur) {
2429
+ if (seen.has(cur)) {
2430
+ warnings.push(`broke a parent cycle at node "${cur}"`);
2431
+ parentOf.set(cur, null);
2432
+ break;
2433
+ }
2434
+ seen.add(cur);
2435
+ cur = parentOf.get(cur) ?? null;
2436
+ }
2437
+ }
2438
+ const treeOf = /* @__PURE__ */ new Map();
2439
+ for (const node of byId.values()) {
2440
+ treeOf.set(node.id, {
2441
+ id: node.id,
2442
+ title: node.title.trim(),
2443
+ summary: node.summary?.trim() ?? "",
2444
+ kind: node.kind?.trim() || "topic",
2445
+ files: node.files ?? [],
2446
+ children: []
2447
+ });
1283
2448
  }
1284
- const repo = latest && typeof latest === "object" && "repo" in latest ? latest.repo : void 0;
1285
- return { repo, latest, history, runs };
2449
+ const roots = [];
2450
+ for (const node of byId.values()) {
2451
+ const tree = treeOf.get(node.id);
2452
+ const parent = parentOf.get(node.id);
2453
+ if (parent) treeOf.get(parent).children.push(tree);
2454
+ else roots.push(tree);
2455
+ }
2456
+ let root;
2457
+ if (roots.length === 1 && roots[0]) {
2458
+ root = roots[0];
2459
+ } else {
2460
+ if (roots.length > 1) warnings.push(`${roots.length} root nodes \u2014 gathered under the project`);
2461
+ let rootId = "root";
2462
+ for (let n = 2; treeOf.has(rootId); n++) rootId = `root-${n}`;
2463
+ root = {
2464
+ id: rootId,
2465
+ title: fallback.repo,
2466
+ summary: result.profile.summary,
2467
+ kind: "area",
2468
+ files: [],
2469
+ children: roots
2470
+ };
2471
+ }
2472
+ const nodeCount = countNodes(root);
2473
+ const depth = treeDepth(root);
2474
+ if (nodeCount <= 1) log.warn("The agent returned an (almost) empty mind map.");
2475
+ for (const w of warnings) log.warn(`mindmap: ${w}`);
2476
+ return { profile: result.profile, root, nodeCount, depth, warnings, notes: result.notes ?? null };
1286
2477
  }
1287
- function hasData(data) {
1288
- return Boolean(data.latest) || data.history.length > 0;
2478
+ function countNodes(node) {
2479
+ return 1 + node.children.reduce((acc, c) => acc + countNodes(c), 0);
1289
2480
  }
1290
- function findViewerDir() {
1291
- const dir = join5(findBundledAssetsDir(), "viewer");
1292
- if (!existsSync3(join5(dir, "index.html"))) {
1293
- throw new Error(
1294
- "Dashboard viewer assets not found. Reinstall edsger, or run `npm run sync-viewer` in a dev checkout."
1295
- );
1296
- }
1297
- return dir;
2481
+ function treeDepth(node) {
2482
+ return 1 + node.children.reduce((acc, c) => Math.max(acc, treeDepth(c)), 0);
1298
2483
  }
1299
- function readViewerSingleTemplate() {
1300
- const file = join5(findBundledAssetsDir(), "viewer-single.html");
1301
- if (!existsSync3(file)) {
1302
- throw new Error(
1303
- "Single-file viewer template not found. Reinstall edsger, or run `npm run sync-viewer`."
1304
- );
2484
+ function renderMindmapMarkdown(prepared, meta) {
2485
+ const lines = [];
2486
+ lines.push(`# Edsger Mind Map`);
2487
+ lines.push("");
2488
+ lines.push(`> ${prepared.profile.summary}`);
2489
+ lines.push("");
2490
+ lines.push(`| Field | Value |`);
2491
+ lines.push(`| --- | --- |`);
2492
+ lines.push(`| Repository | \`${meta.repo}\` |`);
2493
+ lines.push(`| Languages | ${meta.languages.join(", ") || "n/a"} |`);
2494
+ lines.push(`| Project kinds | ${prepared.profile.kinds.join(", ") || "n/a"} |`);
2495
+ lines.push(`| Nodes | ${prepared.nodeCount} (depth ${prepared.depth}) |`);
2496
+ lines.push(`| Standard sources | ${meta.layers.join(" \u2192 ")} |`);
2497
+ lines.push(`| Model | ${meta.model} |`);
2498
+ lines.push(`| Generated | ${meta.generatedAt} |`);
2499
+ lines.push("");
2500
+ lines.push(`## ${prepared.root.title}`);
2501
+ if (prepared.root.summary && prepared.root.summary !== prepared.profile.summary) {
2502
+ lines.push("");
2503
+ lines.push(prepared.root.summary);
2504
+ }
2505
+ lines.push("");
2506
+ for (const child of prepared.root.children) renderNode(lines, child, 0);
2507
+ if (prepared.notes) {
2508
+ lines.push("");
2509
+ lines.push(`## Notes`);
2510
+ lines.push("");
2511
+ lines.push(prepared.notes);
1305
2512
  }
1306
- return readFileSync2(file, "utf8");
2513
+ lines.push("");
2514
+ lines.push(`---`);
2515
+ lines.push(`<sub>Generated by [edsger](https://www.npmjs.com/package/edsger).</sub>`);
2516
+ return lines.join("\n");
1307
2517
  }
1308
- function renderSingleFile(template, data) {
1309
- const json = JSON.stringify(data).replace(/<\//g, "<\\/");
1310
- const tag = `<script>window.__EDSGER_DATA__=${json}</script>`;
1311
- if (template.includes("</head>")) return template.replace("</head>", `${tag}</head>`);
1312
- return template.replace("<body>", `<body>${tag}`);
2518
+ function renderNode(lines, node, indent2) {
2519
+ const pad = " ".repeat(indent2);
2520
+ const files = node.files.length ? ` ${node.files.map((f) => `\`${f}\``).join(" ")}` : "";
2521
+ const summary = node.summary ? ` \u2014 ${node.summary}` : "";
2522
+ lines.push(`${pad}- **${node.title}**${summary}${files}`);
2523
+ for (const child of node.children) renderNode(lines, child, indent2 + 1);
1313
2524
  }
1314
- function readJson(path) {
1315
- try {
1316
- return JSON.parse(readFileSync2(path, "utf8"));
1317
- } catch {
1318
- return null;
2525
+
2526
+ // src/commands/mindmap.ts
2527
+ async function runMindmapGenerate(opts) {
2528
+ const { config, path: configPath } = loadConfig(opts.cwd);
2529
+ const configDir = configPath ? join11(configPath, "..") : opts.cwd;
2530
+ const resolved = resolveStandard("mindmap", opts, config, configDir);
2531
+ const standard = resolved.standard;
2532
+ log.info(
2533
+ `Mind-mapping ${pc10.bold(basename4(opts.cwd))} with ${pc10.bold(standard.name)} (languages: ${resolved.languages.join(", ") || "generic"})`
2534
+ );
2535
+ log.debug(`standard layers: ${resolved.layers.join(" \u2192 ")}`);
2536
+ const prompt = buildMindmapPrompt(standard, opts.cwd, opts.focus);
2537
+ const { result, costUsd, numTurns } = await runAgent({
2538
+ prompt,
2539
+ cwd: opts.cwd,
2540
+ model: opts.model,
2541
+ maxTurns: opts.maxTurns,
2542
+ timeoutMs: opts.timeoutMs,
2543
+ allowWrite: false,
2544
+ verbose: opts.verbose,
2545
+ resultShape: MindmapResultSchema.shape,
2546
+ 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."
2547
+ });
2548
+ if (!result) {
2549
+ log.error(
2550
+ "The agent did not produce a result. Try increasing --max-turns or check credentials."
2551
+ );
2552
+ return 1;
1319
2553
  }
1320
- }
1321
- function readJsonl(path) {
1322
- if (!existsSync3(path)) return [];
1323
- const out = [];
1324
- for (const line of readFileSync2(path, "utf8").split("\n")) {
1325
- const t = line.trim();
1326
- if (!t) continue;
1327
- try {
1328
- out.push(JSON.parse(t));
1329
- } catch {
1330
- }
2554
+ const prepared = prepareMindmap(result, { repo: basename4(opts.cwd) });
2555
+ const generatedAt = (/* @__PURE__ */ new Date()).toISOString();
2556
+ const runId = generatedAt.replace(/[-:]/g, "").replace(/\.\d+Z$/, "Z");
2557
+ const git = gitInfo(opts.cwd);
2558
+ const md = renderMindmapMarkdown(prepared, {
2559
+ repo: basename4(opts.cwd),
2560
+ languages: resolved.languages,
2561
+ layers: resolved.layers,
2562
+ generatedAt,
2563
+ model: opts.model
2564
+ });
2565
+ const jsonReport = {
2566
+ schema: "edsger.mindmap/1",
2567
+ runId,
2568
+ generatedAt,
2569
+ model: opts.model,
2570
+ repo: basename4(opts.cwd),
2571
+ git,
2572
+ languages: resolved.languages,
2573
+ standard: { name: standard.name, version: standard.version, layers: resolved.layers },
2574
+ profile: prepared.profile,
2575
+ root: prepared.root,
2576
+ nodeCount: prepared.nodeCount,
2577
+ depth: prepared.depth,
2578
+ focus: opts.focus ?? null,
2579
+ notes: prepared.notes,
2580
+ costUsd
2581
+ };
2582
+ const historyRecord = {
2583
+ runId,
2584
+ generatedAt,
2585
+ commit: git.commit,
2586
+ branch: git.branch,
2587
+ dirty: git.dirty,
2588
+ model: opts.model,
2589
+ standardVersion: standard.version,
2590
+ nodeCount: prepared.nodeCount,
2591
+ depth: prepared.depth,
2592
+ costUsd
2593
+ };
2594
+ if (!opts.noWrite && !opts.dryRun) {
2595
+ const baseDir = join11(opts.cwd, opts.out);
2596
+ const mdOut = md + "\n";
2597
+ const jsonOut = JSON.stringify(jsonReport, null, 2) + "\n";
2598
+ const runDir = join11(baseDir, "runs", runId);
2599
+ mkdirSync5(runDir, { recursive: true });
2600
+ writeFileSync6(join11(runDir, "mindmap.json"), jsonOut, "utf8");
2601
+ writeFileSync6(join11(runDir, "mindmap.md"), mdOut, "utf8");
2602
+ writeFileSync6(join11(baseDir, "mindmap.json"), jsonOut, "utf8");
2603
+ writeFileSync6(join11(baseDir, "mindmap.md"), mdOut, "utf8");
2604
+ appendFileSync4(join11(baseDir, "history.jsonl"), JSON.stringify(historyRecord) + "\n", "utf8");
2605
+ log.success(
2606
+ `Mind map written to ${pc10.bold(join11(opts.out, "mindmap.md"))} ${pc10.dim(`(run archived at ${join11(opts.out, "runs", runId)})`)}`
2607
+ );
1331
2608
  }
1332
- return out;
2609
+ printSummary5(prepared, numTurns, costUsd);
2610
+ if (opts.json) process.stdout.write(JSON.stringify(jsonReport, null, 2) + "\n");
2611
+ return 0;
1333
2612
  }
1334
-
1335
- // src/core/open.ts
1336
- import { spawn } from "child_process";
1337
- function openInBrowser(target) {
1338
- const platform = process.platform;
1339
- const cmd = platform === "darwin" ? "open" : platform === "win32" ? "cmd" : "xdg-open";
1340
- const args = platform === "win32" ? ["/c", "start", "", target] : [target];
1341
- try {
1342
- spawn(cmd, args, { stdio: "ignore", detached: true }).unref();
1343
- } catch {
2613
+ function printSummary5(prepared, numTurns, costUsd) {
2614
+ log.heading(`${prepared.root.title} \u2014 ${prepared.nodeCount} nodes, depth ${prepared.depth}`);
2615
+ const line = (node, indent2) => {
2616
+ const kids = node.children.length ? pc10.dim(` (${countNodes2(node) - 1})`) : "";
2617
+ process.stderr.write(`${indent2}${pc10.cyan("\u2022")} ${node.title}${kids}
2618
+ `);
2619
+ };
2620
+ for (const branch of prepared.root.children) {
2621
+ line(branch, " ");
2622
+ for (const child of branch.children) line(child, " ");
1344
2623
  }
2624
+ log.step(`${numTurns} turns \xB7 ~$${costUsd.toFixed(3)}`);
1345
2625
  }
1346
-
1347
- // src/commands/serve.ts
1348
- var MIME = {
1349
- ".html": "text/html; charset=utf-8",
1350
- ".js": "text/javascript; charset=utf-8",
1351
- ".css": "text/css; charset=utf-8",
1352
- ".json": "application/json; charset=utf-8",
1353
- ".svg": "image/svg+xml",
1354
- ".map": "application/json",
1355
- ".ico": "image/x-icon",
1356
- ".woff2": "font/woff2",
1357
- ".woff": "font/woff"
1358
- };
1359
- async function runServe(opts) {
1360
- const benchmarkDir = join6(opts.cwd, opts.dir);
1361
- const viewerDir = findViewerDir();
1362
- const server = createServer((req, res) => {
1363
- const url = (req.url ?? "/").split("?")[0] ?? "/";
1364
- if (url === "/edsger-data.json") {
1365
- res.writeHead(200, {
1366
- "content-type": "application/json; charset=utf-8",
1367
- "cache-control": "no-store"
1368
- });
1369
- res.end(JSON.stringify(assembleData(benchmarkDir)));
1370
- return;
1371
- }
1372
- const rel = url === "/" ? "index.html" : decodeURIComponent(url.replace(/^\/+/, ""));
1373
- const safe = normalize(rel).replace(/^(\.\.(?:[/\\]|$))+/, "");
1374
- let file = join6(viewerDir, safe);
1375
- if (!file.startsWith(viewerDir)) {
1376
- res.writeHead(403);
1377
- res.end("forbidden");
1378
- return;
1379
- }
1380
- if (!existsSync4(file) || statSync2(file).isDirectory()) file = join6(viewerDir, "index.html");
1381
- res.writeHead(200, { "content-type": MIME[extname(file)] ?? "application/octet-stream" });
1382
- createReadStream(file).pipe(res);
1383
- });
1384
- return new Promise((resolve3, reject) => {
1385
- server.on("error", reject);
1386
- server.listen(opts.port, opts.host, () => {
1387
- const addr = server.address();
1388
- const port = typeof addr === "object" && addr ? addr.port : opts.port;
1389
- const shownHost = opts.host === "0.0.0.0" ? "localhost" : opts.host;
1390
- const url = `http://${shownHost}:${port}/`;
1391
- if (!hasData(assembleData(benchmarkDir))) {
1392
- log.warn(`No benchmark data in ${opts.dir} yet \u2014 run \`edsger benchmark\` first.`);
1393
- }
1394
- log.success(`Dashboard at ${pc6.bold(pc6.cyan(url))} ${pc6.dim("(Ctrl-C to stop)")}`);
1395
- if (opts.open) openInBrowser(url);
1396
- });
1397
- const shutdown = () => {
1398
- server.close(() => resolve3(0));
1399
- };
1400
- process.on("SIGINT", shutdown);
1401
- process.on("SIGTERM", shutdown);
2626
+ function countNodes2(node) {
2627
+ return 1 + node.children.reduce((acc, c) => acc + countNodes2(c), 0);
2628
+ }
2629
+ async function runMindmapServe(opts) {
2630
+ const mindmapDir = join11(opts.cwd, opts.dir);
2631
+ const viewerDir = findMindmapViewerDir();
2632
+ return startViewerServer({
2633
+ viewerDir,
2634
+ indexFile: "mindmap.html",
2635
+ dataRoute: "/edsger-mindmap.json",
2636
+ getData: () => assembleMindmapData(mindmapDir),
2637
+ port: opts.port,
2638
+ host: opts.host,
2639
+ open: opts.open,
2640
+ emptyHint: hasData(assembleMindmapData(mindmapDir)) ? void 0 : `No mind-map data in ${opts.dir} yet \u2014 run \`edsger mindmap\` first.`
1402
2641
  });
1403
2642
  }
1404
-
1405
- // src/commands/report.ts
1406
- import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync3 } from "fs";
1407
- import { dirname as dirname2, isAbsolute as isAbsolute2, join as join7 } from "path";
1408
- import pc7 from "picocolors";
1409
- async function runReport(opts) {
1410
- const benchmarkDir = join7(opts.cwd, opts.dir);
1411
- const data = assembleData(benchmarkDir);
2643
+ async function runMindmapReport(opts) {
2644
+ const mindmapDir = join11(opts.cwd, opts.dir);
2645
+ const data = assembleMindmapData(mindmapDir);
1412
2646
  if (!hasData(data)) {
1413
- log.error(`No benchmark data in ${opts.dir}. Run \`edsger benchmark\` first.`);
2647
+ log.error(`No mind-map data in ${opts.dir}. Run \`edsger mindmap\` first.`);
1414
2648
  return 1;
1415
2649
  }
1416
- const html = renderSingleFile(readViewerSingleTemplate(), data);
1417
- const out = isAbsolute2(opts.html) ? opts.html : join7(opts.cwd, opts.html);
1418
- mkdirSync2(dirname2(out), { recursive: true });
1419
- writeFileSync3(out, html, "utf8");
2650
+ const html = renderMindmapSingleFile(readMindmapViewerSingleTemplate(), data);
2651
+ const out = isAbsolute5(opts.html) ? opts.html : join11(opts.cwd, opts.html);
2652
+ mkdirSync5(dirname5(out), { recursive: true });
2653
+ writeFileSync6(out, html, "utf8");
1420
2654
  log.success(
1421
- `Wrote self-contained report to ${pc7.bold(opts.html)} ${pc7.dim(`(${Math.round(html.length / 1024)} kB, open in any browser)`)}`
2655
+ `Wrote self-contained mind-map viewer to ${pc10.bold(opts.html)} ${pc10.dim(`(${Math.round(html.length / 1024)} kB, open in any browser)`)}`
1422
2656
  );
1423
2657
  if (opts.open) openInBrowser(out);
1424
2658
  return 0;
@@ -1428,22 +2662,22 @@ async function runReport(opts) {
1428
2662
  import {
1429
2663
  cpSync,
1430
2664
  existsSync as existsSync5,
1431
- mkdirSync as mkdirSync3,
2665
+ mkdirSync as mkdirSync6,
1432
2666
  readFileSync as readFileSync3,
1433
2667
  readdirSync as readdirSync3,
1434
2668
  rmSync as rmSync2,
1435
2669
  statSync as statSync3,
1436
- writeFileSync as writeFileSync4
2670
+ writeFileSync as writeFileSync7
1437
2671
  } from "fs";
1438
2672
  import { homedir as homedir2 } from "os";
1439
- import { join as join8 } from "path";
1440
- import pc8 from "picocolors";
1441
- var MANAGED = ["benchmark", "pr-review", "pr-resolve"];
2673
+ import { join as join12 } from "path";
2674
+ import pc11 from "picocolors";
2675
+ var MANAGED = ["benchmark", "audit", "pr-review", "pr-resolve"];
1442
2676
  function targetSkillsDir(scope) {
1443
- return scope.global ? join8(homedir2(), ".claude", "skills") : join8(scope.cwd, ".claude", "skills");
2677
+ return scope.global ? join12(homedir2(), ".claude", "skills") : join12(scope.cwd, ".claude", "skills");
1444
2678
  }
1445
2679
  function sourceSkillsDir() {
1446
- return join8(findBundledAssetsDir(), "skills");
2680
+ return join12(findBundledAssetsDir(), "skills");
1447
2681
  }
1448
2682
  async function runSkillsInstall(opts) {
1449
2683
  const assetsDir = findBundledAssetsDir();
@@ -1453,20 +2687,20 @@ async function runSkillsInstall(opts) {
1453
2687
  return 1;
1454
2688
  }
1455
2689
  const dest = targetSkillsDir(opts);
1456
- mkdirSync3(dest, { recursive: true });
1457
- const available = readdirSync3(src).filter((n) => existsSync5(join8(src, n, "SKILL.md")));
2690
+ mkdirSync6(dest, { recursive: true });
2691
+ const available = readdirSync3(src).filter((n) => existsSync5(join12(src, n, "SKILL.md")));
1458
2692
  let installed = 0;
1459
2693
  for (const name of available) {
1460
- const outDir = join8(dest, name);
2694
+ const outDir = join12(dest, name);
1461
2695
  if (existsSync5(outDir) && !opts.force) {
1462
2696
  log.warn(`skill "${name}" already exists \u2014 use --force to overwrite`);
1463
2697
  continue;
1464
2698
  }
1465
2699
  rmSync2(outDir, { recursive: true, force: true });
1466
- cpSync(join8(src, name), outDir, { recursive: true });
1467
- const skillFile = join8(outDir, "SKILL.md");
2700
+ cpSync(join12(src, name), outDir, { recursive: true });
2701
+ const skillFile = join12(outDir, "SKILL.md");
1468
2702
  const rewritten = readFileSync3(skillFile, "utf8").split("${CLAUDE_PLUGIN_ROOT}").join(assetsDir);
1469
- writeFileSync4(skillFile, rewritten, "utf8");
2703
+ writeFileSync7(skillFile, rewritten, "utf8");
1470
2704
  installed++;
1471
2705
  log.success(`installed /${name}`);
1472
2706
  }
@@ -1474,17 +2708,17 @@ async function runSkillsInstall(opts) {
1474
2708
  log.info("Nothing installed.");
1475
2709
  return 0;
1476
2710
  }
1477
- log.heading(`Installed ${installed} skill(s) into ${pc8.bold(dest)}`);
2711
+ log.heading(`Installed ${installed} skill(s) into ${pc11.bold(dest)}`);
1478
2712
  log.step(`scope: ${opts.global ? "user (all projects)" : "this project"}`);
1479
2713
  log.step(`standards: ${assetsDir}`);
1480
- log.step("Open Claude Code and run /benchmark, /pr-review, or /pr-resolve.");
2714
+ log.step("Open Claude Code and run /benchmark, /audit, /pr-review, or /pr-resolve.");
1481
2715
  return 0;
1482
2716
  }
1483
2717
  async function runSkillsUninstall(opts) {
1484
2718
  const dest = targetSkillsDir(opts);
1485
2719
  let removed = 0;
1486
2720
  for (const name of MANAGED) {
1487
- const dir = join8(dest, name);
2721
+ const dir = join12(dest, name);
1488
2722
  if (existsSync5(dir)) {
1489
2723
  rmSync2(dir, { recursive: true, force: true });
1490
2724
  removed++;
@@ -1501,13 +2735,13 @@ async function runSkillsList(opts) {
1501
2735
  ]) {
1502
2736
  const dir = targetSkillsDir(scope);
1503
2737
  const present = MANAGED.filter((n) => {
1504
- const p = join8(dir, n, "SKILL.md");
2738
+ const p = join12(dir, n, "SKILL.md");
1505
2739
  return existsSync5(p) && statSync3(p).isFile();
1506
2740
  });
1507
2741
  log.heading(`${label}: ${dir}`);
1508
- if (present.length) for (const n of present) process.stderr.write(` ${pc8.green("\u2713")} /${n}
2742
+ if (present.length) for (const n of present) process.stderr.write(` ${pc11.green("\u2713")} /${n}
1509
2743
  `);
1510
- else process.stderr.write(` ${pc8.dim("(none installed)")}
2744
+ else process.stderr.write(` ${pc11.dim("(none installed)")}
1511
2745
  `);
1512
2746
  }
1513
2747
  return 0;
@@ -1549,7 +2783,7 @@ async function guard(fn) {
1549
2783
  }
1550
2784
  var program = new Command();
1551
2785
  program.name("edsger").description(
1552
- "Agentic CLI to benchmark repositories and review/resolve pull requests\nagainst industrial-grade standards (from edsger-assets, fully overridable)."
2786
+ "Agentic CLI to benchmark and audit repositories and review/resolve pull requests\nagainst industrial-grade standards (from edsger-assets, fully overridable)."
1553
2787
  ).version(VERSION, "-V, --version");
1554
2788
  addGlobalOptions(
1555
2789
  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")
@@ -1563,6 +2797,66 @@ addGlobalOptions(
1563
2797
  return code;
1564
2798
  });
1565
2799
  });
2800
+ var FAIL_ON_SEVERITIES = ["critical", "high", "medium", "low"];
2801
+ var audit = program.command("audit").alias("review").description(
2802
+ "Audit the whole repository: hunt for unprofessional, outdated, or fragile implementations across every dimension and write an organised issue report"
2803
+ );
2804
+ addGlobalOptions(
2805
+ 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(
2806
+ new Option(
2807
+ "--fail-on <severity>",
2808
+ "exit with code 2 if findings exist at or above this severity (CI gate)"
2809
+ ).choices(FAIL_ON_SEVERITIES)
2810
+ ).option("--serve", "open the findings dashboard after auditing")
2811
+ ).action(
2812
+ async (opts) => {
2813
+ await guard(async () => {
2814
+ const g = toGlobal(opts);
2815
+ const code = await runAudit({
2816
+ ...g,
2817
+ out: opts.out,
2818
+ noWrite: opts.write === false,
2819
+ only: opts.only ? opts.only.split(",").map((s) => s.trim()).filter(Boolean) : void 0,
2820
+ focus: opts.focus,
2821
+ failOn: opts.failOn
2822
+ });
2823
+ if (code !== 1 && opts.serve) {
2824
+ const serveCode = await runAuditServe({
2825
+ cwd: g.cwd,
2826
+ dir: opts.out,
2827
+ port: 4580,
2828
+ host: "127.0.0.1",
2829
+ open: true
2830
+ });
2831
+ return code || serveCode;
2832
+ }
2833
+ return code;
2834
+ });
2835
+ }
2836
+ );
2837
+ 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(
2838
+ async (opts) => {
2839
+ await guard(
2840
+ () => runAuditServe({
2841
+ cwd: resolve2(opts.cwd ?? process.cwd()),
2842
+ dir: opts.dir,
2843
+ port: Number.parseInt(opts.port, 10) || 4580,
2844
+ host: opts.host,
2845
+ open: opts.open !== false
2846
+ })
2847
+ );
2848
+ }
2849
+ );
2850
+ 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) => {
2851
+ await guard(
2852
+ () => runAuditReport({
2853
+ cwd: resolve2(opts.cwd ?? process.cwd()),
2854
+ dir: opts.dir,
2855
+ html: opts.html,
2856
+ open: Boolean(opts.open)
2857
+ })
2858
+ );
2859
+ });
1566
2860
  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(
1567
2861
  async (opts) => {
1568
2862
  await guard(
@@ -1586,6 +2880,105 @@ program.command("report").description("Export a self-contained HTML dashboard (d
1586
2880
  })
1587
2881
  );
1588
2882
  });
2883
+ var diagram = program.command("diagram").description("Generate & view diagrams (architecture, ER, sequence, flow) for a repository");
2884
+ addGlobalOptions(
2885
+ diagram.command("generate", { isDefault: true }).description("Analyze the repository and generate diagrams as Mermaid").option("--out <dir>", "diagram output directory (relative to repo)", ".edsger/diagrams").option("--no-write", "do not write diagram files into the repo").option("--only <ids>", "comma-separated catalog diagram ids to generate").option("--serve", "open the diagram dashboard after generating")
2886
+ ).action(
2887
+ async (opts) => {
2888
+ await guard(async () => {
2889
+ const g = toGlobal(opts);
2890
+ const only = opts.only ? opts.only.split(",").map((s) => s.trim()).filter(Boolean) : void 0;
2891
+ const code = await runDiagramGenerate({
2892
+ ...g,
2893
+ out: opts.out,
2894
+ noWrite: opts.write === false,
2895
+ only
2896
+ });
2897
+ if (code === 0 && opts.serve) {
2898
+ return runDiagramServe({
2899
+ cwd: g.cwd,
2900
+ dir: opts.out,
2901
+ port: 4578,
2902
+ host: "127.0.0.1",
2903
+ open: true
2904
+ });
2905
+ }
2906
+ return code;
2907
+ });
2908
+ }
2909
+ );
2910
+ diagram.command("serve").description("Serve the diagram dashboard locally").option("-C, --cwd <dir>", "repository directory").option("--dir <dir>", "diagram output directory (relative to repo)", ".edsger/diagrams").option("--port <n>", "port to listen on", "4578").option("--host <host>", "host to bind", "127.0.0.1").option("--no-open", "do not open the browser automatically").action(
2911
+ async (opts) => {
2912
+ await guard(
2913
+ () => runDiagramServe({
2914
+ cwd: resolve2(opts.cwd ?? process.cwd()),
2915
+ dir: opts.dir,
2916
+ port: Number.parseInt(opts.port, 10) || 4578,
2917
+ host: opts.host,
2918
+ open: opts.open !== false
2919
+ })
2920
+ );
2921
+ }
2922
+ );
2923
+ diagram.command("report").description("Export a self-contained HTML diagram viewer (data inlined) for sharing").option("-C, --cwd <dir>", "repository directory").option("--dir <dir>", "diagram output directory (relative to repo)", ".edsger/diagrams").option("--html <path>", "output HTML file path", "edsger-diagrams.html").option("--open", "open the exported file after writing").action(async (opts) => {
2924
+ await guard(
2925
+ () => runDiagramReport({
2926
+ cwd: resolve2(opts.cwd ?? process.cwd()),
2927
+ dir: opts.dir,
2928
+ html: opts.html,
2929
+ open: Boolean(opts.open)
2930
+ })
2931
+ );
2932
+ });
2933
+ var mindmap = program.command("mindmap").description("Generate & browse an implementation mind map (Workflowy-style outline) of a repo");
2934
+ addGlobalOptions(
2935
+ 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")
2936
+ ).action(
2937
+ async (opts) => {
2938
+ await guard(async () => {
2939
+ const g = toGlobal(opts);
2940
+ const code = await runMindmapGenerate({
2941
+ ...g,
2942
+ out: opts.out,
2943
+ noWrite: opts.write === false,
2944
+ focus: opts.focus
2945
+ });
2946
+ if (code === 0 && opts.serve) {
2947
+ return runMindmapServe({
2948
+ cwd: g.cwd,
2949
+ dir: opts.out,
2950
+ port: 4579,
2951
+ host: "127.0.0.1",
2952
+ open: true
2953
+ });
2954
+ }
2955
+ return code;
2956
+ });
2957
+ }
2958
+ );
2959
+ 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(
2960
+ async (opts) => {
2961
+ await guard(
2962
+ () => runMindmapServe({
2963
+ cwd: resolve2(opts.cwd ?? process.cwd()),
2964
+ dir: opts.dir,
2965
+ port: Number.parseInt(opts.port, 10) || 4579,
2966
+ host: opts.host,
2967
+ open: opts.open !== false
2968
+ })
2969
+ );
2970
+ }
2971
+ );
2972
+ 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) => {
2973
+ await guard(
2974
+ () => runMindmapReport({
2975
+ cwd: resolve2(opts.cwd ?? process.cwd()),
2976
+ dir: opts.dir,
2977
+ html: opts.html,
2978
+ open: Boolean(opts.open)
2979
+ })
2980
+ );
2981
+ });
1589
2982
  addGlobalOptions(
1590
2983
  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)")
1591
2984
  ).action(async (pr, opts) => {
@@ -1609,7 +3002,9 @@ addGlobalOptions(
1609
3002
  );
1610
3003
  }
1611
3004
  );
1612
- var skills = program.command("skills").description("install edsger as Claude Code skills (/benchmark, /pr-review, /pr-resolve)");
3005
+ var skills = program.command("skills").description(
3006
+ "install edsger as Claude Code skills (/benchmark, /audit, /pr-review, /pr-resolve)"
3007
+ );
1613
3008
  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) => {
1614
3009
  await guard(
1615
3010
  () => runSkillsInstall({
@@ -1634,11 +3029,18 @@ program.addHelpText(
1634
3029
  "after",
1635
3030
  `
1636
3031
  Examples:
1637
- ${pc9.dim("$")} edsger benchmark
1638
- ${pc9.dim("$")} edsger benchmark -C ./my-repo --json
1639
- ${pc9.dim("$")} edsger pr-review 123 --post
1640
- ${pc9.dim("$")} edsger pr-resolve 123 --instruction "fix the failing test" --push
1641
- ${pc9.dim("$")} edsger skills install ${pc9.dim("# enable /benchmark etc. in Claude Code")}
3032
+ ${pc12.dim("$")} edsger benchmark
3033
+ ${pc12.dim("$")} edsger benchmark -C ./my-repo --json
3034
+ ${pc12.dim("$")} edsger audit ${pc12.dim("# nitpick the whole repo, report issues")}
3035
+ ${pc12.dim("$")} edsger audit --fail-on high ${pc12.dim("# CI gate: fail on high/critical findings")}
3036
+ ${pc12.dim("$")} edsger diagram ${pc12.dim("# generate diagrams for the current repo")}
3037
+ ${pc12.dim("$")} edsger diagram --serve ${pc12.dim("# \u2026and open them in the browser")}
3038
+ ${pc12.dim("$")} edsger diagram report --html diagrams.html
3039
+ ${pc12.dim("$")} edsger mindmap ${pc12.dim("# implementation mind map of the repo")}
3040
+ ${pc12.dim("$")} edsger mindmap --serve ${pc12.dim("# \u2026browse it Workflowy-style")}
3041
+ ${pc12.dim("$")} edsger pr-review 123 --post
3042
+ ${pc12.dim("$")} edsger pr-resolve 123 --instruction "fix the failing test" --push
3043
+ ${pc12.dim("$")} edsger skills install ${pc12.dim("# enable /benchmark etc. in Claude Code")}
1642
3044
 
1643
3045
  Auth: the agent uses your Claude Code login (run \`claude\` once to sign in); run \`gh auth login\` for PR commands.
1644
3046
  `