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