@praneeth_54/agentdoctor 1.1.1 → 2.0.1
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/CHANGELOG.md +164 -7
- package/README.md +263 -513
- package/dist/agents/aider/adapter.d.ts +1 -0
- package/dist/agents/aider/adapter.js +1 -0
- package/dist/agents/aider/detector.d.ts +14 -0
- package/dist/agents/aider/detector.js +120 -0
- package/dist/agents/copilot/adapter.d.ts +1 -0
- package/dist/agents/copilot/adapter.js +1 -0
- package/dist/agents/copilot/detector.d.ts +13 -0
- package/dist/agents/copilot/detector.js +113 -0
- package/dist/agents/gemini/adapter.d.ts +1 -0
- package/dist/agents/gemini/adapter.js +1 -0
- package/dist/agents/gemini/detector.d.ts +13 -0
- package/dist/agents/gemini/detector.js +151 -0
- package/dist/agents/registry.js +13 -1
- package/dist/agents/types.d.ts +1 -1
- package/dist/agents/windsurf/adapter.d.ts +1 -0
- package/dist/agents/windsurf/adapter.js +1 -0
- package/dist/agents/windsurf/detector.d.ts +14 -0
- package/dist/agents/windsurf/detector.js +119 -0
- package/dist/architecture/c4.d.ts +22 -0
- package/dist/architecture/c4.js +62 -0
- package/dist/architecture/contract.d.ts +64 -0
- package/dist/architecture/contract.js +373 -0
- package/dist/assurance/change.d.ts +211 -0
- package/dist/assurance/change.js +624 -0
- package/dist/assurance/proof.d.ts +136 -0
- package/dist/assurance/proof.js +338 -0
- package/dist/auth/index.d.ts +81 -0
- package/dist/auth/index.js +130 -0
- package/dist/auth/rbac.d.ts +9 -0
- package/dist/auth/rbac.js +26 -0
- package/dist/cli/commands/architecture.d.ts +6 -0
- package/dist/cli/commands/architecture.js +109 -0
- package/dist/cli/commands/assurance.d.ts +67 -0
- package/dist/cli/commands/assurance.js +210 -0
- package/dist/cli/commands/brain.d.ts +12 -0
- package/dist/cli/commands/brain.js +162 -0
- package/dist/cli/commands/complete.d.ts +23 -0
- package/dist/cli/commands/complete.js +232 -0
- package/dist/cli/commands/explain.js +1 -1
- package/dist/cli/commands/fix.js +22 -2
- package/dist/cli/commands/mcp.d.ts +8 -0
- package/dist/cli/commands/mcp.js +14 -0
- package/dist/cli/commands/platform.d.ts +17 -0
- package/dist/cli/commands/platform.js +195 -0
- package/dist/cli/commands/policy-graph-run.d.ts +40 -0
- package/dist/cli/commands/policy-graph-run.js +200 -0
- package/dist/cli/commands/v2.d.ts +55 -0
- package/dist/cli/commands/v2.js +278 -0
- package/dist/cli/commands/workspace.d.ts +9 -0
- package/dist/cli/commands/workspace.js +82 -0
- package/dist/cli/program.js +1328 -3
- package/dist/constants.d.ts +3 -2
- package/dist/constants.js +5 -1
- package/dist/contracts/adapters.d.ts +7 -0
- package/dist/contracts/adapters.js +60 -0
- package/dist/contracts/index.d.ts +162 -0
- package/dist/contracts/index.js +18 -0
- package/dist/core/baseline/store.d.ts +48 -0
- package/dist/core/baseline/store.js +163 -0
- package/dist/core/brain-cli/service.d.ts +26 -0
- package/dist/core/brain-cli/service.js +175 -0
- package/dist/core/brain-product/init.d.ts +45 -0
- package/dist/core/brain-product/init.js +235 -0
- package/dist/core/changes/analyze.d.ts +41 -0
- package/dist/core/changes/analyze.js +223 -0
- package/dist/core/context-health/analyze.d.ts +15 -0
- package/dist/core/context-health/analyze.js +234 -0
- package/dist/core/fix/apply.d.ts +21 -4
- package/dist/core/fix/apply.js +118 -18
- package/dist/core/fix/backup.d.ts +30 -0
- package/dist/core/fix/backup.js +193 -0
- package/dist/core/fix/plan.d.ts +6 -3
- package/dist/core/fix/plan.js +113 -9
- package/dist/core/fix/render.d.ts +2 -0
- package/dist/core/fix/render.js +36 -21
- package/dist/core/fix/run.js +5 -1
- package/dist/core/fix/safe-target.d.ts +14 -0
- package/dist/core/fix/safe-target.js +81 -0
- package/dist/core/fix/types.d.ts +12 -1
- package/dist/core/fix/types.js +2 -0
- package/dist/core/fix/writers/claude-settings.js +3 -1
- package/dist/core/fix/writers/codex-config.d.ts +3 -0
- package/dist/core/fix/writers/codex-config.js +24 -3
- package/dist/core/fix/writers/cursorignore.js +3 -1
- package/dist/core/fix/writers/simple-ignore.d.ts +16 -0
- package/dist/core/fix/writers/simple-ignore.js +68 -0
- package/dist/core/monorepo/detect.d.ts +13 -0
- package/dist/core/monorepo/detect.js +121 -0
- package/dist/core/rules/build-context.js +22 -3
- package/dist/core/rules/context/generated-directory.js +11 -1
- package/dist/core/rules/context/large-instruction-file.js +6 -0
- package/dist/core/rules/context/large-log-file.js +16 -1
- package/dist/core/rules/ignore.d.ts +11 -1
- package/dist/core/rules/ignore.js +22 -1
- package/dist/core/rules/instructions/empty-instructions.js +7 -0
- package/dist/core/rules/security/env-file-exposure.js +23 -2
- package/dist/core/rules/security/private-key-file.js +1 -1
- package/dist/core/scanner/scan.js +1 -1
- package/dist/core/schemas/validate.d.ts +6 -0
- package/dist/core/schemas/validate.js +62 -0
- package/dist/core/scoring/compute-scores.d.ts +1 -1
- package/dist/core/scoring/compute-scores.js +5 -1
- package/dist/core/scoring/placeholder.d.ts +1 -1
- package/dist/core/scoring/placeholder.js +5 -1
- package/dist/core/secrets/scan.d.ts +29 -0
- package/dist/core/secrets/scan.js +172 -0
- package/dist/coverage/cobertura.d.ts +6 -0
- package/dist/coverage/cobertura.js +52 -0
- package/dist/coverage/istanbul.d.ts +7 -0
- package/dist/coverage/istanbul.js +134 -0
- package/dist/coverage/lcov.d.ts +7 -0
- package/dist/coverage/lcov.js +59 -0
- package/dist/coverage/load.d.ts +9 -0
- package/dist/coverage/load.js +39 -0
- package/dist/coverage/types.d.ts +23 -0
- package/dist/coverage/types.js +5 -0
- package/dist/dashboard/server.d.ts +24 -0
- package/dist/dashboard/server.js +402 -0
- package/dist/enforcement/runner.d.ts +67 -0
- package/dist/enforcement/runner.js +460 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.js +25 -0
- package/dist/integrations/github/pr-analyze.d.ts +28 -0
- package/dist/integrations/github/pr-analyze.js +98 -0
- package/dist/integrations/local-ai/provider.d.ts +45 -0
- package/dist/integrations/local-ai/provider.js +104 -0
- package/dist/intelligence/git/analyze.d.ts +34 -0
- package/dist/intelligence/git/analyze.js +136 -0
- package/dist/intelligence/graph/build.d.ts +15 -0
- package/dist/intelligence/graph/build.js +259 -0
- package/dist/intelligence/graph/incremental.d.ts +68 -0
- package/dist/intelligence/graph/incremental.js +234 -0
- package/dist/intelligence/resolve/imports.d.ts +36 -0
- package/dist/intelligence/resolve/imports.js +245 -0
- package/dist/knowledge/store.d.ts +25 -0
- package/dist/knowledge/store.js +91 -0
- package/dist/languages/go.d.ts +9 -0
- package/dist/languages/go.js +45 -0
- package/dist/languages/index.d.ts +11 -0
- package/dist/languages/index.js +76 -0
- package/dist/languages/php.d.ts +7 -0
- package/dist/languages/php.js +193 -0
- package/dist/languages/python.d.ts +7 -0
- package/dist/languages/python.js +150 -0
- package/dist/languages/types.d.ts +42 -0
- package/dist/languages/types.js +26 -0
- package/dist/languages/typescript.d.ts +3 -0
- package/dist/languages/typescript.js +95 -0
- package/dist/mcp/agentdoctor/server.d.ts +16 -0
- package/dist/mcp/agentdoctor/server.js +67 -0
- package/dist/mcp/brain/session.js +7 -7
- package/dist/mcp/intelligence/handlers.d.ts +16 -0
- package/dist/mcp/intelligence/handlers.js +364 -0
- package/dist/mcp/intelligence/path-safety.d.ts +9 -0
- package/dist/mcp/intelligence/path-safety.js +63 -0
- package/dist/mcp/intelligence/registry.d.ts +8 -0
- package/dist/mcp/intelligence/registry.js +228 -0
- package/dist/ops/health.d.ts +18 -0
- package/dist/ops/health.js +51 -0
- package/dist/platform/ai-quality/analyze.d.ts +9 -0
- package/dist/platform/ai-quality/analyze.js +65 -0
- package/dist/platform/architecture/drift.d.ts +17 -0
- package/dist/platform/architecture/drift.js +74 -0
- package/dist/platform/auth/local.d.ts +15 -0
- package/dist/platform/auth/local.js +58 -0
- package/dist/platform/context-security/analyze.d.ts +6 -0
- package/dist/platform/context-security/analyze.js +97 -0
- package/dist/platform/firewall/evaluate.d.ts +57 -0
- package/dist/platform/firewall/evaluate.js +359 -0
- package/dist/platform/graph/build.d.ts +6 -0
- package/dist/platform/graph/build.js +189 -0
- package/dist/platform/health/analyze.d.ts +7 -0
- package/dist/platform/health/analyze.js +192 -0
- package/dist/platform/index.d.ts +27 -0
- package/dist/platform/index.js +89 -0
- package/dist/platform/knowledge/analyze.d.ts +20 -0
- package/dist/platform/knowledge/analyze.js +96 -0
- package/dist/platform/provenance/build.d.ts +44 -0
- package/dist/platform/provenance/build.js +65 -0
- package/dist/platform/readiness/scorecard.d.ts +5 -0
- package/dist/platform/readiness/scorecard.js +98 -0
- package/dist/platform/refactor/impact.d.ts +20 -0
- package/dist/platform/refactor/impact.js +60 -0
- package/dist/platform/reports/export.d.ts +12 -0
- package/dist/platform/reports/export.js +92 -0
- package/dist/platform/security/redact.d.ts +15 -0
- package/dist/platform/security/redact.js +87 -0
- package/dist/platform/sessions/store.d.ts +32 -0
- package/dist/platform/sessions/store.js +97 -0
- package/dist/platform/store.d.ts +6 -0
- package/dist/platform/store.js +57 -0
- package/dist/platform/test-impact/analyze.d.ts +50 -0
- package/dist/platform/test-impact/analyze.js +350 -0
- package/dist/platform/time-machine/compare.d.ts +17 -0
- package/dist/platform/time-machine/compare.js +62 -0
- package/dist/platform/tokens/plan.d.ts +28 -0
- package/dist/platform/tokens/plan.js +108 -0
- package/dist/platform/types.d.ts +120 -0
- package/dist/platform/types.js +5 -0
- package/dist/plugins/runtime.d.ts +36 -0
- package/dist/plugins/runtime.js +101 -0
- package/dist/plugins/sdk.d.ts +29 -0
- package/dist/plugins/sdk.js +114 -0
- package/dist/policy/compose.d.ts +34 -0
- package/dist/policy/compose.js +118 -0
- package/dist/policy/packs.d.ts +9 -0
- package/dist/policy/packs.js +91 -0
- package/dist/reporters/terminal/report.js +38 -5
- package/dist/security/paths.d.ts +21 -0
- package/dist/security/paths.js +105 -0
- package/dist/storage/postgres.d.ts +26 -0
- package/dist/storage/postgres.js +90 -0
- package/dist/storage/provider.d.ts +30 -0
- package/dist/storage/provider.js +109 -0
- package/dist/storage/sqlite.d.ts +33 -0
- package/dist/storage/sqlite.js +77 -0
- package/dist/team/auth.d.ts +35 -0
- package/dist/team/auth.js +70 -0
- package/dist/types/index.d.ts +5 -1
- package/dist/workspace/index.d.ts +69 -0
- package/dist/workspace/index.js +220 -0
- package/package.json +15 -5
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { readTextFile } from "../../utils/fs.js";
|
|
4
|
+
import { resolveRepoRoot, toPosixRelative } from "../../utils/path.js";
|
|
5
|
+
const SECRET_PATTERNS = [
|
|
6
|
+
{
|
|
7
|
+
id: "aws-access-key",
|
|
8
|
+
re: /\bAKIA[0-9A-Z]{16}\b/g,
|
|
9
|
+
severity: "critical",
|
|
10
|
+
confidence: "high",
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
id: "generic-api-key",
|
|
14
|
+
re: /\b(?:api[_-]?key|secret[_-]?key|access[_-]?token)\s*[:=]\s*['"]?[A-Za-z0-9_-]{16,}['"]?/gi,
|
|
15
|
+
severity: "warning",
|
|
16
|
+
confidence: "medium",
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
id: "private-key-header",
|
|
20
|
+
re: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/g,
|
|
21
|
+
severity: "critical",
|
|
22
|
+
confidence: "high",
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: "github-pat",
|
|
26
|
+
re: /\bghp_[A-Za-z0-9]{36}\b/g,
|
|
27
|
+
severity: "critical",
|
|
28
|
+
confidence: "high",
|
|
29
|
+
},
|
|
30
|
+
];
|
|
31
|
+
const SKIP_DIR = new Set([
|
|
32
|
+
"node_modules",
|
|
33
|
+
".git",
|
|
34
|
+
"dist",
|
|
35
|
+
"coverage",
|
|
36
|
+
".agentdoctor",
|
|
37
|
+
"vendor",
|
|
38
|
+
".next",
|
|
39
|
+
]);
|
|
40
|
+
const TEXT_EXT = new Set([
|
|
41
|
+
".ts",
|
|
42
|
+
".tsx",
|
|
43
|
+
".js",
|
|
44
|
+
".jsx",
|
|
45
|
+
".mjs",
|
|
46
|
+
".cjs",
|
|
47
|
+
".json",
|
|
48
|
+
".yml",
|
|
49
|
+
".yaml",
|
|
50
|
+
".md",
|
|
51
|
+
".env",
|
|
52
|
+
".txt",
|
|
53
|
+
".toml",
|
|
54
|
+
".ini",
|
|
55
|
+
".cfg",
|
|
56
|
+
".py",
|
|
57
|
+
".go",
|
|
58
|
+
".rs",
|
|
59
|
+
".java",
|
|
60
|
+
".kt",
|
|
61
|
+
".php",
|
|
62
|
+
".rb",
|
|
63
|
+
".sh",
|
|
64
|
+
]);
|
|
65
|
+
function redactMatch(line, match) {
|
|
66
|
+
const idx = line.indexOf(match);
|
|
67
|
+
if (idx < 0) {
|
|
68
|
+
return "[REDACTED]";
|
|
69
|
+
}
|
|
70
|
+
const before = line.slice(0, idx).slice(-40);
|
|
71
|
+
const after = line.slice(idx + match.length).slice(0, 20);
|
|
72
|
+
return `${before}[REDACTED]${after}`.trim();
|
|
73
|
+
}
|
|
74
|
+
async function walkFiles(root, dir, out, limit) {
|
|
75
|
+
if (out.length >= limit)
|
|
76
|
+
return;
|
|
77
|
+
let entries;
|
|
78
|
+
try {
|
|
79
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
for (const entry of entries) {
|
|
85
|
+
if (out.length >= limit)
|
|
86
|
+
return;
|
|
87
|
+
if (entry.name.startsWith(".") && entry.name !== ".env" && entry.name !== ".env.local") {
|
|
88
|
+
if (SKIP_DIR.has(entry.name))
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (SKIP_DIR.has(entry.name))
|
|
92
|
+
continue;
|
|
93
|
+
const absolute = path.join(dir, entry.name);
|
|
94
|
+
if (entry.isSymbolicLink())
|
|
95
|
+
continue;
|
|
96
|
+
if (entry.isDirectory()) {
|
|
97
|
+
await walkFiles(root, absolute, out, limit);
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (!entry.isFile())
|
|
101
|
+
continue;
|
|
102
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
103
|
+
if (!TEXT_EXT.has(ext) && !entry.name.startsWith(".env"))
|
|
104
|
+
continue;
|
|
105
|
+
out.push(absolute);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Opt-in content secret scan. Findings never include raw secret values.
|
|
110
|
+
*/
|
|
111
|
+
export async function scanSecrets(options) {
|
|
112
|
+
const root = resolveRepoRoot(options.root);
|
|
113
|
+
if (!options.enabled) {
|
|
114
|
+
return {
|
|
115
|
+
root,
|
|
116
|
+
enabled: false,
|
|
117
|
+
filesScanned: 0,
|
|
118
|
+
findings: [],
|
|
119
|
+
limitations: ["Content secret scanning disabled (pass --secrets to enable)"],
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
const maxFiles = options.maxFiles ?? 400;
|
|
123
|
+
const files = [];
|
|
124
|
+
await walkFiles(root, root, files, maxFiles);
|
|
125
|
+
const findings = [];
|
|
126
|
+
const limitations = [];
|
|
127
|
+
if (files.length >= maxFiles) {
|
|
128
|
+
limitations.push(`Scan capped at ${maxFiles} files`);
|
|
129
|
+
}
|
|
130
|
+
for (const absolute of files) {
|
|
131
|
+
// open+fstat+read via readTextFile — avoids exists/stat→read TOCTOU
|
|
132
|
+
const content = await readTextFile(absolute, 512 * 1024);
|
|
133
|
+
if (content === null)
|
|
134
|
+
continue;
|
|
135
|
+
const lines = content.split(/\r?\n/);
|
|
136
|
+
const rel = toPosixRelative(root, absolute);
|
|
137
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
138
|
+
const line = lines[i] ?? "";
|
|
139
|
+
for (const pattern of SECRET_PATTERNS) {
|
|
140
|
+
pattern.re.lastIndex = 0;
|
|
141
|
+
let match;
|
|
142
|
+
while ((match = pattern.re.exec(line)) !== null) {
|
|
143
|
+
findings.push({
|
|
144
|
+
file: rel,
|
|
145
|
+
line: i + 1,
|
|
146
|
+
ruleId: pattern.id,
|
|
147
|
+
redactedSnippet: redactMatch(line, match[0] ?? ""),
|
|
148
|
+
severity: pattern.severity,
|
|
149
|
+
confidence: pattern.confidence,
|
|
150
|
+
});
|
|
151
|
+
if (findings.length >= 200) {
|
|
152
|
+
limitations.push("Finding cap reached (200)");
|
|
153
|
+
return {
|
|
154
|
+
root,
|
|
155
|
+
enabled: true,
|
|
156
|
+
filesScanned: files.length,
|
|
157
|
+
findings,
|
|
158
|
+
limitations,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return {
|
|
166
|
+
root,
|
|
167
|
+
enabled: true,
|
|
168
|
+
filesScanned: files.length,
|
|
169
|
+
findings,
|
|
170
|
+
limitations,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { type NormalizedCoverage } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* DOM-free Cobertura XML parser focused on class/@filename and line hits.
|
|
4
|
+
*/
|
|
5
|
+
export declare function parseCobertura(xml: string): NormalizedCoverage;
|
|
6
|
+
export declare function isCoberturaXml(text: string): boolean;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { normalizeCoveragePath } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* DOM-free Cobertura XML parser focused on class/@filename and line hits.
|
|
4
|
+
*/
|
|
5
|
+
export function parseCobertura(xml) {
|
|
6
|
+
const files = {};
|
|
7
|
+
const limitations = [
|
|
8
|
+
"Cobertura parser extracts class filename + line hits only (DOM-free regex)",
|
|
9
|
+
"No test→source mapping in standard Cobertura — test attribution is hybrid/heuristic",
|
|
10
|
+
];
|
|
11
|
+
if (!xml.trim()) {
|
|
12
|
+
return { format: "cobertura", files, limitations: ["Empty Cobertura input", ...limitations] };
|
|
13
|
+
}
|
|
14
|
+
const classRe = /<class\b([^>]*)>([\s\S]*?)<\/class>/gi;
|
|
15
|
+
let classMatch;
|
|
16
|
+
while ((classMatch = classRe.exec(xml)) !== null) {
|
|
17
|
+
const attrs = classMatch[1] ?? "";
|
|
18
|
+
const body = classMatch[2] ?? "";
|
|
19
|
+
const filename = attrValue(attrs, "filename") ?? attrValue(attrs, "name") ?? "";
|
|
20
|
+
if (!filename)
|
|
21
|
+
continue;
|
|
22
|
+
const filePath = normalizeCoveragePath(filename);
|
|
23
|
+
const file = files[filePath] ?? {
|
|
24
|
+
path: filePath,
|
|
25
|
+
lines: {},
|
|
26
|
+
functions: [],
|
|
27
|
+
};
|
|
28
|
+
const lineRe = /<line\b([^>]*)\/?>/gi;
|
|
29
|
+
let lineMatch;
|
|
30
|
+
while ((lineMatch = lineRe.exec(body)) !== null) {
|
|
31
|
+
const lineAttrs = lineMatch[1] ?? "";
|
|
32
|
+
const number = Number(attrValue(lineAttrs, "number"));
|
|
33
|
+
const hits = Number(attrValue(lineAttrs, "hits") ?? "0");
|
|
34
|
+
if (!Number.isFinite(number))
|
|
35
|
+
continue;
|
|
36
|
+
file.lines[number] = Math.max(file.lines[number] ?? 0, Number.isFinite(hits) ? hits : 0);
|
|
37
|
+
}
|
|
38
|
+
files[filePath] = file;
|
|
39
|
+
}
|
|
40
|
+
return { format: "cobertura", files, limitations };
|
|
41
|
+
}
|
|
42
|
+
function attrValue(attrs, name) {
|
|
43
|
+
const re = new RegExp(`${name}\\s*=\\s*(["'])([\\s\\S]*?)\\1`, "i");
|
|
44
|
+
const m = attrs.match(re);
|
|
45
|
+
return m?.[2] ?? null;
|
|
46
|
+
}
|
|
47
|
+
export function isCoberturaXml(text) {
|
|
48
|
+
const sample = text.slice(0, 8000);
|
|
49
|
+
return (/<coverage\b/i.test(sample) ||
|
|
50
|
+
/<!DOCTYPE\s+coverage/i.test(sample) ||
|
|
51
|
+
(/<class\b/i.test(sample) && /<line\b/i.test(sample)));
|
|
52
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type NormalizedCoverage } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Istanbul / nyc / vitest coverage JSON (coverage-final.json style).
|
|
4
|
+
* Accepts a JSON string or already-parsed object.
|
|
5
|
+
* Supports optional testMap / coveredByTests when present.
|
|
6
|
+
*/
|
|
7
|
+
export declare function parseIstanbul(input: string | object): NormalizedCoverage;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { normalizeCoveragePath } from "./types.js";
|
|
2
|
+
function asRecord(value) {
|
|
3
|
+
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
4
|
+
return value;
|
|
5
|
+
}
|
|
6
|
+
return null;
|
|
7
|
+
}
|
|
8
|
+
function looksLikeFileRecord(value) {
|
|
9
|
+
const rec = asRecord(value);
|
|
10
|
+
if (!rec)
|
|
11
|
+
return false;
|
|
12
|
+
return ("statementMap" in rec ||
|
|
13
|
+
"s" in rec ||
|
|
14
|
+
"fnMap" in rec ||
|
|
15
|
+
"f" in rec ||
|
|
16
|
+
typeof rec.path === "string");
|
|
17
|
+
}
|
|
18
|
+
function extractTests(rec) {
|
|
19
|
+
const fromMeta = rec.meta?.tests ?? rec.meta?.coveredByTests ?? [];
|
|
20
|
+
const direct = rec.coveredByTests ?? rec.tests ?? [];
|
|
21
|
+
return [...new Set([...fromMeta, ...direct].map(normalizeCoveragePath).filter(Boolean))].sort();
|
|
22
|
+
}
|
|
23
|
+
function collectTestMap(root) {
|
|
24
|
+
const raw = root.testMap ?? root.tests;
|
|
25
|
+
const map = asRecord(raw);
|
|
26
|
+
if (!map)
|
|
27
|
+
return undefined;
|
|
28
|
+
const out = {};
|
|
29
|
+
for (const [testPath, sources] of Object.entries(map)) {
|
|
30
|
+
if (!Array.isArray(sources))
|
|
31
|
+
continue;
|
|
32
|
+
const files = sources
|
|
33
|
+
.filter((s) => typeof s === "string")
|
|
34
|
+
.map(normalizeCoveragePath);
|
|
35
|
+
if (files.length)
|
|
36
|
+
out[normalizeCoveragePath(testPath)] = [...new Set(files)].sort();
|
|
37
|
+
}
|
|
38
|
+
return Object.keys(out).length ? out : undefined;
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Istanbul / nyc / vitest coverage JSON (coverage-final.json style).
|
|
42
|
+
* Accepts a JSON string or already-parsed object.
|
|
43
|
+
* Supports optional testMap / coveredByTests when present.
|
|
44
|
+
*/
|
|
45
|
+
export function parseIstanbul(input) {
|
|
46
|
+
let root;
|
|
47
|
+
if (typeof input === "string") {
|
|
48
|
+
try {
|
|
49
|
+
root = JSON.parse(input);
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
throw new Error(`Invalid Istanbul JSON: ${error instanceof Error ? error.message : String(error)}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
else if (input && typeof input === "object" && !Array.isArray(input)) {
|
|
56
|
+
root = input;
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
throw new Error("Invalid Istanbul JSON: expected object or JSON string");
|
|
60
|
+
}
|
|
61
|
+
const candidate = asRecord(root.coverageMap) ?? asRecord(root.result) ?? asRecord(root.coverage) ?? root;
|
|
62
|
+
const files = {};
|
|
63
|
+
const limitations = [
|
|
64
|
+
"Istanbul statement/branch maps vary by version; line hits taken from s/statementMap when present",
|
|
65
|
+
];
|
|
66
|
+
for (const [key, value] of Object.entries(candidate)) {
|
|
67
|
+
if (key === "testMap" || key === "tests" || key === "meta")
|
|
68
|
+
continue;
|
|
69
|
+
if (!looksLikeFileRecord(value))
|
|
70
|
+
continue;
|
|
71
|
+
const pathKey = normalizeCoveragePath(typeof value.path === "string" ? value.path : key.replace(/^.*:/, ""));
|
|
72
|
+
if (!pathKey)
|
|
73
|
+
continue;
|
|
74
|
+
const prev = files[pathKey];
|
|
75
|
+
const file = {
|
|
76
|
+
path: pathKey,
|
|
77
|
+
lines: { ...(prev?.lines ?? {}) },
|
|
78
|
+
functions: [...(prev?.functions ?? [])],
|
|
79
|
+
...(prev?.coveredByTests ? { coveredByTests: [...prev.coveredByTests] } : {}),
|
|
80
|
+
};
|
|
81
|
+
const statementMap = value.statementMap ?? {};
|
|
82
|
+
const statements = value.s ?? {};
|
|
83
|
+
for (const [id, hits] of Object.entries(statements)) {
|
|
84
|
+
const loc = statementMap[id];
|
|
85
|
+
const line = loc?.start?.line ?? loc?.end?.line;
|
|
86
|
+
if (typeof line !== "number" || !Number.isFinite(hits))
|
|
87
|
+
continue;
|
|
88
|
+
file.lines[line] = Math.max(file.lines[line] ?? 0, Number(hits));
|
|
89
|
+
}
|
|
90
|
+
const fnMap = value.fnMap ?? {};
|
|
91
|
+
const fnHits = value.f ?? {};
|
|
92
|
+
for (const [id, hits] of Object.entries(fnHits)) {
|
|
93
|
+
const meta = fnMap[id];
|
|
94
|
+
const name = meta?.name || `fn_${id}`;
|
|
95
|
+
const line = meta?.decl?.start?.line ?? meta?.line ?? 0;
|
|
96
|
+
const existing = file.functions.find((f) => f.name === name);
|
|
97
|
+
if (existing) {
|
|
98
|
+
existing.hits = Math.max(existing.hits, Number(hits) || 0);
|
|
99
|
+
if (!existing.line && line)
|
|
100
|
+
existing.line = line;
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
file.functions.push({ name, line, hits: Number(hits) || 0 });
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const tests = extractTests(value);
|
|
107
|
+
if (tests.length) {
|
|
108
|
+
file.coveredByTests = [...new Set([...(file.coveredByTests ?? []), ...tests])].sort();
|
|
109
|
+
}
|
|
110
|
+
files[pathKey] = file;
|
|
111
|
+
}
|
|
112
|
+
let testToFiles = collectTestMap(root) ?? collectTestMap(candidate);
|
|
113
|
+
for (const [source, cov] of Object.entries(files)) {
|
|
114
|
+
for (const test of cov.coveredByTests ?? []) {
|
|
115
|
+
testToFiles ??= {};
|
|
116
|
+
const list = testToFiles[test] ?? [];
|
|
117
|
+
if (!list.includes(source))
|
|
118
|
+
list.push(source);
|
|
119
|
+
testToFiles[test] = list.sort();
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
if (!testToFiles) {
|
|
123
|
+
limitations.push("Istanbul JSON had no test→source map — test attribution falls back to heuristics");
|
|
124
|
+
}
|
|
125
|
+
else {
|
|
126
|
+
limitations.push("Istanbul test→source map used where present");
|
|
127
|
+
}
|
|
128
|
+
return {
|
|
129
|
+
format: "istanbul",
|
|
130
|
+
files,
|
|
131
|
+
...(testToFiles ? { testToFiles } : {}),
|
|
132
|
+
limitations,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { type NormalizedCoverage } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Parse LCOV text into NormalizedCoverage.
|
|
4
|
+
* LCOV records source line/function hits only — no test→source map.
|
|
5
|
+
*/
|
|
6
|
+
export declare function parseLcov(text: string): NormalizedCoverage;
|
|
7
|
+
export declare function isLcovText(text: string): boolean;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { normalizeCoveragePath } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Parse LCOV text into NormalizedCoverage.
|
|
4
|
+
* LCOV records source line/function hits only — no test→source map.
|
|
5
|
+
*/
|
|
6
|
+
export function parseLcov(text) {
|
|
7
|
+
const files = {};
|
|
8
|
+
let current = null;
|
|
9
|
+
const limitations = [
|
|
10
|
+
"LCOV provides source line/function hits only — no test→source mapping",
|
|
11
|
+
"Test recommendation with LCOV uses hybrid naming/graph heuristics for test attribution",
|
|
12
|
+
];
|
|
13
|
+
for (const raw of text.split(/\r?\n/)) {
|
|
14
|
+
const line = raw.trimEnd();
|
|
15
|
+
if (line.startsWith("SF:")) {
|
|
16
|
+
const p = normalizeCoveragePath(line.slice(3).trim());
|
|
17
|
+
current = { path: p, lines: {}, functions: [] };
|
|
18
|
+
files[p] = current;
|
|
19
|
+
}
|
|
20
|
+
else if (line.startsWith("DA:") && current) {
|
|
21
|
+
const [ln, hits] = line.slice(3).split(",");
|
|
22
|
+
const n = Number(ln);
|
|
23
|
+
const h = Number(hits);
|
|
24
|
+
if (Number.isFinite(n) && Number.isFinite(h))
|
|
25
|
+
current.lines[n] = h;
|
|
26
|
+
}
|
|
27
|
+
else if (line.startsWith("FN:") && current) {
|
|
28
|
+
const body = line.slice(3);
|
|
29
|
+
const comma = body.indexOf(",");
|
|
30
|
+
if (comma < 0)
|
|
31
|
+
continue;
|
|
32
|
+
const n = Number(body.slice(0, comma));
|
|
33
|
+
const name = body.slice(comma + 1).trim();
|
|
34
|
+
if (Number.isFinite(n) && name) {
|
|
35
|
+
current.functions.push({ name, line: n, hits: 0 });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
else if (line.startsWith("FNDA:") && current) {
|
|
39
|
+
const body = line.slice(5);
|
|
40
|
+
const comma = body.indexOf(",");
|
|
41
|
+
if (comma < 0)
|
|
42
|
+
continue;
|
|
43
|
+
const h = Number(body.slice(0, comma));
|
|
44
|
+
const name = body.slice(comma + 1).trim();
|
|
45
|
+
const fn = current.functions.find((f) => f.name === name);
|
|
46
|
+
if (fn && Number.isFinite(h))
|
|
47
|
+
fn.hits = h;
|
|
48
|
+
}
|
|
49
|
+
else if (line === "end_of_record") {
|
|
50
|
+
current = null;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return { format: "lcov", files, limitations };
|
|
54
|
+
}
|
|
55
|
+
export function isLcovText(text) {
|
|
56
|
+
const sample = text.slice(0, 4000);
|
|
57
|
+
return (/(^|\n)SF:/.test(sample) &&
|
|
58
|
+
(/(^|\n)DA:/.test(sample) || /(^|\n)end_of_record/.test(sample) || /(^|\n)TN:/.test(sample)));
|
|
59
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { NormalizedCoverage } from "./types.js";
|
|
2
|
+
export type { NormalizedCoverage, CoverageFormat, CoverageFile } from "./types.js";
|
|
3
|
+
export { parseLcov } from "./lcov.js";
|
|
4
|
+
export { parseIstanbul } from "./istanbul.js";
|
|
5
|
+
export { parseCobertura } from "./cobertura.js";
|
|
6
|
+
/**
|
|
7
|
+
* Load coverage from a file path, auto-detecting format by extension then content.
|
|
8
|
+
*/
|
|
9
|
+
export declare function loadCoverage(coveragePath: string): Promise<NormalizedCoverage>;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { isCoberturaXml, parseCobertura } from "./cobertura.js";
|
|
4
|
+
import { parseIstanbul } from "./istanbul.js";
|
|
5
|
+
import { isLcovText, parseLcov } from "./lcov.js";
|
|
6
|
+
export { parseLcov } from "./lcov.js";
|
|
7
|
+
export { parseIstanbul } from "./istanbul.js";
|
|
8
|
+
export { parseCobertura } from "./cobertura.js";
|
|
9
|
+
/**
|
|
10
|
+
* Load coverage from a file path, auto-detecting format by extension then content.
|
|
11
|
+
*/
|
|
12
|
+
export async function loadCoverage(coveragePath) {
|
|
13
|
+
const abs = path.resolve(coveragePath);
|
|
14
|
+
let text;
|
|
15
|
+
try {
|
|
16
|
+
text = await fs.readFile(abs, "utf8");
|
|
17
|
+
}
|
|
18
|
+
catch (error) {
|
|
19
|
+
throw new Error(`Unable to read coverage file: ${error instanceof Error ? error.message : String(error)}`);
|
|
20
|
+
}
|
|
21
|
+
if (!text.trim()) {
|
|
22
|
+
throw new Error("Coverage file is empty");
|
|
23
|
+
}
|
|
24
|
+
const base = path.basename(abs).toLowerCase();
|
|
25
|
+
const ext = path.extname(abs).toLowerCase();
|
|
26
|
+
if (ext === ".lcov" || base === "lcov.info" || base.endsWith(".info") || isLcovText(text)) {
|
|
27
|
+
if (text.includes("SF:") || isLcovText(text))
|
|
28
|
+
return parseLcov(text);
|
|
29
|
+
}
|
|
30
|
+
if (ext === ".xml" || base.includes("cobertura") || isCoberturaXml(text)) {
|
|
31
|
+
return parseCobertura(text);
|
|
32
|
+
}
|
|
33
|
+
if (ext === ".json" || text.trimStart().startsWith("{")) {
|
|
34
|
+
return parseIstanbul(text);
|
|
35
|
+
}
|
|
36
|
+
if (text.includes("end_of_record"))
|
|
37
|
+
return parseLcov(text);
|
|
38
|
+
return parseIstanbul(text);
|
|
39
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export type CoverageFormat = "lcov" | "istanbul" | "cobertura";
|
|
2
|
+
export interface CoverageFunction {
|
|
3
|
+
name: string;
|
|
4
|
+
line: number;
|
|
5
|
+
hits: number;
|
|
6
|
+
}
|
|
7
|
+
export interface CoverageFile {
|
|
8
|
+
path: string;
|
|
9
|
+
lines: Record<number, number>;
|
|
10
|
+
functions: CoverageFunction[];
|
|
11
|
+
/** Optional test files known to cover this source (when format provides it). */
|
|
12
|
+
coveredByTests?: string[];
|
|
13
|
+
}
|
|
14
|
+
export interface NormalizedCoverage {
|
|
15
|
+
format: CoverageFormat;
|
|
16
|
+
files: Record<string, CoverageFile>;
|
|
17
|
+
/** Optional reverse map test → source files when available. */
|
|
18
|
+
testToFiles?: Record<string, string[]>;
|
|
19
|
+
limitations: string[];
|
|
20
|
+
}
|
|
21
|
+
export declare function normalizeCoveragePath(p: string): string;
|
|
22
|
+
/** @deprecated use normalizeCoveragePath */
|
|
23
|
+
export declare const normalizePath: typeof normalizeCoveragePath;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export interface DashboardServerOptions {
|
|
2
|
+
root: string;
|
|
3
|
+
host?: string;
|
|
4
|
+
port?: number;
|
|
5
|
+
/** Read-only by default; mutating routes are never registered. */
|
|
6
|
+
readOnly?: boolean;
|
|
7
|
+
/**
|
|
8
|
+
* Unsafe opt-in to bind outside loopback. Not an enterprise security boundary.
|
|
9
|
+
* Query ?user= is a localDevIdentityHint only; role elevation requires AGENTDOCTOR_ALLOW_LOCAL_IDENTITY_HINT=1.
|
|
10
|
+
*/
|
|
11
|
+
allowNonLoopback?: boolean;
|
|
12
|
+
}
|
|
13
|
+
export declare function isLoopbackHost(host: string): boolean;
|
|
14
|
+
/**
|
|
15
|
+
* Local read-only HTTP dashboard. Never applies Safe Fix or mutates the repo.
|
|
16
|
+
* Defaults to loopback binding; non-loopback requires explicit allowNonLoopback.
|
|
17
|
+
*/
|
|
18
|
+
export declare function startDashboardServer(options: DashboardServerOptions): Promise<{
|
|
19
|
+
host: string;
|
|
20
|
+
port: number;
|
|
21
|
+
close: () => Promise<void>;
|
|
22
|
+
}>;
|
|
23
|
+
/** Write a static snapshot HTML for offline viewing (optional helper). */
|
|
24
|
+
export declare function writeDashboardSnapshot(rootInput: string, outputPath: string): Promise<string>;
|