edsger 0.75.1 → 0.77.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/commands/api-docs/index.d.ts +18 -0
- package/dist/commands/api-docs/index.js +41 -0
- package/dist/commands/find-architecture/index.d.ts +3 -1
- package/dist/commands/find-architecture/index.js +14 -5
- package/dist/commands/find-bugs/index.d.ts +3 -1
- package/dist/commands/find-bugs/index.js +14 -5
- package/dist/commands/find-smells/index.d.ts +3 -1
- package/dist/commands/find-smells/index.js +14 -5
- package/dist/commands/quality-benchmark/index.d.ts +5 -0
- package/dist/commands/quality-benchmark/index.js +28 -0
- package/dist/index.js +38 -6
- package/dist/phases/api-docs/index.d.ts +47 -0
- package/dist/phases/api-docs/index.js +254 -0
- package/dist/phases/api-docs/mcp-server.d.ts +25 -0
- package/dist/phases/api-docs/mcp-server.js +82 -0
- package/dist/phases/api-docs/prompts.d.ts +16 -0
- package/dist/phases/api-docs/prompts.js +65 -0
- package/dist/phases/api-docs/types.d.ts +22 -0
- package/dist/phases/api-docs/types.js +10 -0
- package/dist/phases/find-architecture/index.d.ts +4 -1
- package/dist/phases/find-architecture/index.js +46 -26
- package/dist/phases/find-architecture/prompts.d.ts +2 -1
- package/dist/phases/find-architecture/prompts.js +3 -2
- package/dist/phases/find-bugs/index.d.ts +4 -1
- package/dist/phases/find-bugs/index.js +32 -19
- package/dist/phases/find-shared/baseline.d.ts +45 -0
- package/dist/phases/find-shared/baseline.js +56 -0
- package/dist/phases/find-shared/custom-rules.d.ts +39 -0
- package/dist/phases/find-shared/custom-rules.js +75 -0
- package/dist/phases/find-shared/detect-context.d.ts +40 -0
- package/dist/phases/find-shared/detect-context.js +247 -0
- package/dist/phases/find-shared/mcp.d.ts +24 -3
- package/dist/phases/find-shared/mcp.js +41 -4
- package/dist/phases/find-shared/rule-config.d.ts +37 -0
- package/dist/phases/find-shared/rule-config.js +67 -0
- package/dist/phases/find-shared/rule-packs.d.ts +65 -0
- package/dist/phases/find-shared/rule-packs.js +124 -0
- package/dist/phases/find-shared/scoped-read.d.ts +12 -0
- package/dist/phases/find-shared/scoped-read.js +33 -0
- package/dist/phases/find-smells/index.d.ts +4 -1
- package/dist/phases/find-smells/index.js +43 -23
- package/dist/phases/find-smells/prompts.d.ts +2 -1
- package/dist/phases/find-smells/prompts.js +4 -3
- package/dist/phases/quality-benchmark/gate.d.ts +50 -0
- package/dist/phases/quality-benchmark/gate.js +91 -0
- package/dist/phases/quality-benchmark/index.js +15 -1
- package/dist/phases/quality-benchmark/parsers.d.ts +23 -0
- package/dist/phases/quality-benchmark/parsers.js +210 -0
- package/dist/phases/quality-benchmark/rubric.md +37 -0
- package/dist/phases/quality-benchmark/tool-catalog.js +58 -1
- package/dist/phases/quality-benchmark/types.d.ts +8 -1
- package/package.json +1 -1
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Quality gate evaluation for the CLI.
|
|
3
|
+
*
|
|
4
|
+
* Lets `edsger quality-benchmark --gate` enforce a per-scope pass/fail
|
|
5
|
+
* threshold against the report it just produced — the "fail the build" gate
|
|
6
|
+
* NDepend/DCM-style tools expose, usable directly in a user's own CI without
|
|
7
|
+
* any webhook. The gate (overall-score floor, critical-finding cap, per-
|
|
8
|
+
* dimension minimums) is stored in `quality_gates`, mirroring the desktop
|
|
9
|
+
* evaluator in desktop-app/.../services/db/quality-gate.ts.
|
|
10
|
+
*
|
|
11
|
+
* The evaluator is pure + exported for testing; the read is RLS-scoped via the
|
|
12
|
+
* user's supabase session and never throws (a missing gate = nothing to
|
|
13
|
+
* enforce).
|
|
14
|
+
*/
|
|
15
|
+
import { readScopedRow } from '../find-shared/scoped-read.js';
|
|
16
|
+
/** Count critical-severity findings across every dimension's evidence. */
|
|
17
|
+
export function countCriticalFindings(report) {
|
|
18
|
+
let count = 0;
|
|
19
|
+
for (const dim of Object.values(report.dimension_scores ?? {})) {
|
|
20
|
+
for (const ev of dim?.evidence ?? []) {
|
|
21
|
+
if (ev.severity === 'critical') {
|
|
22
|
+
count++;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return count;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Evaluate a report against a gate, returning every violated axis. A disabled
|
|
30
|
+
* gate passes vacuously. A report with no overall score is not failed on the
|
|
31
|
+
* overall-score axis (there is nothing to compare). Pure + exported for tests.
|
|
32
|
+
*/
|
|
33
|
+
export function evaluateGate(report, gate) {
|
|
34
|
+
const violations = [];
|
|
35
|
+
if (!gate.enabled) {
|
|
36
|
+
return { passed: true, violations };
|
|
37
|
+
}
|
|
38
|
+
if (gate.min_overall_score !== null &&
|
|
39
|
+
report.overall_score !== null &&
|
|
40
|
+
report.overall_score < gate.min_overall_score) {
|
|
41
|
+
violations.push({
|
|
42
|
+
label: 'Overall score',
|
|
43
|
+
required: `>= ${gate.min_overall_score}`,
|
|
44
|
+
actual: report.overall_score.toFixed(1),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
if (gate.max_critical_findings !== null) {
|
|
48
|
+
const critical = countCriticalFindings(report);
|
|
49
|
+
if (critical > gate.max_critical_findings) {
|
|
50
|
+
violations.push({
|
|
51
|
+
label: 'Critical findings',
|
|
52
|
+
required: `<= ${gate.max_critical_findings}`,
|
|
53
|
+
actual: String(critical),
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
for (const [dim, min] of Object.entries(gate.min_dimension_scores)) {
|
|
58
|
+
if (min === null || min === undefined) {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
const entry = report.dimension_scores?.[dim];
|
|
62
|
+
// A null/N-A dimension score isn't measurable against a floor — skip it.
|
|
63
|
+
if (entry?.score === null || entry?.score === undefined) {
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (entry.score < min) {
|
|
67
|
+
violations.push({
|
|
68
|
+
label: dim,
|
|
69
|
+
required: `>= ${min}`,
|
|
70
|
+
actual: entry.score.toFixed(0),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return { passed: violations.length === 0, violations };
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Fetch the gate configured for a scope, or null if none is set / no session.
|
|
78
|
+
* Never throws — a read failure degrades to "no gate" (nothing enforced).
|
|
79
|
+
*/
|
|
80
|
+
export async function getQualityGate(scope) {
|
|
81
|
+
const row = await readScopedRow('quality_gates', 'enabled, min_overall_score, max_critical_findings, min_dimension_scores', scope);
|
|
82
|
+
if (!row) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
return {
|
|
86
|
+
enabled: row.enabled ?? true,
|
|
87
|
+
min_overall_score: row.min_overall_score ?? null,
|
|
88
|
+
max_critical_findings: row.max_critical_findings ?? null,
|
|
89
|
+
min_dimension_scores: row.min_dimension_scores ?? {},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
@@ -151,7 +151,7 @@ export async function runQualityBenchmark(opts) {
|
|
|
151
151
|
...state.unavailable_tools,
|
|
152
152
|
...(report.unavailable_tools ?? []),
|
|
153
153
|
]),
|
|
154
|
-
tool_outputs: { ...state.tool_outputs, ...(report.tool_outputs ?? {}) },
|
|
154
|
+
tool_outputs: enrichToolOutputsWithMetrics({ ...state.tool_outputs, ...(report.tool_outputs ?? {}) }, state.parsed_summaries),
|
|
155
155
|
dropped_findings: Math.max(state.dropped_findings, report.dropped_findings ?? 0),
|
|
156
156
|
};
|
|
157
157
|
const completedAt = new Date().toISOString();
|
|
@@ -180,6 +180,20 @@ function readGitHead(repoRoot) {
|
|
|
180
180
|
}
|
|
181
181
|
return res.stdout.trim() || null;
|
|
182
182
|
}
|
|
183
|
+
/**
|
|
184
|
+
* Fold tier-3 (`metrics`) parser output into the persisted tool_outputs so
|
|
185
|
+
* trend charts can read numeric values (duplication %, complexity, …) without
|
|
186
|
+
* re-parsing oneliners. Count/finding tools carry no metrics and are untouched.
|
|
187
|
+
*/
|
|
188
|
+
function enrichToolOutputsWithMetrics(toolOutputs, parsedSummaries) {
|
|
189
|
+
const out = { ...toolOutputs };
|
|
190
|
+
for (const [id, parsed] of Object.entries(parsedSummaries)) {
|
|
191
|
+
if (parsed.summary.tier === 'metrics' && out[id]) {
|
|
192
|
+
out[id] = { ...out[id], metrics: parsed.summary.metrics };
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return out;
|
|
196
|
+
}
|
|
183
197
|
function dedupUnavailable(list) {
|
|
184
198
|
const seen = new Set();
|
|
185
199
|
const out = [];
|
|
@@ -17,6 +17,29 @@
|
|
|
17
17
|
* - Stable: same input → same output (no randomness, no clocks).
|
|
18
18
|
*/
|
|
19
19
|
import type { ParsedToolOutput, ParserContext, ParserFn } from './types.js';
|
|
20
|
+
interface DepGraphNode {
|
|
21
|
+
id: string;
|
|
22
|
+
label: string;
|
|
23
|
+
fan_in: number;
|
|
24
|
+
fan_out: number;
|
|
25
|
+
in_cycle: boolean;
|
|
26
|
+
}
|
|
27
|
+
interface DepGraph {
|
|
28
|
+
nodes: DepGraphNode[];
|
|
29
|
+
edges: {
|
|
30
|
+
from: string;
|
|
31
|
+
to: string;
|
|
32
|
+
}[];
|
|
33
|
+
total_modules: number;
|
|
34
|
+
truncated: boolean;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Build a bounded dependency graph from madge's adjacency map. Cycle nodes are
|
|
38
|
+
* always kept; the remaining slots go to the highest-degree modules. Edges are
|
|
39
|
+
* restricted to kept nodes.
|
|
40
|
+
*/
|
|
41
|
+
export declare function buildDependencyGraph(adjObj: Record<string, string[]>): DepGraph;
|
|
20
42
|
export declare const PARSERS: Record<string, ParserFn>;
|
|
21
43
|
/** Run the parser for a tool, defensively swallowing errors. */
|
|
22
44
|
export declare function parseToolOutput(toolId: string, stdout: string, stderr: string, ctx: ParserContext): ParsedToolOutput;
|
|
45
|
+
export {};
|
|
@@ -326,6 +326,55 @@ const dotnetListOutdatedParser = (stdout) => {
|
|
|
326
326
|
oneliner: `${count} outdated NuGet packages`,
|
|
327
327
|
};
|
|
328
328
|
};
|
|
329
|
+
const dartAnalyzeParser = (stdout) => {
|
|
330
|
+
// `dart analyze --format=machine` emits one finding per line, pipe-separated:
|
|
331
|
+
// SEVERITY|TYPE|ERROR_CODE|FILE|LINE|COLUMN|LENGTH|MESSAGE
|
|
332
|
+
// SEVERITY is INFO | WARNING | ERROR. Message pipes are backslash-escaped, so
|
|
333
|
+
// reading the first field (the severity) is unambiguous.
|
|
334
|
+
let errors = 0;
|
|
335
|
+
let warnings = 0;
|
|
336
|
+
let info = 0;
|
|
337
|
+
for (const line of stdout.split(/\r?\n/)) {
|
|
338
|
+
const pipe = line.indexOf('|');
|
|
339
|
+
if (pipe === -1) {
|
|
340
|
+
continue; // not a machine-format finding line (banner / blank)
|
|
341
|
+
}
|
|
342
|
+
const sev = line.slice(0, pipe).trim().toUpperCase();
|
|
343
|
+
if (sev === 'ERROR') {
|
|
344
|
+
errors++;
|
|
345
|
+
}
|
|
346
|
+
else if (sev === 'WARNING') {
|
|
347
|
+
warnings++;
|
|
348
|
+
}
|
|
349
|
+
else if (sev === 'INFO') {
|
|
350
|
+
info++;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
return {
|
|
354
|
+
tool_id: 'dart-analyze',
|
|
355
|
+
summary: { tier: 'counts', counts: { errors, warnings, info } },
|
|
356
|
+
oneliner: `${errors} errors, ${warnings} warnings`,
|
|
357
|
+
};
|
|
358
|
+
};
|
|
359
|
+
const dartPubOutdatedParser = (stdout) => {
|
|
360
|
+
// `dart pub outdated --json` →
|
|
361
|
+
// { packages: [{ package, current: {version} | null, latest: {version} | null }] }
|
|
362
|
+
const data = safeJson(stdout);
|
|
363
|
+
const pkgs = data?.packages ?? [];
|
|
364
|
+
const count = pkgs.filter((p) => {
|
|
365
|
+
const cur = p.current?.version;
|
|
366
|
+
const latest = p.latest?.version;
|
|
367
|
+
return Boolean(cur) && Boolean(latest) && cur !== latest;
|
|
368
|
+
}).length;
|
|
369
|
+
return {
|
|
370
|
+
tool_id: 'dart-pub-outdated',
|
|
371
|
+
summary: {
|
|
372
|
+
tier: 'counts',
|
|
373
|
+
counts: { errors: 0, warnings: count, info: 0 },
|
|
374
|
+
},
|
|
375
|
+
oneliner: `${count} outdated packages`,
|
|
376
|
+
};
|
|
377
|
+
};
|
|
329
378
|
// ---------------------------------------------------------------------------
|
|
330
379
|
// Tier 2 — counts + top-N findings (severity + file:line preserved)
|
|
331
380
|
// ---------------------------------------------------------------------------
|
|
@@ -891,6 +940,164 @@ const gocycloParser = (stdout, _stderr, ctx) => {
|
|
|
891
940
|
};
|
|
892
941
|
};
|
|
893
942
|
// ---------------------------------------------------------------------------
|
|
943
|
+
// Dependency graph (madge --json) — a bounded module graph for visualization
|
|
944
|
+
// ---------------------------------------------------------------------------
|
|
945
|
+
/** Max nodes kept in the persisted graph (cycle nodes prioritised). */
|
|
946
|
+
const MAX_GRAPH_NODES = 80;
|
|
947
|
+
/** Last two path segments — enough to disambiguate without the full path. */
|
|
948
|
+
function shortLabel(path) {
|
|
949
|
+
const parts = path.split('/');
|
|
950
|
+
return parts.slice(-2).join('/');
|
|
951
|
+
}
|
|
952
|
+
/**
|
|
953
|
+
* Nodes that participate in an import cycle, via iterative Tarjan SCC (any SCC
|
|
954
|
+
* of size > 1, plus self-loops). Iterative to stay safe on large repos.
|
|
955
|
+
*/
|
|
956
|
+
function findCycleNodes(adj) {
|
|
957
|
+
const index = new Map();
|
|
958
|
+
const low = new Map();
|
|
959
|
+
const onStack = new Set();
|
|
960
|
+
const stack = [];
|
|
961
|
+
const cycleNodes = new Set();
|
|
962
|
+
let idx = 0;
|
|
963
|
+
for (const start of adj.keys()) {
|
|
964
|
+
if (index.has(start)) {
|
|
965
|
+
continue;
|
|
966
|
+
}
|
|
967
|
+
const work = [{ node: start, i: 0 }];
|
|
968
|
+
while (work.length > 0) {
|
|
969
|
+
const frame = work[work.length - 1];
|
|
970
|
+
const node = frame.node;
|
|
971
|
+
if (frame.i === 0) {
|
|
972
|
+
index.set(node, idx);
|
|
973
|
+
low.set(node, idx);
|
|
974
|
+
idx++;
|
|
975
|
+
stack.push(node);
|
|
976
|
+
onStack.add(node);
|
|
977
|
+
}
|
|
978
|
+
const succs = adj.get(node) ?? [];
|
|
979
|
+
if (frame.i < succs.length) {
|
|
980
|
+
const w = succs[frame.i];
|
|
981
|
+
frame.i++;
|
|
982
|
+
if (!index.has(w)) {
|
|
983
|
+
work.push({ node: w, i: 0 });
|
|
984
|
+
}
|
|
985
|
+
else if (onStack.has(w)) {
|
|
986
|
+
low.set(node, Math.min(low.get(node) ?? 0, index.get(w) ?? 0));
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
else {
|
|
990
|
+
if (low.get(node) === index.get(node)) {
|
|
991
|
+
const scc = [];
|
|
992
|
+
let w = '';
|
|
993
|
+
do {
|
|
994
|
+
w = stack.pop();
|
|
995
|
+
onStack.delete(w);
|
|
996
|
+
scc.push(w);
|
|
997
|
+
} while (w !== node);
|
|
998
|
+
if (scc.length > 1) {
|
|
999
|
+
for (const n of scc) {
|
|
1000
|
+
cycleNodes.add(n);
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
else if ((adj.get(node) ?? []).includes(node)) {
|
|
1004
|
+
cycleNodes.add(node);
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
work.pop();
|
|
1008
|
+
const parent = work[work.length - 1]?.node;
|
|
1009
|
+
if (parent !== undefined) {
|
|
1010
|
+
low.set(parent, Math.min(low.get(parent) ?? 0, low.get(node) ?? 0));
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
return cycleNodes;
|
|
1016
|
+
}
|
|
1017
|
+
/**
|
|
1018
|
+
* Build a bounded dependency graph from madge's adjacency map. Cycle nodes are
|
|
1019
|
+
* always kept; the remaining slots go to the highest-degree modules. Edges are
|
|
1020
|
+
* restricted to kept nodes.
|
|
1021
|
+
*/
|
|
1022
|
+
export function buildDependencyGraph(adjObj) {
|
|
1023
|
+
// Normalise: every referenced module is a node (targets may have no key).
|
|
1024
|
+
const adj = new Map();
|
|
1025
|
+
for (const [k, deps] of Object.entries(adjObj)) {
|
|
1026
|
+
const list = Array.isArray(deps) ? deps : [];
|
|
1027
|
+
if (!adj.has(k)) {
|
|
1028
|
+
adj.set(k, []);
|
|
1029
|
+
}
|
|
1030
|
+
adj.get(k).push(...list);
|
|
1031
|
+
for (const d of list) {
|
|
1032
|
+
if (!adj.has(d)) {
|
|
1033
|
+
adj.set(d, []);
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
1037
|
+
const fanOut = new Map();
|
|
1038
|
+
const fanIn = new Map();
|
|
1039
|
+
for (const [k, deps] of adj) {
|
|
1040
|
+
fanOut.set(k, deps.length);
|
|
1041
|
+
for (const d of deps) {
|
|
1042
|
+
fanIn.set(d, (fanIn.get(d) ?? 0) + 1);
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
const cycleNodes = findCycleNodes(adj);
|
|
1046
|
+
const allIds = [...adj.keys()];
|
|
1047
|
+
const degree = (id) => (fanIn.get(id) ?? 0) + (fanOut.get(id) ?? 0);
|
|
1048
|
+
// Prioritise cycle nodes, then highest-degree modules, capped at the limit
|
|
1049
|
+
// so even a large strongly-connected cluster can't blow past the bound.
|
|
1050
|
+
const ranked = [...allIds].sort((a, b) => {
|
|
1051
|
+
const ca = cycleNodes.has(a) ? 1 : 0;
|
|
1052
|
+
const cb = cycleNodes.has(b) ? 1 : 0;
|
|
1053
|
+
return ca !== cb ? cb - ca : degree(b) - degree(a);
|
|
1054
|
+
});
|
|
1055
|
+
const kept = new Set(ranked.slice(0, MAX_GRAPH_NODES));
|
|
1056
|
+
const nodes = [...kept].map((id) => ({
|
|
1057
|
+
id,
|
|
1058
|
+
label: shortLabel(id),
|
|
1059
|
+
fan_in: fanIn.get(id) ?? 0,
|
|
1060
|
+
fan_out: fanOut.get(id) ?? 0,
|
|
1061
|
+
in_cycle: cycleNodes.has(id),
|
|
1062
|
+
}));
|
|
1063
|
+
const edges = [];
|
|
1064
|
+
const seen = new Set();
|
|
1065
|
+
for (const [from, deps] of adj) {
|
|
1066
|
+
if (!kept.has(from)) {
|
|
1067
|
+
continue;
|
|
1068
|
+
}
|
|
1069
|
+
for (const to of deps) {
|
|
1070
|
+
if (!kept.has(to) || from === to) {
|
|
1071
|
+
continue;
|
|
1072
|
+
}
|
|
1073
|
+
const key = `${from}\n${to}`;
|
|
1074
|
+
if (seen.has(key)) {
|
|
1075
|
+
continue;
|
|
1076
|
+
}
|
|
1077
|
+
seen.add(key);
|
|
1078
|
+
edges.push({ from, to });
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
return {
|
|
1082
|
+
nodes,
|
|
1083
|
+
edges,
|
|
1084
|
+
total_modules: allIds.length,
|
|
1085
|
+
truncated: allIds.length > kept.size,
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
const madgeGraphParser = (stdout) => {
|
|
1089
|
+
const adjObj = safeJson(stdout);
|
|
1090
|
+
const graph = adjObj
|
|
1091
|
+
? buildDependencyGraph(adjObj)
|
|
1092
|
+
: { nodes: [], edges: [], total_modules: 0, truncated: false };
|
|
1093
|
+
const cycleCount = graph.nodes.filter((n) => n.in_cycle).length;
|
|
1094
|
+
return {
|
|
1095
|
+
tool_id: 'madge-graph',
|
|
1096
|
+
summary: { tier: 'metrics', metrics: { graph } },
|
|
1097
|
+
oneliner: `${graph.total_modules} modules, ${cycleCount} in cycles`,
|
|
1098
|
+
};
|
|
1099
|
+
};
|
|
1100
|
+
// ---------------------------------------------------------------------------
|
|
894
1101
|
// Tier 3 — structured metrics
|
|
895
1102
|
// ---------------------------------------------------------------------------
|
|
896
1103
|
const sccParser = (stdout) => {
|
|
@@ -1051,6 +1258,8 @@ export const PARSERS = {
|
|
|
1051
1258
|
'go-mod-outdated': goModOutdatedParser,
|
|
1052
1259
|
'cargo-outdated': cargoOutdatedParser,
|
|
1053
1260
|
'dotnet-list-outdated': dotnetListOutdatedParser,
|
|
1261
|
+
'dart-analyze': dartAnalyzeParser,
|
|
1262
|
+
'dart-pub-outdated': dartPubOutdatedParser,
|
|
1054
1263
|
// Tier 2
|
|
1055
1264
|
semgrep: semgrepParser,
|
|
1056
1265
|
bandit: banditParser,
|
|
@@ -1072,6 +1281,7 @@ export const PARSERS = {
|
|
|
1072
1281
|
gocyclo: gocycloParser,
|
|
1073
1282
|
// Mixed (T3 metrics + T2 sub-findings)
|
|
1074
1283
|
jscpd: jscpdParser,
|
|
1284
|
+
'madge-graph': madgeGraphParser,
|
|
1075
1285
|
// Tier 3
|
|
1076
1286
|
scc: sccParser,
|
|
1077
1287
|
lizard: lizardParser,
|
|
@@ -352,6 +352,7 @@ For each dimension, the rubric specifies:
|
|
|
352
352
|
| JS/TS | 60 | 100 |
|
|
353
353
|
| Rust | 80 | 120 |
|
|
354
354
|
| Ruby | 30 | 60 |
|
|
355
|
+
| Dart | 60 | 100 |
|
|
355
356
|
|
|
356
357
|
These are measurement calibration, not different standards.
|
|
357
358
|
|
|
@@ -575,6 +576,15 @@ madge:
|
|
|
575
576
|
parser: madge
|
|
576
577
|
subscores: [architecture.circular_deps]
|
|
577
578
|
|
|
579
|
+
madge-graph:
|
|
580
|
+
applies_to: [js, ts] # full module graph for visualization (bounded)
|
|
581
|
+
probe: command -v madge
|
|
582
|
+
install: null # use npx
|
|
583
|
+
command: npx --yes madge --json --extensions js,jsx,ts,tsx %REPO_ROOT%
|
|
584
|
+
timeout_minutes: 5
|
|
585
|
+
parser: madge-graph
|
|
586
|
+
subscores: [architecture.circular_deps]
|
|
587
|
+
|
|
578
588
|
depcheck:
|
|
579
589
|
applies_to: [js, ts]
|
|
580
590
|
probe: command -v depcheck
|
|
@@ -847,6 +857,33 @@ dotnet-list-outdated:
|
|
|
847
857
|
subscores: [dependency_health.freshness]
|
|
848
858
|
```
|
|
849
859
|
|
|
860
|
+
### Dart / Flutter
|
|
861
|
+
|
|
862
|
+
Both tools ship with the Dart (and Flutter) SDK — no extra install. We never
|
|
863
|
+
auto-install an SDK, so on a machine without `dart` they degrade to unmeasured.
|
|
864
|
+
`dart analyze` is AST-based (same analyzer as the IDE plugins); duplication is
|
|
865
|
+
covered by the polyglot `jscpd` entry (Dart is in its `applies_to`).
|
|
866
|
+
|
|
867
|
+
```
|
|
868
|
+
dart-analyze:
|
|
869
|
+
applies_to: [dart]
|
|
870
|
+
probe: command -v dart
|
|
871
|
+
install: null # bundled with the Dart / Flutter SDK
|
|
872
|
+
command: dart analyze %REPO_ROOT% --format=machine
|
|
873
|
+
timeout_minutes: 5
|
|
874
|
+
parser: dart-analyze
|
|
875
|
+
subscores: [code_quality.lint]
|
|
876
|
+
|
|
877
|
+
dart-pub-outdated:
|
|
878
|
+
applies_to: [dart] # only when pubspec.yaml present
|
|
879
|
+
probe: command -v dart
|
|
880
|
+
install: null # bundled with the Dart / Flutter SDK
|
|
881
|
+
command: dart pub outdated --json
|
|
882
|
+
timeout_minutes: 3
|
|
883
|
+
parser: dart-pub-outdated
|
|
884
|
+
subscores: [dependency_health.freshness]
|
|
885
|
+
```
|
|
886
|
+
|
|
850
887
|
### Multi-language
|
|
851
888
|
|
|
852
889
|
```
|
|
@@ -58,7 +58,7 @@ const jscpd = {
|
|
|
58
58
|
id: 'jscpd',
|
|
59
59
|
label: 'jscpd (duplication)',
|
|
60
60
|
category: 'duplication',
|
|
61
|
-
applies_to: ['js', 'ts', 'py', 'java', 'go', 'cs', 'ruby'],
|
|
61
|
+
applies_to: ['js', 'ts', 'py', 'java', 'go', 'cs', 'ruby', 'dart'],
|
|
62
62
|
probe: 'command -v jscpd',
|
|
63
63
|
install: null, // always use npx
|
|
64
64
|
install_prereq: 'npx',
|
|
@@ -82,6 +82,20 @@ const madge = {
|
|
|
82
82
|
subscores: ['architecture.circular_deps'],
|
|
83
83
|
tolerate_nonzero_exit: true,
|
|
84
84
|
};
|
|
85
|
+
const madgeGraph = {
|
|
86
|
+
id: 'madge-graph',
|
|
87
|
+
label: 'madge (dependency graph)',
|
|
88
|
+
category: 'cycles',
|
|
89
|
+
applies_to: ['js', 'ts'],
|
|
90
|
+
probe: 'command -v madge',
|
|
91
|
+
install: null,
|
|
92
|
+
install_prereq: 'npx',
|
|
93
|
+
command: 'npx --yes madge --json --extensions js,jsx,ts,tsx %REPO_ROOT%',
|
|
94
|
+
timeout_minutes: 5,
|
|
95
|
+
parser: 'madge-graph',
|
|
96
|
+
subscores: ['architecture.circular_deps'],
|
|
97
|
+
tolerate_nonzero_exit: true,
|
|
98
|
+
};
|
|
85
99
|
const depcheck = {
|
|
86
100
|
id: 'depcheck',
|
|
87
101
|
label: 'depcheck (unused deps)',
|
|
@@ -470,6 +484,45 @@ const dotnetListOutdated = {
|
|
|
470
484
|
tolerate_nonzero_exit: true,
|
|
471
485
|
};
|
|
472
486
|
// ---------------------------------------------------------------------------
|
|
487
|
+
// Dart / Flutter
|
|
488
|
+
//
|
|
489
|
+
// Both tools ship with the Dart (and Flutter) SDK itself — no extra install,
|
|
490
|
+
// no pipx/go/cargo. We never auto-install an SDK (`install: null`), so on a
|
|
491
|
+
// machine without Dart they degrade to unmeasured rather than triggering a
|
|
492
|
+
// multi-hundred-MB toolchain download. `dart analyze` is AST-based (the same
|
|
493
|
+
// analyzer the IDE plugins use); duplication is covered by the polyglot jscpd
|
|
494
|
+
// entry above (Dart is in its `applies_to`).
|
|
495
|
+
// ---------------------------------------------------------------------------
|
|
496
|
+
const dartAnalyze = {
|
|
497
|
+
id: 'dart-analyze',
|
|
498
|
+
label: 'dart analyze',
|
|
499
|
+
category: 'lint',
|
|
500
|
+
applies_to: ['dart'],
|
|
501
|
+
probe: 'command -v dart',
|
|
502
|
+
install: null, // bundled with the Dart / Flutter SDK
|
|
503
|
+
install_prereq: null,
|
|
504
|
+
command: 'dart analyze %REPO_ROOT% --format=machine',
|
|
505
|
+
timeout_minutes: 5,
|
|
506
|
+
parser: 'dart-analyze',
|
|
507
|
+
subscores: ['code_quality.lint'],
|
|
508
|
+
tolerate_nonzero_exit: true, // exits 1/2/3 when analysis issues are found
|
|
509
|
+
};
|
|
510
|
+
const dartPubOutdated = {
|
|
511
|
+
id: 'dart-pub-outdated',
|
|
512
|
+
label: 'dart pub outdated',
|
|
513
|
+
category: 'dep-outdated',
|
|
514
|
+
applies_to: ['dart'],
|
|
515
|
+
probe: 'command -v dart',
|
|
516
|
+
install: null, // bundled with the Dart / Flutter SDK
|
|
517
|
+
install_prereq: null,
|
|
518
|
+
command: 'dart pub outdated --json',
|
|
519
|
+
timeout_minutes: 3,
|
|
520
|
+
parser: 'dart-pub-outdated',
|
|
521
|
+
subscores: ['dependency_health.freshness'],
|
|
522
|
+
tolerate_nonzero_exit: true,
|
|
523
|
+
requires: { file_present: ['pubspec.yaml'] },
|
|
524
|
+
};
|
|
525
|
+
// ---------------------------------------------------------------------------
|
|
473
526
|
// Multi-language / polyglot
|
|
474
527
|
// ---------------------------------------------------------------------------
|
|
475
528
|
const semgrep = {
|
|
@@ -549,6 +602,7 @@ export const TOOL_CATALOG = [
|
|
|
549
602
|
tscTypecheck,
|
|
550
603
|
jscpd,
|
|
551
604
|
madge,
|
|
605
|
+
madgeGraph,
|
|
552
606
|
depcheck,
|
|
553
607
|
npmAudit,
|
|
554
608
|
pnpmAudit,
|
|
@@ -580,6 +634,9 @@ export const TOOL_CATALOG = [
|
|
|
580
634
|
// C# / .NET
|
|
581
635
|
dotnetListVulnerable,
|
|
582
636
|
dotnetListOutdated,
|
|
637
|
+
// Dart / Flutter
|
|
638
|
+
dartAnalyze,
|
|
639
|
+
dartPubOutdated,
|
|
583
640
|
// Polyglot
|
|
584
641
|
semgrep,
|
|
585
642
|
gitleaks,
|
|
@@ -29,7 +29,7 @@ export interface DetectedContext {
|
|
|
29
29
|
total_loc_approx: number;
|
|
30
30
|
}
|
|
31
31
|
/** Language tag used to gate tool applicability. `'all'` = polyglot tools. */
|
|
32
|
-
export type LanguageTag = 'js' | 'ts' | 'py' | 'go' | 'rust' | 'java' | 'kotlin' | 'ruby' | 'c' | 'cpp' | 'cs' | 'swift' | 'all';
|
|
32
|
+
export type LanguageTag = 'js' | 'ts' | 'py' | 'go' | 'rust' | 'java' | 'kotlin' | 'ruby' | 'c' | 'cpp' | 'cs' | 'swift' | 'dart' | 'all';
|
|
33
33
|
export type ToolCategory = 'lint' | 'sast' | 'duplication' | 'complexity' | 'dead-code' | 'cycles' | 'coverage' | 'dep-vuln' | 'dep-outdated' | 'dep-unused' | 'dep-license' | 'secrets' | 'loc-stats' | 'typecheck';
|
|
34
34
|
export type InstallerPrereq = 'pipx' | 'go' | 'cargo' | 'npx' | 'gem' | null;
|
|
35
35
|
export interface ToolCatalogEntry {
|
|
@@ -102,6 +102,13 @@ export interface ToolRunOutput {
|
|
|
102
102
|
stderr_tail?: string;
|
|
103
103
|
/** Path on disk where the full tool output is saved for audit. */
|
|
104
104
|
raw_output_path?: string;
|
|
105
|
+
/**
|
|
106
|
+
* Structured metrics from a tier-3 (`metrics`) parser, persisted so trend
|
|
107
|
+
* charts can read numeric values (e.g. jscpd `duplication_pct`, lizard
|
|
108
|
+
* `avg_cyclomatic_complexity`) without re-parsing the oneliner. Absent for
|
|
109
|
+
* count/finding tools.
|
|
110
|
+
*/
|
|
111
|
+
metrics?: Record<string, unknown>;
|
|
105
112
|
}
|
|
106
113
|
/**
|
|
107
114
|
* Tier 1 — counts only. Used for style/quality linters where individual
|