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.
- package/README.md +101 -2
- package/assets/README.md +31 -18
- package/assets/audit/base.yaml +187 -0
- package/assets/audit/csharp.yaml +46 -0
- package/assets/audit/dart.yaml +46 -0
- package/assets/audit/go.yaml +46 -0
- package/assets/audit/java.yaml +46 -0
- package/assets/audit/python.yaml +49 -0
- package/assets/audit/typescript.yaml +51 -0
- package/assets/audit-viewer/assets/audit--os0TXXZ.css +1 -0
- package/assets/audit-viewer/assets/audit-CSblYELs.js +2 -0
- package/assets/audit-viewer/audit.html +31 -0
- package/assets/audit-viewer-single.html +32 -0
- package/assets/diagrams/base.yaml +351 -0
- package/assets/diagrams/csharp.yaml +48 -0
- package/assets/diagrams/dart.yaml +41 -0
- package/assets/diagrams/go.yaml +44 -0
- package/assets/diagrams/java.yaml +43 -0
- package/assets/diagrams/python.yaml +41 -0
- package/assets/diagrams/typescript.yaml +45 -0
- package/assets/diagrams-viewer/assets/abnfDiagram-VRR7QNED-CWLpbgGU.js +1 -0
- package/assets/diagrams-viewer/assets/arc-B4VipCr_.js +1 -0
- package/assets/diagrams-viewer/assets/architectureDiagram-ZJ3FMSHR-BMDodQXV.js +36 -0
- package/assets/diagrams-viewer/assets/blockDiagram-677ZJIJ3-DbHoVVH-.js +132 -0
- package/assets/diagrams-viewer/assets/c4Diagram-LMCZKHZV-CyUEj98S.js +10 -0
- package/assets/diagrams-viewer/assets/channel-DWh7ErxY.js +1 -0
- package/assets/diagrams-viewer/assets/chunk-2Q5K7J3B-CWu0p0os.js +1 -0
- package/assets/diagrams-viewer/assets/chunk-32BRIVSS-BhVWSCoK.js +1 -0
- package/assets/diagrams-viewer/assets/chunk-5VM5RSS4-B_2wZBAn.js +15 -0
- package/assets/diagrams-viewer/assets/chunk-EX3LRPZG-D_MRgtfL.js +231 -0
- package/assets/diagrams-viewer/assets/chunk-JWPE2WC7-dsh1qiy0.js +1 -0
- package/assets/diagrams-viewer/assets/chunk-MOJQB5TN-BvLm4a6B.js +88 -0
- package/assets/diagrams-viewer/assets/chunk-RYQCIY6F-gJyPbIpx.js +1 -0
- package/assets/diagrams-viewer/assets/chunk-V7JOEXUC-BMvj0zJM.js +206 -0
- package/assets/diagrams-viewer/assets/chunk-VR4S4FIN-Di2iBUXs.js +1 -0
- package/assets/diagrams-viewer/assets/chunk-XXDRQBXY-Cfajbd4I.js +1 -0
- package/assets/diagrams-viewer/assets/classDiagram-OUVF2IWQ-winWhRce.js +1 -0
- package/assets/diagrams-viewer/assets/classDiagram-v2-EOCWNBFH-winWhRce.js +1 -0
- package/assets/diagrams-viewer/assets/cose-bilkent-JH36ORCC-ClE2EhTL.js +1 -0
- package/assets/diagrams-viewer/assets/cynefin-VYW2F7L2-nm5z0Jwc.js +178 -0
- package/assets/diagrams-viewer/assets/cynefinDiagram-TSTJHNR4-CGhnRh5V.js +62 -0
- package/assets/diagrams-viewer/assets/cytoscape.esm-CUqq0XTU.js +331 -0
- package/assets/diagrams-viewer/assets/dagre-VKFMJZFB-COlahHKk.js +4 -0
- package/assets/diagrams-viewer/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/assets/diagrams-viewer/assets/diagram-FQU43EPY-DsUclBK0.js +3 -0
- package/assets/diagrams-viewer/assets/diagram-G47NLZAW-CnjT9SvX.js +24 -0
- package/assets/diagrams-viewer/assets/diagram-NH7WQ7WH-DiGoGOyA.js +24 -0
- package/assets/diagrams-viewer/assets/diagram-OA4YK3LP-qi8nz9qe.js +30 -0
- package/assets/diagrams-viewer/assets/diagram-WEI45ONY-BV3PGzwL.js +41 -0
- package/assets/diagrams-viewer/assets/diagrams-8nCc1utO.css +1 -0
- package/assets/diagrams-viewer/assets/diagrams-CF48btj0.js +314 -0
- package/assets/diagrams-viewer/assets/ebnfDiagram-CCIWWBDH-CtKWe-dh.js +1 -0
- package/assets/diagrams-viewer/assets/erDiagram-Q63AITRT-Aajee3Fz.js +85 -0
- package/assets/diagrams-viewer/assets/flowDiagram-23GEKE2U-N3Xe2K9t.js +156 -0
- package/assets/diagrams-viewer/assets/ganttDiagram-NO4QXBWP-Bjicdg8S.js +292 -0
- package/assets/diagrams-viewer/assets/gitGraphDiagram-IHSO6WYX-CacIXKIe.js +106 -0
- package/assets/diagrams-viewer/assets/graph-C2PyVmSI.js +1 -0
- package/assets/diagrams-viewer/assets/infoDiagram-FWYZ7A6U-CVmDapTx.js +2 -0
- package/assets/diagrams-viewer/assets/init-Gi6I4Gst.js +1 -0
- package/assets/diagrams-viewer/assets/ishikawaDiagram-FXEZZL3T-DMJf1YQN.js +70 -0
- package/assets/diagrams-viewer/assets/journeyDiagram-5HDEW3XC-Bk_U6MPk.js +139 -0
- package/assets/diagrams-viewer/assets/kanban-definition-HUTT4EX6-n05wGXiD.js +89 -0
- package/assets/diagrams-viewer/assets/katex-C5jXJg4s.js +257 -0
- package/assets/diagrams-viewer/assets/layout-HEHzrzvT.js +1 -0
- package/assets/diagrams-viewer/assets/linear-DD_zky65.js +1 -0
- package/assets/diagrams-viewer/assets/map-BQHxRNoK.js +1 -0
- package/assets/diagrams-viewer/assets/mindmap-definition-LN4V7U3C-BWt_I9vr.js +96 -0
- package/assets/diagrams-viewer/assets/ordinal-Cboi1Yqb.js +1 -0
- package/assets/diagrams-viewer/assets/pegDiagram-2B236MQR-CCZTIXBZ.js +1 -0
- package/assets/diagrams-viewer/assets/pieDiagram-ENE6RG2P-BmIQql1w.js +39 -0
- package/assets/diagrams-viewer/assets/quadrantDiagram-ABIIQ3AL-BPvOH5QN.js +7 -0
- package/assets/diagrams-viewer/assets/railroadDiagram-RFXS5EU6-DpgVbjSl.js +1 -0
- package/assets/diagrams-viewer/assets/requirementDiagram-TGXJPOKE-CDVpI3MH.js +84 -0
- package/assets/diagrams-viewer/assets/sankeyDiagram-HTMAVEWB-DU2J23Xy.js +40 -0
- package/assets/diagrams-viewer/assets/sequenceDiagram-DBY2YBRQ-BIVlCWU4.js +162 -0
- package/assets/diagrams-viewer/assets/sizeCapture-X5ZJPWSS-C9KxyKN2.js +1 -0
- package/assets/diagrams-viewer/assets/stateDiagram-2N3HPSRC-Ba5mRI9d.js +1 -0
- package/assets/diagrams-viewer/assets/stateDiagram-v2-6OUMAXLB-Dw-0OG6b.js +1 -0
- package/assets/diagrams-viewer/assets/swimlanes-5IMT3BWC-aOsD2O80.js +2 -0
- package/assets/diagrams-viewer/assets/swimlanesDiagram-G3AALYLV-KujklZml.js +8 -0
- package/assets/diagrams-viewer/assets/timeline-definition-FHXFAJF6-CmYd84Hc.js +120 -0
- package/assets/diagrams-viewer/assets/vennDiagram-L72KCM5P-CDiMK-z9.js +34 -0
- package/assets/diagrams-viewer/assets/wardleyDiagram-EHGQE667-BKLsv25u.js +78 -0
- package/assets/diagrams-viewer/assets/xychartDiagram-FW5EYKEG-Bpj6Ub-a.js +7 -0
- package/assets/diagrams-viewer/diagrams.html +31 -0
- package/assets/diagrams-viewer-single.html +3593 -0
- package/assets/manifest.yaml +13 -1
- package/assets/mindmap/base.yaml +115 -0
- package/assets/mindmap/csharp.yaml +32 -0
- package/assets/mindmap/dart.yaml +42 -0
- package/assets/mindmap/go.yaml +33 -0
- package/assets/mindmap/java.yaml +40 -0
- package/assets/mindmap/python.yaml +43 -0
- package/assets/mindmap/typescript.yaml +47 -0
- package/assets/mindmap-viewer/assets/mindmap-D7KMR9EM.css +1 -0
- package/assets/mindmap-viewer/assets/mindmap-Di5RM-4N.js +4 -0
- package/assets/mindmap-viewer/mindmap.html +31 -0
- package/assets/mindmap-viewer-single.html +34 -0
- package/assets/schema/audit.schema.json +66 -0
- package/assets/schema/diagrams.schema.json +100 -0
- package/assets/schema/mindmap.schema.json +69 -0
- package/assets/skills/audit/SKILL.md +76 -0
- package/assets/skills/diagram/SKILL.md +70 -0
- package/assets/skills/mindmap/SKILL.md +69 -0
- package/assets/viewer/assets/index-BFtGar9N.css +1 -0
- package/assets/viewer/assets/index-XOKyyudT.js +3 -0
- package/assets/viewer/index.html +19 -2
- package/assets/viewer-single.html +21 -4
- package/dist/index.js +1720 -318
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/assets/viewer/assets/index-C7hwGkuU.js +0 -3
- 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
|
|
7
|
+
import pc12 from "picocolors";
|
|
8
8
|
|
|
9
|
-
// src/commands/
|
|
9
|
+
// src/commands/audit.ts
|
|
10
10
|
import { appendFileSync, mkdirSync, writeFileSync } from "fs";
|
|
11
|
-
import { basename, join as
|
|
12
|
-
import
|
|
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
|
|
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/
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
const
|
|
521
|
-
const
|
|
522
|
-
const
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
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
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
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
|
|
554
|
-
|
|
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
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
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
|
-
|
|
573
|
-
|
|
574
|
-
|
|
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
|
-
|
|
584
|
-
|
|
585
|
-
|
|
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
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
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
|
|
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
|
|
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(`> ${
|
|
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 | ${
|
|
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(`##
|
|
867
|
+
lines.push(`## Findings by severity`);
|
|
613
868
|
lines.push("");
|
|
614
|
-
lines.push(`|
|
|
615
|
-
lines.push(`| --- | ---:
|
|
616
|
-
for (const
|
|
617
|
-
lines.push(`| ${
|
|
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(`| **
|
|
874
|
+
lines.push(`| **total** | **${organized.total}** |`);
|
|
620
875
|
lines.push("");
|
|
621
|
-
if (
|
|
622
|
-
lines.push(
|
|
623
|
-
|
|
624
|
-
|
|
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 (
|
|
628
|
-
lines.push(`##
|
|
882
|
+
if (organized.quickWins.length) {
|
|
883
|
+
lines.push(`## Quick wins`);
|
|
629
884
|
lines.push("");
|
|
630
|
-
for (const
|
|
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
|
-
|
|
634
|
-
|
|
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
|
|
637
|
-
lines.push(`- **[${
|
|
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
|
-
|
|
642
|
-
|
|
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
|
|
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
|
|
657
|
-
|
|
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
|
|
660
|
-
|
|
661
|
-
return
|
|
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 ?
|
|
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 ${
|
|
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 =
|
|
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:
|
|
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:
|
|
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 =
|
|
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 =
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
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 ${
|
|
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
|
-
|
|
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
|
|
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 ?
|
|
813
|
-
return color("\u2588".repeat(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
|
|
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
|
|
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
|
|
1528
|
+
import { mkdtempSync, rmSync, writeFileSync as writeFileSync3 } from "fs";
|
|
853
1529
|
import { tmpdir } from "os";
|
|
854
|
-
import { join as
|
|
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(
|
|
916
|
-
const file =
|
|
917
|
-
|
|
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
|
|
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 ${
|
|
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 "${
|
|
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:
|
|
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 ${
|
|
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) =>
|
|
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(` ${
|
|
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
|
|
1088
|
-
if (s === "critical" || s === "high") return
|
|
1089
|
-
if (s === "medium") return
|
|
1090
|
-
return
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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 ${
|
|
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 "${
|
|
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:
|
|
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
|
-
|
|
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 ${
|
|
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
|
|
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(` ${
|
|
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
|
|
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 {
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
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/
|
|
1271
|
-
import {
|
|
1272
|
-
import { join as
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
const
|
|
1276
|
-
const
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
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
|
|
1285
|
-
|
|
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
|
|
1288
|
-
return
|
|
2478
|
+
function countNodes(node) {
|
|
2479
|
+
return 1 + node.children.reduce((acc, c) => acc + countNodes(c), 0);
|
|
1289
2480
|
}
|
|
1290
|
-
function
|
|
1291
|
-
|
|
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
|
|
1300
|
-
const
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
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
|
-
|
|
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
|
|
1309
|
-
const
|
|
1310
|
-
const
|
|
1311
|
-
|
|
1312
|
-
|
|
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
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
}
|
|
1318
|
-
|
|
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
|
-
|
|
1322
|
-
|
|
1323
|
-
const
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
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
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
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
|
-
|
|
1406
|
-
|
|
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
|
|
2647
|
+
log.error(`No mind-map data in ${opts.dir}. Run \`edsger mindmap\` first.`);
|
|
1414
2648
|
return 1;
|
|
1415
2649
|
}
|
|
1416
|
-
const html =
|
|
1417
|
-
const out =
|
|
1418
|
-
|
|
1419
|
-
|
|
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
|
|
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
|
|
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
|
|
2670
|
+
writeFileSync as writeFileSync7
|
|
1437
2671
|
} from "fs";
|
|
1438
2672
|
import { homedir as homedir2 } from "os";
|
|
1439
|
-
import { join as
|
|
1440
|
-
import
|
|
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 ?
|
|
2677
|
+
return scope.global ? join12(homedir2(), ".claude", "skills") : join12(scope.cwd, ".claude", "skills");
|
|
1444
2678
|
}
|
|
1445
2679
|
function sourceSkillsDir() {
|
|
1446
|
-
return
|
|
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
|
-
|
|
1457
|
-
const available = readdirSync3(src).filter((n) => existsSync5(
|
|
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 =
|
|
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(
|
|
1467
|
-
const skillFile =
|
|
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
|
-
|
|
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 ${
|
|
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 =
|
|
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 =
|
|
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(` ${
|
|
2742
|
+
if (present.length) for (const n of present) process.stderr.write(` ${pc11.green("\u2713")} /${n}
|
|
1509
2743
|
`);
|
|
1510
|
-
else process.stderr.write(` ${
|
|
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(
|
|
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
|
-
${
|
|
1638
|
-
${
|
|
1639
|
-
${
|
|
1640
|
-
${
|
|
1641
|
-
${
|
|
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
|
`
|