@praneeth_54/agentdoctor 2.0.0 → 2.1.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/CHANGELOG.md +141 -4
- package/README.md +329 -153
- package/dist/agent/approvals.d.ts +24 -0
- package/dist/agent/approvals.js +64 -0
- package/dist/agent/chat/index.d.ts +7 -0
- package/dist/agent/chat/index.js +5 -0
- package/dist/agent/chat/memory.d.ts +44 -0
- package/dist/agent/chat/memory.js +103 -0
- package/dist/agent/chat/project-summary.d.ts +16 -0
- package/dist/agent/chat/project-summary.js +70 -0
- package/dist/agent/chat/prompts.d.ts +6 -0
- package/dist/agent/chat/prompts.js +39 -0
- package/dist/agent/chat/response.d.ts +19 -0
- package/dist/agent/chat/response.js +109 -0
- package/dist/agent/chat/service.d.ts +42 -0
- package/dist/agent/chat/service.js +252 -0
- package/dist/agent/chat/types.d.ts +48 -0
- package/dist/agent/chat/types.js +1 -0
- package/dist/agent/context/retrieve.d.ts +21 -0
- package/dist/agent/context/retrieve.js +117 -0
- package/dist/agent/context/truth.d.ts +6 -0
- package/dist/agent/context/truth.js +19 -0
- package/dist/agent/context/types.d.ts +23 -0
- package/dist/agent/context/types.js +4 -0
- package/dist/agent/index.d.ts +26 -0
- package/dist/agent/index.js +14 -0
- package/dist/agent/loop.d.ts +51 -0
- package/dist/agent/loop.js +222 -0
- package/dist/agent/modes.d.ts +18 -0
- package/dist/agent/modes.js +102 -0
- package/dist/agent/plan.d.ts +30 -0
- package/dist/agent/plan.js +121 -0
- package/dist/agent/runtime.d.ts +67 -0
- package/dist/agent/runtime.js +180 -0
- package/dist/agent/state.d.ts +30 -0
- package/dist/agent/state.js +95 -0
- package/dist/agent/student.d.ts +56 -0
- package/dist/agent/student.js +230 -0
- package/dist/agent/tools/execute.d.ts +18 -0
- package/dist/agent/tools/execute.js +355 -0
- package/dist/agent/tools/index.d.ts +6 -0
- package/dist/agent/tools/index.js +5 -0
- package/dist/agent/tools/registry.d.ts +7 -0
- package/dist/agent/tools/registry.js +212 -0
- package/dist/agent/tools/run.d.ts +24 -0
- package/dist/agent/tools/run.js +44 -0
- package/dist/agent/tools/types.d.ts +32 -0
- package/dist/agent/tools/types.js +10 -0
- package/dist/agent/tools/write.d.ts +24 -0
- package/dist/agent/tools/write.js +121 -0
- package/dist/agent/verify.d.ts +29 -0
- package/dist/agent/verify.js +210 -0
- package/dist/ai/config.d.ts +22 -0
- package/dist/ai/config.js +68 -0
- package/dist/ai/index.d.ts +17 -0
- package/dist/ai/index.js +52 -0
- package/dist/ai/providers/mock.d.ts +20 -0
- package/dist/ai/providers/mock.js +84 -0
- package/dist/ai/providers/none.d.ts +6 -0
- package/dist/ai/providers/none.js +23 -0
- package/dist/ai/providers/openai-compatible.d.ts +21 -0
- package/dist/ai/providers/openai-compatible.js +151 -0
- package/dist/ai/redact.d.ts +8 -0
- package/dist/ai/redact.js +21 -0
- package/dist/ai/types.d.ts +71 -0
- package/dist/ai/types.js +6 -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/agent.d.ts +27 -0
- package/dist/cli/commands/agent.js +153 -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/chat.d.ts +14 -0
- package/dist/cli/commands/chat.js +155 -0
- package/dist/cli/commands/complete.js +2 -2
- package/dist/cli/commands/learn.d.ts +12 -0
- package/dist/cli/commands/learn.js +107 -0
- package/dist/cli/commands/platform.d.ts +2 -0
- package/dist/cli/commands/platform.js +5 -1
- 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/workspace.d.ts +9 -0
- package/dist/cli/commands/workspace.js +82 -0
- package/dist/cli/program.js +627 -17
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -1
- package/dist/contracts/index.d.ts +1 -1
- package/dist/contracts/index.js +2 -2
- package/dist/core/brain-product/init.js +5 -2
- package/dist/core/secrets/scan.d.ts +7 -1
- package/dist/core/secrets/scan.js +9 -9
- 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 +7 -1
- package/dist/dashboard/server.js +110 -6
- package/dist/enforcement/runner.d.ts +52 -3
- package/dist/enforcement/runner.js +467 -16
- package/dist/index.d.ts +36 -2
- package/dist/index.js +23 -2
- package/dist/intelligence/graph/build.d.ts +2 -0
- package/dist/intelligence/graph/build.js +49 -5
- 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/languages/go.d.ts +9 -0
- package/dist/languages/go.js +50 -0
- package/dist/languages/index.d.ts +11 -0
- package/dist/languages/index.js +76 -0
- package/dist/languages/php.d.ts +8 -0
- package/dist/languages/php.js +202 -0
- package/dist/languages/python.d.ts +11 -0
- package/dist/languages/python.js +180 -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/agent/registry.d.ts +13 -0
- package/dist/mcp/agent/registry.js +234 -0
- package/dist/mcp/agentdoctor/server.js +8 -1
- package/dist/mcp/intelligence/handlers.d.ts +5 -0
- package/dist/mcp/intelligence/handlers.js +229 -40
- package/dist/mcp/intelligence/path-safety.d.ts +2 -4
- package/dist/mcp/intelligence/path-safety.js +25 -44
- package/dist/mcp/intelligence/registry.d.ts +1 -1
- package/dist/mcp/intelligence/registry.js +69 -1
- package/dist/platform/firewall/evaluate.d.ts +6 -3
- package/dist/platform/firewall/evaluate.js +66 -15
- package/dist/platform/graph/build.js +19 -23
- package/dist/platform/health/analyze.js +23 -17
- package/dist/platform/index.js +2 -0
- package/dist/platform/test-impact/analyze.d.ts +21 -3
- package/dist/platform/test-impact/analyze.js +214 -28
- package/dist/platform/tokens/plan.js +39 -20
- package/dist/platform/types.d.ts +9 -0
- package/dist/policy/compose.d.ts +34 -0
- package/dist/policy/compose.js +118 -0
- package/dist/policy/packs.js +33 -1
- package/dist/security/paths.d.ts +21 -0
- package/dist/security/paths.js +133 -0
- package/dist/storage/postgres.d.ts +26 -0
- package/dist/storage/postgres.js +90 -0
- package/dist/storage/provider.d.ts +2 -7
- package/dist/storage/provider.js +22 -16
- package/dist/storage/sqlite.d.ts +33 -0
- package/dist/storage/sqlite.js +77 -0
- package/dist/team/auth.js +1 -1
- package/dist/workspace/index.d.ts +69 -0
- package/dist/workspace/index.js +220 -0
- package/package.json +9 -3
package/dist/constants.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { AgentId } from "./types/index.js";
|
|
2
|
-
export declare const PACKAGE_VERSION = "2.
|
|
2
|
+
export declare const PACKAGE_VERSION = "2.1.0";
|
|
3
3
|
export declare const DEFAULT_MAX_FILE_SIZE_BYTES: number;
|
|
4
4
|
/** Directories skipped during normal discovery (unless a rule needs them later). */
|
|
5
5
|
export declare const DEFAULT_IGNORE_DIRECTORIES: Set<string>;
|
package/dist/constants.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const PACKAGE_VERSION = "2.
|
|
1
|
+
export const PACKAGE_VERSION = "2.1.0";
|
|
2
2
|
export const DEFAULT_MAX_FILE_SIZE_BYTES = 2 * 1024 * 1024; // 2 MiB
|
|
3
3
|
/** Directories skipped during normal discovery (unless a rule needs them later). */
|
|
4
4
|
export const DEFAULT_IGNORE_DIRECTORIES = new Set([
|
|
@@ -105,7 +105,7 @@ export interface PolicyDecisionContract {
|
|
|
105
105
|
inputClassification: string;
|
|
106
106
|
evidence: EvidenceRef[];
|
|
107
107
|
riskLevel: Severity;
|
|
108
|
-
executionStatus: "not-executed" | "executed" | "blocked-by-enforcement";
|
|
108
|
+
executionStatus: "not-executed" | "executed" | "blocked-by-enforcement" | "execution-failed";
|
|
109
109
|
approvalStatus: "not-required" | "pending" | "approved" | "denied";
|
|
110
110
|
timestamp: string;
|
|
111
111
|
}
|
package/dist/contracts/index.js
CHANGED
|
@@ -5,9 +5,9 @@
|
|
|
5
5
|
export const CONTRACTS_VERSION = "2.0.0-contracts";
|
|
6
6
|
export const DEFAULT_FEATURE_FLAGS = {
|
|
7
7
|
typescriptAst: true,
|
|
8
|
-
sqliteStorage:
|
|
8
|
+
sqliteStorage: true,
|
|
9
9
|
teamMode: false,
|
|
10
|
-
enforcementRunner:
|
|
10
|
+
enforcementRunner: true,
|
|
11
11
|
vectorSearch: false,
|
|
12
12
|
architectureC4: true,
|
|
13
13
|
};
|
|
@@ -201,8 +201,11 @@ export async function reviewProposal(options) {
|
|
|
201
201
|
else {
|
|
202
202
|
content = `${statusLine}\n\n${content}`;
|
|
203
203
|
}
|
|
204
|
-
// Drop stale "not approved" parenthetical if present after replacement edge cases
|
|
205
|
-
|
|
204
|
+
// Drop stale "not approved" parenthetical if present after replacement edge cases.
|
|
205
|
+
// Avoid /\s*(...)/g — polynomial ReDoS on uncontrolled proposal markdown.
|
|
206
|
+
if (content.includes("(not approved)")) {
|
|
207
|
+
content = content.split("(not approved)").join("");
|
|
208
|
+
}
|
|
206
209
|
await atomicWriteTextFile(abs, content.endsWith("\n") ? content : `${content}\n`);
|
|
207
210
|
}
|
|
208
211
|
}
|
|
@@ -1,10 +1,16 @@
|
|
|
1
|
+
export type SecretSeverity = "critical" | "warning";
|
|
2
|
+
/** Pattern match confidence — independent from severity impact rating. */
|
|
3
|
+
export type SecretConfidence = "low" | "medium" | "high";
|
|
1
4
|
export interface SecretFinding {
|
|
2
5
|
file: string;
|
|
3
6
|
line: number;
|
|
4
7
|
ruleId: string;
|
|
5
8
|
/** Always redacted — never contains the secret value */
|
|
6
9
|
redactedSnippet: string;
|
|
7
|
-
|
|
10
|
+
/** Impact if the match is a real secret — not the same as confidence. */
|
|
11
|
+
severity: SecretSeverity;
|
|
12
|
+
/** How confident the heuristic is that this match is a secret. */
|
|
13
|
+
confidence: SecretConfidence;
|
|
8
14
|
}
|
|
9
15
|
export interface SecretScanReport {
|
|
10
16
|
root: string;
|
|
@@ -1,26 +1,31 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import path from "node:path";
|
|
3
|
+
import { readTextFile } from "../../utils/fs.js";
|
|
3
4
|
import { resolveRepoRoot, toPosixRelative } from "../../utils/path.js";
|
|
4
5
|
const SECRET_PATTERNS = [
|
|
5
6
|
{
|
|
6
7
|
id: "aws-access-key",
|
|
7
8
|
re: /\bAKIA[0-9A-Z]{16}\b/g,
|
|
8
9
|
severity: "critical",
|
|
10
|
+
confidence: "high",
|
|
9
11
|
},
|
|
10
12
|
{
|
|
11
13
|
id: "generic-api-key",
|
|
12
14
|
re: /\b(?:api[_-]?key|secret[_-]?key|access[_-]?token)\s*[:=]\s*['"]?[A-Za-z0-9_-]{16,}['"]?/gi,
|
|
13
15
|
severity: "warning",
|
|
16
|
+
confidence: "medium",
|
|
14
17
|
},
|
|
15
18
|
{
|
|
16
19
|
id: "private-key-header",
|
|
17
20
|
re: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/g,
|
|
18
21
|
severity: "critical",
|
|
22
|
+
confidence: "high",
|
|
19
23
|
},
|
|
20
24
|
{
|
|
21
25
|
id: "github-pat",
|
|
22
26
|
re: /\bghp_[A-Za-z0-9]{36}\b/g,
|
|
23
27
|
severity: "critical",
|
|
28
|
+
confidence: "high",
|
|
24
29
|
},
|
|
25
30
|
];
|
|
26
31
|
const SKIP_DIR = new Set([
|
|
@@ -123,16 +128,10 @@ export async function scanSecrets(options) {
|
|
|
123
128
|
limitations.push(`Scan capped at ${maxFiles} files`);
|
|
124
129
|
}
|
|
125
130
|
for (const absolute of files) {
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
if (st.size > 512 * 1024)
|
|
130
|
-
continue;
|
|
131
|
-
content = await fs.readFile(absolute, "utf8");
|
|
132
|
-
}
|
|
133
|
-
catch {
|
|
131
|
+
// open+fstat+read via readTextFile — avoids exists/stat→read TOCTOU
|
|
132
|
+
const content = await readTextFile(absolute, 512 * 1024);
|
|
133
|
+
if (content === null)
|
|
134
134
|
continue;
|
|
135
|
-
}
|
|
136
135
|
const lines = content.split(/\r?\n/);
|
|
137
136
|
const rel = toPosixRelative(root, absolute);
|
|
138
137
|
for (let i = 0; i < lines.length; i += 1) {
|
|
@@ -147,6 +146,7 @@ export async function scanSecrets(options) {
|
|
|
147
146
|
ruleId: pattern.id,
|
|
148
147
|
redactedSnippet: redactMatch(line, match[0] ?? ""),
|
|
149
148
|
severity: pattern.severity,
|
|
149
|
+
confidence: pattern.confidence,
|
|
150
150
|
});
|
|
151
151
|
if (findings.length >= 200) {
|
|
152
152
|
limitations.push("Finding cap reached (200)");
|
|
@@ -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;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ModelProvider } from "../ai/types.js";
|
|
1
2
|
export interface DashboardServerOptions {
|
|
2
3
|
root: string;
|
|
3
4
|
host?: string;
|
|
@@ -6,9 +7,14 @@ export interface DashboardServerOptions {
|
|
|
6
7
|
readOnly?: boolean;
|
|
7
8
|
/**
|
|
8
9
|
* Unsafe opt-in to bind outside loopback. Not an enterprise security boundary.
|
|
9
|
-
*
|
|
10
|
+
* Query ?user= is a localDevIdentityHint only; role elevation requires AGENTDOCTOR_ALLOW_LOCAL_IDENTITY_HINT=1.
|
|
10
11
|
*/
|
|
11
12
|
allowNonLoopback?: boolean;
|
|
13
|
+
/**
|
|
14
|
+
* Optional provider override (tests). Production uses loadAiConfig().
|
|
15
|
+
* When unset and provider resolves to none, /api/chat fails closed.
|
|
16
|
+
*/
|
|
17
|
+
chatProvider?: ModelProvider;
|
|
12
18
|
}
|
|
13
19
|
export declare function isLoopbackHost(host: string): boolean;
|
|
14
20
|
/**
|