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.
Files changed (52) hide show
  1. package/dist/commands/api-docs/index.d.ts +18 -0
  2. package/dist/commands/api-docs/index.js +41 -0
  3. package/dist/commands/find-architecture/index.d.ts +3 -1
  4. package/dist/commands/find-architecture/index.js +14 -5
  5. package/dist/commands/find-bugs/index.d.ts +3 -1
  6. package/dist/commands/find-bugs/index.js +14 -5
  7. package/dist/commands/find-smells/index.d.ts +3 -1
  8. package/dist/commands/find-smells/index.js +14 -5
  9. package/dist/commands/quality-benchmark/index.d.ts +5 -0
  10. package/dist/commands/quality-benchmark/index.js +28 -0
  11. package/dist/index.js +38 -6
  12. package/dist/phases/api-docs/index.d.ts +47 -0
  13. package/dist/phases/api-docs/index.js +254 -0
  14. package/dist/phases/api-docs/mcp-server.d.ts +25 -0
  15. package/dist/phases/api-docs/mcp-server.js +82 -0
  16. package/dist/phases/api-docs/prompts.d.ts +16 -0
  17. package/dist/phases/api-docs/prompts.js +65 -0
  18. package/dist/phases/api-docs/types.d.ts +22 -0
  19. package/dist/phases/api-docs/types.js +10 -0
  20. package/dist/phases/find-architecture/index.d.ts +4 -1
  21. package/dist/phases/find-architecture/index.js +46 -26
  22. package/dist/phases/find-architecture/prompts.d.ts +2 -1
  23. package/dist/phases/find-architecture/prompts.js +3 -2
  24. package/dist/phases/find-bugs/index.d.ts +4 -1
  25. package/dist/phases/find-bugs/index.js +32 -19
  26. package/dist/phases/find-shared/baseline.d.ts +45 -0
  27. package/dist/phases/find-shared/baseline.js +56 -0
  28. package/dist/phases/find-shared/custom-rules.d.ts +39 -0
  29. package/dist/phases/find-shared/custom-rules.js +75 -0
  30. package/dist/phases/find-shared/detect-context.d.ts +40 -0
  31. package/dist/phases/find-shared/detect-context.js +247 -0
  32. package/dist/phases/find-shared/mcp.d.ts +24 -3
  33. package/dist/phases/find-shared/mcp.js +41 -4
  34. package/dist/phases/find-shared/rule-config.d.ts +37 -0
  35. package/dist/phases/find-shared/rule-config.js +67 -0
  36. package/dist/phases/find-shared/rule-packs.d.ts +65 -0
  37. package/dist/phases/find-shared/rule-packs.js +124 -0
  38. package/dist/phases/find-shared/scoped-read.d.ts +12 -0
  39. package/dist/phases/find-shared/scoped-read.js +33 -0
  40. package/dist/phases/find-smells/index.d.ts +4 -1
  41. package/dist/phases/find-smells/index.js +43 -23
  42. package/dist/phases/find-smells/prompts.d.ts +2 -1
  43. package/dist/phases/find-smells/prompts.js +4 -3
  44. package/dist/phases/quality-benchmark/gate.d.ts +50 -0
  45. package/dist/phases/quality-benchmark/gate.js +91 -0
  46. package/dist/phases/quality-benchmark/index.js +15 -1
  47. package/dist/phases/quality-benchmark/parsers.d.ts +23 -0
  48. package/dist/phases/quality-benchmark/parsers.js +210 -0
  49. package/dist/phases/quality-benchmark/rubric.md +37 -0
  50. package/dist/phases/quality-benchmark/tool-catalog.js +58 -1
  51. package/dist/phases/quality-benchmark/types.d.ts +8 -1
  52. 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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "edsger",
3
- "version": "0.75.1",
3
+ "version": "0.77.0",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "edsger": "dist/index.js"