@harness-engineering/cli 1.13.1 → 1.14.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/dist/agents/skills/claude-code/harness-brainstorming/SKILL.md +39 -0
- package/dist/agents/skills/claude-code/harness-code-review/SKILL.md +44 -0
- package/dist/agents/skills/claude-code/harness-execution/SKILL.md +44 -0
- package/dist/agents/skills/claude-code/harness-planning/SKILL.md +39 -0
- package/dist/agents/skills/claude-code/harness-release-readiness/SKILL.md +3 -3
- package/dist/agents/skills/claude-code/harness-verification/SKILL.md +35 -0
- package/dist/agents/skills/claude-code/initialize-harness-project/SKILL.md +11 -3
- package/dist/agents/skills/gemini-cli/harness-brainstorming/SKILL.md +39 -0
- package/dist/agents/skills/gemini-cli/harness-code-review/SKILL.md +44 -0
- package/dist/agents/skills/gemini-cli/harness-execution/SKILL.md +44 -0
- package/dist/agents/skills/gemini-cli/harness-planning/SKILL.md +39 -0
- package/dist/agents/skills/gemini-cli/harness-release-readiness/SKILL.md +3 -3
- package/dist/agents/skills/gemini-cli/harness-verification/SKILL.md +35 -0
- package/dist/agents/skills/gemini-cli/initialize-harness-project/SKILL.md +11 -3
- package/dist/agents-md-YTYQDA3P.js +8 -0
- package/dist/{architecture-2R5Z4ZAF.js → architecture-JQZYM4US.js} +4 -4
- package/dist/bin/harness-mcp.js +14 -14
- package/dist/bin/harness.js +24 -24
- package/dist/{check-phase-gate-2OFZ7OWW.js → check-phase-gate-L3RADYWO.js} +4 -4
- package/dist/{chunk-QY4T6YAZ.js → chunk-3C2MLBPJ.js} +4 -4
- package/dist/{chunk-UAX4I5ZE.js → chunk-6KTUUFRN.js} +2 -2
- package/dist/{chunk-ND6PNADU.js → chunk-7IP4JIFL.js} +9 -9
- package/dist/{chunk-C2ERUR3L.js → chunk-7MJAPE3Z.js} +165 -49
- package/dist/{chunk-PQ5YK4AY.js → chunk-ABQHQ6I5.js} +1583 -1169
- package/dist/{chunk-QPEH2QPG.js → chunk-DBSOCI3G.js} +53 -54
- package/dist/{chunk-MHBMTPW7.js → chunk-ERS5EVUZ.js} +9 -0
- package/dist/{chunk-JSTQ3AWB.js → chunk-FIAPHX37.js} +1 -1
- package/dist/{chunk-IMFVFNJE.js → chunk-FTMXDOR6.js} +1 -1
- package/dist/{chunk-72GHBOL2.js → chunk-GZKSBLQL.js} +1 -1
- package/dist/{chunk-K6XAPGML.js → chunk-H7Y5CKTM.js} +1 -1
- package/dist/{chunk-4ZMOCPYO.js → chunk-NLVUVUGD.js} +1 -1
- package/dist/{chunk-Z77YQRQT.js → chunk-O5OJVPL6.js} +16 -5
- package/dist/{chunk-NKDM3FMH.js → chunk-OD3S2NHN.js} +1 -1
- package/dist/{chunk-65FRIL4D.js → chunk-OSXBPAMK.js} +1 -1
- package/dist/{chunk-DZS7CJKL.js → chunk-OXLLOSSR.js} +45 -47
- package/dist/{chunk-TS3XWPW5.js → chunk-RCWZBSK5.js} +1 -1
- package/dist/{chunk-NOPU4RZ4.js → chunk-S2FXOWOR.js} +3 -3
- package/dist/{chunk-VUCPTQ6G.js → chunk-SD3SQOZ2.js} +1 -1
- package/dist/{chunk-IM32EEDM.js → chunk-TPOTOBR7.js} +9 -9
- package/dist/{chunk-SSKDAOX5.js → chunk-XKECDXJS.js} +436 -340
- package/dist/{chunk-TKJZKICB.js → chunk-YPYGXRDR.js} +7 -7
- package/dist/{chunk-Q6AB7W5Z.js → chunk-YQ6KC6TE.js} +1 -1
- package/dist/{chunk-NERR4TAO.js → chunk-YZD2MRNQ.js} +972 -747
- package/dist/ci-workflow-EQZFVX3P.js +8 -0
- package/dist/{dist-HXHWB7SV.js → dist-B26DFXMP.js} +571 -478
- package/dist/{dist-L7LAAQAS.js → dist-DZ63LLUD.js} +1 -1
- package/dist/{dist-2B363XUH.js → dist-HWXF2C3R.js} +18 -2
- package/dist/{dist-D4RYGUZE.js → dist-USY2C5JL.js} +3 -1
- package/dist/{docs-FZOPM4GK.js → docs-7ECGYMAV.js} +4 -4
- package/dist/engine-EG4EH4IX.js +8 -0
- package/dist/{entropy-LVHJMFGH.js → entropy-5USWKLVS.js} +3 -3
- package/dist/{feedback-IHLVLMRD.js → feedback-UTBXZZHF.js} +1 -1
- package/dist/{generate-agent-definitions-64S3CLEZ.js → generate-agent-definitions-3PM5EU7V.js} +4 -4
- package/dist/{graph-loader-GJZ4FN4Y.js → graph-loader-2M2HXDQI.js} +1 -1
- package/dist/index.d.ts +148 -9
- package/dist/index.js +24 -24
- package/dist/loader-ZPALXIVR.js +10 -0
- package/dist/{mcp-JQUI7BVZ.js → mcp-362EZHF4.js} +14 -14
- package/dist/{performance-ZTVSUANN.js → performance-OQAFMJUD.js} +3 -3
- package/dist/{review-pipeline-76JHKGSV.js → review-pipeline-C4GCFVGP.js} +1 -1
- package/dist/runtime-7YLVK453.js +9 -0
- package/dist/{security-FWQZF2IZ.js → security-PZOX7AQS.js} +1 -1
- package/dist/templates/axum/Cargo.toml.hbs +8 -0
- package/dist/templates/axum/src/main.rs +12 -0
- package/dist/templates/axum/template.json +16 -0
- package/dist/templates/django/manage.py.hbs +19 -0
- package/dist/templates/django/requirements.txt.hbs +1 -0
- package/dist/templates/django/src/settings.py.hbs +44 -0
- package/dist/templates/django/src/urls.py +6 -0
- package/dist/templates/django/src/wsgi.py.hbs +9 -0
- package/dist/templates/django/template.json +21 -0
- package/dist/templates/express/package.json.hbs +15 -0
- package/dist/templates/express/src/app.ts +12 -0
- package/dist/templates/express/src/lib/.gitkeep +0 -0
- package/dist/templates/express/template.json +16 -0
- package/dist/templates/fastapi/requirements.txt.hbs +2 -0
- package/dist/templates/fastapi/src/main.py +8 -0
- package/dist/templates/fastapi/template.json +20 -0
- package/dist/templates/gin/go.mod.hbs +5 -0
- package/dist/templates/gin/main.go +15 -0
- package/dist/templates/gin/template.json +19 -0
- package/dist/templates/go-base/.golangci.yml +16 -0
- package/dist/templates/go-base/AGENTS.md.hbs +35 -0
- package/dist/templates/go-base/go.mod.hbs +3 -0
- package/dist/templates/go-base/harness.config.json.hbs +17 -0
- package/dist/templates/go-base/main.go +7 -0
- package/dist/templates/go-base/template.json +14 -0
- package/dist/templates/java-base/AGENTS.md.hbs +35 -0
- package/dist/templates/java-base/checkstyle.xml +20 -0
- package/dist/templates/java-base/harness.config.json.hbs +16 -0
- package/dist/templates/java-base/pom.xml.hbs +39 -0
- package/dist/templates/java-base/src/main/java/App.java.hbs +5 -0
- package/dist/templates/java-base/template.json +13 -0
- package/dist/templates/nestjs/nest-cli.json +5 -0
- package/dist/templates/nestjs/package.json.hbs +18 -0
- package/dist/templates/nestjs/src/app.module.ts +8 -0
- package/dist/templates/nestjs/src/lib/.gitkeep +0 -0
- package/dist/templates/nestjs/src/main.ts +11 -0
- package/dist/templates/nestjs/template.json +16 -0
- package/dist/templates/nextjs/template.json +15 -1
- package/dist/templates/python-base/.python-version +1 -0
- package/dist/templates/python-base/AGENTS.md.hbs +32 -0
- package/dist/templates/python-base/harness.config.json.hbs +16 -0
- package/dist/templates/python-base/pyproject.toml.hbs +18 -0
- package/dist/templates/python-base/ruff.toml +5 -0
- package/dist/templates/python-base/src/__init__.py +0 -0
- package/dist/templates/python-base/template.json +13 -0
- package/dist/templates/react-vite/index.html +12 -0
- package/dist/templates/react-vite/package.json.hbs +18 -0
- package/dist/templates/react-vite/src/App.tsx +7 -0
- package/dist/templates/react-vite/src/lib/.gitkeep +0 -0
- package/dist/templates/react-vite/src/main.tsx +9 -0
- package/dist/templates/react-vite/template.json +19 -0
- package/dist/templates/react-vite/vite.config.ts +6 -0
- package/dist/templates/rust-base/AGENTS.md.hbs +35 -0
- package/dist/templates/rust-base/Cargo.toml.hbs +6 -0
- package/dist/templates/rust-base/clippy.toml +2 -0
- package/dist/templates/rust-base/harness.config.json.hbs +17 -0
- package/dist/templates/rust-base/src/main.rs +3 -0
- package/dist/templates/rust-base/template.json +14 -0
- package/dist/templates/spring-boot/pom.xml.hbs +50 -0
- package/dist/templates/spring-boot/src/main/java/Application.java.hbs +19 -0
- package/dist/templates/spring-boot/template.json +15 -0
- package/dist/templates/vue/index.html +12 -0
- package/dist/templates/vue/package.json.hbs +16 -0
- package/dist/templates/vue/src/App.vue +7 -0
- package/dist/templates/vue/src/lib/.gitkeep +0 -0
- package/dist/templates/vue/src/main.ts +4 -0
- package/dist/templates/vue/template.json +19 -0
- package/dist/templates/vue/vite.config.ts +6 -0
- package/dist/{validate-GCHZJIL7.js → validate-FD3Z6VJD.js} +4 -4
- package/dist/validate-cross-check-WNJM6H2D.js +8 -0
- package/package.json +5 -5
- package/dist/agents-md-XU3BHE22.js +0 -8
- package/dist/ci-workflow-EHV65NQB.js +0 -8
- package/dist/engine-OL4T6NZS.js +0 -8
- package/dist/loader-DPYFB6R6.js +0 -10
- package/dist/runtime-X7U6SC7K.js +0 -9
- package/dist/validate-cross-check-STFHYMAZ.js +0 -8
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
Err,
|
|
3
|
-
Ok
|
|
4
|
-
|
|
3
|
+
Ok,
|
|
4
|
+
SESSION_SECTION_NAMES
|
|
5
|
+
} from "./chunk-ERS5EVUZ.js";
|
|
5
6
|
|
|
6
|
-
// ../core/dist/chunk-
|
|
7
|
+
// ../core/dist/chunk-BQUWXBGR.mjs
|
|
7
8
|
import { z } from "zod";
|
|
8
9
|
import { createHash } from "crypto";
|
|
9
10
|
import { minimatch } from "minimatch";
|
|
@@ -134,17 +135,17 @@ function resolveFileToLayer(file, layers) {
|
|
|
134
135
|
}
|
|
135
136
|
var accessAsync = promisify(access);
|
|
136
137
|
var readFileAsync = promisify(readFile);
|
|
137
|
-
async function fileExists(
|
|
138
|
+
async function fileExists(path22) {
|
|
138
139
|
try {
|
|
139
|
-
await accessAsync(
|
|
140
|
+
await accessAsync(path22, constants.F_OK);
|
|
140
141
|
return true;
|
|
141
142
|
} catch {
|
|
142
143
|
return false;
|
|
143
144
|
}
|
|
144
145
|
}
|
|
145
|
-
async function readFileContent(
|
|
146
|
+
async function readFileContent(path22) {
|
|
146
147
|
try {
|
|
147
|
-
const content = await readFileAsync(
|
|
148
|
+
const content = await readFileAsync(path22, "utf-8");
|
|
148
149
|
return Ok(content);
|
|
149
150
|
} catch (error) {
|
|
150
151
|
return Err(error);
|
|
@@ -290,65 +291,71 @@ async function validateDependencies(config) {
|
|
|
290
291
|
graph: graphResult.value
|
|
291
292
|
});
|
|
292
293
|
}
|
|
293
|
-
function
|
|
294
|
-
const nodeMap = /* @__PURE__ */ new Map();
|
|
295
|
-
const stack = [];
|
|
296
|
-
const sccs = [];
|
|
297
|
-
let index = 0;
|
|
294
|
+
function buildAdjacencyList(graph) {
|
|
298
295
|
const adjacency = /* @__PURE__ */ new Map();
|
|
296
|
+
const nodeSet = new Set(graph.nodes);
|
|
299
297
|
for (const node of graph.nodes) {
|
|
300
298
|
adjacency.set(node, []);
|
|
301
299
|
}
|
|
302
300
|
for (const edge of graph.edges) {
|
|
303
301
|
const neighbors = adjacency.get(edge.from);
|
|
304
|
-
if (neighbors &&
|
|
302
|
+
if (neighbors && nodeSet.has(edge.to)) {
|
|
305
303
|
neighbors.push(edge.to);
|
|
306
304
|
}
|
|
307
305
|
}
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
306
|
+
return adjacency;
|
|
307
|
+
}
|
|
308
|
+
function isCyclicSCC(scc, adjacency) {
|
|
309
|
+
if (scc.length > 1) return true;
|
|
310
|
+
if (scc.length === 1) {
|
|
311
|
+
const selfNode = scc[0];
|
|
312
|
+
const selfNeighbors = adjacency.get(selfNode) ?? [];
|
|
313
|
+
return selfNeighbors.includes(selfNode);
|
|
314
|
+
}
|
|
315
|
+
return false;
|
|
316
|
+
}
|
|
317
|
+
function processNeighbors(node, neighbors, nodeMap, stack, adjacency, sccs, indexRef) {
|
|
318
|
+
for (const neighbor of neighbors) {
|
|
319
|
+
const neighborData = nodeMap.get(neighbor);
|
|
320
|
+
if (!neighborData) {
|
|
321
|
+
strongConnectImpl(neighbor, nodeMap, stack, adjacency, sccs, indexRef);
|
|
322
|
+
const nodeData = nodeMap.get(node);
|
|
323
|
+
const updatedNeighborData = nodeMap.get(neighbor);
|
|
324
|
+
nodeData.lowlink = Math.min(nodeData.lowlink, updatedNeighborData.lowlink);
|
|
325
|
+
} else if (neighborData.onStack) {
|
|
326
|
+
const nodeData = nodeMap.get(node);
|
|
327
|
+
nodeData.lowlink = Math.min(nodeData.lowlink, neighborData.index);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
function strongConnectImpl(node, nodeMap, stack, adjacency, sccs, indexRef) {
|
|
332
|
+
nodeMap.set(node, { index: indexRef.value, lowlink: indexRef.value, onStack: true });
|
|
333
|
+
indexRef.value++;
|
|
334
|
+
stack.push(node);
|
|
335
|
+
processNeighbors(node, adjacency.get(node) ?? [], nodeMap, stack, adjacency, sccs, indexRef);
|
|
336
|
+
const nodeData = nodeMap.get(node);
|
|
337
|
+
if (nodeData.lowlink === nodeData.index) {
|
|
338
|
+
const scc = [];
|
|
339
|
+
let w;
|
|
340
|
+
do {
|
|
341
|
+
w = stack.pop();
|
|
342
|
+
nodeMap.get(w).onStack = false;
|
|
343
|
+
scc.push(w);
|
|
344
|
+
} while (w !== node);
|
|
345
|
+
if (isCyclicSCC(scc, adjacency)) {
|
|
346
|
+
sccs.push(scc);
|
|
347
347
|
}
|
|
348
348
|
}
|
|
349
|
+
}
|
|
350
|
+
function tarjanSCC(graph) {
|
|
351
|
+
const nodeMap = /* @__PURE__ */ new Map();
|
|
352
|
+
const stack = [];
|
|
353
|
+
const sccs = [];
|
|
354
|
+
const indexRef = { value: 0 };
|
|
355
|
+
const adjacency = buildAdjacencyList(graph);
|
|
349
356
|
for (const node of graph.nodes) {
|
|
350
357
|
if (!nodeMap.has(node)) {
|
|
351
|
-
|
|
358
|
+
strongConnectImpl(node, nodeMap, stack, adjacency, sccs, indexRef);
|
|
352
359
|
}
|
|
353
360
|
}
|
|
354
361
|
return sccs;
|
|
@@ -607,6 +614,31 @@ function aggregateByCategory(results) {
|
|
|
607
614
|
}
|
|
608
615
|
return map;
|
|
609
616
|
}
|
|
617
|
+
function classifyViolations(violations, baselineViolationIds) {
|
|
618
|
+
const newViolations = [];
|
|
619
|
+
const preExisting = [];
|
|
620
|
+
for (const violation of violations) {
|
|
621
|
+
if (baselineViolationIds.has(violation.id)) {
|
|
622
|
+
preExisting.push(violation.id);
|
|
623
|
+
} else {
|
|
624
|
+
newViolations.push(violation);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
return { newViolations, preExisting };
|
|
628
|
+
}
|
|
629
|
+
function findResolvedViolations(baselineCategory, currentViolationIds) {
|
|
630
|
+
if (!baselineCategory) return [];
|
|
631
|
+
return baselineCategory.violationIds.filter((id) => !currentViolationIds.has(id));
|
|
632
|
+
}
|
|
633
|
+
function collectOrphanedBaselineViolations(baseline, visitedCategories) {
|
|
634
|
+
const resolved = [];
|
|
635
|
+
for (const [category, baselineCategory] of Object.entries(baseline.metrics)) {
|
|
636
|
+
if (!visitedCategories.has(category) && baselineCategory) {
|
|
637
|
+
resolved.push(...baselineCategory.violationIds);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
return resolved;
|
|
641
|
+
}
|
|
610
642
|
function diff(current, baseline) {
|
|
611
643
|
const aggregated = aggregateByCategory(current);
|
|
612
644
|
const newViolations = [];
|
|
@@ -619,21 +651,11 @@ function diff(current, baseline) {
|
|
|
619
651
|
const baselineCategory = baseline.metrics[category];
|
|
620
652
|
const baselineViolationIds = new Set(baselineCategory?.violationIds ?? []);
|
|
621
653
|
const baselineValue = baselineCategory?.value ?? 0;
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
} else {
|
|
626
|
-
newViolations.push(violation);
|
|
627
|
-
}
|
|
628
|
-
}
|
|
654
|
+
const classified = classifyViolations(agg.violations, baselineViolationIds);
|
|
655
|
+
newViolations.push(...classified.newViolations);
|
|
656
|
+
preExisting.push(...classified.preExisting);
|
|
629
657
|
const currentViolationIds = new Set(agg.violations.map((v) => v.id));
|
|
630
|
-
|
|
631
|
-
for (const id of baselineCategory.violationIds) {
|
|
632
|
-
if (!currentViolationIds.has(id)) {
|
|
633
|
-
resolvedViolations.push(id);
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
}
|
|
658
|
+
resolvedViolations.push(...findResolvedViolations(baselineCategory, currentViolationIds));
|
|
637
659
|
if (baselineCategory && agg.value > baselineValue) {
|
|
638
660
|
regressions.push({
|
|
639
661
|
category,
|
|
@@ -643,16 +665,9 @@ function diff(current, baseline) {
|
|
|
643
665
|
});
|
|
644
666
|
}
|
|
645
667
|
}
|
|
646
|
-
|
|
647
|
-
if (!visitedCategories.has(category) && baselineCategory) {
|
|
648
|
-
for (const id of baselineCategory.violationIds) {
|
|
649
|
-
resolvedViolations.push(id);
|
|
650
|
-
}
|
|
651
|
-
}
|
|
652
|
-
}
|
|
653
|
-
const passed = newViolations.length === 0 && regressions.length === 0;
|
|
668
|
+
resolvedViolations.push(...collectOrphanedBaselineViolations(baseline, visitedCategories));
|
|
654
669
|
return {
|
|
655
|
-
passed,
|
|
670
|
+
passed: newViolations.length === 0 && regressions.length === 0,
|
|
656
671
|
newViolations,
|
|
657
672
|
resolvedViolations,
|
|
658
673
|
preExisting,
|
|
@@ -667,22 +682,22 @@ var DEFAULT_THRESHOLDS = {
|
|
|
667
682
|
fileLength: { info: 300 },
|
|
668
683
|
hotspotPercentile: { error: 95 }
|
|
669
684
|
};
|
|
685
|
+
var FUNCTION_PATTERNS = [
|
|
686
|
+
// function declarations: function name(params) {
|
|
687
|
+
/^\s*(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)/,
|
|
688
|
+
// method declarations: name(params) {
|
|
689
|
+
/^\s*(?:async\s+)?(\w+)\s*\(([^)]*)\)\s*(?::\s*[^{]+)?\s*\{/,
|
|
690
|
+
// arrow functions assigned to const/let/var: const name = (params) =>
|
|
691
|
+
/^\s*(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?\(([^)]*)\)\s*(?::\s*[^=]+)?\s*=>/,
|
|
692
|
+
// arrow functions assigned to const/let/var with single param: const name = param =>
|
|
693
|
+
/^\s*(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?(\w+)\s*=>/
|
|
694
|
+
];
|
|
670
695
|
function extractFunctions(content) {
|
|
671
696
|
const functions = [];
|
|
672
697
|
const lines = content.split("\n");
|
|
673
|
-
const patterns = [
|
|
674
|
-
// function declarations: function name(params) {
|
|
675
|
-
/^\s*(?:export\s+)?(?:async\s+)?function\s+(\w+)\s*\(([^)]*)\)/,
|
|
676
|
-
// method declarations: name(params) {
|
|
677
|
-
/^\s*(?:async\s+)?(\w+)\s*\(([^)]*)\)\s*(?::\s*[^{]+)?\s*\{/,
|
|
678
|
-
// arrow functions assigned to const/let/var: const name = (params) =>
|
|
679
|
-
/^\s*(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?\(([^)]*)\)\s*(?::\s*[^=]+)?\s*=>/,
|
|
680
|
-
// arrow functions assigned to const/let/var with single param: const name = param =>
|
|
681
|
-
/^\s*(?:export\s+)?(?:const|let|var)\s+(\w+)\s*=\s*(?:async\s+)?(\w+)\s*=>/
|
|
682
|
-
];
|
|
683
698
|
for (let i = 0; i < lines.length; i++) {
|
|
684
699
|
const line = lines[i];
|
|
685
|
-
for (const pattern of
|
|
700
|
+
for (const pattern of FUNCTION_PATTERNS) {
|
|
686
701
|
const match = line.match(pattern);
|
|
687
702
|
if (match) {
|
|
688
703
|
const name = match[1] ?? "anonymous";
|
|
@@ -771,26 +786,155 @@ function computeNestingDepth(body) {
|
|
|
771
786
|
}
|
|
772
787
|
return maxDepth;
|
|
773
788
|
}
|
|
774
|
-
|
|
775
|
-
const
|
|
776
|
-
|
|
789
|
+
function resolveThresholds(config) {
|
|
790
|
+
const userThresholds = config?.thresholds;
|
|
791
|
+
if (!userThresholds) return { ...DEFAULT_THRESHOLDS };
|
|
792
|
+
return {
|
|
777
793
|
cyclomaticComplexity: {
|
|
778
|
-
|
|
779
|
-
|
|
794
|
+
...DEFAULT_THRESHOLDS.cyclomaticComplexity,
|
|
795
|
+
...stripUndefined(userThresholds.cyclomaticComplexity)
|
|
780
796
|
},
|
|
781
797
|
nestingDepth: {
|
|
782
|
-
|
|
798
|
+
...DEFAULT_THRESHOLDS.nestingDepth,
|
|
799
|
+
...stripUndefined(userThresholds.nestingDepth)
|
|
783
800
|
},
|
|
784
801
|
functionLength: {
|
|
785
|
-
|
|
802
|
+
...DEFAULT_THRESHOLDS.functionLength,
|
|
803
|
+
...stripUndefined(userThresholds.functionLength)
|
|
786
804
|
},
|
|
787
805
|
parameterCount: {
|
|
788
|
-
|
|
806
|
+
...DEFAULT_THRESHOLDS.parameterCount,
|
|
807
|
+
...stripUndefined(userThresholds.parameterCount)
|
|
789
808
|
},
|
|
790
|
-
fileLength: {
|
|
791
|
-
|
|
792
|
-
|
|
809
|
+
fileLength: { ...DEFAULT_THRESHOLDS.fileLength, ...stripUndefined(userThresholds.fileLength) }
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
function stripUndefined(obj) {
|
|
813
|
+
if (!obj) return {};
|
|
814
|
+
const result = {};
|
|
815
|
+
for (const [key, val] of Object.entries(obj)) {
|
|
816
|
+
if (val !== void 0) result[key] = val;
|
|
817
|
+
}
|
|
818
|
+
return result;
|
|
819
|
+
}
|
|
820
|
+
function checkFileLengthViolation(filePath, lineCount, threshold) {
|
|
821
|
+
if (lineCount <= threshold) return null;
|
|
822
|
+
return {
|
|
823
|
+
file: filePath,
|
|
824
|
+
function: "<file>",
|
|
825
|
+
line: 1,
|
|
826
|
+
metric: "fileLength",
|
|
827
|
+
value: lineCount,
|
|
828
|
+
threshold,
|
|
829
|
+
tier: 3,
|
|
830
|
+
severity: "info",
|
|
831
|
+
message: `File has ${lineCount} lines (threshold: ${threshold})`
|
|
832
|
+
};
|
|
833
|
+
}
|
|
834
|
+
function checkCyclomaticComplexity(filePath, fn, thresholds) {
|
|
835
|
+
const complexity = computeCyclomaticComplexity(fn.body);
|
|
836
|
+
if (complexity > thresholds.error) {
|
|
837
|
+
return {
|
|
838
|
+
file: filePath,
|
|
839
|
+
function: fn.name,
|
|
840
|
+
line: fn.line,
|
|
841
|
+
metric: "cyclomaticComplexity",
|
|
842
|
+
value: complexity,
|
|
843
|
+
threshold: thresholds.error,
|
|
844
|
+
tier: 1,
|
|
845
|
+
severity: "error",
|
|
846
|
+
message: `Function "${fn.name}" has cyclomatic complexity of ${complexity} (error threshold: ${thresholds.error})`
|
|
847
|
+
};
|
|
848
|
+
}
|
|
849
|
+
if (complexity > thresholds.warn) {
|
|
850
|
+
return {
|
|
851
|
+
file: filePath,
|
|
852
|
+
function: fn.name,
|
|
853
|
+
line: fn.line,
|
|
854
|
+
metric: "cyclomaticComplexity",
|
|
855
|
+
value: complexity,
|
|
856
|
+
threshold: thresholds.warn,
|
|
857
|
+
tier: 2,
|
|
858
|
+
severity: "warning",
|
|
859
|
+
message: `Function "${fn.name}" has cyclomatic complexity of ${complexity} (warning threshold: ${thresholds.warn})`
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
return null;
|
|
863
|
+
}
|
|
864
|
+
function checkNestingDepth(filePath, fn, threshold) {
|
|
865
|
+
const depth = computeNestingDepth(fn.body);
|
|
866
|
+
if (depth <= threshold) return null;
|
|
867
|
+
return {
|
|
868
|
+
file: filePath,
|
|
869
|
+
function: fn.name,
|
|
870
|
+
line: fn.line,
|
|
871
|
+
metric: "nestingDepth",
|
|
872
|
+
value: depth,
|
|
873
|
+
threshold,
|
|
874
|
+
tier: 2,
|
|
875
|
+
severity: "warning",
|
|
876
|
+
message: `Function "${fn.name}" has nesting depth of ${depth} (threshold: ${threshold})`
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
function checkFunctionLength(filePath, fn, threshold) {
|
|
880
|
+
const fnLength = fn.endLine - fn.startLine + 1;
|
|
881
|
+
if (fnLength <= threshold) return null;
|
|
882
|
+
return {
|
|
883
|
+
file: filePath,
|
|
884
|
+
function: fn.name,
|
|
885
|
+
line: fn.line,
|
|
886
|
+
metric: "functionLength",
|
|
887
|
+
value: fnLength,
|
|
888
|
+
threshold,
|
|
889
|
+
tier: 2,
|
|
890
|
+
severity: "warning",
|
|
891
|
+
message: `Function "${fn.name}" is ${fnLength} lines long (threshold: ${threshold})`
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
function checkParameterCount(filePath, fn, threshold) {
|
|
895
|
+
if (fn.params <= threshold) return null;
|
|
896
|
+
return {
|
|
897
|
+
file: filePath,
|
|
898
|
+
function: fn.name,
|
|
899
|
+
line: fn.line,
|
|
900
|
+
metric: "parameterCount",
|
|
901
|
+
value: fn.params,
|
|
902
|
+
threshold,
|
|
903
|
+
tier: 2,
|
|
904
|
+
severity: "warning",
|
|
905
|
+
message: `Function "${fn.name}" has ${fn.params} parameters (threshold: ${threshold})`
|
|
906
|
+
};
|
|
907
|
+
}
|
|
908
|
+
function checkHotspot(filePath, fn, graphData) {
|
|
909
|
+
const hotspot = graphData.hotspots.find((h) => h.file === filePath && h.function === fn.name);
|
|
910
|
+
if (!hotspot || hotspot.hotspotScore <= graphData.percentile95Score) return null;
|
|
911
|
+
return {
|
|
912
|
+
file: filePath,
|
|
913
|
+
function: fn.name,
|
|
914
|
+
line: fn.line,
|
|
915
|
+
metric: "hotspotScore",
|
|
916
|
+
value: hotspot.hotspotScore,
|
|
917
|
+
threshold: graphData.percentile95Score,
|
|
918
|
+
tier: 1,
|
|
919
|
+
severity: "error",
|
|
920
|
+
message: `Function "${fn.name}" is a complexity hotspot (score: ${hotspot.hotspotScore}, p95: ${graphData.percentile95Score})`
|
|
793
921
|
};
|
|
922
|
+
}
|
|
923
|
+
function collectFunctionViolations(filePath, fn, thresholds, graphData) {
|
|
924
|
+
const checks = [
|
|
925
|
+
checkCyclomaticComplexity(filePath, fn, thresholds.cyclomaticComplexity),
|
|
926
|
+
checkNestingDepth(filePath, fn, thresholds.nestingDepth.warn),
|
|
927
|
+
checkFunctionLength(filePath, fn, thresholds.functionLength.warn),
|
|
928
|
+
checkParameterCount(filePath, fn, thresholds.parameterCount.warn)
|
|
929
|
+
];
|
|
930
|
+
if (graphData) {
|
|
931
|
+
checks.push(checkHotspot(filePath, fn, graphData));
|
|
932
|
+
}
|
|
933
|
+
return checks.filter((v) => v !== null);
|
|
934
|
+
}
|
|
935
|
+
async function detectComplexityViolations(snapshot, config, graphData) {
|
|
936
|
+
const violations = [];
|
|
937
|
+
const thresholds = resolveThresholds(config);
|
|
794
938
|
let totalFunctions = 0;
|
|
795
939
|
for (const file of snapshot.files) {
|
|
796
940
|
let content;
|
|
@@ -800,107 +944,16 @@ async function detectComplexityViolations(snapshot, config, graphData) {
|
|
|
800
944
|
continue;
|
|
801
945
|
}
|
|
802
946
|
const lines = content.split("\n");
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
value: lines.length,
|
|
810
|
-
threshold: thresholds.fileLength.info,
|
|
811
|
-
tier: 3,
|
|
812
|
-
severity: "info",
|
|
813
|
-
message: `File has ${lines.length} lines (threshold: ${thresholds.fileLength.info})`
|
|
814
|
-
});
|
|
815
|
-
}
|
|
947
|
+
const fileLenViolation = checkFileLengthViolation(
|
|
948
|
+
file.path,
|
|
949
|
+
lines.length,
|
|
950
|
+
thresholds.fileLength.info
|
|
951
|
+
);
|
|
952
|
+
if (fileLenViolation) violations.push(fileLenViolation);
|
|
816
953
|
const functions = extractFunctions(content);
|
|
817
954
|
totalFunctions += functions.length;
|
|
818
955
|
for (const fn of functions) {
|
|
819
|
-
|
|
820
|
-
if (complexity > thresholds.cyclomaticComplexity.error) {
|
|
821
|
-
violations.push({
|
|
822
|
-
file: file.path,
|
|
823
|
-
function: fn.name,
|
|
824
|
-
line: fn.line,
|
|
825
|
-
metric: "cyclomaticComplexity",
|
|
826
|
-
value: complexity,
|
|
827
|
-
threshold: thresholds.cyclomaticComplexity.error,
|
|
828
|
-
tier: 1,
|
|
829
|
-
severity: "error",
|
|
830
|
-
message: `Function "${fn.name}" has cyclomatic complexity of ${complexity} (error threshold: ${thresholds.cyclomaticComplexity.error})`
|
|
831
|
-
});
|
|
832
|
-
} else if (complexity > thresholds.cyclomaticComplexity.warn) {
|
|
833
|
-
violations.push({
|
|
834
|
-
file: file.path,
|
|
835
|
-
function: fn.name,
|
|
836
|
-
line: fn.line,
|
|
837
|
-
metric: "cyclomaticComplexity",
|
|
838
|
-
value: complexity,
|
|
839
|
-
threshold: thresholds.cyclomaticComplexity.warn,
|
|
840
|
-
tier: 2,
|
|
841
|
-
severity: "warning",
|
|
842
|
-
message: `Function "${fn.name}" has cyclomatic complexity of ${complexity} (warning threshold: ${thresholds.cyclomaticComplexity.warn})`
|
|
843
|
-
});
|
|
844
|
-
}
|
|
845
|
-
const nestingDepth = computeNestingDepth(fn.body);
|
|
846
|
-
if (nestingDepth > thresholds.nestingDepth.warn) {
|
|
847
|
-
violations.push({
|
|
848
|
-
file: file.path,
|
|
849
|
-
function: fn.name,
|
|
850
|
-
line: fn.line,
|
|
851
|
-
metric: "nestingDepth",
|
|
852
|
-
value: nestingDepth,
|
|
853
|
-
threshold: thresholds.nestingDepth.warn,
|
|
854
|
-
tier: 2,
|
|
855
|
-
severity: "warning",
|
|
856
|
-
message: `Function "${fn.name}" has nesting depth of ${nestingDepth} (threshold: ${thresholds.nestingDepth.warn})`
|
|
857
|
-
});
|
|
858
|
-
}
|
|
859
|
-
const fnLength = fn.endLine - fn.startLine + 1;
|
|
860
|
-
if (fnLength > thresholds.functionLength.warn) {
|
|
861
|
-
violations.push({
|
|
862
|
-
file: file.path,
|
|
863
|
-
function: fn.name,
|
|
864
|
-
line: fn.line,
|
|
865
|
-
metric: "functionLength",
|
|
866
|
-
value: fnLength,
|
|
867
|
-
threshold: thresholds.functionLength.warn,
|
|
868
|
-
tier: 2,
|
|
869
|
-
severity: "warning",
|
|
870
|
-
message: `Function "${fn.name}" is ${fnLength} lines long (threshold: ${thresholds.functionLength.warn})`
|
|
871
|
-
});
|
|
872
|
-
}
|
|
873
|
-
if (fn.params > thresholds.parameterCount.warn) {
|
|
874
|
-
violations.push({
|
|
875
|
-
file: file.path,
|
|
876
|
-
function: fn.name,
|
|
877
|
-
line: fn.line,
|
|
878
|
-
metric: "parameterCount",
|
|
879
|
-
value: fn.params,
|
|
880
|
-
threshold: thresholds.parameterCount.warn,
|
|
881
|
-
tier: 2,
|
|
882
|
-
severity: "warning",
|
|
883
|
-
message: `Function "${fn.name}" has ${fn.params} parameters (threshold: ${thresholds.parameterCount.warn})`
|
|
884
|
-
});
|
|
885
|
-
}
|
|
886
|
-
if (graphData) {
|
|
887
|
-
const hotspot = graphData.hotspots.find(
|
|
888
|
-
(h) => h.file === file.path && h.function === fn.name
|
|
889
|
-
);
|
|
890
|
-
if (hotspot && hotspot.hotspotScore > graphData.percentile95Score) {
|
|
891
|
-
violations.push({
|
|
892
|
-
file: file.path,
|
|
893
|
-
function: fn.name,
|
|
894
|
-
line: fn.line,
|
|
895
|
-
metric: "hotspotScore",
|
|
896
|
-
value: hotspot.hotspotScore,
|
|
897
|
-
threshold: graphData.percentile95Score,
|
|
898
|
-
tier: 1,
|
|
899
|
-
severity: "error",
|
|
900
|
-
message: `Function "${fn.name}" is a complexity hotspot (score: ${hotspot.hotspotScore}, p95: ${graphData.percentile95Score})`
|
|
901
|
-
});
|
|
902
|
-
}
|
|
903
|
-
}
|
|
956
|
+
violations.push(...collectFunctionViolations(file.path, fn, thresholds, graphData));
|
|
904
957
|
}
|
|
905
958
|
}
|
|
906
959
|
const errorCount = violations.filter((v) => v.severity === "error").length;
|
|
@@ -1788,24 +1841,28 @@ import * as path9 from "path";
|
|
|
1788
1841
|
import { execSync as execSync2 } from "child_process";
|
|
1789
1842
|
import * as fs13 from "fs";
|
|
1790
1843
|
import * as path10 from "path";
|
|
1791
|
-
import * as fs15 from "fs/promises";
|
|
1792
|
-
import { z as z5 } from "zod";
|
|
1793
1844
|
import * as fs14 from "fs";
|
|
1794
1845
|
import * as path11 from "path";
|
|
1846
|
+
import * as fs15 from "fs";
|
|
1795
1847
|
import * as path12 from "path";
|
|
1848
|
+
import * as fs17 from "fs/promises";
|
|
1849
|
+
import { z as z5 } from "zod";
|
|
1850
|
+
import * as fs16 from "fs";
|
|
1796
1851
|
import * as path13 from "path";
|
|
1797
1852
|
import * as path14 from "path";
|
|
1798
1853
|
import * as path15 from "path";
|
|
1799
|
-
import * as fs16 from "fs";
|
|
1800
1854
|
import * as path16 from "path";
|
|
1801
|
-
import { z as z6 } from "zod";
|
|
1802
|
-
import * as fs17 from "fs/promises";
|
|
1803
1855
|
import * as path17 from "path";
|
|
1804
|
-
import * as fs18 from "fs
|
|
1856
|
+
import * as fs18 from "fs";
|
|
1805
1857
|
import * as path18 from "path";
|
|
1806
|
-
import
|
|
1807
|
-
import * as fs19 from "fs";
|
|
1858
|
+
import { z as z6 } from "zod";
|
|
1859
|
+
import * as fs19 from "fs/promises";
|
|
1808
1860
|
import * as path19 from "path";
|
|
1861
|
+
import * as fs20 from "fs/promises";
|
|
1862
|
+
import * as path20 from "path";
|
|
1863
|
+
import * as ejs from "ejs";
|
|
1864
|
+
import * as fs21 from "fs";
|
|
1865
|
+
import * as path21 from "path";
|
|
1809
1866
|
import * as os from "os";
|
|
1810
1867
|
import { spawn } from "child_process";
|
|
1811
1868
|
async function validateFileStructure(projectPath, conventions) {
|
|
@@ -1843,15 +1900,15 @@ function validateConfig(data, schema) {
|
|
|
1843
1900
|
let message = "Configuration validation failed";
|
|
1844
1901
|
const suggestions = [];
|
|
1845
1902
|
if (firstError) {
|
|
1846
|
-
const
|
|
1847
|
-
const pathDisplay =
|
|
1903
|
+
const path22 = firstError.path.join(".");
|
|
1904
|
+
const pathDisplay = path22 ? ` at "${path22}"` : "";
|
|
1848
1905
|
if (firstError.code === "invalid_type") {
|
|
1849
1906
|
const received = firstError.received;
|
|
1850
1907
|
const expected = firstError.expected;
|
|
1851
1908
|
if (received === "undefined") {
|
|
1852
1909
|
code = "MISSING_FIELD";
|
|
1853
1910
|
message = `Missing required field${pathDisplay}: ${firstError.message}`;
|
|
1854
|
-
suggestions.push(`Field "${
|
|
1911
|
+
suggestions.push(`Field "${path22}" is required and must be of type "${expected}"`);
|
|
1855
1912
|
} else {
|
|
1856
1913
|
code = "INVALID_TYPE";
|
|
1857
1914
|
message = `Invalid type${pathDisplay}: ${firstError.message}`;
|
|
@@ -1998,6 +2055,43 @@ function extractMarkdownLinks(content) {
|
|
|
1998
2055
|
}
|
|
1999
2056
|
return links;
|
|
2000
2057
|
}
|
|
2058
|
+
function isDescriptionTerminator(trimmed) {
|
|
2059
|
+
return trimmed.startsWith("#") || trimmed.startsWith("-") || trimmed.startsWith("*") || trimmed.startsWith("```");
|
|
2060
|
+
}
|
|
2061
|
+
function extractDescription(sectionLines) {
|
|
2062
|
+
const descriptionLines = [];
|
|
2063
|
+
for (const line of sectionLines) {
|
|
2064
|
+
const trimmed = line.trim();
|
|
2065
|
+
if (trimmed === "") {
|
|
2066
|
+
if (descriptionLines.length > 0) break;
|
|
2067
|
+
continue;
|
|
2068
|
+
}
|
|
2069
|
+
if (isDescriptionTerminator(trimmed)) break;
|
|
2070
|
+
descriptionLines.push(trimmed);
|
|
2071
|
+
}
|
|
2072
|
+
return descriptionLines.length > 0 ? descriptionLines.join(" ") : void 0;
|
|
2073
|
+
}
|
|
2074
|
+
function buildAgentMapSection(section, lines) {
|
|
2075
|
+
const endIndex = section.endIndex ?? lines.length;
|
|
2076
|
+
const sectionLines = lines.slice(section.startIndex + 1, endIndex);
|
|
2077
|
+
const sectionContent = sectionLines.join("\n");
|
|
2078
|
+
const links = extractMarkdownLinks(sectionContent).map((link) => ({
|
|
2079
|
+
...link,
|
|
2080
|
+
line: link.line + section.startIndex + 1,
|
|
2081
|
+
exists: false
|
|
2082
|
+
}));
|
|
2083
|
+
const result = {
|
|
2084
|
+
title: section.title,
|
|
2085
|
+
level: section.level,
|
|
2086
|
+
line: section.line,
|
|
2087
|
+
links
|
|
2088
|
+
};
|
|
2089
|
+
const description = extractDescription(sectionLines);
|
|
2090
|
+
if (description) {
|
|
2091
|
+
result.description = description;
|
|
2092
|
+
}
|
|
2093
|
+
return result;
|
|
2094
|
+
}
|
|
2001
2095
|
function extractSections(content) {
|
|
2002
2096
|
const lines = content.split("\n");
|
|
2003
2097
|
const sections = [];
|
|
@@ -2010,7 +2104,6 @@ function extractSections(content) {
|
|
|
2010
2104
|
title: match[2].trim(),
|
|
2011
2105
|
level: match[1].length,
|
|
2012
2106
|
line: i + 1,
|
|
2013
|
-
// 1-indexed
|
|
2014
2107
|
startIndex: i
|
|
2015
2108
|
});
|
|
2016
2109
|
}
|
|
@@ -2022,62 +2115,29 @@ function extractSections(content) {
|
|
|
2022
2115
|
currentSection.endIndex = nextSection ? nextSection.startIndex : lines.length;
|
|
2023
2116
|
}
|
|
2024
2117
|
}
|
|
2025
|
-
return sections.map((section) =>
|
|
2026
|
-
const endIndex = section.endIndex ?? lines.length;
|
|
2027
|
-
const sectionLines = lines.slice(section.startIndex + 1, endIndex);
|
|
2028
|
-
const sectionContent = sectionLines.join("\n");
|
|
2029
|
-
const links = extractMarkdownLinks(sectionContent).map((link) => ({
|
|
2030
|
-
...link,
|
|
2031
|
-
line: link.line + section.startIndex + 1,
|
|
2032
|
-
// Adjust line number
|
|
2033
|
-
exists: false
|
|
2034
|
-
// Will be set later by validateAgentsMap
|
|
2035
|
-
}));
|
|
2036
|
-
const descriptionLines = [];
|
|
2037
|
-
for (const line of sectionLines) {
|
|
2038
|
-
const trimmed = line.trim();
|
|
2039
|
-
if (trimmed === "") {
|
|
2040
|
-
if (descriptionLines.length > 0) break;
|
|
2041
|
-
continue;
|
|
2042
|
-
}
|
|
2043
|
-
if (trimmed.startsWith("#")) break;
|
|
2044
|
-
if (trimmed.startsWith("-") || trimmed.startsWith("*")) break;
|
|
2045
|
-
if (trimmed.startsWith("```")) break;
|
|
2046
|
-
descriptionLines.push(trimmed);
|
|
2047
|
-
}
|
|
2048
|
-
const result = {
|
|
2049
|
-
title: section.title,
|
|
2050
|
-
level: section.level,
|
|
2051
|
-
line: section.line,
|
|
2052
|
-
links
|
|
2053
|
-
};
|
|
2054
|
-
if (descriptionLines.length > 0) {
|
|
2055
|
-
result.description = descriptionLines.join(" ");
|
|
2056
|
-
}
|
|
2057
|
-
return result;
|
|
2058
|
-
});
|
|
2118
|
+
return sections.map((section) => buildAgentMapSection(section, lines));
|
|
2059
2119
|
}
|
|
2060
|
-
function isExternalLink(
|
|
2061
|
-
return
|
|
2120
|
+
function isExternalLink(path22) {
|
|
2121
|
+
return path22.startsWith("http://") || path22.startsWith("https://") || path22.startsWith("#") || path22.startsWith("mailto:");
|
|
2062
2122
|
}
|
|
2063
2123
|
function resolveLinkPath(linkPath, baseDir) {
|
|
2064
2124
|
return linkPath.startsWith(".") ? join4(baseDir, linkPath) : linkPath;
|
|
2065
2125
|
}
|
|
2066
|
-
async function validateAgentsMap(
|
|
2067
|
-
const contentResult = await readFileContent(
|
|
2126
|
+
async function validateAgentsMap(path22 = "./AGENTS.md") {
|
|
2127
|
+
const contentResult = await readFileContent(path22);
|
|
2068
2128
|
if (!contentResult.ok) {
|
|
2069
2129
|
return Err(
|
|
2070
2130
|
createError(
|
|
2071
2131
|
"PARSE_ERROR",
|
|
2072
2132
|
`Failed to read AGENTS.md: ${contentResult.error.message}`,
|
|
2073
|
-
{ path:
|
|
2133
|
+
{ path: path22 },
|
|
2074
2134
|
["Ensure the file exists", "Check file permissions"]
|
|
2075
2135
|
)
|
|
2076
2136
|
);
|
|
2077
2137
|
}
|
|
2078
2138
|
const content = contentResult.value;
|
|
2079
2139
|
const sections = extractSections(content);
|
|
2080
|
-
const baseDir = dirname4(
|
|
2140
|
+
const baseDir = dirname4(path22);
|
|
2081
2141
|
const sectionTitles = sections.map((s) => s.title);
|
|
2082
2142
|
const missingSections = REQUIRED_SECTIONS.filter(
|
|
2083
2143
|
(required) => !sectionTitles.some((title) => title.toLowerCase().includes(required.toLowerCase()))
|
|
@@ -2211,8 +2271,8 @@ async function checkDocCoverage(domain, options = {}) {
|
|
|
2211
2271
|
);
|
|
2212
2272
|
}
|
|
2213
2273
|
}
|
|
2214
|
-
function suggestFix(
|
|
2215
|
-
const targetName = basename2(
|
|
2274
|
+
function suggestFix(path22, existingFiles) {
|
|
2275
|
+
const targetName = basename2(path22).toLowerCase();
|
|
2216
2276
|
const similar = existingFiles.find((file) => {
|
|
2217
2277
|
const fileName = basename2(file).toLowerCase();
|
|
2218
2278
|
return fileName.includes(targetName) || targetName.includes(fileName);
|
|
@@ -2220,7 +2280,7 @@ function suggestFix(path20, existingFiles) {
|
|
|
2220
2280
|
if (similar) {
|
|
2221
2281
|
return `Did you mean "${similar}"?`;
|
|
2222
2282
|
}
|
|
2223
|
-
return `Create the file "${
|
|
2283
|
+
return `Create the file "${path22}" or remove the link`;
|
|
2224
2284
|
}
|
|
2225
2285
|
async function validateKnowledgeMap(rootDir = process.cwd()) {
|
|
2226
2286
|
const agentsPath = join22(rootDir, "AGENTS.md");
|
|
@@ -2563,8 +2623,8 @@ function createBoundaryValidator(schema, name) {
|
|
|
2563
2623
|
return Ok(result.data);
|
|
2564
2624
|
}
|
|
2565
2625
|
const suggestions = result.error.issues.map((issue) => {
|
|
2566
|
-
const
|
|
2567
|
-
return
|
|
2626
|
+
const path22 = issue.path.join(".");
|
|
2627
|
+
return path22 ? `${path22}: ${issue.message}` : issue.message;
|
|
2568
2628
|
});
|
|
2569
2629
|
return Err(
|
|
2570
2630
|
createError(
|
|
@@ -2774,175 +2834,183 @@ function stringArraysEqual(a, b) {
|
|
|
2774
2834
|
const sortedB = [...b].sort();
|
|
2775
2835
|
return sortedA.every((val, i) => val === sortedB[i]);
|
|
2776
2836
|
}
|
|
2777
|
-
function
|
|
2778
|
-
const
|
|
2779
|
-
const
|
|
2780
|
-
const
|
|
2781
|
-
|
|
2782
|
-
const
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2837
|
+
function mergeLayers(localConfig, bundleLayers, config, contributions, conflicts) {
|
|
2838
|
+
const localLayers = Array.isArray(localConfig.layers) ? localConfig.layers : [];
|
|
2839
|
+
const mergedLayers = [...localLayers];
|
|
2840
|
+
const contributedLayerNames = [];
|
|
2841
|
+
for (const bundleLayer of bundleLayers) {
|
|
2842
|
+
const existing = localLayers.find((l) => l.name === bundleLayer.name);
|
|
2843
|
+
if (!existing) {
|
|
2844
|
+
mergedLayers.push(bundleLayer);
|
|
2845
|
+
contributedLayerNames.push(bundleLayer.name);
|
|
2846
|
+
} else {
|
|
2847
|
+
const same = existing.pattern === bundleLayer.pattern && stringArraysEqual(existing.allowedDependencies, bundleLayer.allowedDependencies);
|
|
2848
|
+
if (!same) {
|
|
2849
|
+
conflicts.push({
|
|
2850
|
+
section: "layers",
|
|
2851
|
+
key: bundleLayer.name,
|
|
2852
|
+
localValue: existing,
|
|
2853
|
+
packageValue: bundleLayer,
|
|
2854
|
+
description: `Layer '${bundleLayer.name}' already exists locally with different configuration`
|
|
2855
|
+
});
|
|
2856
|
+
}
|
|
2857
|
+
}
|
|
2858
|
+
}
|
|
2859
|
+
config.layers = mergedLayers;
|
|
2860
|
+
if (contributedLayerNames.length > 0) contributions.layers = contributedLayerNames;
|
|
2861
|
+
}
|
|
2862
|
+
function mergeForbiddenImports(localConfig, bundleRules, config, contributions, conflicts) {
|
|
2863
|
+
const localFI = Array.isArray(localConfig.forbiddenImports) ? localConfig.forbiddenImports : [];
|
|
2864
|
+
const mergedFI = [...localFI];
|
|
2865
|
+
const contributedFromKeys = [];
|
|
2866
|
+
for (const bundleRule of bundleRules) {
|
|
2867
|
+
const existing = localFI.find((r) => r.from === bundleRule.from);
|
|
2868
|
+
if (!existing) {
|
|
2869
|
+
const entry = { from: bundleRule.from, disallow: bundleRule.disallow };
|
|
2870
|
+
if (bundleRule.message !== void 0) entry.message = bundleRule.message;
|
|
2871
|
+
mergedFI.push(entry);
|
|
2872
|
+
contributedFromKeys.push(bundleRule.from);
|
|
2873
|
+
} else {
|
|
2874
|
+
if (!stringArraysEqual(existing.disallow, bundleRule.disallow)) {
|
|
2875
|
+
conflicts.push({
|
|
2876
|
+
section: "forbiddenImports",
|
|
2877
|
+
key: bundleRule.from,
|
|
2878
|
+
localValue: existing,
|
|
2879
|
+
packageValue: bundleRule,
|
|
2880
|
+
description: `Forbidden import rule for '${bundleRule.from}' already exists locally with different disallow list`
|
|
2881
|
+
});
|
|
2801
2882
|
}
|
|
2802
2883
|
}
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2884
|
+
}
|
|
2885
|
+
config.forbiddenImports = mergedFI;
|
|
2886
|
+
if (contributedFromKeys.length > 0) contributions.forbiddenImports = contributedFromKeys;
|
|
2887
|
+
}
|
|
2888
|
+
function mergeBoundaries(localConfig, bundleBoundaries, config, contributions) {
|
|
2889
|
+
const localBoundaries = localConfig.boundaries ?? { requireSchema: [] };
|
|
2890
|
+
const localSchemas = new Set(localBoundaries.requireSchema ?? []);
|
|
2891
|
+
const newSchemas = [];
|
|
2892
|
+
for (const schema of bundleBoundaries.requireSchema ?? []) {
|
|
2893
|
+
if (!localSchemas.has(schema)) {
|
|
2894
|
+
newSchemas.push(schema);
|
|
2895
|
+
localSchemas.add(schema);
|
|
2896
|
+
}
|
|
2897
|
+
}
|
|
2898
|
+
config.boundaries = { requireSchema: [...localBoundaries.requireSchema ?? [], ...newSchemas] };
|
|
2899
|
+
if (newSchemas.length > 0) contributions.boundaries = newSchemas;
|
|
2900
|
+
}
|
|
2901
|
+
function mergeArchitecture(localConfig, bundleArch, config, contributions, conflicts) {
|
|
2902
|
+
const localArch = localConfig.architecture ?? { thresholds: {}, modules: {} };
|
|
2903
|
+
const mergedThresholds = { ...localArch.thresholds };
|
|
2904
|
+
const contributedThresholdKeys = [];
|
|
2905
|
+
for (const [category, value] of Object.entries(bundleArch.thresholds ?? {})) {
|
|
2906
|
+
if (!(category in mergedThresholds)) {
|
|
2907
|
+
mergedThresholds[category] = value;
|
|
2908
|
+
contributedThresholdKeys.push(category);
|
|
2909
|
+
} else if (!deepEqual(mergedThresholds[category], value)) {
|
|
2910
|
+
conflicts.push({
|
|
2911
|
+
section: "architecture.thresholds",
|
|
2912
|
+
key: category,
|
|
2913
|
+
localValue: mergedThresholds[category],
|
|
2914
|
+
packageValue: value,
|
|
2915
|
+
description: `Architecture threshold '${category}' already exists locally with a different value`
|
|
2916
|
+
});
|
|
2806
2917
|
}
|
|
2807
2918
|
}
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
const
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
}
|
|
2822
|
-
mergedFI.push(entry);
|
|
2823
|
-
contributedFromKeys.push(bundleRule.from);
|
|
2824
|
-
} else {
|
|
2825
|
-
const same = stringArraysEqual(existing.disallow, bundleRule.disallow);
|
|
2826
|
-
if (!same) {
|
|
2919
|
+
const mergedModules = { ...localArch.modules };
|
|
2920
|
+
const contributedModuleKeys = [];
|
|
2921
|
+
for (const [modulePath, bundleCategoryMap] of Object.entries(bundleArch.modules ?? {})) {
|
|
2922
|
+
if (!(modulePath in mergedModules)) {
|
|
2923
|
+
mergedModules[modulePath] = bundleCategoryMap;
|
|
2924
|
+
for (const cat of Object.keys(bundleCategoryMap))
|
|
2925
|
+
contributedModuleKeys.push(`${modulePath}:${cat}`);
|
|
2926
|
+
} else {
|
|
2927
|
+
const mergedCategoryMap = { ...mergedModules[modulePath] };
|
|
2928
|
+
for (const [category, value] of Object.entries(bundleCategoryMap)) {
|
|
2929
|
+
if (!(category in mergedCategoryMap)) {
|
|
2930
|
+
mergedCategoryMap[category] = value;
|
|
2931
|
+
contributedModuleKeys.push(`${modulePath}:${category}`);
|
|
2932
|
+
} else if (!deepEqual(mergedCategoryMap[category], value)) {
|
|
2827
2933
|
conflicts.push({
|
|
2828
|
-
section: "
|
|
2829
|
-
key:
|
|
2830
|
-
localValue:
|
|
2831
|
-
packageValue:
|
|
2832
|
-
description: `
|
|
2934
|
+
section: "architecture.modules",
|
|
2935
|
+
key: `${modulePath}:${category}`,
|
|
2936
|
+
localValue: mergedCategoryMap[category],
|
|
2937
|
+
packageValue: value,
|
|
2938
|
+
description: `Architecture module override '${modulePath}' category '${category}' already exists locally with a different value`
|
|
2833
2939
|
});
|
|
2834
2940
|
}
|
|
2835
2941
|
}
|
|
2942
|
+
mergedModules[modulePath] = mergedCategoryMap;
|
|
2943
|
+
}
|
|
2944
|
+
}
|
|
2945
|
+
config.architecture = { ...localArch, thresholds: mergedThresholds, modules: mergedModules };
|
|
2946
|
+
if (contributedThresholdKeys.length > 0)
|
|
2947
|
+
contributions["architecture.thresholds"] = contributedThresholdKeys;
|
|
2948
|
+
if (contributedModuleKeys.length > 0)
|
|
2949
|
+
contributions["architecture.modules"] = contributedModuleKeys;
|
|
2950
|
+
}
|
|
2951
|
+
function mergeSecurityRules(localConfig, bundleRules, config, contributions, conflicts) {
|
|
2952
|
+
const localSecurity = localConfig.security ?? { rules: {} };
|
|
2953
|
+
const localRules = localSecurity.rules ?? {};
|
|
2954
|
+
const mergedRules = { ...localRules };
|
|
2955
|
+
const contributedRuleIds = [];
|
|
2956
|
+
for (const [ruleId, severity] of Object.entries(bundleRules)) {
|
|
2957
|
+
if (!(ruleId in mergedRules)) {
|
|
2958
|
+
mergedRules[ruleId] = severity;
|
|
2959
|
+
contributedRuleIds.push(ruleId);
|
|
2960
|
+
} else if (mergedRules[ruleId] !== severity) {
|
|
2961
|
+
conflicts.push({
|
|
2962
|
+
section: "security.rules",
|
|
2963
|
+
key: ruleId,
|
|
2964
|
+
localValue: mergedRules[ruleId],
|
|
2965
|
+
packageValue: severity,
|
|
2966
|
+
description: `Security rule '${ruleId}' already exists locally with severity '${mergedRules[ruleId]}', bundle has '${severity}'`
|
|
2967
|
+
});
|
|
2836
2968
|
}
|
|
2837
|
-
|
|
2838
|
-
|
|
2839
|
-
|
|
2840
|
-
|
|
2969
|
+
}
|
|
2970
|
+
config.security = { ...localSecurity, rules: mergedRules };
|
|
2971
|
+
if (contributedRuleIds.length > 0) contributions["security.rules"] = contributedRuleIds;
|
|
2972
|
+
}
|
|
2973
|
+
function deepMergeConstraints(localConfig, bundleConstraints, _existingContributions) {
|
|
2974
|
+
const config = { ...localConfig };
|
|
2975
|
+
const contributions = {};
|
|
2976
|
+
const conflicts = [];
|
|
2977
|
+
if (bundleConstraints.layers && bundleConstraints.layers.length > 0) {
|
|
2978
|
+
mergeLayers(localConfig, bundleConstraints.layers, config, contributions, conflicts);
|
|
2979
|
+
}
|
|
2980
|
+
if (bundleConstraints.forbiddenImports && bundleConstraints.forbiddenImports.length > 0) {
|
|
2981
|
+
mergeForbiddenImports(
|
|
2982
|
+
localConfig,
|
|
2983
|
+
bundleConstraints.forbiddenImports,
|
|
2984
|
+
config,
|
|
2985
|
+
contributions,
|
|
2986
|
+
conflicts
|
|
2987
|
+
);
|
|
2841
2988
|
}
|
|
2842
2989
|
if (bundleConstraints.boundaries) {
|
|
2843
|
-
|
|
2844
|
-
|
|
2845
|
-
|
|
2846
|
-
|
|
2847
|
-
|
|
2848
|
-
|
|
2849
|
-
newSchemas.push(schema);
|
|
2850
|
-
localSchemas.add(schema);
|
|
2851
|
-
}
|
|
2852
|
-
}
|
|
2853
|
-
config.boundaries = {
|
|
2854
|
-
requireSchema: [...localBoundaries.requireSchema ?? [], ...newSchemas]
|
|
2855
|
-
};
|
|
2856
|
-
if (newSchemas.length > 0) {
|
|
2857
|
-
contributions.boundaries = newSchemas;
|
|
2858
|
-
}
|
|
2990
|
+
mergeBoundaries(
|
|
2991
|
+
localConfig,
|
|
2992
|
+
bundleConstraints.boundaries,
|
|
2993
|
+
config,
|
|
2994
|
+
contributions
|
|
2995
|
+
);
|
|
2859
2996
|
}
|
|
2860
2997
|
if (bundleConstraints.architecture) {
|
|
2861
|
-
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
|
|
2867
|
-
|
|
2868
|
-
for (const [category, value] of Object.entries(bundleThresholds)) {
|
|
2869
|
-
if (!(category in mergedThresholds)) {
|
|
2870
|
-
mergedThresholds[category] = value;
|
|
2871
|
-
contributedThresholdKeys.push(category);
|
|
2872
|
-
} else if (!deepEqual(mergedThresholds[category], value)) {
|
|
2873
|
-
conflicts.push({
|
|
2874
|
-
section: "architecture.thresholds",
|
|
2875
|
-
key: category,
|
|
2876
|
-
localValue: mergedThresholds[category],
|
|
2877
|
-
packageValue: value,
|
|
2878
|
-
description: `Architecture threshold '${category}' already exists locally with a different value`
|
|
2879
|
-
});
|
|
2880
|
-
}
|
|
2881
|
-
}
|
|
2882
|
-
const mergedModules = { ...localArch.modules };
|
|
2883
|
-
const contributedModuleKeys = [];
|
|
2884
|
-
const bundleModules = bundleConstraints.architecture.modules ?? {};
|
|
2885
|
-
for (const [modulePath, bundleCategoryMap] of Object.entries(bundleModules)) {
|
|
2886
|
-
if (!(modulePath in mergedModules)) {
|
|
2887
|
-
mergedModules[modulePath] = bundleCategoryMap;
|
|
2888
|
-
for (const cat of Object.keys(bundleCategoryMap)) {
|
|
2889
|
-
contributedModuleKeys.push(`${modulePath}:${cat}`);
|
|
2890
|
-
}
|
|
2891
|
-
} else {
|
|
2892
|
-
const localCategoryMap = mergedModules[modulePath];
|
|
2893
|
-
const mergedCategoryMap = { ...localCategoryMap };
|
|
2894
|
-
for (const [category, value] of Object.entries(bundleCategoryMap)) {
|
|
2895
|
-
if (!(category in mergedCategoryMap)) {
|
|
2896
|
-
mergedCategoryMap[category] = value;
|
|
2897
|
-
contributedModuleKeys.push(`${modulePath}:${category}`);
|
|
2898
|
-
} else if (!deepEqual(mergedCategoryMap[category], value)) {
|
|
2899
|
-
conflicts.push({
|
|
2900
|
-
section: "architecture.modules",
|
|
2901
|
-
key: `${modulePath}:${category}`,
|
|
2902
|
-
localValue: mergedCategoryMap[category],
|
|
2903
|
-
packageValue: value,
|
|
2904
|
-
description: `Architecture module override '${modulePath}' category '${category}' already exists locally with a different value`
|
|
2905
|
-
});
|
|
2906
|
-
}
|
|
2907
|
-
}
|
|
2908
|
-
mergedModules[modulePath] = mergedCategoryMap;
|
|
2909
|
-
}
|
|
2910
|
-
}
|
|
2911
|
-
config.architecture = {
|
|
2912
|
-
...localArch,
|
|
2913
|
-
thresholds: mergedThresholds,
|
|
2914
|
-
modules: mergedModules
|
|
2915
|
-
};
|
|
2916
|
-
if (contributedThresholdKeys.length > 0) {
|
|
2917
|
-
contributions["architecture.thresholds"] = contributedThresholdKeys;
|
|
2918
|
-
}
|
|
2919
|
-
if (contributedModuleKeys.length > 0) {
|
|
2920
|
-
contributions["architecture.modules"] = contributedModuleKeys;
|
|
2921
|
-
}
|
|
2998
|
+
mergeArchitecture(
|
|
2999
|
+
localConfig,
|
|
3000
|
+
bundleConstraints.architecture,
|
|
3001
|
+
config,
|
|
3002
|
+
contributions,
|
|
3003
|
+
conflicts
|
|
3004
|
+
);
|
|
2922
3005
|
}
|
|
2923
3006
|
if (bundleConstraints.security?.rules) {
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
contributedRuleIds.push(ruleId);
|
|
2932
|
-
} else if (mergedRules[ruleId] !== severity) {
|
|
2933
|
-
conflicts.push({
|
|
2934
|
-
section: "security.rules",
|
|
2935
|
-
key: ruleId,
|
|
2936
|
-
localValue: mergedRules[ruleId],
|
|
2937
|
-
packageValue: severity,
|
|
2938
|
-
description: `Security rule '${ruleId}' already exists locally with severity '${mergedRules[ruleId]}', bundle has '${severity}'`
|
|
2939
|
-
});
|
|
2940
|
-
}
|
|
2941
|
-
}
|
|
2942
|
-
config.security = { ...localSecurity, rules: mergedRules };
|
|
2943
|
-
if (contributedRuleIds.length > 0) {
|
|
2944
|
-
contributions["security.rules"] = contributedRuleIds;
|
|
2945
|
-
}
|
|
3007
|
+
mergeSecurityRules(
|
|
3008
|
+
localConfig,
|
|
3009
|
+
bundleConstraints.security.rules,
|
|
3010
|
+
config,
|
|
3011
|
+
contributions,
|
|
3012
|
+
conflicts
|
|
3013
|
+
);
|
|
2946
3014
|
}
|
|
2947
3015
|
return { config, contributions, conflicts };
|
|
2948
3016
|
}
|
|
@@ -3091,14 +3159,84 @@ function walk(node, visitor) {
|
|
|
3091
3159
|
}
|
|
3092
3160
|
}
|
|
3093
3161
|
}
|
|
3162
|
+
function makeLocation(node) {
|
|
3163
|
+
return {
|
|
3164
|
+
file: "",
|
|
3165
|
+
line: node.loc?.start.line ?? 0,
|
|
3166
|
+
column: node.loc?.start.column ?? 0
|
|
3167
|
+
};
|
|
3168
|
+
}
|
|
3169
|
+
function processImportSpecifiers(importDecl, imp) {
|
|
3170
|
+
for (const spec of importDecl.specifiers) {
|
|
3171
|
+
if (spec.type === "ImportDefaultSpecifier") {
|
|
3172
|
+
imp.default = spec.local.name;
|
|
3173
|
+
} else if (spec.type === "ImportNamespaceSpecifier") {
|
|
3174
|
+
imp.namespace = spec.local.name;
|
|
3175
|
+
} else if (spec.type === "ImportSpecifier") {
|
|
3176
|
+
imp.specifiers.push(spec.local.name);
|
|
3177
|
+
if (spec.importKind === "type") {
|
|
3178
|
+
imp.kind = "type";
|
|
3179
|
+
}
|
|
3180
|
+
}
|
|
3181
|
+
}
|
|
3182
|
+
}
|
|
3183
|
+
function getExportedName(exported) {
|
|
3184
|
+
return exported.type === "Identifier" ? exported.name : String(exported.value);
|
|
3185
|
+
}
|
|
3186
|
+
function processReExportSpecifiers(exportDecl, exports) {
|
|
3187
|
+
for (const spec of exportDecl.specifiers) {
|
|
3188
|
+
if (spec.type !== "ExportSpecifier") continue;
|
|
3189
|
+
exports.push({
|
|
3190
|
+
name: getExportedName(spec.exported),
|
|
3191
|
+
type: "named",
|
|
3192
|
+
location: makeLocation(exportDecl),
|
|
3193
|
+
isReExport: true,
|
|
3194
|
+
source: exportDecl.source.value
|
|
3195
|
+
});
|
|
3196
|
+
}
|
|
3197
|
+
}
|
|
3198
|
+
function processExportDeclaration(exportDecl, exports) {
|
|
3199
|
+
const decl = exportDecl.declaration;
|
|
3200
|
+
if (!decl) return;
|
|
3201
|
+
if (decl.type === "VariableDeclaration") {
|
|
3202
|
+
for (const declarator of decl.declarations) {
|
|
3203
|
+
if (declarator.id.type === "Identifier") {
|
|
3204
|
+
exports.push({
|
|
3205
|
+
name: declarator.id.name,
|
|
3206
|
+
type: "named",
|
|
3207
|
+
location: makeLocation(decl),
|
|
3208
|
+
isReExport: false
|
|
3209
|
+
});
|
|
3210
|
+
}
|
|
3211
|
+
}
|
|
3212
|
+
} else if ((decl.type === "FunctionDeclaration" || decl.type === "ClassDeclaration") && decl.id) {
|
|
3213
|
+
exports.push({
|
|
3214
|
+
name: decl.id.name,
|
|
3215
|
+
type: "named",
|
|
3216
|
+
location: makeLocation(decl),
|
|
3217
|
+
isReExport: false
|
|
3218
|
+
});
|
|
3219
|
+
}
|
|
3220
|
+
}
|
|
3221
|
+
function processExportListSpecifiers(exportDecl, exports) {
|
|
3222
|
+
for (const spec of exportDecl.specifiers) {
|
|
3223
|
+
if (spec.type !== "ExportSpecifier") continue;
|
|
3224
|
+
exports.push({
|
|
3225
|
+
name: getExportedName(spec.exported),
|
|
3226
|
+
type: "named",
|
|
3227
|
+
location: makeLocation(exportDecl),
|
|
3228
|
+
isReExport: false
|
|
3229
|
+
});
|
|
3230
|
+
}
|
|
3231
|
+
}
|
|
3094
3232
|
var TypeScriptParser = class {
|
|
3095
3233
|
name = "typescript";
|
|
3096
3234
|
extensions = [".ts", ".tsx", ".mts", ".cts"];
|
|
3097
|
-
async parseFile(
|
|
3098
|
-
const contentResult = await readFileContent(
|
|
3235
|
+
async parseFile(path22) {
|
|
3236
|
+
const contentResult = await readFileContent(path22);
|
|
3099
3237
|
if (!contentResult.ok) {
|
|
3100
3238
|
return Err(
|
|
3101
|
-
createParseError("NOT_FOUND", `File not found: ${
|
|
3239
|
+
createParseError("NOT_FOUND", `File not found: ${path22}`, { path: path22 }, [
|
|
3102
3240
|
"Check that the file exists",
|
|
3103
3241
|
"Verify the path is correct"
|
|
3104
3242
|
])
|
|
@@ -3108,7 +3246,7 @@ var TypeScriptParser = class {
|
|
|
3108
3246
|
const ast = parse(contentResult.value, {
|
|
3109
3247
|
loc: true,
|
|
3110
3248
|
range: true,
|
|
3111
|
-
jsx:
|
|
3249
|
+
jsx: path22.endsWith(".tsx"),
|
|
3112
3250
|
errorOnUnknownASTType: false
|
|
3113
3251
|
});
|
|
3114
3252
|
return Ok({
|
|
@@ -3119,7 +3257,7 @@ var TypeScriptParser = class {
|
|
|
3119
3257
|
} catch (e) {
|
|
3120
3258
|
const error = e;
|
|
3121
3259
|
return Err(
|
|
3122
|
-
createParseError("SYNTAX_ERROR", `Failed to parse ${
|
|
3260
|
+
createParseError("SYNTAX_ERROR", `Failed to parse ${path22}: ${error.message}`, { path: path22 }, [
|
|
3123
3261
|
"Check for syntax errors in the file",
|
|
3124
3262
|
"Ensure valid TypeScript syntax"
|
|
3125
3263
|
])
|
|
@@ -3135,26 +3273,12 @@ var TypeScriptParser = class {
|
|
|
3135
3273
|
const imp = {
|
|
3136
3274
|
source: importDecl.source.value,
|
|
3137
3275
|
specifiers: [],
|
|
3138
|
-
location:
|
|
3139
|
-
file: "",
|
|
3140
|
-
line: importDecl.loc?.start.line ?? 0,
|
|
3141
|
-
column: importDecl.loc?.start.column ?? 0
|
|
3142
|
-
},
|
|
3276
|
+
location: makeLocation(importDecl),
|
|
3143
3277
|
kind: importDecl.importKind === "type" ? "type" : "value"
|
|
3144
3278
|
};
|
|
3145
|
-
|
|
3146
|
-
if (spec.type === "ImportDefaultSpecifier") {
|
|
3147
|
-
imp.default = spec.local.name;
|
|
3148
|
-
} else if (spec.type === "ImportNamespaceSpecifier") {
|
|
3149
|
-
imp.namespace = spec.local.name;
|
|
3150
|
-
} else if (spec.type === "ImportSpecifier") {
|
|
3151
|
-
imp.specifiers.push(spec.local.name);
|
|
3152
|
-
if (spec.importKind === "type") {
|
|
3153
|
-
imp.kind = "type";
|
|
3154
|
-
}
|
|
3155
|
-
}
|
|
3156
|
-
}
|
|
3279
|
+
processImportSpecifiers(importDecl, imp);
|
|
3157
3280
|
imports.push(imp);
|
|
3281
|
+
return;
|
|
3158
3282
|
}
|
|
3159
3283
|
if (node.type === "ImportExpression") {
|
|
3160
3284
|
const importExpr = node;
|
|
@@ -3162,11 +3286,7 @@ var TypeScriptParser = class {
|
|
|
3162
3286
|
imports.push({
|
|
3163
3287
|
source: importExpr.source.value,
|
|
3164
3288
|
specifiers: [],
|
|
3165
|
-
location:
|
|
3166
|
-
file: "",
|
|
3167
|
-
line: importExpr.loc?.start.line ?? 0,
|
|
3168
|
-
column: importExpr.loc?.start.column ?? 0
|
|
3169
|
-
},
|
|
3289
|
+
location: makeLocation(importExpr),
|
|
3170
3290
|
kind: "value"
|
|
3171
3291
|
});
|
|
3172
3292
|
}
|
|
@@ -3181,97 +3301,29 @@ var TypeScriptParser = class {
|
|
|
3181
3301
|
if (node.type === "ExportNamedDeclaration") {
|
|
3182
3302
|
const exportDecl = node;
|
|
3183
3303
|
if (exportDecl.source) {
|
|
3184
|
-
|
|
3185
|
-
if (spec.type === "ExportSpecifier") {
|
|
3186
|
-
const exported = spec.exported;
|
|
3187
|
-
const name = exported.type === "Identifier" ? exported.name : String(exported.value);
|
|
3188
|
-
exports.push({
|
|
3189
|
-
name,
|
|
3190
|
-
type: "named",
|
|
3191
|
-
location: {
|
|
3192
|
-
file: "",
|
|
3193
|
-
line: exportDecl.loc?.start.line ?? 0,
|
|
3194
|
-
column: exportDecl.loc?.start.column ?? 0
|
|
3195
|
-
},
|
|
3196
|
-
isReExport: true,
|
|
3197
|
-
source: exportDecl.source.value
|
|
3198
|
-
});
|
|
3199
|
-
}
|
|
3200
|
-
}
|
|
3304
|
+
processReExportSpecifiers(exportDecl, exports);
|
|
3201
3305
|
return;
|
|
3202
3306
|
}
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
for (const declarator of decl.declarations) {
|
|
3207
|
-
if (declarator.id.type === "Identifier") {
|
|
3208
|
-
exports.push({
|
|
3209
|
-
name: declarator.id.name,
|
|
3210
|
-
type: "named",
|
|
3211
|
-
location: {
|
|
3212
|
-
file: "",
|
|
3213
|
-
line: decl.loc?.start.line ?? 0,
|
|
3214
|
-
column: decl.loc?.start.column ?? 0
|
|
3215
|
-
},
|
|
3216
|
-
isReExport: false
|
|
3217
|
-
});
|
|
3218
|
-
}
|
|
3219
|
-
}
|
|
3220
|
-
} else if (decl.type === "FunctionDeclaration" || decl.type === "ClassDeclaration") {
|
|
3221
|
-
if (decl.id) {
|
|
3222
|
-
exports.push({
|
|
3223
|
-
name: decl.id.name,
|
|
3224
|
-
type: "named",
|
|
3225
|
-
location: {
|
|
3226
|
-
file: "",
|
|
3227
|
-
line: decl.loc?.start.line ?? 0,
|
|
3228
|
-
column: decl.loc?.start.column ?? 0
|
|
3229
|
-
},
|
|
3230
|
-
isReExport: false
|
|
3231
|
-
});
|
|
3232
|
-
}
|
|
3233
|
-
}
|
|
3234
|
-
}
|
|
3235
|
-
for (const spec of exportDecl.specifiers) {
|
|
3236
|
-
if (spec.type === "ExportSpecifier") {
|
|
3237
|
-
const exported = spec.exported;
|
|
3238
|
-
const name = exported.type === "Identifier" ? exported.name : String(exported.value);
|
|
3239
|
-
exports.push({
|
|
3240
|
-
name,
|
|
3241
|
-
type: "named",
|
|
3242
|
-
location: {
|
|
3243
|
-
file: "",
|
|
3244
|
-
line: exportDecl.loc?.start.line ?? 0,
|
|
3245
|
-
column: exportDecl.loc?.start.column ?? 0
|
|
3246
|
-
},
|
|
3247
|
-
isReExport: false
|
|
3248
|
-
});
|
|
3249
|
-
}
|
|
3250
|
-
}
|
|
3307
|
+
processExportDeclaration(exportDecl, exports);
|
|
3308
|
+
processExportListSpecifiers(exportDecl, exports);
|
|
3309
|
+
return;
|
|
3251
3310
|
}
|
|
3252
3311
|
if (node.type === "ExportDefaultDeclaration") {
|
|
3253
3312
|
const exportDecl = node;
|
|
3254
3313
|
exports.push({
|
|
3255
3314
|
name: "default",
|
|
3256
3315
|
type: "default",
|
|
3257
|
-
location:
|
|
3258
|
-
file: "",
|
|
3259
|
-
line: exportDecl.loc?.start.line ?? 0,
|
|
3260
|
-
column: exportDecl.loc?.start.column ?? 0
|
|
3261
|
-
},
|
|
3316
|
+
location: makeLocation(exportDecl),
|
|
3262
3317
|
isReExport: false
|
|
3263
3318
|
});
|
|
3319
|
+
return;
|
|
3264
3320
|
}
|
|
3265
3321
|
if (node.type === "ExportAllDeclaration") {
|
|
3266
3322
|
const exportDecl = node;
|
|
3267
3323
|
exports.push({
|
|
3268
3324
|
name: exportDecl.exported?.name ?? "*",
|
|
3269
3325
|
type: "namespace",
|
|
3270
|
-
location:
|
|
3271
|
-
file: "",
|
|
3272
|
-
line: exportDecl.loc?.start.line ?? 0,
|
|
3273
|
-
column: exportDecl.loc?.start.column ?? 0
|
|
3274
|
-
},
|
|
3326
|
+
location: makeLocation(exportDecl),
|
|
3275
3327
|
isReExport: true,
|
|
3276
3328
|
source: exportDecl.source.value
|
|
3277
3329
|
});
|
|
@@ -3283,10 +3335,27 @@ var TypeScriptParser = class {
|
|
|
3283
3335
|
return Ok({ available: true, version: "7.0.0" });
|
|
3284
3336
|
}
|
|
3285
3337
|
};
|
|
3338
|
+
function collectFieldEntries(rootDir, field) {
|
|
3339
|
+
if (typeof field === "string") return [resolve3(rootDir, field)];
|
|
3340
|
+
if (typeof field === "object" && field !== null) {
|
|
3341
|
+
return Object.values(field).filter((v) => typeof v === "string").map((v) => resolve3(rootDir, v));
|
|
3342
|
+
}
|
|
3343
|
+
return [];
|
|
3344
|
+
}
|
|
3345
|
+
function extractPackageEntries(rootDir, pkg) {
|
|
3346
|
+
const entries = [];
|
|
3347
|
+
entries.push(...collectFieldEntries(rootDir, pkg["exports"]));
|
|
3348
|
+
if (entries.length === 0 && typeof pkg["main"] === "string") {
|
|
3349
|
+
entries.push(resolve3(rootDir, pkg["main"]));
|
|
3350
|
+
}
|
|
3351
|
+
if (pkg["bin"]) {
|
|
3352
|
+
entries.push(...collectFieldEntries(rootDir, pkg["bin"]));
|
|
3353
|
+
}
|
|
3354
|
+
return entries;
|
|
3355
|
+
}
|
|
3286
3356
|
async function resolveEntryPoints(rootDir, explicitEntries) {
|
|
3287
3357
|
if (explicitEntries && explicitEntries.length > 0) {
|
|
3288
|
-
|
|
3289
|
-
return Ok(resolved);
|
|
3358
|
+
return Ok(explicitEntries.map((e) => resolve3(rootDir, e)));
|
|
3290
3359
|
}
|
|
3291
3360
|
const pkgPath = join32(rootDir, "package.json");
|
|
3292
3361
|
if (await fileExists(pkgPath)) {
|
|
@@ -3294,38 +3363,8 @@ async function resolveEntryPoints(rootDir, explicitEntries) {
|
|
|
3294
3363
|
if (pkgContent.ok) {
|
|
3295
3364
|
try {
|
|
3296
3365
|
const pkg = JSON.parse(pkgContent.value);
|
|
3297
|
-
const entries =
|
|
3298
|
-
if (
|
|
3299
|
-
const exports = pkg["exports"];
|
|
3300
|
-
if (typeof exports === "string") {
|
|
3301
|
-
entries.push(resolve3(rootDir, exports));
|
|
3302
|
-
} else if (typeof exports === "object" && exports !== null) {
|
|
3303
|
-
for (const value of Object.values(exports)) {
|
|
3304
|
-
if (typeof value === "string") {
|
|
3305
|
-
entries.push(resolve3(rootDir, value));
|
|
3306
|
-
}
|
|
3307
|
-
}
|
|
3308
|
-
}
|
|
3309
|
-
}
|
|
3310
|
-
const main = pkg["main"];
|
|
3311
|
-
if (typeof main === "string" && entries.length === 0) {
|
|
3312
|
-
entries.push(resolve3(rootDir, main));
|
|
3313
|
-
}
|
|
3314
|
-
const bin = pkg["bin"];
|
|
3315
|
-
if (bin) {
|
|
3316
|
-
if (typeof bin === "string") {
|
|
3317
|
-
entries.push(resolve3(rootDir, bin));
|
|
3318
|
-
} else if (typeof bin === "object") {
|
|
3319
|
-
for (const value of Object.values(bin)) {
|
|
3320
|
-
if (typeof value === "string") {
|
|
3321
|
-
entries.push(resolve3(rootDir, value));
|
|
3322
|
-
}
|
|
3323
|
-
}
|
|
3324
|
-
}
|
|
3325
|
-
}
|
|
3326
|
-
if (entries.length > 0) {
|
|
3327
|
-
return Ok(entries);
|
|
3328
|
-
}
|
|
3366
|
+
const entries = extractPackageEntries(rootDir, pkg);
|
|
3367
|
+
if (entries.length > 0) return Ok(entries);
|
|
3329
3368
|
} catch {
|
|
3330
3369
|
}
|
|
3331
3370
|
}
|
|
@@ -3399,66 +3438,49 @@ function extractInlineRefs(content) {
|
|
|
3399
3438
|
}
|
|
3400
3439
|
return refs;
|
|
3401
3440
|
}
|
|
3402
|
-
async function parseDocumentationFile(
|
|
3403
|
-
const contentResult = await readFileContent(
|
|
3441
|
+
async function parseDocumentationFile(path22) {
|
|
3442
|
+
const contentResult = await readFileContent(path22);
|
|
3404
3443
|
if (!contentResult.ok) {
|
|
3405
3444
|
return Err(
|
|
3406
3445
|
createEntropyError(
|
|
3407
3446
|
"PARSE_ERROR",
|
|
3408
|
-
`Failed to read documentation file: ${
|
|
3409
|
-
{ file:
|
|
3447
|
+
`Failed to read documentation file: ${path22}`,
|
|
3448
|
+
{ file: path22 },
|
|
3410
3449
|
["Check that the file exists"]
|
|
3411
3450
|
)
|
|
3412
3451
|
);
|
|
3413
3452
|
}
|
|
3414
3453
|
const content = contentResult.value;
|
|
3415
|
-
const type =
|
|
3454
|
+
const type = path22.endsWith(".md") ? "markdown" : "text";
|
|
3416
3455
|
return Ok({
|
|
3417
|
-
path:
|
|
3456
|
+
path: path22,
|
|
3418
3457
|
type,
|
|
3419
3458
|
content,
|
|
3420
3459
|
codeBlocks: extractCodeBlocks(content),
|
|
3421
3460
|
inlineRefs: extractInlineRefs(content)
|
|
3422
3461
|
});
|
|
3423
3462
|
}
|
|
3463
|
+
function makeInternalSymbol(name, type, line) {
|
|
3464
|
+
return { name, type, line, references: 0, calledBy: [] };
|
|
3465
|
+
}
|
|
3466
|
+
function extractSymbolsFromNode(node) {
|
|
3467
|
+
const line = node.loc?.start?.line || 0;
|
|
3468
|
+
if (node.type === "FunctionDeclaration" && node.id?.name) {
|
|
3469
|
+
return [makeInternalSymbol(node.id.name, "function", line)];
|
|
3470
|
+
}
|
|
3471
|
+
if (node.type === "VariableDeclaration") {
|
|
3472
|
+
return (node.declarations || []).filter((decl) => decl.id?.name).map((decl) => makeInternalSymbol(decl.id.name, "variable", line));
|
|
3473
|
+
}
|
|
3474
|
+
if (node.type === "ClassDeclaration" && node.id?.name) {
|
|
3475
|
+
return [makeInternalSymbol(node.id.name, "class", line)];
|
|
3476
|
+
}
|
|
3477
|
+
return [];
|
|
3478
|
+
}
|
|
3424
3479
|
function extractInternalSymbols(ast) {
|
|
3425
|
-
const symbols = [];
|
|
3426
3480
|
const body = ast.body;
|
|
3427
|
-
if (!body?.body) return
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
symbols.push({
|
|
3431
|
-
name: node.id.name,
|
|
3432
|
-
type: "function",
|
|
3433
|
-
line: node.loc?.start?.line || 0,
|
|
3434
|
-
references: 0,
|
|
3435
|
-
calledBy: []
|
|
3436
|
-
});
|
|
3437
|
-
}
|
|
3438
|
-
if (node.type === "VariableDeclaration") {
|
|
3439
|
-
for (const decl of node.declarations || []) {
|
|
3440
|
-
if (decl.id?.name) {
|
|
3441
|
-
symbols.push({
|
|
3442
|
-
name: decl.id.name,
|
|
3443
|
-
type: "variable",
|
|
3444
|
-
line: node.loc?.start?.line || 0,
|
|
3445
|
-
references: 0,
|
|
3446
|
-
calledBy: []
|
|
3447
|
-
});
|
|
3448
|
-
}
|
|
3449
|
-
}
|
|
3450
|
-
}
|
|
3451
|
-
if (node.type === "ClassDeclaration" && node.id?.name) {
|
|
3452
|
-
symbols.push({
|
|
3453
|
-
name: node.id.name,
|
|
3454
|
-
type: "class",
|
|
3455
|
-
line: node.loc?.start?.line || 0,
|
|
3456
|
-
references: 0,
|
|
3457
|
-
calledBy: []
|
|
3458
|
-
});
|
|
3459
|
-
}
|
|
3460
|
-
}
|
|
3461
|
-
return symbols;
|
|
3481
|
+
if (!body?.body) return [];
|
|
3482
|
+
const nodes = body.body;
|
|
3483
|
+
return nodes.flatMap(extractSymbolsFromNode);
|
|
3462
3484
|
}
|
|
3463
3485
|
function extractJSDocComments(ast) {
|
|
3464
3486
|
const comments = [];
|
|
@@ -3596,27 +3618,34 @@ async function buildSnapshot(config) {
|
|
|
3596
3618
|
buildTime
|
|
3597
3619
|
});
|
|
3598
3620
|
}
|
|
3599
|
-
function
|
|
3621
|
+
function initLevenshteinMatrix(aLen, bLen) {
|
|
3600
3622
|
const matrix = [];
|
|
3601
|
-
for (let i = 0; i <=
|
|
3623
|
+
for (let i = 0; i <= bLen; i++) {
|
|
3602
3624
|
matrix[i] = [i];
|
|
3603
3625
|
}
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3626
|
+
const firstRow = matrix[0];
|
|
3627
|
+
if (firstRow) {
|
|
3628
|
+
for (let j = 0; j <= aLen; j++) {
|
|
3629
|
+
firstRow[j] = j;
|
|
3608
3630
|
}
|
|
3609
3631
|
}
|
|
3632
|
+
return matrix;
|
|
3633
|
+
}
|
|
3634
|
+
function computeLevenshteinCell(row, prevRow, j, charsMatch) {
|
|
3635
|
+
if (charsMatch) {
|
|
3636
|
+
row[j] = prevRow[j - 1] ?? 0;
|
|
3637
|
+
} else {
|
|
3638
|
+
row[j] = Math.min((prevRow[j - 1] ?? 0) + 1, (row[j - 1] ?? 0) + 1, (prevRow[j] ?? 0) + 1);
|
|
3639
|
+
}
|
|
3640
|
+
}
|
|
3641
|
+
function levenshteinDistance(a, b) {
|
|
3642
|
+
const matrix = initLevenshteinMatrix(a.length, b.length);
|
|
3610
3643
|
for (let i = 1; i <= b.length; i++) {
|
|
3611
3644
|
for (let j = 1; j <= a.length; j++) {
|
|
3612
3645
|
const row = matrix[i];
|
|
3613
3646
|
const prevRow = matrix[i - 1];
|
|
3614
3647
|
if (!row || !prevRow) continue;
|
|
3615
|
-
|
|
3616
|
-
row[j] = prevRow[j - 1] ?? 0;
|
|
3617
|
-
} else {
|
|
3618
|
-
row[j] = Math.min((prevRow[j - 1] ?? 0) + 1, (row[j - 1] ?? 0) + 1, (prevRow[j] ?? 0) + 1);
|
|
3619
|
-
}
|
|
3648
|
+
computeLevenshteinCell(row, prevRow, j, b.charAt(i - 1) === a.charAt(j - 1));
|
|
3620
3649
|
}
|
|
3621
3650
|
}
|
|
3622
3651
|
const lastRow = matrix[b.length];
|
|
@@ -3899,32 +3928,27 @@ function findDeadExports(snapshot, usageMap, reachability) {
|
|
|
3899
3928
|
}
|
|
3900
3929
|
return deadExports;
|
|
3901
3930
|
}
|
|
3902
|
-
function
|
|
3903
|
-
if (
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
for (const key of Object.keys(node)) {
|
|
3912
|
-
const value = node[key];
|
|
3913
|
-
if (Array.isArray(value)) {
|
|
3914
|
-
for (const item of value) {
|
|
3915
|
-
traverse(item);
|
|
3916
|
-
}
|
|
3917
|
-
} else if (value && typeof value === "object") {
|
|
3918
|
-
traverse(value);
|
|
3919
|
-
}
|
|
3920
|
-
}
|
|
3931
|
+
function findMaxLineInNode(node) {
|
|
3932
|
+
if (!node || typeof node !== "object") return 0;
|
|
3933
|
+
const n = node;
|
|
3934
|
+
let maxLine = n.loc?.end?.line ?? 0;
|
|
3935
|
+
for (const key of Object.keys(node)) {
|
|
3936
|
+
const value = node[key];
|
|
3937
|
+
if (Array.isArray(value)) {
|
|
3938
|
+
for (const item of value) {
|
|
3939
|
+
maxLine = Math.max(maxLine, findMaxLineInNode(item));
|
|
3921
3940
|
}
|
|
3922
|
-
}
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
return Math.max(ast.body.length * 3, 1);
|
|
3941
|
+
} else if (value && typeof value === "object") {
|
|
3942
|
+
maxLine = Math.max(maxLine, findMaxLineInNode(value));
|
|
3943
|
+
}
|
|
3926
3944
|
}
|
|
3927
|
-
return
|
|
3945
|
+
return maxLine;
|
|
3946
|
+
}
|
|
3947
|
+
function countLinesFromAST(ast) {
|
|
3948
|
+
if (!ast.body || !Array.isArray(ast.body)) return 1;
|
|
3949
|
+
const maxLine = findMaxLineInNode(ast);
|
|
3950
|
+
if (maxLine > 0) return maxLine;
|
|
3951
|
+
return Math.max(ast.body.length * 3, 1);
|
|
3928
3952
|
}
|
|
3929
3953
|
function findDeadFiles(snapshot, reachability) {
|
|
3930
3954
|
const deadFiles = [];
|
|
@@ -4072,130 +4096,146 @@ function fileMatchesPattern(filePath, pattern, rootDir) {
|
|
|
4072
4096
|
const relativePath = relativePosix(rootDir, filePath);
|
|
4073
4097
|
return minimatch3(relativePath, pattern);
|
|
4074
4098
|
}
|
|
4075
|
-
|
|
4099
|
+
var CONVENTION_DESCRIPTIONS = {
|
|
4100
|
+
camelCase: "camelCase (e.g., myFunction)",
|
|
4101
|
+
PascalCase: "PascalCase (e.g., MyClass)",
|
|
4102
|
+
UPPER_SNAKE: "UPPER_SNAKE_CASE (e.g., MY_CONSTANT)",
|
|
4103
|
+
"kebab-case": "kebab-case (e.g., my-component)"
|
|
4104
|
+
};
|
|
4105
|
+
function checkMustExport(rule, file, message) {
|
|
4106
|
+
if (rule.type !== "must-export") return [];
|
|
4076
4107
|
const matches = [];
|
|
4077
|
-
const
|
|
4078
|
-
|
|
4079
|
-
|
|
4080
|
-
|
|
4081
|
-
|
|
4082
|
-
|
|
4083
|
-
|
|
4084
|
-
for (const name of rule.names) {
|
|
4085
|
-
const hasExport = file.exports.some((e) => e.name === name);
|
|
4086
|
-
if (!hasExport) {
|
|
4087
|
-
matches.push({
|
|
4088
|
-
line: 1,
|
|
4089
|
-
message: pattern.message || `Missing required export: "${name}"`,
|
|
4090
|
-
suggestion: `Add export for "${name}"`
|
|
4091
|
-
});
|
|
4092
|
-
}
|
|
4093
|
-
}
|
|
4094
|
-
break;
|
|
4095
|
-
}
|
|
4096
|
-
case "must-export-default": {
|
|
4097
|
-
const hasDefault = file.exports.some((e) => e.type === "default");
|
|
4098
|
-
if (!hasDefault) {
|
|
4099
|
-
matches.push({
|
|
4100
|
-
line: 1,
|
|
4101
|
-
message: pattern.message || "File must have a default export",
|
|
4102
|
-
suggestion: "Add a default export"
|
|
4103
|
-
});
|
|
4104
|
-
}
|
|
4105
|
-
break;
|
|
4106
|
-
}
|
|
4107
|
-
case "no-export": {
|
|
4108
|
-
for (const name of rule.names) {
|
|
4109
|
-
const exp = file.exports.find((e) => e.name === name);
|
|
4110
|
-
if (exp) {
|
|
4111
|
-
matches.push({
|
|
4112
|
-
line: exp.location.line,
|
|
4113
|
-
message: pattern.message || `Forbidden export: "${name}"`,
|
|
4114
|
-
suggestion: `Remove export "${name}"`
|
|
4115
|
-
});
|
|
4116
|
-
}
|
|
4117
|
-
}
|
|
4118
|
-
break;
|
|
4108
|
+
for (const name of rule.names) {
|
|
4109
|
+
if (!file.exports.some((e) => e.name === name)) {
|
|
4110
|
+
matches.push({
|
|
4111
|
+
line: 1,
|
|
4112
|
+
message: message || `Missing required export: "${name}"`,
|
|
4113
|
+
suggestion: `Add export for "${name}"`
|
|
4114
|
+
});
|
|
4119
4115
|
}
|
|
4120
|
-
|
|
4121
|
-
|
|
4122
|
-
|
|
4123
|
-
|
|
4124
|
-
|
|
4125
|
-
|
|
4126
|
-
|
|
4127
|
-
|
|
4128
|
-
|
|
4129
|
-
|
|
4116
|
+
}
|
|
4117
|
+
return matches;
|
|
4118
|
+
}
|
|
4119
|
+
function checkMustExportDefault(_rule, file, message) {
|
|
4120
|
+
if (!file.exports.some((e) => e.type === "default")) {
|
|
4121
|
+
return [
|
|
4122
|
+
{
|
|
4123
|
+
line: 1,
|
|
4124
|
+
message: message || "File must have a default export",
|
|
4125
|
+
suggestion: "Add a default export"
|
|
4130
4126
|
}
|
|
4131
|
-
|
|
4127
|
+
];
|
|
4128
|
+
}
|
|
4129
|
+
return [];
|
|
4130
|
+
}
|
|
4131
|
+
function checkNoExport(rule, file, message) {
|
|
4132
|
+
if (rule.type !== "no-export") return [];
|
|
4133
|
+
const matches = [];
|
|
4134
|
+
for (const name of rule.names) {
|
|
4135
|
+
const exp = file.exports.find((e) => e.name === name);
|
|
4136
|
+
if (exp) {
|
|
4137
|
+
matches.push({
|
|
4138
|
+
line: exp.location.line,
|
|
4139
|
+
message: message || `Forbidden export: "${name}"`,
|
|
4140
|
+
suggestion: `Remove export "${name}"`
|
|
4141
|
+
});
|
|
4132
4142
|
}
|
|
4133
|
-
|
|
4134
|
-
|
|
4135
|
-
|
|
4136
|
-
|
|
4137
|
-
|
|
4138
|
-
|
|
4139
|
-
|
|
4140
|
-
|
|
4141
|
-
|
|
4142
|
-
|
|
4143
|
+
}
|
|
4144
|
+
return matches;
|
|
4145
|
+
}
|
|
4146
|
+
function checkMustImport(rule, file, message) {
|
|
4147
|
+
if (rule.type !== "must-import") return [];
|
|
4148
|
+
const hasImport = file.imports.some(
|
|
4149
|
+
(i) => i.source === rule.from || i.source.endsWith(rule.from)
|
|
4150
|
+
);
|
|
4151
|
+
if (!hasImport) {
|
|
4152
|
+
return [
|
|
4153
|
+
{
|
|
4154
|
+
line: 1,
|
|
4155
|
+
message: message || `Missing required import from "${rule.from}"`,
|
|
4156
|
+
suggestion: `Add import from "${rule.from}"`
|
|
4143
4157
|
}
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
|
|
4157
|
-
|
|
4158
|
-
|
|
4159
|
-
expected = "UPPER_SNAKE_CASE (e.g., MY_CONSTANT)";
|
|
4160
|
-
break;
|
|
4161
|
-
case "kebab-case":
|
|
4162
|
-
expected = "kebab-case (e.g., my-component)";
|
|
4163
|
-
break;
|
|
4164
|
-
}
|
|
4165
|
-
matches.push({
|
|
4166
|
-
line: exp.location.line,
|
|
4167
|
-
message: pattern.message || `"${exp.name}" does not follow ${rule.convention} convention`,
|
|
4168
|
-
suggestion: `Rename to follow ${expected}`
|
|
4169
|
-
});
|
|
4170
|
-
}
|
|
4158
|
+
];
|
|
4159
|
+
}
|
|
4160
|
+
return [];
|
|
4161
|
+
}
|
|
4162
|
+
function checkNoImport(rule, file, message) {
|
|
4163
|
+
if (rule.type !== "no-import") return [];
|
|
4164
|
+
const forbiddenImport = file.imports.find(
|
|
4165
|
+
(i) => i.source === rule.from || i.source.endsWith(rule.from)
|
|
4166
|
+
);
|
|
4167
|
+
if (forbiddenImport) {
|
|
4168
|
+
return [
|
|
4169
|
+
{
|
|
4170
|
+
line: forbiddenImport.location.line,
|
|
4171
|
+
message: message || `Forbidden import from "${rule.from}"`,
|
|
4172
|
+
suggestion: `Remove import from "${rule.from}"`
|
|
4171
4173
|
}
|
|
4172
|
-
|
|
4174
|
+
];
|
|
4175
|
+
}
|
|
4176
|
+
return [];
|
|
4177
|
+
}
|
|
4178
|
+
function checkNaming(rule, file, message) {
|
|
4179
|
+
if (rule.type !== "naming") return [];
|
|
4180
|
+
const regex = new RegExp(rule.match);
|
|
4181
|
+
const matches = [];
|
|
4182
|
+
for (const exp of file.exports) {
|
|
4183
|
+
if (!regex.test(exp.name)) {
|
|
4184
|
+
const expected = CONVENTION_DESCRIPTIONS[rule.convention] ?? rule.convention;
|
|
4185
|
+
matches.push({
|
|
4186
|
+
line: exp.location.line,
|
|
4187
|
+
message: message || `"${exp.name}" does not follow ${rule.convention} convention`,
|
|
4188
|
+
suggestion: `Rename to follow ${expected}`
|
|
4189
|
+
});
|
|
4173
4190
|
}
|
|
4174
|
-
|
|
4175
|
-
|
|
4176
|
-
|
|
4177
|
-
|
|
4178
|
-
|
|
4179
|
-
|
|
4180
|
-
|
|
4191
|
+
}
|
|
4192
|
+
return matches;
|
|
4193
|
+
}
|
|
4194
|
+
function checkMaxExports(rule, file, message) {
|
|
4195
|
+
if (rule.type !== "max-exports") return [];
|
|
4196
|
+
if (file.exports.length > rule.count) {
|
|
4197
|
+
return [
|
|
4198
|
+
{
|
|
4199
|
+
line: 1,
|
|
4200
|
+
message: message || `File has ${file.exports.length} exports, max is ${rule.count}`,
|
|
4201
|
+
suggestion: `Split into multiple files or reduce exports to ${rule.count}`
|
|
4181
4202
|
}
|
|
4182
|
-
|
|
4183
|
-
|
|
4184
|
-
|
|
4185
|
-
|
|
4186
|
-
|
|
4187
|
-
|
|
4188
|
-
|
|
4189
|
-
|
|
4190
|
-
|
|
4191
|
-
|
|
4192
|
-
|
|
4193
|
-
|
|
4203
|
+
];
|
|
4204
|
+
}
|
|
4205
|
+
return [];
|
|
4206
|
+
}
|
|
4207
|
+
function checkMaxLines(_rule, _file, _message) {
|
|
4208
|
+
return [];
|
|
4209
|
+
}
|
|
4210
|
+
function checkRequireJsdoc(_rule, file, message) {
|
|
4211
|
+
if (file.jsDocComments.length === 0 && file.exports.length > 0) {
|
|
4212
|
+
return [
|
|
4213
|
+
{
|
|
4214
|
+
line: 1,
|
|
4215
|
+
message: message || "Exported symbols require JSDoc documentation",
|
|
4216
|
+
suggestion: "Add JSDoc comments to exports"
|
|
4194
4217
|
}
|
|
4195
|
-
|
|
4196
|
-
}
|
|
4218
|
+
];
|
|
4197
4219
|
}
|
|
4198
|
-
return
|
|
4220
|
+
return [];
|
|
4221
|
+
}
|
|
4222
|
+
var RULE_CHECKERS = {
|
|
4223
|
+
"must-export": checkMustExport,
|
|
4224
|
+
"must-export-default": checkMustExportDefault,
|
|
4225
|
+
"no-export": checkNoExport,
|
|
4226
|
+
"must-import": checkMustImport,
|
|
4227
|
+
"no-import": checkNoImport,
|
|
4228
|
+
naming: checkNaming,
|
|
4229
|
+
"max-exports": checkMaxExports,
|
|
4230
|
+
"max-lines": checkMaxLines,
|
|
4231
|
+
"require-jsdoc": checkRequireJsdoc
|
|
4232
|
+
};
|
|
4233
|
+
function checkConfigPattern(pattern, file, rootDir) {
|
|
4234
|
+
const fileMatches = pattern.files.some((glob2) => fileMatchesPattern(file.path, glob2, rootDir));
|
|
4235
|
+
if (!fileMatches) return [];
|
|
4236
|
+
const checker = RULE_CHECKERS[pattern.rule.type];
|
|
4237
|
+
if (!checker) return [];
|
|
4238
|
+
return checker(pattern.rule, file, pattern.message);
|
|
4199
4239
|
}
|
|
4200
4240
|
async function detectPatternViolations(snapshot, config) {
|
|
4201
4241
|
const violations = [];
|
|
@@ -4701,17 +4741,35 @@ function createUnusedImportFixes(deadCodeReport) {
|
|
|
4701
4741
|
reversible: true
|
|
4702
4742
|
}));
|
|
4703
4743
|
}
|
|
4744
|
+
var EXPORT_TYPE_KEYWORD = {
|
|
4745
|
+
class: "class",
|
|
4746
|
+
function: "function",
|
|
4747
|
+
variable: "const",
|
|
4748
|
+
type: "type",
|
|
4749
|
+
interface: "interface",
|
|
4750
|
+
enum: "enum"
|
|
4751
|
+
};
|
|
4752
|
+
function getExportKeyword(exportType) {
|
|
4753
|
+
return EXPORT_TYPE_KEYWORD[exportType] ?? "enum";
|
|
4754
|
+
}
|
|
4755
|
+
function getDefaultExportKeyword(exportType) {
|
|
4756
|
+
if (exportType === "class" || exportType === "function") return exportType;
|
|
4757
|
+
return "";
|
|
4758
|
+
}
|
|
4704
4759
|
function createDeadExportFixes(deadCodeReport) {
|
|
4705
|
-
return deadCodeReport.deadExports.filter((exp) => exp.reason === "NO_IMPORTERS").map((exp) =>
|
|
4706
|
-
type:
|
|
4707
|
-
|
|
4708
|
-
|
|
4709
|
-
|
|
4710
|
-
|
|
4711
|
-
|
|
4712
|
-
|
|
4713
|
-
|
|
4714
|
-
|
|
4760
|
+
return deadCodeReport.deadExports.filter((exp) => exp.reason === "NO_IMPORTERS").map((exp) => {
|
|
4761
|
+
const keyword = exp.isDefault ? getDefaultExportKeyword(exp.type) : getExportKeyword(exp.type);
|
|
4762
|
+
return {
|
|
4763
|
+
type: "dead-exports",
|
|
4764
|
+
file: exp.file,
|
|
4765
|
+
description: `Remove export keyword from ${exp.name} (${exp.reason})`,
|
|
4766
|
+
action: "replace",
|
|
4767
|
+
oldContent: exp.isDefault ? `export default ${keyword} ${exp.name}` : `export ${keyword} ${exp.name}`,
|
|
4768
|
+
newContent: `${keyword} ${exp.name}`,
|
|
4769
|
+
safe: true,
|
|
4770
|
+
reversible: true
|
|
4771
|
+
};
|
|
4772
|
+
});
|
|
4715
4773
|
}
|
|
4716
4774
|
function createCommentedCodeFixes(blocks) {
|
|
4717
4775
|
return blocks.map((block) => ({
|
|
@@ -4886,53 +4944,80 @@ var ALWAYS_UNSAFE_TYPES = /* @__PURE__ */ new Set([
|
|
|
4886
4944
|
"dead-internal"
|
|
4887
4945
|
]);
|
|
4888
4946
|
var idCounter = 0;
|
|
4947
|
+
var DEAD_CODE_FIX_ACTIONS = {
|
|
4948
|
+
"dead-export": "Remove export keyword",
|
|
4949
|
+
"dead-file": "Delete file",
|
|
4950
|
+
"commented-code": "Delete commented block",
|
|
4951
|
+
"unused-import": "Remove import"
|
|
4952
|
+
};
|
|
4953
|
+
function classifyDeadCode(input) {
|
|
4954
|
+
if (input.isPublicApi) {
|
|
4955
|
+
return {
|
|
4956
|
+
safety: "unsafe",
|
|
4957
|
+
safetyReason: "Public API export may have external consumers",
|
|
4958
|
+
suggestion: "Deprecate before removing"
|
|
4959
|
+
};
|
|
4960
|
+
}
|
|
4961
|
+
const fixAction = DEAD_CODE_FIX_ACTIONS[input.type];
|
|
4962
|
+
if (fixAction) {
|
|
4963
|
+
return {
|
|
4964
|
+
safety: "safe",
|
|
4965
|
+
safetyReason: "zero importers, non-public",
|
|
4966
|
+
fixAction,
|
|
4967
|
+
suggestion: fixAction
|
|
4968
|
+
};
|
|
4969
|
+
}
|
|
4970
|
+
if (input.type === "orphaned-dep") {
|
|
4971
|
+
return {
|
|
4972
|
+
safety: "probably-safe",
|
|
4973
|
+
safetyReason: "No imports found, but needs install+test verification",
|
|
4974
|
+
fixAction: "Remove from package.json",
|
|
4975
|
+
suggestion: "Remove from package.json"
|
|
4976
|
+
};
|
|
4977
|
+
}
|
|
4978
|
+
return {
|
|
4979
|
+
safety: "unsafe",
|
|
4980
|
+
safetyReason: "Unknown dead code type",
|
|
4981
|
+
suggestion: "Manual review required"
|
|
4982
|
+
};
|
|
4983
|
+
}
|
|
4984
|
+
function classifyArchitecture(input) {
|
|
4985
|
+
if (input.type === "import-ordering") {
|
|
4986
|
+
return {
|
|
4987
|
+
safety: "safe",
|
|
4988
|
+
safetyReason: "Mechanical reorder, no semantic change",
|
|
4989
|
+
fixAction: "Reorder imports",
|
|
4990
|
+
suggestion: "Reorder imports"
|
|
4991
|
+
};
|
|
4992
|
+
}
|
|
4993
|
+
if (input.type === "forbidden-import" && input.hasAlternative) {
|
|
4994
|
+
return {
|
|
4995
|
+
safety: "probably-safe",
|
|
4996
|
+
safetyReason: "Alternative configured, needs typecheck+test",
|
|
4997
|
+
fixAction: "Replace with configured alternative",
|
|
4998
|
+
suggestion: "Replace with configured alternative"
|
|
4999
|
+
};
|
|
5000
|
+
}
|
|
5001
|
+
return {
|
|
5002
|
+
safety: "unsafe",
|
|
5003
|
+
safetyReason: `${input.type} requires structural changes`,
|
|
5004
|
+
suggestion: "Restructure code to fix violation"
|
|
5005
|
+
};
|
|
5006
|
+
}
|
|
4889
5007
|
function classifyFinding(input) {
|
|
4890
5008
|
idCounter++;
|
|
4891
5009
|
const id = `${input.concern === "dead-code" ? "dc" : "arch"}-${idCounter}`;
|
|
4892
|
-
let
|
|
4893
|
-
let safetyReason;
|
|
4894
|
-
let fixAction;
|
|
4895
|
-
let suggestion;
|
|
5010
|
+
let classification;
|
|
4896
5011
|
if (ALWAYS_UNSAFE_TYPES.has(input.type)) {
|
|
4897
|
-
|
|
4898
|
-
|
|
4899
|
-
|
|
5012
|
+
classification = {
|
|
5013
|
+
safety: "unsafe",
|
|
5014
|
+
safetyReason: `${input.type} requires human judgment`,
|
|
5015
|
+
suggestion: "Review and refactor manually"
|
|
5016
|
+
};
|
|
4900
5017
|
} else if (input.concern === "dead-code") {
|
|
4901
|
-
|
|
4902
|
-
safety = "unsafe";
|
|
4903
|
-
safetyReason = "Public API export may have external consumers";
|
|
4904
|
-
suggestion = "Deprecate before removing";
|
|
4905
|
-
} else if (input.type === "dead-export" || input.type === "unused-import" || input.type === "commented-code" || input.type === "dead-file") {
|
|
4906
|
-
safety = "safe";
|
|
4907
|
-
safetyReason = "zero importers, non-public";
|
|
4908
|
-
fixAction = input.type === "dead-export" ? "Remove export keyword" : input.type === "dead-file" ? "Delete file" : input.type === "commented-code" ? "Delete commented block" : "Remove import";
|
|
4909
|
-
suggestion = fixAction;
|
|
4910
|
-
} else if (input.type === "orphaned-dep") {
|
|
4911
|
-
safety = "probably-safe";
|
|
4912
|
-
safetyReason = "No imports found, but needs install+test verification";
|
|
4913
|
-
fixAction = "Remove from package.json";
|
|
4914
|
-
suggestion = fixAction;
|
|
4915
|
-
} else {
|
|
4916
|
-
safety = "unsafe";
|
|
4917
|
-
safetyReason = "Unknown dead code type";
|
|
4918
|
-
suggestion = "Manual review required";
|
|
4919
|
-
}
|
|
5018
|
+
classification = classifyDeadCode(input);
|
|
4920
5019
|
} else {
|
|
4921
|
-
|
|
4922
|
-
safety = "safe";
|
|
4923
|
-
safetyReason = "Mechanical reorder, no semantic change";
|
|
4924
|
-
fixAction = "Reorder imports";
|
|
4925
|
-
suggestion = fixAction;
|
|
4926
|
-
} else if (input.type === "forbidden-import" && input.hasAlternative) {
|
|
4927
|
-
safety = "probably-safe";
|
|
4928
|
-
safetyReason = "Alternative configured, needs typecheck+test";
|
|
4929
|
-
fixAction = "Replace with configured alternative";
|
|
4930
|
-
suggestion = fixAction;
|
|
4931
|
-
} else {
|
|
4932
|
-
safety = "unsafe";
|
|
4933
|
-
safetyReason = `${input.type} requires structural changes`;
|
|
4934
|
-
suggestion = "Restructure code to fix violation";
|
|
4935
|
-
}
|
|
5020
|
+
classification = classifyArchitecture(input);
|
|
4936
5021
|
}
|
|
4937
5022
|
return {
|
|
4938
5023
|
id,
|
|
@@ -4941,11 +5026,11 @@ function classifyFinding(input) {
|
|
|
4941
5026
|
...input.line !== void 0 ? { line: input.line } : {},
|
|
4942
5027
|
type: input.type,
|
|
4943
5028
|
description: input.description,
|
|
4944
|
-
safety,
|
|
4945
|
-
safetyReason,
|
|
5029
|
+
safety: classification.safety,
|
|
5030
|
+
safetyReason: classification.safetyReason,
|
|
4946
5031
|
hotspotDowngraded: false,
|
|
4947
|
-
...fixAction !== void 0 ? { fixAction } : {},
|
|
4948
|
-
suggestion
|
|
5032
|
+
...classification.fixAction !== void 0 ? { fixAction: classification.fixAction } : {},
|
|
5033
|
+
suggestion: classification.suggestion
|
|
4949
5034
|
};
|
|
4950
5035
|
}
|
|
4951
5036
|
function applyHotspotDowngrade(finding, hotspot) {
|
|
@@ -5229,43 +5314,57 @@ var BenchmarkRunner = class {
|
|
|
5229
5314
|
};
|
|
5230
5315
|
}
|
|
5231
5316
|
}
|
|
5317
|
+
/**
|
|
5318
|
+
* Extract a BenchmarkResult from a single assertion with benchmark data.
|
|
5319
|
+
*/
|
|
5320
|
+
parseBenchAssertion(assertion, file) {
|
|
5321
|
+
if (!assertion.benchmark) return null;
|
|
5322
|
+
const bench = assertion.benchmark;
|
|
5323
|
+
return {
|
|
5324
|
+
name: assertion.fullName || assertion.title || "unknown",
|
|
5325
|
+
file: file.replace(process.cwd() + "/", ""),
|
|
5326
|
+
opsPerSec: Math.round(bench.hz || 0),
|
|
5327
|
+
meanMs: bench.mean ? bench.mean * 1e3 : 0,
|
|
5328
|
+
p99Ms: bench.p99 ? bench.p99 * 1e3 : bench.mean ? bench.mean * 1e3 * 1.5 : 0,
|
|
5329
|
+
marginOfError: bench.rme ? bench.rme / 100 : 0.05
|
|
5330
|
+
};
|
|
5331
|
+
}
|
|
5332
|
+
/**
|
|
5333
|
+
* Extract JSON from output that may contain non-JSON preamble.
|
|
5334
|
+
*/
|
|
5335
|
+
extractJson(output) {
|
|
5336
|
+
const jsonStart = output.indexOf("{");
|
|
5337
|
+
const jsonEnd = output.lastIndexOf("}");
|
|
5338
|
+
if (jsonStart === -1 || jsonEnd === -1) return null;
|
|
5339
|
+
return JSON.parse(output.slice(jsonStart, jsonEnd + 1));
|
|
5340
|
+
}
|
|
5232
5341
|
/**
|
|
5233
5342
|
* Parse vitest bench JSON reporter output into BenchmarkResult[].
|
|
5234
5343
|
* Vitest bench JSON output contains testResults with benchmark data.
|
|
5235
5344
|
*/
|
|
5236
|
-
|
|
5345
|
+
collectAssertionResults(testResults) {
|
|
5237
5346
|
const results = [];
|
|
5238
|
-
|
|
5239
|
-
const
|
|
5240
|
-
const
|
|
5241
|
-
|
|
5242
|
-
|
|
5243
|
-
|
|
5244
|
-
if (parsed.testResults) {
|
|
5245
|
-
for (const testResult of parsed.testResults) {
|
|
5246
|
-
const file = testResult.name || testResult.filepath || "";
|
|
5247
|
-
if (testResult.assertionResults) {
|
|
5248
|
-
for (const assertion of testResult.assertionResults) {
|
|
5249
|
-
if (assertion.benchmark) {
|
|
5250
|
-
const bench = assertion.benchmark;
|
|
5251
|
-
results.push({
|
|
5252
|
-
name: assertion.fullName || assertion.title || "unknown",
|
|
5253
|
-
file: file.replace(process.cwd() + "/", ""),
|
|
5254
|
-
opsPerSec: Math.round(bench.hz || 0),
|
|
5255
|
-
meanMs: bench.mean ? bench.mean * 1e3 : 0,
|
|
5256
|
-
// p99: use actual p99 if available, otherwise estimate as 1.5× mean
|
|
5257
|
-
p99Ms: bench.p99 ? bench.p99 * 1e3 : bench.mean ? bench.mean * 1e3 * 1.5 : 0,
|
|
5258
|
-
marginOfError: bench.rme ? bench.rme / 100 : 0.05
|
|
5259
|
-
});
|
|
5260
|
-
}
|
|
5261
|
-
}
|
|
5262
|
-
}
|
|
5263
|
-
}
|
|
5347
|
+
for (const testResult of testResults) {
|
|
5348
|
+
const file = testResult.name || testResult.filepath || "";
|
|
5349
|
+
const assertions = testResult.assertionResults ?? [];
|
|
5350
|
+
for (const assertion of assertions) {
|
|
5351
|
+
const result = this.parseBenchAssertion(assertion, file);
|
|
5352
|
+
if (result) results.push(result);
|
|
5264
5353
|
}
|
|
5265
|
-
} catch {
|
|
5266
5354
|
}
|
|
5267
5355
|
return results;
|
|
5268
5356
|
}
|
|
5357
|
+
parseVitestBenchOutput(output) {
|
|
5358
|
+
try {
|
|
5359
|
+
const parsed = this.extractJson(output);
|
|
5360
|
+
if (!parsed) return [];
|
|
5361
|
+
const testResults = parsed.testResults;
|
|
5362
|
+
if (!testResults) return [];
|
|
5363
|
+
return this.collectAssertionResults(testResults);
|
|
5364
|
+
} catch {
|
|
5365
|
+
return [];
|
|
5366
|
+
}
|
|
5367
|
+
}
|
|
5269
5368
|
};
|
|
5270
5369
|
var RegressionDetector = class {
|
|
5271
5370
|
detect(results, baselines, criticalPaths) {
|
|
@@ -5557,39 +5656,31 @@ function getFeedbackConfig() {
|
|
|
5557
5656
|
function resetFeedbackConfig() {
|
|
5558
5657
|
feedbackConfig = null;
|
|
5559
5658
|
}
|
|
5659
|
+
function detectFileStatus(part) {
|
|
5660
|
+
if (/new file mode/.test(part)) return "added";
|
|
5661
|
+
if (/deleted file mode/.test(part)) return "deleted";
|
|
5662
|
+
if (part.includes("rename from")) return "renamed";
|
|
5663
|
+
return "modified";
|
|
5664
|
+
}
|
|
5665
|
+
function parseDiffPart(part) {
|
|
5666
|
+
if (!part.trim()) return null;
|
|
5667
|
+
const headerMatch = /diff --git a\/(.+?) b\/(.+?)(?:\n|$)/.exec(part);
|
|
5668
|
+
if (!headerMatch || !headerMatch[2]) return null;
|
|
5669
|
+
const additionRegex = /^\+(?!\+\+)/gm;
|
|
5670
|
+
const deletionRegex = /^-(?!--)/gm;
|
|
5671
|
+
return {
|
|
5672
|
+
path: headerMatch[2],
|
|
5673
|
+
status: detectFileStatus(part),
|
|
5674
|
+
additions: (part.match(additionRegex) || []).length,
|
|
5675
|
+
deletions: (part.match(deletionRegex) || []).length
|
|
5676
|
+
};
|
|
5677
|
+
}
|
|
5560
5678
|
function parseDiff(diff2) {
|
|
5561
5679
|
try {
|
|
5562
5680
|
if (!diff2.trim()) {
|
|
5563
5681
|
return Ok({ diff: diff2, files: [] });
|
|
5564
5682
|
}
|
|
5565
|
-
const files =
|
|
5566
|
-
const newFileRegex = /new file mode/;
|
|
5567
|
-
const deletedFileRegex = /deleted file mode/;
|
|
5568
|
-
const additionRegex = /^\+(?!\+\+)/gm;
|
|
5569
|
-
const deletionRegex = /^-(?!--)/gm;
|
|
5570
|
-
const diffParts = diff2.split(/(?=diff --git)/);
|
|
5571
|
-
for (const part of diffParts) {
|
|
5572
|
-
if (!part.trim()) continue;
|
|
5573
|
-
const headerMatch = /diff --git a\/(.+?) b\/(.+?)(?:\n|$)/.exec(part);
|
|
5574
|
-
if (!headerMatch || !headerMatch[2]) continue;
|
|
5575
|
-
const filePath = headerMatch[2];
|
|
5576
|
-
let status = "modified";
|
|
5577
|
-
if (newFileRegex.test(part)) {
|
|
5578
|
-
status = "added";
|
|
5579
|
-
} else if (deletedFileRegex.test(part)) {
|
|
5580
|
-
status = "deleted";
|
|
5581
|
-
} else if (part.includes("rename from")) {
|
|
5582
|
-
status = "renamed";
|
|
5583
|
-
}
|
|
5584
|
-
const additions = (part.match(additionRegex) || []).length;
|
|
5585
|
-
const deletions = (part.match(deletionRegex) || []).length;
|
|
5586
|
-
files.push({
|
|
5587
|
-
path: filePath,
|
|
5588
|
-
status,
|
|
5589
|
-
additions,
|
|
5590
|
-
deletions
|
|
5591
|
-
});
|
|
5592
|
-
}
|
|
5683
|
+
const files = diff2.split(/(?=diff --git)/).map(parseDiffPart).filter((f) => f !== null);
|
|
5593
5684
|
return Ok({ diff: diff2, files });
|
|
5594
5685
|
} catch (error) {
|
|
5595
5686
|
return Err({
|
|
@@ -5753,107 +5844,123 @@ var ChecklistBuilder = class {
|
|
|
5753
5844
|
this.graphImpactData = graphImpactData;
|
|
5754
5845
|
return this;
|
|
5755
5846
|
}
|
|
5756
|
-
|
|
5757
|
-
|
|
5758
|
-
|
|
5759
|
-
|
|
5760
|
-
|
|
5761
|
-
|
|
5762
|
-
|
|
5763
|
-
|
|
5764
|
-
|
|
5765
|
-
|
|
5766
|
-
|
|
5767
|
-
|
|
5768
|
-
|
|
5769
|
-
|
|
5770
|
-
|
|
5771
|
-
|
|
5772
|
-
|
|
5773
|
-
|
|
5774
|
-
|
|
5775
|
-
|
|
5776
|
-
|
|
5777
|
-
|
|
5778
|
-
|
|
5779
|
-
|
|
5780
|
-
|
|
5781
|
-
|
|
5782
|
-
|
|
5783
|
-
|
|
5784
|
-
|
|
5785
|
-
|
|
5786
|
-
|
|
5787
|
-
check: "Constraint validation",
|
|
5788
|
-
passed: violations === 0,
|
|
5789
|
-
severity: violations > 0 ? "error" : "info",
|
|
5790
|
-
details: violations === 0 ? "No constraint violations detected" : `${violations} constraint violation(s) detected`
|
|
5791
|
-
});
|
|
5792
|
-
} else {
|
|
5793
|
-
items.push({
|
|
5794
|
-
id: "harness-constraints",
|
|
5795
|
-
category: "harness",
|
|
5796
|
-
check: "Constraint validation",
|
|
5797
|
-
passed: true,
|
|
5798
|
-
severity: "info",
|
|
5799
|
-
details: "Harness constraint validation not yet integrated (run with graph for real checks)"
|
|
5800
|
-
});
|
|
5801
|
-
}
|
|
5802
|
-
}
|
|
5803
|
-
if (this.harnessOptions.entropy !== false) {
|
|
5804
|
-
if (this.graphHarnessData) {
|
|
5805
|
-
const issues = this.graphHarnessData.unreachableNodes + this.graphHarnessData.undocumentedFiles;
|
|
5806
|
-
items.push({
|
|
5807
|
-
id: "harness-entropy",
|
|
5808
|
-
category: "harness",
|
|
5809
|
-
check: "Entropy detection",
|
|
5810
|
-
passed: issues === 0,
|
|
5811
|
-
severity: issues > 0 ? "warning" : "info",
|
|
5812
|
-
details: issues === 0 ? "No entropy issues detected" : `${this.graphHarnessData.unreachableNodes} unreachable node(s), ${this.graphHarnessData.undocumentedFiles} undocumented file(s)`
|
|
5813
|
-
});
|
|
5814
|
-
} else {
|
|
5815
|
-
items.push({
|
|
5816
|
-
id: "harness-entropy",
|
|
5847
|
+
/**
|
|
5848
|
+
* Build a single harness check item with or without graph data.
|
|
5849
|
+
*/
|
|
5850
|
+
buildHarnessCheckItem(id, check, fallbackDetails, graphItemBuilder) {
|
|
5851
|
+
if (this.graphHarnessData && graphItemBuilder) {
|
|
5852
|
+
return graphItemBuilder();
|
|
5853
|
+
}
|
|
5854
|
+
return {
|
|
5855
|
+
id,
|
|
5856
|
+
category: "harness",
|
|
5857
|
+
check,
|
|
5858
|
+
passed: true,
|
|
5859
|
+
severity: "info",
|
|
5860
|
+
details: fallbackDetails
|
|
5861
|
+
};
|
|
5862
|
+
}
|
|
5863
|
+
/**
|
|
5864
|
+
* Build all harness check items based on harnessOptions and graph data.
|
|
5865
|
+
*/
|
|
5866
|
+
buildHarnessItems() {
|
|
5867
|
+
if (!this.harnessOptions) return [];
|
|
5868
|
+
const items = [];
|
|
5869
|
+
const graphData = this.graphHarnessData;
|
|
5870
|
+
if (this.harnessOptions.context !== false) {
|
|
5871
|
+
items.push(
|
|
5872
|
+
this.buildHarnessCheckItem(
|
|
5873
|
+
"harness-context",
|
|
5874
|
+
"Context validation",
|
|
5875
|
+
"Harness context validation not yet integrated (run with graph for real checks)",
|
|
5876
|
+
graphData ? () => ({
|
|
5877
|
+
id: "harness-context",
|
|
5817
5878
|
category: "harness",
|
|
5818
|
-
check: "
|
|
5819
|
-
passed:
|
|
5879
|
+
check: "Context validation",
|
|
5880
|
+
passed: graphData.graphExists && graphData.nodeCount > 0,
|
|
5820
5881
|
severity: "info",
|
|
5821
|
-
details:
|
|
5822
|
-
})
|
|
5823
|
-
|
|
5824
|
-
|
|
5882
|
+
details: graphData.graphExists ? `Graph loaded: ${graphData.nodeCount} nodes, ${graphData.edgeCount} edges` : "No graph available \u2014 run harness scan to build the knowledge graph"
|
|
5883
|
+
}) : void 0
|
|
5884
|
+
)
|
|
5885
|
+
);
|
|
5886
|
+
}
|
|
5887
|
+
if (this.harnessOptions.constraints !== false) {
|
|
5888
|
+
items.push(
|
|
5889
|
+
this.buildHarnessCheckItem(
|
|
5890
|
+
"harness-constraints",
|
|
5891
|
+
"Constraint validation",
|
|
5892
|
+
"Harness constraint validation not yet integrated (run with graph for real checks)",
|
|
5893
|
+
graphData ? () => {
|
|
5894
|
+
const violations = graphData.constraintViolations;
|
|
5895
|
+
return {
|
|
5896
|
+
id: "harness-constraints",
|
|
5897
|
+
category: "harness",
|
|
5898
|
+
check: "Constraint validation",
|
|
5899
|
+
passed: violations === 0,
|
|
5900
|
+
severity: violations > 0 ? "error" : "info",
|
|
5901
|
+
details: violations === 0 ? "No constraint violations detected" : `${violations} constraint violation(s) detected`
|
|
5902
|
+
};
|
|
5903
|
+
} : void 0
|
|
5904
|
+
)
|
|
5905
|
+
);
|
|
5906
|
+
}
|
|
5907
|
+
if (this.harnessOptions.entropy !== false) {
|
|
5908
|
+
items.push(
|
|
5909
|
+
this.buildHarnessCheckItem(
|
|
5910
|
+
"harness-entropy",
|
|
5911
|
+
"Entropy detection",
|
|
5912
|
+
"Harness entropy detection not yet integrated (run with graph for real checks)",
|
|
5913
|
+
graphData ? () => {
|
|
5914
|
+
const issues = graphData.unreachableNodes + graphData.undocumentedFiles;
|
|
5915
|
+
return {
|
|
5916
|
+
id: "harness-entropy",
|
|
5917
|
+
category: "harness",
|
|
5918
|
+
check: "Entropy detection",
|
|
5919
|
+
passed: issues === 0,
|
|
5920
|
+
severity: issues > 0 ? "warning" : "info",
|
|
5921
|
+
details: issues === 0 ? "No entropy issues detected" : `${graphData.unreachableNodes} unreachable node(s), ${graphData.undocumentedFiles} undocumented file(s)`
|
|
5922
|
+
};
|
|
5923
|
+
} : void 0
|
|
5924
|
+
)
|
|
5925
|
+
);
|
|
5926
|
+
}
|
|
5927
|
+
return items;
|
|
5928
|
+
}
|
|
5929
|
+
/**
|
|
5930
|
+
* Execute a single custom rule and return a ReviewItem.
|
|
5931
|
+
*/
|
|
5932
|
+
async executeCustomRule(rule, changes) {
|
|
5933
|
+
try {
|
|
5934
|
+
const result = await rule.check(changes, this.rootDir);
|
|
5935
|
+
const item = {
|
|
5936
|
+
id: rule.id,
|
|
5937
|
+
category: "custom",
|
|
5938
|
+
check: rule.name,
|
|
5939
|
+
passed: result.passed,
|
|
5940
|
+
severity: rule.severity,
|
|
5941
|
+
details: result.details
|
|
5942
|
+
};
|
|
5943
|
+
if (result.suggestion !== void 0) item.suggestion = result.suggestion;
|
|
5944
|
+
if (result.file !== void 0) item.file = result.file;
|
|
5945
|
+
if (result.line !== void 0) item.line = result.line;
|
|
5946
|
+
return item;
|
|
5947
|
+
} catch (error) {
|
|
5948
|
+
return {
|
|
5949
|
+
id: rule.id,
|
|
5950
|
+
category: "custom",
|
|
5951
|
+
check: rule.name,
|
|
5952
|
+
passed: false,
|
|
5953
|
+
severity: "error",
|
|
5954
|
+
details: `Rule execution failed: ${String(error)}`
|
|
5955
|
+
};
|
|
5825
5956
|
}
|
|
5957
|
+
}
|
|
5958
|
+
async run(changes) {
|
|
5959
|
+
const startTime = Date.now();
|
|
5960
|
+
const items = [];
|
|
5961
|
+
items.push(...this.buildHarnessItems());
|
|
5826
5962
|
for (const rule of this.customRules) {
|
|
5827
|
-
|
|
5828
|
-
const result = await rule.check(changes, this.rootDir);
|
|
5829
|
-
const item = {
|
|
5830
|
-
id: rule.id,
|
|
5831
|
-
category: "custom",
|
|
5832
|
-
check: rule.name,
|
|
5833
|
-
passed: result.passed,
|
|
5834
|
-
severity: rule.severity,
|
|
5835
|
-
details: result.details
|
|
5836
|
-
};
|
|
5837
|
-
if (result.suggestion !== void 0) {
|
|
5838
|
-
item.suggestion = result.suggestion;
|
|
5839
|
-
}
|
|
5840
|
-
if (result.file !== void 0) {
|
|
5841
|
-
item.file = result.file;
|
|
5842
|
-
}
|
|
5843
|
-
if (result.line !== void 0) {
|
|
5844
|
-
item.line = result.line;
|
|
5845
|
-
}
|
|
5846
|
-
items.push(item);
|
|
5847
|
-
} catch (error) {
|
|
5848
|
-
items.push({
|
|
5849
|
-
id: rule.id,
|
|
5850
|
-
category: "custom",
|
|
5851
|
-
check: rule.name,
|
|
5852
|
-
passed: false,
|
|
5853
|
-
severity: "error",
|
|
5854
|
-
details: `Rule execution failed: ${String(error)}`
|
|
5855
|
-
});
|
|
5856
|
-
}
|
|
5963
|
+
items.push(await this.executeCustomRule(rule, changes));
|
|
5857
5964
|
}
|
|
5858
5965
|
if (this.diffOptions) {
|
|
5859
5966
|
const diffResult = await analyzeDiff(changes, this.diffOptions, this.graphImpactData);
|
|
@@ -5868,7 +5975,6 @@ var ChecklistBuilder = class {
|
|
|
5868
5975
|
const checklist = {
|
|
5869
5976
|
items,
|
|
5870
5977
|
passed: failed === 0,
|
|
5871
|
-
// Pass if no failed items
|
|
5872
5978
|
summary: {
|
|
5873
5979
|
total: items.length,
|
|
5874
5980
|
passed,
|
|
@@ -6252,7 +6358,7 @@ function detectStaleConstraints(store, windowDays = 30, category) {
|
|
|
6252
6358
|
staleConstraints.sort((a, b) => b.daysSinceLastViolation - a.daysSinceLastViolation);
|
|
6253
6359
|
return { staleConstraints, totalConstraints, windowDays };
|
|
6254
6360
|
}
|
|
6255
|
-
function
|
|
6361
|
+
function resolveThresholds2(scope, config) {
|
|
6256
6362
|
const projectThresholds = {};
|
|
6257
6363
|
for (const [key, val] of Object.entries(config.thresholds)) {
|
|
6258
6364
|
projectThresholds[key] = typeof val === "object" && val !== null && !Array.isArray(val) ? { ...val } : val;
|
|
@@ -6382,6 +6488,8 @@ var INDEX_FILE = "index.json";
|
|
|
6382
6488
|
var SESSIONS_DIR = "sessions";
|
|
6383
6489
|
var SESSION_INDEX_FILE = "index.md";
|
|
6384
6490
|
var SUMMARY_FILE = "summary.md";
|
|
6491
|
+
var SESSION_STATE_FILE = "session-state.json";
|
|
6492
|
+
var ARCHIVE_DIR = "archive";
|
|
6385
6493
|
var STREAMS_DIR = "streams";
|
|
6386
6494
|
var STREAM_NAME_REGEX = /^[a-z0-9][a-z0-9._-]*$/;
|
|
6387
6495
|
function streamsDir(projectPath) {
|
|
@@ -7258,6 +7366,134 @@ function listActiveSessions(projectPath) {
|
|
|
7258
7366
|
);
|
|
7259
7367
|
}
|
|
7260
7368
|
}
|
|
7369
|
+
function emptySections() {
|
|
7370
|
+
const sections = {};
|
|
7371
|
+
for (const name of SESSION_SECTION_NAMES) {
|
|
7372
|
+
sections[name] = [];
|
|
7373
|
+
}
|
|
7374
|
+
return sections;
|
|
7375
|
+
}
|
|
7376
|
+
async function loadSessionState(projectPath, sessionSlug) {
|
|
7377
|
+
const dirResult = resolveSessionDir(projectPath, sessionSlug);
|
|
7378
|
+
if (!dirResult.ok) return dirResult;
|
|
7379
|
+
const sessionDir = dirResult.value;
|
|
7380
|
+
const filePath = path11.join(sessionDir, SESSION_STATE_FILE);
|
|
7381
|
+
if (!fs14.existsSync(filePath)) {
|
|
7382
|
+
return Ok(emptySections());
|
|
7383
|
+
}
|
|
7384
|
+
try {
|
|
7385
|
+
const raw = fs14.readFileSync(filePath, "utf-8");
|
|
7386
|
+
const parsed = JSON.parse(raw);
|
|
7387
|
+
const sections = emptySections();
|
|
7388
|
+
for (const name of SESSION_SECTION_NAMES) {
|
|
7389
|
+
if (Array.isArray(parsed[name])) {
|
|
7390
|
+
sections[name] = parsed[name];
|
|
7391
|
+
}
|
|
7392
|
+
}
|
|
7393
|
+
return Ok(sections);
|
|
7394
|
+
} catch (error) {
|
|
7395
|
+
return Err(
|
|
7396
|
+
new Error(
|
|
7397
|
+
`Failed to load session state: ${error instanceof Error ? error.message : String(error)}`
|
|
7398
|
+
)
|
|
7399
|
+
);
|
|
7400
|
+
}
|
|
7401
|
+
}
|
|
7402
|
+
async function saveSessionState(projectPath, sessionSlug, sections) {
|
|
7403
|
+
const dirResult = resolveSessionDir(projectPath, sessionSlug, { create: true });
|
|
7404
|
+
if (!dirResult.ok) return dirResult;
|
|
7405
|
+
const sessionDir = dirResult.value;
|
|
7406
|
+
const filePath = path11.join(sessionDir, SESSION_STATE_FILE);
|
|
7407
|
+
try {
|
|
7408
|
+
fs14.writeFileSync(filePath, JSON.stringify(sections, null, 2));
|
|
7409
|
+
return Ok(void 0);
|
|
7410
|
+
} catch (error) {
|
|
7411
|
+
return Err(
|
|
7412
|
+
new Error(
|
|
7413
|
+
`Failed to save session state: ${error instanceof Error ? error.message : String(error)}`
|
|
7414
|
+
)
|
|
7415
|
+
);
|
|
7416
|
+
}
|
|
7417
|
+
}
|
|
7418
|
+
async function readSessionSections(projectPath, sessionSlug) {
|
|
7419
|
+
return loadSessionState(projectPath, sessionSlug);
|
|
7420
|
+
}
|
|
7421
|
+
async function readSessionSection(projectPath, sessionSlug, section) {
|
|
7422
|
+
const result = await loadSessionState(projectPath, sessionSlug);
|
|
7423
|
+
if (!result.ok) return result;
|
|
7424
|
+
return Ok(result.value[section]);
|
|
7425
|
+
}
|
|
7426
|
+
async function appendSessionEntry(projectPath, sessionSlug, section, authorSkill, content) {
|
|
7427
|
+
const loadResult = await loadSessionState(projectPath, sessionSlug);
|
|
7428
|
+
if (!loadResult.ok) return loadResult;
|
|
7429
|
+
const sections = loadResult.value;
|
|
7430
|
+
const entry = {
|
|
7431
|
+
id: generateEntryId(),
|
|
7432
|
+
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
7433
|
+
authorSkill,
|
|
7434
|
+
content,
|
|
7435
|
+
status: "active"
|
|
7436
|
+
};
|
|
7437
|
+
sections[section].push(entry);
|
|
7438
|
+
const saveResult = await saveSessionState(projectPath, sessionSlug, sections);
|
|
7439
|
+
if (!saveResult.ok) return saveResult;
|
|
7440
|
+
return Ok(entry);
|
|
7441
|
+
}
|
|
7442
|
+
async function updateSessionEntryStatus(projectPath, sessionSlug, section, entryId, newStatus) {
|
|
7443
|
+
const loadResult = await loadSessionState(projectPath, sessionSlug);
|
|
7444
|
+
if (!loadResult.ok) return loadResult;
|
|
7445
|
+
const sections = loadResult.value;
|
|
7446
|
+
const entry = sections[section].find((e) => e.id === entryId);
|
|
7447
|
+
if (!entry) {
|
|
7448
|
+
return Err(new Error(`Entry '${entryId}' not found in section '${section}'`));
|
|
7449
|
+
}
|
|
7450
|
+
entry.status = newStatus;
|
|
7451
|
+
const saveResult = await saveSessionState(projectPath, sessionSlug, sections);
|
|
7452
|
+
if (!saveResult.ok) return saveResult;
|
|
7453
|
+
return Ok(entry);
|
|
7454
|
+
}
|
|
7455
|
+
function generateEntryId() {
|
|
7456
|
+
const timestamp = Date.now().toString(36);
|
|
7457
|
+
const random = Math.random().toString(36).substring(2, 8);
|
|
7458
|
+
return `${timestamp}-${random}`;
|
|
7459
|
+
}
|
|
7460
|
+
async function archiveSession(projectPath, sessionSlug) {
|
|
7461
|
+
const dirResult = resolveSessionDir(projectPath, sessionSlug);
|
|
7462
|
+
if (!dirResult.ok) return dirResult;
|
|
7463
|
+
const sessionDir = dirResult.value;
|
|
7464
|
+
if (!fs15.existsSync(sessionDir)) {
|
|
7465
|
+
return Err(new Error(`Session '${sessionSlug}' not found at ${sessionDir}`));
|
|
7466
|
+
}
|
|
7467
|
+
const archiveBase = path12.join(projectPath, HARNESS_DIR, ARCHIVE_DIR, "sessions");
|
|
7468
|
+
try {
|
|
7469
|
+
fs15.mkdirSync(archiveBase, { recursive: true });
|
|
7470
|
+
const date = (/* @__PURE__ */ new Date()).toISOString().split("T")[0];
|
|
7471
|
+
let archiveName = `${sessionSlug}-${date}`;
|
|
7472
|
+
let counter = 1;
|
|
7473
|
+
while (fs15.existsSync(path12.join(archiveBase, archiveName))) {
|
|
7474
|
+
archiveName = `${sessionSlug}-${date}-${counter}`;
|
|
7475
|
+
counter++;
|
|
7476
|
+
}
|
|
7477
|
+
const dest = path12.join(archiveBase, archiveName);
|
|
7478
|
+
try {
|
|
7479
|
+
fs15.renameSync(sessionDir, dest);
|
|
7480
|
+
} catch (renameErr) {
|
|
7481
|
+
if (renameErr instanceof Error && "code" in renameErr && renameErr.code === "EXDEV") {
|
|
7482
|
+
fs15.cpSync(sessionDir, dest, { recursive: true });
|
|
7483
|
+
fs15.rmSync(sessionDir, { recursive: true });
|
|
7484
|
+
} else {
|
|
7485
|
+
throw renameErr;
|
|
7486
|
+
}
|
|
7487
|
+
}
|
|
7488
|
+
return Ok(void 0);
|
|
7489
|
+
} catch (error) {
|
|
7490
|
+
return Err(
|
|
7491
|
+
new Error(
|
|
7492
|
+
`Failed to archive session: ${error instanceof Error ? error.message : String(error)}`
|
|
7493
|
+
)
|
|
7494
|
+
);
|
|
7495
|
+
}
|
|
7496
|
+
}
|
|
7261
7497
|
async function executeWorkflow(workflow, executor) {
|
|
7262
7498
|
const stepResults = [];
|
|
7263
7499
|
const startTime = Date.now();
|
|
@@ -7479,11 +7715,11 @@ function resolveRuleSeverity(ruleId, defaultSeverity, overrides, strict) {
|
|
|
7479
7715
|
}
|
|
7480
7716
|
function detectStack(projectRoot) {
|
|
7481
7717
|
const stacks = [];
|
|
7482
|
-
const pkgJsonPath =
|
|
7483
|
-
if (
|
|
7718
|
+
const pkgJsonPath = path13.join(projectRoot, "package.json");
|
|
7719
|
+
if (fs16.existsSync(pkgJsonPath)) {
|
|
7484
7720
|
stacks.push("node");
|
|
7485
7721
|
try {
|
|
7486
|
-
const pkgJson = JSON.parse(
|
|
7722
|
+
const pkgJson = JSON.parse(fs16.readFileSync(pkgJsonPath, "utf-8"));
|
|
7487
7723
|
const allDeps = {
|
|
7488
7724
|
...pkgJson.dependencies,
|
|
7489
7725
|
...pkgJson.devDependencies
|
|
@@ -7498,13 +7734,13 @@ function detectStack(projectRoot) {
|
|
|
7498
7734
|
} catch {
|
|
7499
7735
|
}
|
|
7500
7736
|
}
|
|
7501
|
-
const goModPath =
|
|
7502
|
-
if (
|
|
7737
|
+
const goModPath = path13.join(projectRoot, "go.mod");
|
|
7738
|
+
if (fs16.existsSync(goModPath)) {
|
|
7503
7739
|
stacks.push("go");
|
|
7504
7740
|
}
|
|
7505
|
-
const requirementsPath =
|
|
7506
|
-
const pyprojectPath =
|
|
7507
|
-
if (
|
|
7741
|
+
const requirementsPath = path13.join(projectRoot, "requirements.txt");
|
|
7742
|
+
const pyprojectPath = path13.join(projectRoot, "pyproject.toml");
|
|
7743
|
+
if (fs16.existsSync(requirementsPath) || fs16.existsSync(pyprojectPath)) {
|
|
7508
7744
|
stacks.push("python");
|
|
7509
7745
|
}
|
|
7510
7746
|
return stacks;
|
|
@@ -7907,7 +8143,7 @@ var SecurityScanner = class {
|
|
|
7907
8143
|
}
|
|
7908
8144
|
async scanFile(filePath) {
|
|
7909
8145
|
if (!this.config.enabled) return [];
|
|
7910
|
-
const content = await
|
|
8146
|
+
const content = await fs17.readFile(filePath, "utf-8");
|
|
7911
8147
|
return this.scanContent(content, filePath, 1);
|
|
7912
8148
|
}
|
|
7913
8149
|
async scanFiles(filePaths) {
|
|
@@ -7942,7 +8178,7 @@ var ALL_CHECKS = [
|
|
|
7942
8178
|
];
|
|
7943
8179
|
async function runValidateCheck(projectRoot, config) {
|
|
7944
8180
|
const issues = [];
|
|
7945
|
-
const agentsPath =
|
|
8181
|
+
const agentsPath = path14.join(projectRoot, config.agentsMapPath ?? "AGENTS.md");
|
|
7946
8182
|
const result = await validateAgentsMap(agentsPath);
|
|
7947
8183
|
if (!result.ok) {
|
|
7948
8184
|
issues.push({ severity: "error", message: result.error.message });
|
|
@@ -7999,7 +8235,7 @@ async function runDepsCheck(projectRoot, config) {
|
|
|
7999
8235
|
}
|
|
8000
8236
|
async function runDocsCheck(projectRoot, config) {
|
|
8001
8237
|
const issues = [];
|
|
8002
|
-
const docsDir =
|
|
8238
|
+
const docsDir = path14.join(projectRoot, config.docsDir ?? "docs");
|
|
8003
8239
|
const entropyConfig = config.entropy || {};
|
|
8004
8240
|
const result = await checkDocCoverage("project", {
|
|
8005
8241
|
docsDir,
|
|
@@ -8024,10 +8260,14 @@ async function runDocsCheck(projectRoot, config) {
|
|
|
8024
8260
|
}
|
|
8025
8261
|
return issues;
|
|
8026
8262
|
}
|
|
8027
|
-
async function runEntropyCheck(projectRoot,
|
|
8263
|
+
async function runEntropyCheck(projectRoot, config) {
|
|
8028
8264
|
const issues = [];
|
|
8265
|
+
const entropyConfig = config.entropy || {};
|
|
8266
|
+
const perfConfig = config.performance || {};
|
|
8267
|
+
const entryPoints = entropyConfig.entryPoints ?? perfConfig.entryPoints;
|
|
8029
8268
|
const analyzer = new EntropyAnalyzer({
|
|
8030
8269
|
rootDir: projectRoot,
|
|
8270
|
+
...entryPoints ? { entryPoints } : {},
|
|
8031
8271
|
analyze: { drift: true, deadCode: true, patterns: false }
|
|
8032
8272
|
});
|
|
8033
8273
|
const result = await analyzer.analyze();
|
|
@@ -8089,8 +8329,10 @@ async function runSecurityCheck(projectRoot, config) {
|
|
|
8089
8329
|
async function runPerfCheck(projectRoot, config) {
|
|
8090
8330
|
const issues = [];
|
|
8091
8331
|
const perfConfig = config.performance || {};
|
|
8332
|
+
const entryPoints = perfConfig.entryPoints;
|
|
8092
8333
|
const perfAnalyzer = new EntropyAnalyzer({
|
|
8093
8334
|
rootDir: projectRoot,
|
|
8335
|
+
...entryPoints ? { entryPoints } : {},
|
|
8094
8336
|
analyze: {
|
|
8095
8337
|
complexity: perfConfig.complexity || true,
|
|
8096
8338
|
coupling: perfConfig.coupling || true,
|
|
@@ -8280,7 +8522,7 @@ async function runMechanicalChecks(options) {
|
|
|
8280
8522
|
};
|
|
8281
8523
|
if (!skip.includes("validate")) {
|
|
8282
8524
|
try {
|
|
8283
|
-
const agentsPath =
|
|
8525
|
+
const agentsPath = path15.join(projectRoot, config.agentsMapPath ?? "AGENTS.md");
|
|
8284
8526
|
const result = await validateAgentsMap(agentsPath);
|
|
8285
8527
|
if (!result.ok) {
|
|
8286
8528
|
statuses.validate = "fail";
|
|
@@ -8317,7 +8559,7 @@ async function runMechanicalChecks(options) {
|
|
|
8317
8559
|
statuses.validate = "fail";
|
|
8318
8560
|
findings.push({
|
|
8319
8561
|
tool: "validate",
|
|
8320
|
-
file:
|
|
8562
|
+
file: path15.join(projectRoot, "AGENTS.md"),
|
|
8321
8563
|
message: err instanceof Error ? err.message : String(err),
|
|
8322
8564
|
severity: "error"
|
|
8323
8565
|
});
|
|
@@ -8381,7 +8623,7 @@ async function runMechanicalChecks(options) {
|
|
|
8381
8623
|
(async () => {
|
|
8382
8624
|
const localFindings = [];
|
|
8383
8625
|
try {
|
|
8384
|
-
const docsDir =
|
|
8626
|
+
const docsDir = path15.join(projectRoot, config.docsDir ?? "docs");
|
|
8385
8627
|
const result = await checkDocCoverage("project", { docsDir });
|
|
8386
8628
|
if (!result.ok) {
|
|
8387
8629
|
statuses["check-docs"] = "warn";
|
|
@@ -8408,7 +8650,7 @@ async function runMechanicalChecks(options) {
|
|
|
8408
8650
|
statuses["check-docs"] = "warn";
|
|
8409
8651
|
localFindings.push({
|
|
8410
8652
|
tool: "check-docs",
|
|
8411
|
-
file:
|
|
8653
|
+
file: path15.join(projectRoot, "docs"),
|
|
8412
8654
|
message: err instanceof Error ? err.message : String(err),
|
|
8413
8655
|
severity: "warning"
|
|
8414
8656
|
});
|
|
@@ -8557,18 +8799,18 @@ function computeContextBudget(diffLines) {
|
|
|
8557
8799
|
return diffLines;
|
|
8558
8800
|
}
|
|
8559
8801
|
function isWithinProject(absPath, projectRoot) {
|
|
8560
|
-
const resolvedRoot =
|
|
8561
|
-
const resolvedPath =
|
|
8562
|
-
return resolvedPath.startsWith(resolvedRoot) || resolvedPath ===
|
|
8802
|
+
const resolvedRoot = path16.resolve(projectRoot) + path16.sep;
|
|
8803
|
+
const resolvedPath = path16.resolve(absPath);
|
|
8804
|
+
return resolvedPath.startsWith(resolvedRoot) || resolvedPath === path16.resolve(projectRoot);
|
|
8563
8805
|
}
|
|
8564
8806
|
async function readContextFile(projectRoot, filePath, reason) {
|
|
8565
|
-
const absPath =
|
|
8807
|
+
const absPath = path16.isAbsolute(filePath) ? filePath : path16.join(projectRoot, filePath);
|
|
8566
8808
|
if (!isWithinProject(absPath, projectRoot)) return null;
|
|
8567
8809
|
const result = await readFileContent(absPath);
|
|
8568
8810
|
if (!result.ok) return null;
|
|
8569
8811
|
const content = result.value;
|
|
8570
8812
|
const lines = content.split("\n").length;
|
|
8571
|
-
const relPath =
|
|
8813
|
+
const relPath = path16.isAbsolute(filePath) ? relativePosix(projectRoot, filePath) : filePath;
|
|
8572
8814
|
return { path: relPath, content, reason, lines };
|
|
8573
8815
|
}
|
|
8574
8816
|
function extractImportSources2(content) {
|
|
@@ -8583,18 +8825,18 @@ function extractImportSources2(content) {
|
|
|
8583
8825
|
}
|
|
8584
8826
|
async function resolveImportPath2(projectRoot, fromFile, importSource) {
|
|
8585
8827
|
if (!importSource.startsWith(".")) return null;
|
|
8586
|
-
const fromDir =
|
|
8587
|
-
const basePath =
|
|
8828
|
+
const fromDir = path16.dirname(path16.join(projectRoot, fromFile));
|
|
8829
|
+
const basePath = path16.resolve(fromDir, importSource);
|
|
8588
8830
|
if (!isWithinProject(basePath, projectRoot)) return null;
|
|
8589
8831
|
const relBase = relativePosix(projectRoot, basePath);
|
|
8590
8832
|
const candidates = [
|
|
8591
8833
|
relBase + ".ts",
|
|
8592
8834
|
relBase + ".tsx",
|
|
8593
8835
|
relBase + ".mts",
|
|
8594
|
-
|
|
8836
|
+
path16.join(relBase, "index.ts")
|
|
8595
8837
|
];
|
|
8596
8838
|
for (const candidate of candidates) {
|
|
8597
|
-
const absCandidate =
|
|
8839
|
+
const absCandidate = path16.join(projectRoot, candidate);
|
|
8598
8840
|
if (await fileExists(absCandidate)) {
|
|
8599
8841
|
return candidate;
|
|
8600
8842
|
}
|
|
@@ -8602,7 +8844,7 @@ async function resolveImportPath2(projectRoot, fromFile, importSource) {
|
|
|
8602
8844
|
return null;
|
|
8603
8845
|
}
|
|
8604
8846
|
async function findTestFiles(projectRoot, sourceFile) {
|
|
8605
|
-
const baseName =
|
|
8847
|
+
const baseName = path16.basename(sourceFile, path16.extname(sourceFile));
|
|
8606
8848
|
const pattern = `**/${baseName}.{test,spec}.{ts,tsx,mts}`;
|
|
8607
8849
|
const results = await findFiles(pattern, projectRoot);
|
|
8608
8850
|
return results.map((f) => relativePosix(projectRoot, f));
|
|
@@ -8887,101 +9129,102 @@ function findMissingJsDoc(bundle) {
|
|
|
8887
9129
|
}
|
|
8888
9130
|
return missing;
|
|
8889
9131
|
}
|
|
8890
|
-
function
|
|
9132
|
+
function checkMissingJsDoc(bundle, rules) {
|
|
9133
|
+
const jsDocRule = rules.find((r) => r.text.toLowerCase().includes("jsdoc"));
|
|
9134
|
+
if (!jsDocRule) return [];
|
|
9135
|
+
const missingDocs = findMissingJsDoc(bundle);
|
|
9136
|
+
return missingDocs.map((m) => ({
|
|
9137
|
+
id: makeFindingId("compliance", m.file, m.line, `Missing JSDoc ${m.exportName}`),
|
|
9138
|
+
file: m.file,
|
|
9139
|
+
lineRange: [m.line, m.line],
|
|
9140
|
+
domain: "compliance",
|
|
9141
|
+
severity: "important",
|
|
9142
|
+
title: `Missing JSDoc on exported \`${m.exportName}\``,
|
|
9143
|
+
rationale: `Convention requires all exports to have JSDoc comments (from ${jsDocRule.source}).`,
|
|
9144
|
+
suggestion: `Add a JSDoc comment above the export of \`${m.exportName}\`.`,
|
|
9145
|
+
evidence: [`changeType: ${bundle.changeType}`, `Convention rule: "${jsDocRule.text}"`],
|
|
9146
|
+
validatedBy: "heuristic"
|
|
9147
|
+
}));
|
|
9148
|
+
}
|
|
9149
|
+
function checkFeatureSpec(bundle) {
|
|
9150
|
+
const hasSpecContext = bundle.contextFiles.some(
|
|
9151
|
+
(f) => f.reason === "spec" || f.reason === "convention"
|
|
9152
|
+
);
|
|
9153
|
+
if (hasSpecContext || bundle.changedFiles.length === 0) return [];
|
|
9154
|
+
const firstFile = bundle.changedFiles[0];
|
|
9155
|
+
return [
|
|
9156
|
+
{
|
|
9157
|
+
id: makeFindingId("compliance", firstFile.path, 1, "No spec for feature"),
|
|
9158
|
+
file: firstFile.path,
|
|
9159
|
+
lineRange: [1, 1],
|
|
9160
|
+
domain: "compliance",
|
|
9161
|
+
severity: "suggestion",
|
|
9162
|
+
title: "No spec/design doc found for feature change",
|
|
9163
|
+
rationale: "Feature changes should reference a spec or design doc to verify alignment. No spec context was included in the review bundle.",
|
|
9164
|
+
evidence: [`changeType: feature`, `contextFiles count: ${bundle.contextFiles.length}`],
|
|
9165
|
+
validatedBy: "heuristic"
|
|
9166
|
+
}
|
|
9167
|
+
];
|
|
9168
|
+
}
|
|
9169
|
+
function checkBugfixHistory(bundle) {
|
|
9170
|
+
if (bundle.commitHistory.length > 0 || bundle.changedFiles.length === 0) return [];
|
|
9171
|
+
const firstFile = bundle.changedFiles[0];
|
|
9172
|
+
return [
|
|
9173
|
+
{
|
|
9174
|
+
id: makeFindingId("compliance", firstFile.path, 1, "Bugfix no history"),
|
|
9175
|
+
file: firstFile.path,
|
|
9176
|
+
lineRange: [1, 1],
|
|
9177
|
+
domain: "compliance",
|
|
9178
|
+
severity: "suggestion",
|
|
9179
|
+
title: "Bugfix without commit history context",
|
|
9180
|
+
rationale: "Bugfix changes benefit from commit history to verify the root cause is addressed, not just the symptom. No commit history was provided.",
|
|
9181
|
+
evidence: [`changeType: bugfix`, `commitHistory entries: ${bundle.commitHistory.length}`],
|
|
9182
|
+
validatedBy: "heuristic"
|
|
9183
|
+
}
|
|
9184
|
+
];
|
|
9185
|
+
}
|
|
9186
|
+
function checkChangeTypeSpecific(bundle) {
|
|
9187
|
+
switch (bundle.changeType) {
|
|
9188
|
+
case "feature":
|
|
9189
|
+
return checkFeatureSpec(bundle);
|
|
9190
|
+
case "bugfix":
|
|
9191
|
+
return checkBugfixHistory(bundle);
|
|
9192
|
+
default:
|
|
9193
|
+
return [];
|
|
9194
|
+
}
|
|
9195
|
+
}
|
|
9196
|
+
function checkResultTypeConvention(bundle, rules) {
|
|
9197
|
+
const resultTypeRule = rules.find((r) => r.text.toLowerCase().includes("result type"));
|
|
9198
|
+
if (!resultTypeRule) return [];
|
|
8891
9199
|
const findings = [];
|
|
8892
|
-
const
|
|
8893
|
-
|
|
8894
|
-
|
|
8895
|
-
|
|
8896
|
-
for (const m of missingDocs) {
|
|
9200
|
+
for (const cf of bundle.changedFiles) {
|
|
9201
|
+
const hasTryCatch = cf.content.includes("try {") || cf.content.includes("try{");
|
|
9202
|
+
const usesResult = cf.content.includes("Result<") || cf.content.includes("Result >") || cf.content.includes(": Result");
|
|
9203
|
+
if (hasTryCatch && !usesResult) {
|
|
8897
9204
|
findings.push({
|
|
8898
|
-
id: makeFindingId("compliance",
|
|
8899
|
-
file:
|
|
8900
|
-
lineRange: [
|
|
9205
|
+
id: makeFindingId("compliance", cf.path, 1, "try-catch not Result"),
|
|
9206
|
+
file: cf.path,
|
|
9207
|
+
lineRange: [1, cf.lines],
|
|
8901
9208
|
domain: "compliance",
|
|
8902
|
-
severity: "
|
|
8903
|
-
title:
|
|
8904
|
-
rationale: `Convention requires
|
|
8905
|
-
suggestion:
|
|
8906
|
-
evidence: [
|
|
8907
|
-
`changeType: ${bundle.changeType}`,
|
|
8908
|
-
`Convention rule: "${rules.find((r) => r.text.toLowerCase().includes("jsdoc"))?.text ?? ""}"`
|
|
8909
|
-
],
|
|
9209
|
+
severity: "suggestion",
|
|
9210
|
+
title: "Fallible operation uses try/catch instead of Result type",
|
|
9211
|
+
rationale: `Convention requires using Result type for fallible operations (from ${resultTypeRule.source}).`,
|
|
9212
|
+
suggestion: "Refactor error handling to use the Result type pattern.",
|
|
9213
|
+
evidence: [`changeType: ${bundle.changeType}`, `Convention rule: "${resultTypeRule.text}"`],
|
|
8910
9214
|
validatedBy: "heuristic"
|
|
8911
9215
|
});
|
|
8912
9216
|
}
|
|
8913
9217
|
}
|
|
8914
|
-
switch (bundle.changeType) {
|
|
8915
|
-
case "feature": {
|
|
8916
|
-
const hasSpecContext = bundle.contextFiles.some(
|
|
8917
|
-
(f) => f.reason === "spec" || f.reason === "convention"
|
|
8918
|
-
);
|
|
8919
|
-
if (!hasSpecContext && bundle.changedFiles.length > 0) {
|
|
8920
|
-
const firstFile = bundle.changedFiles[0];
|
|
8921
|
-
findings.push({
|
|
8922
|
-
id: makeFindingId("compliance", firstFile.path, 1, "No spec for feature"),
|
|
8923
|
-
file: firstFile.path,
|
|
8924
|
-
lineRange: [1, 1],
|
|
8925
|
-
domain: "compliance",
|
|
8926
|
-
severity: "suggestion",
|
|
8927
|
-
title: "No spec/design doc found for feature change",
|
|
8928
|
-
rationale: "Feature changes should reference a spec or design doc to verify alignment. No spec context was included in the review bundle.",
|
|
8929
|
-
evidence: [`changeType: feature`, `contextFiles count: ${bundle.contextFiles.length}`],
|
|
8930
|
-
validatedBy: "heuristic"
|
|
8931
|
-
});
|
|
8932
|
-
}
|
|
8933
|
-
break;
|
|
8934
|
-
}
|
|
8935
|
-
case "bugfix": {
|
|
8936
|
-
if (bundle.commitHistory.length === 0 && bundle.changedFiles.length > 0) {
|
|
8937
|
-
const firstFile = bundle.changedFiles[0];
|
|
8938
|
-
findings.push({
|
|
8939
|
-
id: makeFindingId("compliance", firstFile.path, 1, "Bugfix no history"),
|
|
8940
|
-
file: firstFile.path,
|
|
8941
|
-
lineRange: [1, 1],
|
|
8942
|
-
domain: "compliance",
|
|
8943
|
-
severity: "suggestion",
|
|
8944
|
-
title: "Bugfix without commit history context",
|
|
8945
|
-
rationale: "Bugfix changes benefit from commit history to verify the root cause is addressed, not just the symptom. No commit history was provided.",
|
|
8946
|
-
evidence: [`changeType: bugfix`, `commitHistory entries: ${bundle.commitHistory.length}`],
|
|
8947
|
-
validatedBy: "heuristic"
|
|
8948
|
-
});
|
|
8949
|
-
}
|
|
8950
|
-
break;
|
|
8951
|
-
}
|
|
8952
|
-
case "refactor": {
|
|
8953
|
-
break;
|
|
8954
|
-
}
|
|
8955
|
-
case "docs": {
|
|
8956
|
-
break;
|
|
8957
|
-
}
|
|
8958
|
-
}
|
|
8959
|
-
const resultTypeRule = rules.find((r) => r.text.toLowerCase().includes("result type"));
|
|
8960
|
-
if (resultTypeRule) {
|
|
8961
|
-
for (const cf of bundle.changedFiles) {
|
|
8962
|
-
const hasTryCatch = cf.content.includes("try {") || cf.content.includes("try{");
|
|
8963
|
-
const usesResult = cf.content.includes("Result<") || cf.content.includes("Result >") || cf.content.includes(": Result");
|
|
8964
|
-
if (hasTryCatch && !usesResult) {
|
|
8965
|
-
findings.push({
|
|
8966
|
-
id: makeFindingId("compliance", cf.path, 1, "try-catch not Result"),
|
|
8967
|
-
file: cf.path,
|
|
8968
|
-
lineRange: [1, cf.lines],
|
|
8969
|
-
domain: "compliance",
|
|
8970
|
-
severity: "suggestion",
|
|
8971
|
-
title: "Fallible operation uses try/catch instead of Result type",
|
|
8972
|
-
rationale: `Convention requires using Result type for fallible operations (from ${resultTypeRule.source}).`,
|
|
8973
|
-
suggestion: "Refactor error handling to use the Result type pattern.",
|
|
8974
|
-
evidence: [
|
|
8975
|
-
`changeType: ${bundle.changeType}`,
|
|
8976
|
-
`Convention rule: "${resultTypeRule.text}"`
|
|
8977
|
-
],
|
|
8978
|
-
validatedBy: "heuristic"
|
|
8979
|
-
});
|
|
8980
|
-
}
|
|
8981
|
-
}
|
|
8982
|
-
}
|
|
8983
9218
|
return findings;
|
|
8984
9219
|
}
|
|
9220
|
+
function runComplianceAgent(bundle) {
|
|
9221
|
+
const rules = extractConventionRules(bundle);
|
|
9222
|
+
return [
|
|
9223
|
+
...checkMissingJsDoc(bundle, rules),
|
|
9224
|
+
...checkChangeTypeSpecific(bundle),
|
|
9225
|
+
...checkResultTypeConvention(bundle, rules)
|
|
9226
|
+
];
|
|
9227
|
+
}
|
|
8985
9228
|
var BUG_DETECTION_DESCRIPTOR = {
|
|
8986
9229
|
domain: "bug",
|
|
8987
9230
|
tier: "strong",
|
|
@@ -9252,31 +9495,32 @@ var ARCHITECTURE_DESCRIPTOR = {
|
|
|
9252
9495
|
]
|
|
9253
9496
|
};
|
|
9254
9497
|
var LARGE_FILE_THRESHOLD = 300;
|
|
9498
|
+
function isViolationLine(line) {
|
|
9499
|
+
const lower = line.toLowerCase();
|
|
9500
|
+
return lower.includes("violation") || lower.includes("layer");
|
|
9501
|
+
}
|
|
9502
|
+
function createLayerViolationFinding(line, fallbackPath) {
|
|
9503
|
+
const fileMatch = line.match(/(?:in\s+)?(\S+\.(?:ts|tsx|js|jsx))(?::(\d+))?/);
|
|
9504
|
+
const file = fileMatch?.[1] ?? fallbackPath;
|
|
9505
|
+
const lineNum = fileMatch?.[2] ? parseInt(fileMatch[2], 10) : 1;
|
|
9506
|
+
return {
|
|
9507
|
+
id: makeFindingId("arch", file, lineNum, "layer violation"),
|
|
9508
|
+
file,
|
|
9509
|
+
lineRange: [lineNum, lineNum],
|
|
9510
|
+
domain: "architecture",
|
|
9511
|
+
severity: "critical",
|
|
9512
|
+
title: "Layer boundary violation detected by check-deps",
|
|
9513
|
+
rationale: `Architectural layer violation: ${line.trim()}. Imports must flow in the correct direction per the project's layer definitions.`,
|
|
9514
|
+
suggestion: "Route the dependency through the correct intermediate layer (e.g., routes -> services -> db, not routes -> db).",
|
|
9515
|
+
evidence: [line.trim()],
|
|
9516
|
+
validatedBy: "heuristic"
|
|
9517
|
+
};
|
|
9518
|
+
}
|
|
9255
9519
|
function detectLayerViolations(bundle) {
|
|
9256
|
-
const findings = [];
|
|
9257
9520
|
const checkDepsFile = bundle.contextFiles.find((f) => f.path === "harness-check-deps-output");
|
|
9258
|
-
if (!checkDepsFile) return
|
|
9259
|
-
const
|
|
9260
|
-
|
|
9261
|
-
if (line.toLowerCase().includes("violation") || line.toLowerCase().includes("layer")) {
|
|
9262
|
-
const fileMatch = line.match(/(?:in\s+)?(\S+\.(?:ts|tsx|js|jsx))(?::(\d+))?/);
|
|
9263
|
-
const file = fileMatch?.[1] ?? bundle.changedFiles[0]?.path ?? "unknown";
|
|
9264
|
-
const lineNum = fileMatch?.[2] ? parseInt(fileMatch[2], 10) : 1;
|
|
9265
|
-
findings.push({
|
|
9266
|
-
id: makeFindingId("arch", file, lineNum, "layer violation"),
|
|
9267
|
-
file,
|
|
9268
|
-
lineRange: [lineNum, lineNum],
|
|
9269
|
-
domain: "architecture",
|
|
9270
|
-
severity: "critical",
|
|
9271
|
-
title: "Layer boundary violation detected by check-deps",
|
|
9272
|
-
rationale: `Architectural layer violation: ${line.trim()}. Imports must flow in the correct direction per the project's layer definitions.`,
|
|
9273
|
-
suggestion: "Route the dependency through the correct intermediate layer (e.g., routes -> services -> db, not routes -> db).",
|
|
9274
|
-
evidence: [line.trim()],
|
|
9275
|
-
validatedBy: "heuristic"
|
|
9276
|
-
});
|
|
9277
|
-
}
|
|
9278
|
-
}
|
|
9279
|
-
return findings;
|
|
9521
|
+
if (!checkDepsFile) return [];
|
|
9522
|
+
const fallbackPath = bundle.changedFiles[0]?.path ?? "unknown";
|
|
9523
|
+
return checkDepsFile.content.split("\n").filter(isViolationLine).map((line) => createLayerViolationFinding(line, fallbackPath));
|
|
9280
9524
|
}
|
|
9281
9525
|
function detectLargeFiles(bundle) {
|
|
9282
9526
|
const findings = [];
|
|
@@ -9298,45 +9542,61 @@ function detectLargeFiles(bundle) {
|
|
|
9298
9542
|
}
|
|
9299
9543
|
return findings;
|
|
9300
9544
|
}
|
|
9545
|
+
function extractRelativeImports(content) {
|
|
9546
|
+
const importRegex = /import\s+.*?from\s+['"]([^'"]+)['"]/g;
|
|
9547
|
+
let match;
|
|
9548
|
+
const imports = /* @__PURE__ */ new Set();
|
|
9549
|
+
while ((match = importRegex.exec(content)) !== null) {
|
|
9550
|
+
const source = match[1];
|
|
9551
|
+
if (source.startsWith(".")) {
|
|
9552
|
+
imports.add(source.replace(/^\.\//, "").replace(/^\.\.\//, ""));
|
|
9553
|
+
}
|
|
9554
|
+
}
|
|
9555
|
+
return imports;
|
|
9556
|
+
}
|
|
9557
|
+
function fileBaseName(filePath) {
|
|
9558
|
+
return filePath.replace(/.*\//, "").replace(/\.(ts|tsx|js|jsx)$/, "");
|
|
9559
|
+
}
|
|
9560
|
+
function findCircularImportInCtxFile(ctxFile, changedFilePath, changedPaths, fileImports) {
|
|
9561
|
+
const ctxImportRegex = /import\s+.*?from\s+['"]([^'"]+)['"]/g;
|
|
9562
|
+
let ctxMatch;
|
|
9563
|
+
while ((ctxMatch = ctxImportRegex.exec(ctxFile.content)) !== null) {
|
|
9564
|
+
const ctxSource = ctxMatch[1];
|
|
9565
|
+
if (!ctxSource.startsWith(".")) continue;
|
|
9566
|
+
for (const changedPath of changedPaths) {
|
|
9567
|
+
const baseName = fileBaseName(changedPath);
|
|
9568
|
+
const ctxBaseName = fileBaseName(ctxFile.path);
|
|
9569
|
+
if (ctxSource.includes(baseName) && fileImports.has(ctxBaseName)) {
|
|
9570
|
+
return {
|
|
9571
|
+
id: makeFindingId("arch", changedFilePath, 1, `circular ${ctxFile.path}`),
|
|
9572
|
+
file: changedFilePath,
|
|
9573
|
+
lineRange: [1, 1],
|
|
9574
|
+
domain: "architecture",
|
|
9575
|
+
severity: "important",
|
|
9576
|
+
title: `Potential circular import between ${changedFilePath} and ${ctxFile.path}`,
|
|
9577
|
+
rationale: "Circular imports can cause runtime issues (undefined values at import time) and indicate tightly coupled modules that should be refactored.",
|
|
9578
|
+
suggestion: "Extract shared types/interfaces into a separate module that both files can import from.",
|
|
9579
|
+
evidence: [
|
|
9580
|
+
`${changedFilePath} imports from a module that also imports from ${changedFilePath}`
|
|
9581
|
+
],
|
|
9582
|
+
validatedBy: "heuristic"
|
|
9583
|
+
};
|
|
9584
|
+
}
|
|
9585
|
+
}
|
|
9586
|
+
}
|
|
9587
|
+
return null;
|
|
9588
|
+
}
|
|
9301
9589
|
function detectCircularImports(bundle) {
|
|
9302
9590
|
const findings = [];
|
|
9303
9591
|
const changedPaths = new Set(bundle.changedFiles.map((f) => f.path));
|
|
9592
|
+
const relevantCtxFiles = bundle.contextFiles.filter(
|
|
9593
|
+
(f) => f.reason === "import" || f.reason === "graph-dependency"
|
|
9594
|
+
);
|
|
9304
9595
|
for (const cf of bundle.changedFiles) {
|
|
9305
|
-
const
|
|
9306
|
-
|
|
9307
|
-
|
|
9308
|
-
|
|
9309
|
-
const source = match[1];
|
|
9310
|
-
if (source.startsWith(".")) {
|
|
9311
|
-
imports.add(source.replace(/^\.\//, "").replace(/^\.\.\//, ""));
|
|
9312
|
-
}
|
|
9313
|
-
}
|
|
9314
|
-
for (const ctxFile of bundle.contextFiles) {
|
|
9315
|
-
if (ctxFile.reason !== "import" && ctxFile.reason !== "graph-dependency") continue;
|
|
9316
|
-
const ctxImportRegex = /import\s+.*?from\s+['"]([^'"]+)['"]/g;
|
|
9317
|
-
let ctxMatch;
|
|
9318
|
-
while ((ctxMatch = ctxImportRegex.exec(ctxFile.content)) !== null) {
|
|
9319
|
-
const ctxSource = ctxMatch[1];
|
|
9320
|
-
if (ctxSource.startsWith(".")) {
|
|
9321
|
-
for (const changedPath of changedPaths) {
|
|
9322
|
-
const baseName = changedPath.replace(/.*\//, "").replace(/\.(ts|tsx|js|jsx)$/, "");
|
|
9323
|
-
if (ctxSource.includes(baseName) && imports.has(ctxFile.path.replace(/.*\//, "").replace(/\.(ts|tsx|js|jsx)$/, ""))) {
|
|
9324
|
-
findings.push({
|
|
9325
|
-
id: makeFindingId("arch", cf.path, 1, `circular ${ctxFile.path}`),
|
|
9326
|
-
file: cf.path,
|
|
9327
|
-
lineRange: [1, 1],
|
|
9328
|
-
domain: "architecture",
|
|
9329
|
-
severity: "important",
|
|
9330
|
-
title: `Potential circular import between ${cf.path} and ${ctxFile.path}`,
|
|
9331
|
-
rationale: "Circular imports can cause runtime issues (undefined values at import time) and indicate tightly coupled modules that should be refactored.",
|
|
9332
|
-
suggestion: "Extract shared types/interfaces into a separate module that both files can import from.",
|
|
9333
|
-
evidence: [`${cf.path} imports from a module that also imports from ${cf.path}`],
|
|
9334
|
-
validatedBy: "heuristic"
|
|
9335
|
-
});
|
|
9336
|
-
}
|
|
9337
|
-
}
|
|
9338
|
-
}
|
|
9339
|
-
}
|
|
9596
|
+
const imports = extractRelativeImports(cf.content);
|
|
9597
|
+
for (const ctxFile of relevantCtxFiles) {
|
|
9598
|
+
const finding = findCircularImportInCtxFile(ctxFile, cf.path, changedPaths, imports);
|
|
9599
|
+
if (finding) findings.push(finding);
|
|
9340
9600
|
}
|
|
9341
9601
|
}
|
|
9342
9602
|
return findings;
|
|
@@ -9397,7 +9657,7 @@ function normalizePath(filePath, projectRoot) {
|
|
|
9397
9657
|
let normalized = filePath;
|
|
9398
9658
|
normalized = normalized.replace(/\\/g, "/");
|
|
9399
9659
|
const normalizedRoot = projectRoot.replace(/\\/g, "/");
|
|
9400
|
-
if (
|
|
9660
|
+
if (path17.isAbsolute(normalized)) {
|
|
9401
9661
|
const root = normalizedRoot.endsWith("/") ? normalizedRoot : normalizedRoot + "/";
|
|
9402
9662
|
if (normalized.startsWith(root)) {
|
|
9403
9663
|
normalized = normalized.slice(root.length);
|
|
@@ -9422,12 +9682,12 @@ function followImportChain(fromFile, fileContents, maxDepth = 2) {
|
|
|
9422
9682
|
while ((match = importRegex.exec(content)) !== null) {
|
|
9423
9683
|
const importPath = match[1];
|
|
9424
9684
|
if (!importPath.startsWith(".")) continue;
|
|
9425
|
-
const dir =
|
|
9426
|
-
let resolved =
|
|
9685
|
+
const dir = path17.dirname(current.file);
|
|
9686
|
+
let resolved = path17.join(dir, importPath).replace(/\\/g, "/");
|
|
9427
9687
|
if (!resolved.match(/\.(ts|tsx|js|jsx)$/)) {
|
|
9428
9688
|
resolved += ".ts";
|
|
9429
9689
|
}
|
|
9430
|
-
resolved =
|
|
9690
|
+
resolved = path17.normalize(resolved).replace(/\\/g, "/");
|
|
9431
9691
|
if (!visited.has(resolved) && current.depth + 1 <= maxDepth) {
|
|
9432
9692
|
queue.push({ file: resolved, depth: current.depth + 1 });
|
|
9433
9693
|
}
|
|
@@ -9444,7 +9704,7 @@ async function validateFindings(options) {
|
|
|
9444
9704
|
if (exclusionSet.isExcluded(normalizedFile, finding.lineRange) || exclusionSet.isExcluded(finding.file, finding.lineRange)) {
|
|
9445
9705
|
continue;
|
|
9446
9706
|
}
|
|
9447
|
-
const absoluteFile =
|
|
9707
|
+
const absoluteFile = path17.isAbsolute(finding.file) ? finding.file : path17.join(projectRoot, finding.file).replace(/\\/g, "/");
|
|
9448
9708
|
if (exclusionSet.isExcluded(absoluteFile, finding.lineRange)) {
|
|
9449
9709
|
continue;
|
|
9450
9710
|
}
|
|
@@ -9499,6 +9759,28 @@ async function validateFindings(options) {
|
|
|
9499
9759
|
function rangesOverlap(a, b, gap) {
|
|
9500
9760
|
return a[0] <= b[1] + gap && b[0] <= a[1] + gap;
|
|
9501
9761
|
}
|
|
9762
|
+
function pickLongest(a, b) {
|
|
9763
|
+
if (a && b) return a.length >= b.length ? a : b;
|
|
9764
|
+
return a ?? b;
|
|
9765
|
+
}
|
|
9766
|
+
function buildMergedTitle(a, b, domains) {
|
|
9767
|
+
const primaryFinding = SEVERITY_RANK[a.severity] >= SEVERITY_RANK[b.severity] ? a : b;
|
|
9768
|
+
const domainList = [...domains].sort().join(", ");
|
|
9769
|
+
const cleanTitle = primaryFinding.title.replace(/^\[.*?\]\s*/, "");
|
|
9770
|
+
return { title: `[${domainList}] ${cleanTitle}`, primaryFinding };
|
|
9771
|
+
}
|
|
9772
|
+
function mergeSecurityFields(merged, primary, a, b) {
|
|
9773
|
+
const cweId = primary.cweId ?? a.cweId ?? b.cweId;
|
|
9774
|
+
const owaspCategory = primary.owaspCategory ?? a.owaspCategory ?? b.owaspCategory;
|
|
9775
|
+
const confidence = primary.confidence ?? a.confidence ?? b.confidence;
|
|
9776
|
+
const remediation = pickLongest(a.remediation, b.remediation);
|
|
9777
|
+
const mergedRefs = [.../* @__PURE__ */ new Set([...a.references ?? [], ...b.references ?? []])];
|
|
9778
|
+
if (cweId !== void 0) merged.cweId = cweId;
|
|
9779
|
+
if (owaspCategory !== void 0) merged.owaspCategory = owaspCategory;
|
|
9780
|
+
if (confidence !== void 0) merged.confidence = confidence;
|
|
9781
|
+
if (remediation !== void 0) merged.remediation = remediation;
|
|
9782
|
+
if (mergedRefs.length > 0) merged.references = mergedRefs;
|
|
9783
|
+
}
|
|
9502
9784
|
function mergeFindings(a, b) {
|
|
9503
9785
|
const highestSeverity = SEVERITY_RANK[a.severity] >= SEVERITY_RANK[b.severity] ? a.severity : b.severity;
|
|
9504
9786
|
const highestValidatedBy = (VALIDATED_BY_RANK[a.validatedBy] ?? 0) >= (VALIDATED_BY_RANK[b.validatedBy] ?? 0) ? a.validatedBy : b.validatedBy;
|
|
@@ -9508,18 +9790,12 @@ function mergeFindings(a, b) {
|
|
|
9508
9790
|
Math.min(a.lineRange[0], b.lineRange[0]),
|
|
9509
9791
|
Math.max(a.lineRange[1], b.lineRange[1])
|
|
9510
9792
|
];
|
|
9511
|
-
const domains = /* @__PURE__ */ new Set();
|
|
9512
|
-
|
|
9513
|
-
|
|
9514
|
-
const suggestion = a.suggestion && b.suggestion ? a.suggestion.length >= b.suggestion.length ? a.suggestion : b.suggestion : a.suggestion ?? b.suggestion;
|
|
9515
|
-
const primaryFinding = SEVERITY_RANK[a.severity] >= SEVERITY_RANK[b.severity] ? a : b;
|
|
9516
|
-
const domainList = [...domains].sort().join(", ");
|
|
9517
|
-
const cleanTitle = primaryFinding.title.replace(/^\[.*?\]\s*/, "");
|
|
9518
|
-
const title = `[${domainList}] ${cleanTitle}`;
|
|
9793
|
+
const domains = /* @__PURE__ */ new Set([a.domain, b.domain]);
|
|
9794
|
+
const suggestion = pickLongest(a.suggestion, b.suggestion);
|
|
9795
|
+
const { title, primaryFinding } = buildMergedTitle(a, b, domains);
|
|
9519
9796
|
const merged = {
|
|
9520
9797
|
id: primaryFinding.id,
|
|
9521
9798
|
file: a.file,
|
|
9522
|
-
// same file for all merged findings
|
|
9523
9799
|
lineRange,
|
|
9524
9800
|
domain: primaryFinding.domain,
|
|
9525
9801
|
severity: highestSeverity,
|
|
@@ -9531,16 +9807,7 @@ function mergeFindings(a, b) {
|
|
|
9531
9807
|
if (suggestion !== void 0) {
|
|
9532
9808
|
merged.suggestion = suggestion;
|
|
9533
9809
|
}
|
|
9534
|
-
|
|
9535
|
-
const owaspCategory = primaryFinding.owaspCategory ?? a.owaspCategory ?? b.owaspCategory;
|
|
9536
|
-
const confidence = primaryFinding.confidence ?? a.confidence ?? b.confidence;
|
|
9537
|
-
const remediation = a.remediation && b.remediation ? a.remediation.length >= b.remediation.length ? a.remediation : b.remediation : a.remediation ?? b.remediation;
|
|
9538
|
-
const mergedRefs = [.../* @__PURE__ */ new Set([...a.references ?? [], ...b.references ?? []])];
|
|
9539
|
-
if (cweId !== void 0) merged.cweId = cweId;
|
|
9540
|
-
if (owaspCategory !== void 0) merged.owaspCategory = owaspCategory;
|
|
9541
|
-
if (confidence !== void 0) merged.confidence = confidence;
|
|
9542
|
-
if (remediation !== void 0) merged.remediation = remediation;
|
|
9543
|
-
if (mergedRefs.length > 0) merged.references = mergedRefs;
|
|
9810
|
+
mergeSecurityFields(merged, primaryFinding, a, b);
|
|
9544
9811
|
return merged;
|
|
9545
9812
|
}
|
|
9546
9813
|
function deduplicateFindings(options) {
|
|
@@ -9704,6 +9971,17 @@ function formatTerminalOutput(options) {
|
|
|
9704
9971
|
if (suggestionCount > 0) parts.push(`${suggestionCount} suggestion(s)`);
|
|
9705
9972
|
sections.push(` Found ${issueCount} issue(s): ${parts.join(", ")}.`);
|
|
9706
9973
|
}
|
|
9974
|
+
if (options.evidenceCoverage) {
|
|
9975
|
+
const ec = options.evidenceCoverage;
|
|
9976
|
+
sections.push("");
|
|
9977
|
+
sections.push("## Evidence Coverage\n");
|
|
9978
|
+
sections.push(` Evidence entries: ${ec.totalEntries}`);
|
|
9979
|
+
sections.push(
|
|
9980
|
+
` Findings with evidence: ${ec.findingsWithEvidence}/${ec.findingsWithEvidence + ec.uncitedCount}`
|
|
9981
|
+
);
|
|
9982
|
+
sections.push(` Uncited findings: ${ec.uncitedCount} (flagged as [UNVERIFIED])`);
|
|
9983
|
+
sections.push(` Coverage: ${ec.coveragePercentage}%`);
|
|
9984
|
+
}
|
|
9707
9985
|
return sections.join("\n");
|
|
9708
9986
|
}
|
|
9709
9987
|
var SMALL_SUGGESTION_LINE_LIMIT = 10;
|
|
@@ -9778,8 +10056,105 @@ function formatGitHubSummary(options) {
|
|
|
9778
10056
|
const assessment = determineAssessment(findings);
|
|
9779
10057
|
const assessmentLabel = assessment === "approve" ? "Approve" : assessment === "comment" ? "Comment" : "Request Changes";
|
|
9780
10058
|
sections.push(`## Assessment: ${assessmentLabel}`);
|
|
10059
|
+
if (options.evidenceCoverage) {
|
|
10060
|
+
const ec = options.evidenceCoverage;
|
|
10061
|
+
sections.push("");
|
|
10062
|
+
sections.push("## Evidence Coverage\n");
|
|
10063
|
+
sections.push(`- Evidence entries: ${ec.totalEntries}`);
|
|
10064
|
+
sections.push(
|
|
10065
|
+
`- Findings with evidence: ${ec.findingsWithEvidence}/${ec.findingsWithEvidence + ec.uncitedCount}`
|
|
10066
|
+
);
|
|
10067
|
+
sections.push(`- Uncited findings: ${ec.uncitedCount} (flagged as \\[UNVERIFIED\\])`);
|
|
10068
|
+
sections.push(`- Coverage: ${ec.coveragePercentage}%`);
|
|
10069
|
+
}
|
|
9781
10070
|
return sections.join("\n");
|
|
9782
10071
|
}
|
|
10072
|
+
var FILE_LINE_RANGE_PATTERN = /^([\w./@-]+\.\w+):(\d+)-(\d+)/;
|
|
10073
|
+
var FILE_LINE_PATTERN = /^([\w./@-]+\.\w+):(\d+)/;
|
|
10074
|
+
var FILE_ONLY_PATTERN = /^([\w./@-]+\.\w+)\s/;
|
|
10075
|
+
function parseEvidenceRef(content) {
|
|
10076
|
+
const trimmed = content.trim();
|
|
10077
|
+
const rangeMatch = trimmed.match(FILE_LINE_RANGE_PATTERN);
|
|
10078
|
+
if (rangeMatch) {
|
|
10079
|
+
return {
|
|
10080
|
+
file: rangeMatch[1],
|
|
10081
|
+
lineStart: parseInt(rangeMatch[2], 10),
|
|
10082
|
+
lineEnd: parseInt(rangeMatch[3], 10)
|
|
10083
|
+
};
|
|
10084
|
+
}
|
|
10085
|
+
const lineMatch = trimmed.match(FILE_LINE_PATTERN);
|
|
10086
|
+
if (lineMatch) {
|
|
10087
|
+
return {
|
|
10088
|
+
file: lineMatch[1],
|
|
10089
|
+
lineStart: parseInt(lineMatch[2], 10)
|
|
10090
|
+
};
|
|
10091
|
+
}
|
|
10092
|
+
const fileMatch = trimmed.match(FILE_ONLY_PATTERN);
|
|
10093
|
+
if (fileMatch) {
|
|
10094
|
+
return { file: fileMatch[1] };
|
|
10095
|
+
}
|
|
10096
|
+
return null;
|
|
10097
|
+
}
|
|
10098
|
+
function evidenceMatchesFinding(ref, finding) {
|
|
10099
|
+
if (ref.file !== finding.file) return false;
|
|
10100
|
+
if (ref.lineStart === void 0) return true;
|
|
10101
|
+
const [findStart, findEnd] = finding.lineRange;
|
|
10102
|
+
if (ref.lineEnd !== void 0) {
|
|
10103
|
+
return ref.lineStart <= findEnd && ref.lineEnd >= findStart;
|
|
10104
|
+
}
|
|
10105
|
+
return ref.lineStart >= findStart && ref.lineStart <= findEnd;
|
|
10106
|
+
}
|
|
10107
|
+
function checkEvidenceCoverage(findings, evidenceEntries) {
|
|
10108
|
+
if (findings.length === 0) {
|
|
10109
|
+
return {
|
|
10110
|
+
totalEntries: evidenceEntries.filter((e) => e.status === "active").length,
|
|
10111
|
+
findingsWithEvidence: 0,
|
|
10112
|
+
uncitedCount: 0,
|
|
10113
|
+
uncitedFindings: [],
|
|
10114
|
+
coveragePercentage: 100
|
|
10115
|
+
};
|
|
10116
|
+
}
|
|
10117
|
+
const activeEvidence = evidenceEntries.filter((e) => e.status === "active");
|
|
10118
|
+
const evidenceRefs = [];
|
|
10119
|
+
for (const entry of activeEvidence) {
|
|
10120
|
+
const ref = parseEvidenceRef(entry.content);
|
|
10121
|
+
if (ref) evidenceRefs.push(ref);
|
|
10122
|
+
}
|
|
10123
|
+
let findingsWithEvidence = 0;
|
|
10124
|
+
const uncitedFindings = [];
|
|
10125
|
+
for (const finding of findings) {
|
|
10126
|
+
const hasEvidence = evidenceRefs.some((ref) => evidenceMatchesFinding(ref, finding));
|
|
10127
|
+
if (hasEvidence) {
|
|
10128
|
+
findingsWithEvidence++;
|
|
10129
|
+
} else {
|
|
10130
|
+
uncitedFindings.push(finding.title);
|
|
10131
|
+
}
|
|
10132
|
+
}
|
|
10133
|
+
const uncitedCount = findings.length - findingsWithEvidence;
|
|
10134
|
+
const coveragePercentage = Math.round(findingsWithEvidence / findings.length * 100);
|
|
10135
|
+
return {
|
|
10136
|
+
totalEntries: activeEvidence.length,
|
|
10137
|
+
findingsWithEvidence,
|
|
10138
|
+
uncitedCount,
|
|
10139
|
+
uncitedFindings,
|
|
10140
|
+
coveragePercentage
|
|
10141
|
+
};
|
|
10142
|
+
}
|
|
10143
|
+
function tagUncitedFindings(findings, evidenceEntries) {
|
|
10144
|
+
const activeEvidence = evidenceEntries.filter((e) => e.status === "active");
|
|
10145
|
+
const evidenceRefs = [];
|
|
10146
|
+
for (const entry of activeEvidence) {
|
|
10147
|
+
const ref = parseEvidenceRef(entry.content);
|
|
10148
|
+
if (ref) evidenceRefs.push(ref);
|
|
10149
|
+
}
|
|
10150
|
+
for (const finding of findings) {
|
|
10151
|
+
const hasEvidence = evidenceRefs.some((ref) => evidenceMatchesFinding(ref, finding));
|
|
10152
|
+
if (!hasEvidence && !finding.title.startsWith("[UNVERIFIED]")) {
|
|
10153
|
+
finding.title = `[UNVERIFIED] ${finding.title}`;
|
|
10154
|
+
}
|
|
10155
|
+
}
|
|
10156
|
+
return findings;
|
|
10157
|
+
}
|
|
9783
10158
|
async function runReviewPipeline(options) {
|
|
9784
10159
|
const {
|
|
9785
10160
|
projectRoot,
|
|
@@ -9791,7 +10166,8 @@ async function runReviewPipeline(options) {
|
|
|
9791
10166
|
conventionFiles,
|
|
9792
10167
|
checkDepsOutput,
|
|
9793
10168
|
config = {},
|
|
9794
|
-
commitHistory
|
|
10169
|
+
commitHistory,
|
|
10170
|
+
sessionSlug
|
|
9795
10171
|
} = options;
|
|
9796
10172
|
if (flags.ci && prMetadata) {
|
|
9797
10173
|
const eligibility = checkEligibility(prMetadata, true);
|
|
@@ -9887,13 +10263,25 @@ async function runReviewPipeline(options) {
|
|
|
9887
10263
|
projectRoot,
|
|
9888
10264
|
fileContents
|
|
9889
10265
|
});
|
|
10266
|
+
let evidenceCoverage;
|
|
10267
|
+
if (sessionSlug) {
|
|
10268
|
+
try {
|
|
10269
|
+
const evidenceResult = await readSessionSection(projectRoot, sessionSlug, "evidence");
|
|
10270
|
+
if (evidenceResult.ok) {
|
|
10271
|
+
evidenceCoverage = checkEvidenceCoverage(validatedFindings, evidenceResult.value);
|
|
10272
|
+
tagUncitedFindings(validatedFindings, evidenceResult.value);
|
|
10273
|
+
}
|
|
10274
|
+
} catch {
|
|
10275
|
+
}
|
|
10276
|
+
}
|
|
9890
10277
|
const dedupedFindings = deduplicateFindings({ findings: validatedFindings });
|
|
9891
10278
|
const strengths = [];
|
|
9892
10279
|
const assessment = determineAssessment(dedupedFindings);
|
|
9893
10280
|
const exitCode = getExitCode(assessment);
|
|
9894
10281
|
const terminalOutput = formatTerminalOutput({
|
|
9895
10282
|
findings: dedupedFindings,
|
|
9896
|
-
strengths
|
|
10283
|
+
strengths,
|
|
10284
|
+
...evidenceCoverage != null ? { evidenceCoverage } : {}
|
|
9897
10285
|
});
|
|
9898
10286
|
let githubComments = [];
|
|
9899
10287
|
if (flags.comment) {
|
|
@@ -9908,7 +10296,8 @@ async function runReviewPipeline(options) {
|
|
|
9908
10296
|
terminalOutput,
|
|
9909
10297
|
githubComments,
|
|
9910
10298
|
exitCode,
|
|
9911
|
-
...mechanicalResult
|
|
10299
|
+
...mechanicalResult != null ? { mechanicalResult } : {},
|
|
10300
|
+
...evidenceCoverage != null ? { evidenceCoverage } : {}
|
|
9912
10301
|
};
|
|
9913
10302
|
}
|
|
9914
10303
|
var VALID_STATUSES = /* @__PURE__ */ new Set([
|
|
@@ -10008,13 +10397,29 @@ function parseFeatures(sectionBody) {
|
|
|
10008
10397
|
}
|
|
10009
10398
|
return Ok(features);
|
|
10010
10399
|
}
|
|
10011
|
-
function
|
|
10400
|
+
function extractFieldMap(body) {
|
|
10012
10401
|
const fieldMap = /* @__PURE__ */ new Map();
|
|
10013
10402
|
const fieldPattern = /^- \*\*(.+?):\*\* (.+)$/gm;
|
|
10014
10403
|
let match;
|
|
10015
10404
|
while ((match = fieldPattern.exec(body)) !== null) {
|
|
10016
10405
|
fieldMap.set(match[1], match[2]);
|
|
10017
10406
|
}
|
|
10407
|
+
return fieldMap;
|
|
10408
|
+
}
|
|
10409
|
+
function parseListField(fieldMap, ...keys) {
|
|
10410
|
+
let raw = EM_DASH;
|
|
10411
|
+
for (const key of keys) {
|
|
10412
|
+
const val = fieldMap.get(key);
|
|
10413
|
+
if (val !== void 0) {
|
|
10414
|
+
raw = val;
|
|
10415
|
+
break;
|
|
10416
|
+
}
|
|
10417
|
+
}
|
|
10418
|
+
if (raw === EM_DASH || raw === "none") return [];
|
|
10419
|
+
return raw.split(",").map((s) => s.trim());
|
|
10420
|
+
}
|
|
10421
|
+
function parseFeatureFields(name, body) {
|
|
10422
|
+
const fieldMap = extractFieldMap(body);
|
|
10018
10423
|
const statusRaw = fieldMap.get("Status");
|
|
10019
10424
|
if (!statusRaw || !VALID_STATUSES.has(statusRaw)) {
|
|
10020
10425
|
return Err(
|
|
@@ -10023,15 +10428,17 @@ function parseFeatureFields(name, body) {
|
|
|
10023
10428
|
)
|
|
10024
10429
|
);
|
|
10025
10430
|
}
|
|
10026
|
-
const status = statusRaw;
|
|
10027
10431
|
const specRaw = fieldMap.get("Spec") ?? EM_DASH;
|
|
10028
|
-
const
|
|
10029
|
-
const
|
|
10030
|
-
|
|
10031
|
-
|
|
10032
|
-
|
|
10033
|
-
|
|
10034
|
-
|
|
10432
|
+
const plans = parseListField(fieldMap, "Plans", "Plan");
|
|
10433
|
+
const blockedBy = parseListField(fieldMap, "Blocked by", "Blockers");
|
|
10434
|
+
return Ok({
|
|
10435
|
+
name,
|
|
10436
|
+
status: statusRaw,
|
|
10437
|
+
spec: specRaw === EM_DASH ? null : specRaw,
|
|
10438
|
+
plans,
|
|
10439
|
+
blockedBy,
|
|
10440
|
+
summary: fieldMap.get("Summary") ?? ""
|
|
10441
|
+
});
|
|
10035
10442
|
}
|
|
10036
10443
|
var EM_DASH2 = "\u2014";
|
|
10037
10444
|
function serializeRoadmap(roadmap) {
|
|
@@ -10091,10 +10498,10 @@ function inferStatus(feature, projectPath, allFeatures) {
|
|
|
10091
10498
|
const featuresWithPlans = allFeatures.filter((f) => f.plans.length > 0);
|
|
10092
10499
|
const useRootState = featuresWithPlans.length <= 1;
|
|
10093
10500
|
if (useRootState) {
|
|
10094
|
-
const rootStatePath =
|
|
10095
|
-
if (
|
|
10501
|
+
const rootStatePath = path18.join(projectPath, ".harness", "state.json");
|
|
10502
|
+
if (fs18.existsSync(rootStatePath)) {
|
|
10096
10503
|
try {
|
|
10097
|
-
const raw =
|
|
10504
|
+
const raw = fs18.readFileSync(rootStatePath, "utf-8");
|
|
10098
10505
|
const state = JSON.parse(raw);
|
|
10099
10506
|
if (state.progress) {
|
|
10100
10507
|
for (const status of Object.values(state.progress)) {
|
|
@@ -10105,16 +10512,16 @@ function inferStatus(feature, projectPath, allFeatures) {
|
|
|
10105
10512
|
}
|
|
10106
10513
|
}
|
|
10107
10514
|
}
|
|
10108
|
-
const sessionsDir =
|
|
10109
|
-
if (
|
|
10515
|
+
const sessionsDir = path18.join(projectPath, ".harness", "sessions");
|
|
10516
|
+
if (fs18.existsSync(sessionsDir)) {
|
|
10110
10517
|
try {
|
|
10111
|
-
const sessionDirs =
|
|
10518
|
+
const sessionDirs = fs18.readdirSync(sessionsDir, { withFileTypes: true });
|
|
10112
10519
|
for (const entry of sessionDirs) {
|
|
10113
10520
|
if (!entry.isDirectory()) continue;
|
|
10114
|
-
const autopilotPath =
|
|
10115
|
-
if (!
|
|
10521
|
+
const autopilotPath = path18.join(sessionsDir, entry.name, "autopilot-state.json");
|
|
10522
|
+
if (!fs18.existsSync(autopilotPath)) continue;
|
|
10116
10523
|
try {
|
|
10117
|
-
const raw =
|
|
10524
|
+
const raw = fs18.readFileSync(autopilotPath, "utf-8");
|
|
10118
10525
|
const autopilot = JSON.parse(raw);
|
|
10119
10526
|
if (!autopilot.phases) continue;
|
|
10120
10527
|
const linkedPhases = autopilot.phases.filter(
|
|
@@ -10194,10 +10601,10 @@ var ProjectScanner = class {
|
|
|
10194
10601
|
this.rootDir = rootDir;
|
|
10195
10602
|
}
|
|
10196
10603
|
async scan() {
|
|
10197
|
-
let projectName =
|
|
10604
|
+
let projectName = path19.basename(this.rootDir);
|
|
10198
10605
|
try {
|
|
10199
|
-
const pkgPath =
|
|
10200
|
-
const pkgRaw = await
|
|
10606
|
+
const pkgPath = path19.join(this.rootDir, "package.json");
|
|
10607
|
+
const pkgRaw = await fs19.readFile(pkgPath, "utf-8");
|
|
10201
10608
|
const pkg = JSON.parse(pkgRaw);
|
|
10202
10609
|
if (pkg.name) projectName = pkg.name;
|
|
10203
10610
|
} catch {
|
|
@@ -10310,13 +10717,13 @@ var BlueprintGenerator = class {
|
|
|
10310
10717
|
styles: STYLES,
|
|
10311
10718
|
scripts: SCRIPTS
|
|
10312
10719
|
});
|
|
10313
|
-
await
|
|
10314
|
-
await
|
|
10720
|
+
await fs20.mkdir(options.outputDir, { recursive: true });
|
|
10721
|
+
await fs20.writeFile(path20.join(options.outputDir, "index.html"), html);
|
|
10315
10722
|
}
|
|
10316
10723
|
};
|
|
10317
10724
|
function getStatePath() {
|
|
10318
10725
|
const home = process.env["HOME"] || os.homedir();
|
|
10319
|
-
return
|
|
10726
|
+
return path21.join(home, ".harness", "update-check.json");
|
|
10320
10727
|
}
|
|
10321
10728
|
function isUpdateCheckEnabled(configInterval) {
|
|
10322
10729
|
if (process.env["HARNESS_NO_UPDATE_CHECK"] === "1") return false;
|
|
@@ -10329,7 +10736,7 @@ function shouldRunCheck(state, intervalMs) {
|
|
|
10329
10736
|
}
|
|
10330
10737
|
function readCheckState() {
|
|
10331
10738
|
try {
|
|
10332
|
-
const raw =
|
|
10739
|
+
const raw = fs21.readFileSync(getStatePath(), "utf-8");
|
|
10333
10740
|
const parsed = JSON.parse(raw);
|
|
10334
10741
|
if (typeof parsed === "object" && parsed !== null && "lastCheckTime" in parsed && typeof parsed.lastCheckTime === "number" && "currentVersion" in parsed && typeof parsed.currentVersion === "string") {
|
|
10335
10742
|
const state = parsed;
|
|
@@ -10346,7 +10753,7 @@ function readCheckState() {
|
|
|
10346
10753
|
}
|
|
10347
10754
|
function spawnBackgroundCheck(currentVersion) {
|
|
10348
10755
|
const statePath = getStatePath();
|
|
10349
|
-
const stateDir =
|
|
10756
|
+
const stateDir = path21.dirname(statePath);
|
|
10350
10757
|
const script = `
|
|
10351
10758
|
const { execSync } = require('child_process');
|
|
10352
10759
|
const fs = require('fs');
|
|
@@ -10398,7 +10805,7 @@ function getUpdateNotification(currentVersion) {
|
|
|
10398
10805
|
return `Update available: v${currentVersion} -> v${state.latestVersion}
|
|
10399
10806
|
Run "harness update" to upgrade.`;
|
|
10400
10807
|
}
|
|
10401
|
-
var VERSION = "0.
|
|
10808
|
+
var VERSION = "0.14.0";
|
|
10402
10809
|
|
|
10403
10810
|
export {
|
|
10404
10811
|
ArchMetricCategorySchema,
|
|
@@ -10516,7 +10923,7 @@ export {
|
|
|
10516
10923
|
NoOpSink,
|
|
10517
10924
|
syncConstraintNodes,
|
|
10518
10925
|
detectStaleConstraints,
|
|
10519
|
-
resolveThresholds,
|
|
10926
|
+
resolveThresholds2 as resolveThresholds,
|
|
10520
10927
|
FailureEntrySchema,
|
|
10521
10928
|
HandoffSchema,
|
|
10522
10929
|
GateResultSchema,
|
|
@@ -10558,6 +10965,11 @@ export {
|
|
|
10558
10965
|
writeSessionSummary,
|
|
10559
10966
|
loadSessionSummary,
|
|
10560
10967
|
listActiveSessions,
|
|
10968
|
+
readSessionSections,
|
|
10969
|
+
readSessionSection,
|
|
10970
|
+
appendSessionEntry,
|
|
10971
|
+
updateSessionEntryStatus,
|
|
10972
|
+
archiveSession,
|
|
10561
10973
|
executeWorkflow,
|
|
10562
10974
|
runPipeline,
|
|
10563
10975
|
runMultiTurnPipeline,
|
|
@@ -10607,6 +11019,8 @@ export {
|
|
|
10607
11019
|
isSmallSuggestion,
|
|
10608
11020
|
formatGitHubComment,
|
|
10609
11021
|
formatGitHubSummary,
|
|
11022
|
+
checkEvidenceCoverage,
|
|
11023
|
+
tagUncitedFindings,
|
|
10610
11024
|
runReviewPipeline,
|
|
10611
11025
|
parseRoadmap,
|
|
10612
11026
|
serializeRoadmap,
|