@praneeth_54/agentdoctor 2.1.0 → 3.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 +86 -3
- package/README.md +490 -290
- package/dist/agent/chat/deterministic.d.ts +6 -0
- package/dist/agent/chat/deterministic.js +180 -0
- package/dist/agent/chat/service.js +8 -14
- package/dist/agent/context/retrieve.d.ts +2 -1
- package/dist/agent/context/retrieve.js +14 -1
- package/dist/agent/index.d.ts +2 -0
- package/dist/agent/index.js +1 -0
- package/dist/agent/loop.d.ts +2 -0
- package/dist/agent/loop.js +31 -0
- package/dist/agent/roles.d.ts +12 -0
- package/dist/agent/roles.js +138 -0
- package/dist/agent/runtime.d.ts +21 -5
- package/dist/agent/runtime.js +141 -28
- package/dist/agent/tools/execute.d.ts +2 -0
- package/dist/agent/tools/execute.js +49 -43
- package/dist/agent/tools/write.d.ts +1 -1
- package/dist/agent/tools/write.js +18 -4
- package/dist/ai/config.js +1 -0
- package/dist/ai/providers/adversarial-local.d.ts +7 -0
- package/dist/ai/providers/adversarial-local.js +146 -0
- package/dist/ai/redact.d.ts +1 -0
- package/dist/ai/redact.js +47 -7
- package/dist/ai/types.d.ts +1 -1
- package/dist/cli/commands/agent.d.ts +2 -0
- package/dist/cli/commands/agent.js +32 -11
- package/dist/cli/commands/architecture.js +7 -2
- package/dist/cli/commands/brain.js +7 -2
- package/dist/cli/commands/chat.js +15 -7
- package/dist/cli/commands/complete.js +13 -3
- package/dist/cli/commands/fix.js +6 -0
- package/dist/cli/commands/learn.js +7 -2
- package/dist/cli/commands/mcp.js +7 -3
- package/dist/cli/commands/platform.js +7 -2
- package/dist/cli/commands/policy-graph-run.js +7 -0
- package/dist/cli/commands/product.d.ts +19 -0
- package/dist/cli/commands/product.js +157 -0
- package/dist/cli/commands/scan.js +6 -0
- package/dist/cli/commands/start.d.ts +15 -0
- package/dist/cli/commands/start.js +86 -0
- package/dist/cli/commands/v2.js +61 -11
- package/dist/cli/commands/verify.js +6 -0
- package/dist/cli/program.js +141 -0
- package/dist/cli/safe-root.d.ts +17 -0
- package/dist/cli/safe-root.js +32 -0
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -1
- package/dist/core/brain-cli/service.js +5 -2
- package/dist/core/monorepo/detect.js +10 -0
- package/dist/core/secrets/scan.js +9 -0
- package/dist/core/understanding/brain/storage/store.d.ts +4 -0
- package/dist/core/understanding/brain/storage/store.js +22 -2
- package/dist/dashboard/page.d.ts +2 -0
- package/dist/dashboard/page.js +53 -0
- package/dist/dashboard/server.js +117 -140
- package/dist/dashboard/ui/client.d.ts +2 -0
- package/dist/dashboard/ui/client.js +2 -0
- package/dist/dashboard/ui/styles.d.ts +2 -0
- package/dist/dashboard/ui/styles.js +138 -0
- package/dist/discovery/files.js +13 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/dist/intelligence/graph/build.js +46 -38
- package/dist/intelligence/graph/incremental.d.ts +2 -0
- package/dist/intelligence/graph/incremental.js +9 -0
- package/dist/intelligence/resolve/imports.js +1 -1
- package/dist/languages/dart.d.ts +10 -0
- package/dist/languages/dart.js +99 -0
- package/dist/languages/go.d.ts +13 -7
- package/dist/languages/go.js +84 -24
- package/dist/languages/index.d.ts +5 -1
- package/dist/languages/index.js +13 -36
- package/dist/languages/java.d.ts +10 -0
- package/dist/languages/java.js +80 -0
- package/dist/languages/kotlin.d.ts +10 -0
- package/dist/languages/kotlin.js +85 -0
- package/dist/languages/rust.d.ts +10 -0
- package/dist/languages/rust.js +93 -0
- package/dist/languages/types.d.ts +4 -3
- package/dist/mcp/agent/registry.d.ts +1 -1
- package/dist/mcp/agent/registry.js +109 -23
- package/dist/mcp/intelligence/handlers.d.ts +3 -0
- package/dist/mcp/intelligence/handlers.js +28 -0
- package/dist/mcp/intelligence/path-safety.d.ts +6 -1
- package/dist/mcp/intelligence/path-safety.js +55 -2
- package/dist/mcp/intelligence/registry.d.ts +1 -1
- package/dist/mcp/intelligence/registry.js +30 -1
- package/dist/platform/graph/build.js +9 -0
- package/dist/product/api/doctor.d.ts +14 -0
- package/dist/product/api/doctor.js +185 -0
- package/dist/product/api/openapi.d.ts +7 -0
- package/dist/product/api/openapi.js +122 -0
- package/dist/product/approval/model.d.ts +30 -0
- package/dist/product/approval/model.js +64 -0
- package/dist/product/approval/session.d.ts +49 -0
- package/dist/product/approval/session.js +134 -0
- package/dist/product/database/doctor.d.ts +30 -0
- package/dist/product/database/doctor.js +184 -0
- package/dist/product/decisions/ledger.d.ts +36 -0
- package/dist/product/decisions/ledger.js +141 -0
- package/dist/product/deps/analyze.d.ts +46 -0
- package/dist/product/deps/analyze.js +137 -0
- package/dist/product/deps/lockfiles.d.ts +25 -0
- package/dist/product/deps/lockfiles.js +200 -0
- package/dist/product/discovery/roots.d.ts +52 -0
- package/dist/product/discovery/roots.js +231 -0
- package/dist/product/dna/build.d.ts +50 -0
- package/dist/product/dna/build.js +255 -0
- package/dist/product/eval/lab.d.ts +17 -0
- package/dist/product/eval/lab.js +218 -0
- package/dist/product/events/doctor.d.ts +21 -0
- package/dist/product/events/doctor.js +147 -0
- package/dist/product/evidence-scan.d.ts +12 -0
- package/dist/product/evidence-scan.js +46 -0
- package/dist/product/evolution/timeline.d.ts +29 -0
- package/dist/product/evolution/timeline.js +123 -0
- package/dist/product/features/intelligence.d.ts +23 -0
- package/dist/product/features/intelligence.js +158 -0
- package/dist/product/forensic/mode.d.ts +25 -0
- package/dist/product/forensic/mode.js +70 -0
- package/dist/product/graph/enrich-languages.d.ts +20 -0
- package/dist/product/graph/enrich-languages.js +168 -0
- package/dist/product/health/code-health.d.ts +20 -0
- package/dist/product/health/code-health.js +149 -0
- package/dist/product/index.d.ts +72 -0
- package/dist/product/index.js +36 -0
- package/dist/product/ledger/change-ledger.d.ts +23 -0
- package/dist/product/ledger/change-ledger.js +64 -0
- package/dist/product/map/software-map.d.ts +17 -0
- package/dist/product/map/software-map.js +96 -0
- package/dist/product/memory/institutional.d.ts +16 -0
- package/dist/product/memory/institutional.js +92 -0
- package/dist/product/ops/incident.d.ts +20 -0
- package/dist/product/ops/incident.js +61 -0
- package/dist/product/ops/infra.d.ts +15 -0
- package/dist/product/ops/infra.js +112 -0
- package/dist/product/org/model.d.ts +34 -0
- package/dist/product/org/model.js +195 -0
- package/dist/product/privacy/doctor.d.ts +13 -0
- package/dist/product/privacy/doctor.js +90 -0
- package/dist/product/requirements/trace.d.ts +21 -0
- package/dist/product/requirements/trace.js +166 -0
- package/dist/product/search/index.d.ts +29 -0
- package/dist/product/search/index.js +116 -0
- package/dist/product/search/software-search.d.ts +19 -0
- package/dist/product/search/software-search.js +105 -0
- package/dist/product/security/doctor.d.ts +23 -0
- package/dist/product/security/doctor.js +124 -0
- package/dist/product/self/diagnose.d.ts +15 -0
- package/dist/product/self/diagnose.js +82 -0
- package/dist/product/techdebt/roadmap.d.ts +20 -0
- package/dist/product/techdebt/roadmap.js +118 -0
- package/dist/product/testbrain/analyze.d.ts +29 -0
- package/dist/product/testbrain/analyze.js +129 -0
- package/dist/product/truth.d.ts +12 -0
- package/dist/product/truth.js +30 -0
- package/dist/product/twin/digital-twin.d.ts +23 -0
- package/dist/product/twin/digital-twin.js +52 -0
- package/dist/product/twin/store.d.ts +21 -0
- package/dist/product/twin/store.js +78 -0
- package/dist/product/whatif/engine.d.ts +28 -0
- package/dist/product/whatif/engine.js +91 -0
- package/dist/project/ownership.d.ts +65 -0
- package/dist/project/ownership.js +169 -0
- package/dist/utils/fs.js +10 -2
- package/package.json +1 -1
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ProductEvidence, TruthLabel } from "../truth.js";
|
|
2
|
+
export interface TechnicalDebtItem {
|
|
3
|
+
id: string;
|
|
4
|
+
category: "todo" | "fixme" | "architecture" | "testing";
|
|
5
|
+
summary: string;
|
|
6
|
+
truth: TruthLabel;
|
|
7
|
+
evidence: ProductEvidence[];
|
|
8
|
+
}
|
|
9
|
+
export interface TechnicalDebtRoadmap {
|
|
10
|
+
root: string;
|
|
11
|
+
items: TechnicalDebtItem[];
|
|
12
|
+
metrics: {
|
|
13
|
+
todoCount: number;
|
|
14
|
+
fixmeCount: number;
|
|
15
|
+
testRatio: number | null;
|
|
16
|
+
architectureViolations: number;
|
|
17
|
+
};
|
|
18
|
+
limitations: string[];
|
|
19
|
+
}
|
|
20
|
+
export declare function buildTechnicalDebtRoadmap(rootInput: string): Promise<TechnicalDebtRoadmap>;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { checkArchitectureAtRoot } from "../../architecture/contract.js";
|
|
3
|
+
import { detectProject } from "../../detectors/project.js";
|
|
4
|
+
import { buildRepositoryGraph } from "../../platform/graph/build.js";
|
|
5
|
+
import { readTextFile } from "../../utils/fs.js";
|
|
6
|
+
import { resolveRepoRoot } from "../../utils/path.js";
|
|
7
|
+
function isTestPath(relativePath) {
|
|
8
|
+
const p = relativePath.toLowerCase();
|
|
9
|
+
return (p.includes(".test.") ||
|
|
10
|
+
p.includes(".spec.") ||
|
|
11
|
+
p.startsWith("tests/") ||
|
|
12
|
+
p.startsWith("test/") ||
|
|
13
|
+
p.includes("__tests__/"));
|
|
14
|
+
}
|
|
15
|
+
function isSourcePath(relativePath) {
|
|
16
|
+
const p = relativePath.toLowerCase();
|
|
17
|
+
if (isTestPath(p))
|
|
18
|
+
return false;
|
|
19
|
+
return /\.(ts|tsx|js|jsx|py|php|go|rs)$/i.test(p);
|
|
20
|
+
}
|
|
21
|
+
export async function buildTechnicalDebtRoadmap(rootInput) {
|
|
22
|
+
const root = resolveRepoRoot(rootInput);
|
|
23
|
+
const limitations = [
|
|
24
|
+
"Roadmap aggregates TODO/FIXME markers, architecture contract violations, and test-ratio heuristics.",
|
|
25
|
+
"Not a substitute for issue trackers or formal debt scoring.",
|
|
26
|
+
];
|
|
27
|
+
const detection = await detectProject(root);
|
|
28
|
+
const paths = detection.discovery.files.map((f) => f.relativePath.replace(/\\/g, "/"));
|
|
29
|
+
const sourceFiles = paths.filter(isSourcePath);
|
|
30
|
+
const testFiles = paths.filter(isTestPath);
|
|
31
|
+
let todoCount = 0;
|
|
32
|
+
let fixmeCount = 0;
|
|
33
|
+
const items = [];
|
|
34
|
+
const re = /\b(TODO|FIXME)\b(:[^\n]*)?/g;
|
|
35
|
+
for (const rel of paths) {
|
|
36
|
+
const text = await readTextFile(path.join(root, rel), 256 * 1024);
|
|
37
|
+
if (text === null)
|
|
38
|
+
continue;
|
|
39
|
+
re.lastIndex = 0;
|
|
40
|
+
let match;
|
|
41
|
+
while ((match = re.exec(text)) !== null) {
|
|
42
|
+
const kind = match[1] === "FIXME" ? "fixme" : "todo";
|
|
43
|
+
if (kind === "todo")
|
|
44
|
+
todoCount++;
|
|
45
|
+
else
|
|
46
|
+
fixmeCount++;
|
|
47
|
+
if (items.filter((i) => i.category === kind).length < 15) {
|
|
48
|
+
const line = text.slice(0, match.index).split("\n").length;
|
|
49
|
+
items.push({
|
|
50
|
+
id: `${kind}-${rel}-${line}`,
|
|
51
|
+
category: kind,
|
|
52
|
+
summary: (match[0] ?? kind).trim().slice(0, 100),
|
|
53
|
+
truth: "VERIFIED",
|
|
54
|
+
evidence: [
|
|
55
|
+
{
|
|
56
|
+
path: rel,
|
|
57
|
+
line,
|
|
58
|
+
...(() => {
|
|
59
|
+
const excerpt = text.split(/\r?\n/)[line - 1]?.trim().slice(0, 120);
|
|
60
|
+
return excerpt !== undefined && excerpt.length > 0 ? { excerpt } : {};
|
|
61
|
+
})(),
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
let architectureViolations = 0;
|
|
69
|
+
try {
|
|
70
|
+
const graph = await buildRepositoryGraph(root);
|
|
71
|
+
const arch = await checkArchitectureAtRoot(root, graph);
|
|
72
|
+
architectureViolations = arch.violations.length;
|
|
73
|
+
for (const v of arch.violations.slice(0, 10)) {
|
|
74
|
+
items.push({
|
|
75
|
+
id: `arch-${v.ruleId}-${v.fromPath ?? v.toPath ?? "root"}`,
|
|
76
|
+
category: "architecture",
|
|
77
|
+
summary: v.description.slice(0, 120),
|
|
78
|
+
truth: "VERIFIED",
|
|
79
|
+
evidence: [
|
|
80
|
+
{
|
|
81
|
+
path: v.fromPath || v.toPath || ".",
|
|
82
|
+
excerpt: `${v.ruleId}: ${v.description}`.slice(0, 120),
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
limitations.push("Architecture contract check unavailable or failed.");
|
|
90
|
+
}
|
|
91
|
+
const denom = sourceFiles.length + testFiles.length;
|
|
92
|
+
const testRatio = denom === 0 ? null : testFiles.length / denom;
|
|
93
|
+
if (testRatio !== null && testRatio < 0.15) {
|
|
94
|
+
items.push({
|
|
95
|
+
id: "testing-low-ratio",
|
|
96
|
+
category: "testing",
|
|
97
|
+
summary: `Low test-file ratio (${testRatio.toFixed(3)}) — consider adding tests`,
|
|
98
|
+
truth: "INFERRED",
|
|
99
|
+
evidence: [
|
|
100
|
+
{
|
|
101
|
+
path: ".",
|
|
102
|
+
excerpt: `tests=${testFiles.length}, sources=${sourceFiles.length}`,
|
|
103
|
+
},
|
|
104
|
+
],
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
root,
|
|
109
|
+
items,
|
|
110
|
+
metrics: {
|
|
111
|
+
todoCount,
|
|
112
|
+
fixmeCount,
|
|
113
|
+
testRatio,
|
|
114
|
+
architectureViolations,
|
|
115
|
+
},
|
|
116
|
+
limitations,
|
|
117
|
+
};
|
|
118
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { type AnalyzeTestImpactOptions, type TestImpactReport } from "../../platform/test-impact/analyze.js";
|
|
2
|
+
import type { RepositoryGraph } from "../../platform/types.js";
|
|
3
|
+
export type FileTestMappingEvidence = "graph-import" | "naming-heuristic" | "impact-recommendation";
|
|
4
|
+
export interface FileTestMapping {
|
|
5
|
+
sourceFile: string;
|
|
6
|
+
testFiles: string[];
|
|
7
|
+
evidence: FileTestMappingEvidence[];
|
|
8
|
+
confidence: number;
|
|
9
|
+
}
|
|
10
|
+
export interface TestBrainGraphStats {
|
|
11
|
+
testFileNodes: number;
|
|
12
|
+
sourceToTestLinks: number;
|
|
13
|
+
importEdgesFromTests: number;
|
|
14
|
+
}
|
|
15
|
+
export interface TestBrainReport {
|
|
16
|
+
root: string;
|
|
17
|
+
staticAnalysisLabel: "TECHNICAL_STATIC_ANALYSIS_SUPPORTED";
|
|
18
|
+
testImpact: TestImpactReport;
|
|
19
|
+
fileTestMappings: FileTestMapping[];
|
|
20
|
+
graphStats: TestBrainGraphStats;
|
|
21
|
+
limitations: string[];
|
|
22
|
+
}
|
|
23
|
+
export declare function mapTestsFromGraph(graph: RepositoryGraph): Map<string, Set<string>>;
|
|
24
|
+
/**
|
|
25
|
+
* Test Brain — combines git test-impact with repository graph test↔source import links.
|
|
26
|
+
* Does not execute tests.
|
|
27
|
+
*/
|
|
28
|
+
export declare function analyzeTestBrain(rootInput: string | AnalyzeTestImpactOptions, maybeOptions?: AnalyzeTestImpactOptions): Promise<TestBrainReport>;
|
|
29
|
+
export declare function persistTestBrainReport(rootInput: string, report: TestBrainReport): Promise<string>;
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { buildRepositoryGraph } from "../../platform/graph/build.js";
|
|
3
|
+
import { analyzeTestImpact, } from "../../platform/test-impact/analyze.js";
|
|
4
|
+
import { writeJsonArtifact } from "../../platform/store.js";
|
|
5
|
+
import { resolveRepoRoot } from "../../utils/path.js";
|
|
6
|
+
function isTestPath(file) {
|
|
7
|
+
return /\.(test|spec)\./i.test(file) || file.includes("__tests__");
|
|
8
|
+
}
|
|
9
|
+
function resolveRelativeImport(fromFile, specifier) {
|
|
10
|
+
if (!specifier.startsWith("."))
|
|
11
|
+
return null;
|
|
12
|
+
const dir = path.posix.dirname(fromFile.replace(/\\/g, "/"));
|
|
13
|
+
let resolved = path.posix.normalize(path.posix.join(dir, specifier));
|
|
14
|
+
if (!resolved.startsWith("."))
|
|
15
|
+
resolved = resolved.replace(/^\.\//, "");
|
|
16
|
+
return resolved;
|
|
17
|
+
}
|
|
18
|
+
function sourceCandidatesFromSpecifier(specifier, fromTest) {
|
|
19
|
+
const base = resolveRelativeImport(fromTest, specifier);
|
|
20
|
+
if (!base)
|
|
21
|
+
return [];
|
|
22
|
+
const exts = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".py", ".go", ".java", ".kt", ".rs", ".dart"];
|
|
23
|
+
const out = [base];
|
|
24
|
+
if (!path.posix.extname(base)) {
|
|
25
|
+
for (const ext of exts)
|
|
26
|
+
out.push(`${base}${ext}`);
|
|
27
|
+
out.push(`${base}/index.ts`, `${base}/index.js`);
|
|
28
|
+
}
|
|
29
|
+
return [...new Set(out)];
|
|
30
|
+
}
|
|
31
|
+
export function mapTestsFromGraph(graph) {
|
|
32
|
+
const sourceToTests = new Map();
|
|
33
|
+
const nodesById = new Map(graph.nodes.map((n) => [n.id, n]));
|
|
34
|
+
for (const edge of graph.edges) {
|
|
35
|
+
if (edge.kind !== "imports")
|
|
36
|
+
continue;
|
|
37
|
+
const fromNode = nodesById.get(edge.from);
|
|
38
|
+
const toNode = nodesById.get(edge.to);
|
|
39
|
+
if (!fromNode?.path || !isTestPath(fromNode.path))
|
|
40
|
+
continue;
|
|
41
|
+
const spec = toNode?.meta?.specifier ?? toNode?.label ?? "";
|
|
42
|
+
if (!spec || !spec.startsWith("."))
|
|
43
|
+
continue;
|
|
44
|
+
for (const candidate of sourceCandidatesFromSpecifier(spec, fromNode.path)) {
|
|
45
|
+
if (!sourceToTests.has(candidate))
|
|
46
|
+
sourceToTests.set(candidate, new Set());
|
|
47
|
+
sourceToTests.get(candidate).add(fromNode.path);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return sourceToTests;
|
|
51
|
+
}
|
|
52
|
+
function mergeMapping(map, sourceFile, testFile, evidence, confidence) {
|
|
53
|
+
const key = sourceFile.replace(/\\/g, "/");
|
|
54
|
+
const existing = map.get(key);
|
|
55
|
+
if (!existing) {
|
|
56
|
+
map.set(key, {
|
|
57
|
+
sourceFile: key,
|
|
58
|
+
testFiles: [testFile],
|
|
59
|
+
evidence: [evidence],
|
|
60
|
+
confidence,
|
|
61
|
+
});
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (!existing.testFiles.includes(testFile))
|
|
65
|
+
existing.testFiles.push(testFile);
|
|
66
|
+
if (!existing.evidence.includes(evidence))
|
|
67
|
+
existing.evidence.push(evidence);
|
|
68
|
+
existing.confidence = Math.max(existing.confidence, confidence);
|
|
69
|
+
existing.testFiles.sort();
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Test Brain — combines git test-impact with repository graph test↔source import links.
|
|
73
|
+
* Does not execute tests.
|
|
74
|
+
*/
|
|
75
|
+
export async function analyzeTestBrain(rootInput, maybeOptions) {
|
|
76
|
+
const options = typeof rootInput === "string" ? { ...(maybeOptions ?? {}), root: rootInput } : rootInput;
|
|
77
|
+
const root = resolveRepoRoot(options.root ?? process.cwd());
|
|
78
|
+
const limitations = [
|
|
79
|
+
"Test Brain wraps test-impact plus graph import heuristics — not mutation testing or coverage oracle.",
|
|
80
|
+
"Graph file↔test links are import-based for test files only; missing imports are not proven absence of tests.",
|
|
81
|
+
];
|
|
82
|
+
const testImpact = await analyzeTestImpact(options);
|
|
83
|
+
const graph = await buildRepositoryGraph(root);
|
|
84
|
+
const graphMap = mapTestsFromGraph(graph);
|
|
85
|
+
const mappingBySource = new Map();
|
|
86
|
+
for (const [source, tests] of graphMap) {
|
|
87
|
+
for (const testFile of tests) {
|
|
88
|
+
mergeMapping(mappingBySource, source, testFile, "graph-import", 0.85);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
for (const changed of testImpact.changedFiles) {
|
|
92
|
+
if (isTestPath(changed))
|
|
93
|
+
continue;
|
|
94
|
+
const normalized = changed.replace(/\\/g, "/");
|
|
95
|
+
for (const rec of testImpact.recommendedTests) {
|
|
96
|
+
if (!rec.reason.includes(normalized))
|
|
97
|
+
continue;
|
|
98
|
+
const evidence = rec.evidence === "verified" ? "impact-recommendation" : "naming-heuristic";
|
|
99
|
+
mergeMapping(mappingBySource, normalized, rec.testPath, evidence, rec.confidence);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
const fileTestMappings = [...mappingBySource.values()].sort((a, b) => a.sourceFile.localeCompare(b.sourceFile));
|
|
103
|
+
const testFileNodes = graph.nodes.filter((n) => n.kind === "test").length;
|
|
104
|
+
let importEdgesFromTests = 0;
|
|
105
|
+
const nodesById = new Map(graph.nodes.map((n) => [n.id, n]));
|
|
106
|
+
for (const edge of graph.edges) {
|
|
107
|
+
if (edge.kind !== "imports")
|
|
108
|
+
continue;
|
|
109
|
+
const fromNode = nodesById.get(edge.from);
|
|
110
|
+
if (fromNode?.path && isTestPath(fromNode.path))
|
|
111
|
+
importEdgesFromTests += 1;
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
root,
|
|
115
|
+
staticAnalysisLabel: "TECHNICAL_STATIC_ANALYSIS_SUPPORTED",
|
|
116
|
+
testImpact,
|
|
117
|
+
fileTestMappings,
|
|
118
|
+
graphStats: {
|
|
119
|
+
testFileNodes,
|
|
120
|
+
sourceToTestLinks: fileTestMappings.reduce((n, m) => n + m.testFiles.length, 0),
|
|
121
|
+
importEdgesFromTests,
|
|
122
|
+
},
|
|
123
|
+
limitations: [...limitations, ...testImpact.limitations, ...graph.limitations.slice(0, 2)],
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
export async function persistTestBrainReport(rootInput, report) {
|
|
127
|
+
const root = resolveRepoRoot(rootInput);
|
|
128
|
+
return writeJsonArtifact(root, "reports/test-brain.json", report);
|
|
129
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Product-layer truth labels (extends agent context labels with maturity markers).
|
|
3
|
+
*/
|
|
4
|
+
export type TruthLabel = "VERIFIED" | "INFERRED" | "UNKNOWN" | "EXTERNAL" | "EXPERIMENTAL" | "PARTIAL";
|
|
5
|
+
export interface ProductEvidence {
|
|
6
|
+
path: string;
|
|
7
|
+
line?: number;
|
|
8
|
+
excerpt?: string;
|
|
9
|
+
}
|
|
10
|
+
/** Never promote weaker evidence to stronger by accident. */
|
|
11
|
+
export declare function minProductTruth(a: TruthLabel, b: TruthLabel): TruthLabel;
|
|
12
|
+
export declare function truthLabelDescription(label: TruthLabel): string;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
const TRUTH_ORDER = [
|
|
2
|
+
"VERIFIED",
|
|
3
|
+
"INFERRED",
|
|
4
|
+
"PARTIAL",
|
|
5
|
+
"EXPERIMENTAL",
|
|
6
|
+
"UNKNOWN",
|
|
7
|
+
"EXTERNAL",
|
|
8
|
+
];
|
|
9
|
+
/** Never promote weaker evidence to stronger by accident. */
|
|
10
|
+
export function minProductTruth(a, b) {
|
|
11
|
+
const ia = TRUTH_ORDER.indexOf(a);
|
|
12
|
+
const ib = TRUTH_ORDER.indexOf(b);
|
|
13
|
+
return TRUTH_ORDER[Math.max(ia, ib)];
|
|
14
|
+
}
|
|
15
|
+
export function truthLabelDescription(label) {
|
|
16
|
+
switch (label) {
|
|
17
|
+
case "VERIFIED":
|
|
18
|
+
return "Directly supported by repository file evidence.";
|
|
19
|
+
case "INFERRED":
|
|
20
|
+
return "Heuristic or pattern match; not fully verified.";
|
|
21
|
+
case "PARTIAL":
|
|
22
|
+
return "Some evidence exists; coverage is incomplete.";
|
|
23
|
+
case "EXPERIMENTAL":
|
|
24
|
+
return "Early capability; behavior may change.";
|
|
25
|
+
case "UNKNOWN":
|
|
26
|
+
return "Cannot be determined from current evidence.";
|
|
27
|
+
case "EXTERNAL":
|
|
28
|
+
return "Requires information outside the repository.";
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { getBrainStatus } from "../../core/brain-cli/service.js";
|
|
2
|
+
import { analyzeCodeHealth } from "../health/code-health.js";
|
|
3
|
+
import { type ProjectDna } from "../dna/build.js";
|
|
4
|
+
import type { TruthLabel } from "../truth.js";
|
|
5
|
+
export interface SoftwareDigitalTwin {
|
|
6
|
+
root: string;
|
|
7
|
+
generatedAt: string;
|
|
8
|
+
dna: ProjectDna;
|
|
9
|
+
brain: Awaited<ReturnType<typeof getBrainStatus>>;
|
|
10
|
+
graphSummary: {
|
|
11
|
+
nodeCount: number;
|
|
12
|
+
edgeCount: number;
|
|
13
|
+
truth: TruthLabel;
|
|
14
|
+
};
|
|
15
|
+
gitSummary: {
|
|
16
|
+
gitAvailable: boolean;
|
|
17
|
+
hotspotCount: number;
|
|
18
|
+
truth: TruthLabel;
|
|
19
|
+
};
|
|
20
|
+
health: Awaited<ReturnType<typeof analyzeCodeHealth>>;
|
|
21
|
+
limitations: string[];
|
|
22
|
+
}
|
|
23
|
+
export declare function buildSoftwareDigitalTwinSnapshot(rootInput: string): Promise<SoftwareDigitalTwin>;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { getBrainStatus } from "../../core/brain-cli/service.js";
|
|
2
|
+
import { buildIntelligenceGraph } from "../../intelligence/graph/build.js";
|
|
3
|
+
import { analyzeGitIntelligence } from "../../intelligence/git/analyze.js";
|
|
4
|
+
import { analyzeCodeHealth } from "../health/code-health.js";
|
|
5
|
+
import { buildProjectDna } from "../dna/build.js";
|
|
6
|
+
import { resolveRepoRoot } from "../../utils/path.js";
|
|
7
|
+
export async function buildSoftwareDigitalTwinSnapshot(rootInput) {
|
|
8
|
+
const root = resolveRepoRoot(rootInput);
|
|
9
|
+
const limitations = [
|
|
10
|
+
"Digital twin is a deterministic snapshot of local analyzers, not live runtime state.",
|
|
11
|
+
"Graph summary may be partial when graph build skips files.",
|
|
12
|
+
];
|
|
13
|
+
const [dna, brain, git] = await Promise.all([
|
|
14
|
+
buildProjectDna(root),
|
|
15
|
+
getBrainStatus(root),
|
|
16
|
+
analyzeGitIntelligence(root),
|
|
17
|
+
]);
|
|
18
|
+
let nodeCount = 0;
|
|
19
|
+
let edgeCount = 0;
|
|
20
|
+
let graphTruth = "UNKNOWN";
|
|
21
|
+
try {
|
|
22
|
+
const graph = await buildIntelligenceGraph({ root, mode: "auto" });
|
|
23
|
+
nodeCount = graph.nodes.length;
|
|
24
|
+
edgeCount = graph.edges.length;
|
|
25
|
+
graphTruth = "INFERRED";
|
|
26
|
+
if (graph.limitations?.length) {
|
|
27
|
+
limitations.push(...graph.limitations.slice(0, 3));
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
catch {
|
|
31
|
+
limitations.push("Intelligence graph build failed for twin snapshot.");
|
|
32
|
+
}
|
|
33
|
+
const healthWithGit = await analyzeCodeHealth(root, { gitReport: git });
|
|
34
|
+
return {
|
|
35
|
+
root,
|
|
36
|
+
generatedAt: new Date().toISOString(),
|
|
37
|
+
dna,
|
|
38
|
+
brain,
|
|
39
|
+
graphSummary: {
|
|
40
|
+
nodeCount,
|
|
41
|
+
edgeCount,
|
|
42
|
+
truth: graphTruth,
|
|
43
|
+
},
|
|
44
|
+
gitSummary: {
|
|
45
|
+
gitAvailable: git.gitAvailable,
|
|
46
|
+
hotspotCount: git.hotspots.length,
|
|
47
|
+
truth: git.gitAvailable ? "INFERRED" : "UNKNOWN",
|
|
48
|
+
},
|
|
49
|
+
health: healthWithGit,
|
|
50
|
+
limitations,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { SoftwareDigitalTwin } from "./digital-twin.js";
|
|
2
|
+
export interface TwinSnapshotMeta {
|
|
3
|
+
root: string;
|
|
4
|
+
generatedAt: string;
|
|
5
|
+
invalidationHash: string;
|
|
6
|
+
changedFiles?: string[];
|
|
7
|
+
}
|
|
8
|
+
export interface StoredTwinSnapshot {
|
|
9
|
+
meta: TwinSnapshotMeta;
|
|
10
|
+
twin: SoftwareDigitalTwin;
|
|
11
|
+
}
|
|
12
|
+
export declare function loadTwinSnapshot(rootInput: string): Promise<StoredTwinSnapshot | null>;
|
|
13
|
+
export declare function saveTwinSnapshot(rootInput: string, twin: SoftwareDigitalTwin, meta: Omit<TwinSnapshotMeta, "generatedAt"> & {
|
|
14
|
+
generatedAt?: string;
|
|
15
|
+
}): Promise<string>;
|
|
16
|
+
export declare function invalidateTwinSnapshot(rootInput: string): Promise<void>;
|
|
17
|
+
export declare function updateTwin(rootInput: string, options?: {
|
|
18
|
+
changedFiles?: string[];
|
|
19
|
+
force?: boolean;
|
|
20
|
+
}): Promise<SoftwareDigitalTwin>;
|
|
21
|
+
export declare function buildSoftwareDigitalTwin(rootInput: string): Promise<SoftwareDigitalTwin>;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { DEFAULT_MAX_FILE_SIZE_BYTES } from "../../constants.js";
|
|
5
|
+
import { atomicWriteTextFile, pathExists, readJsonFile } from "../../utils/fs.js";
|
|
6
|
+
import { resolveRepoRoot } from "../../utils/path.js";
|
|
7
|
+
import { buildSoftwareDigitalTwinSnapshot } from "./digital-twin.js";
|
|
8
|
+
import { OWNERSHIP_BOUNDARY_VERSION } from "../../project/ownership.js";
|
|
9
|
+
function twinDir(root) {
|
|
10
|
+
return path.join(resolveRepoRoot(root), ".agentdoctor", "twin");
|
|
11
|
+
}
|
|
12
|
+
function snapshotPath(root) {
|
|
13
|
+
return path.join(twinDir(root), "snapshot.json");
|
|
14
|
+
}
|
|
15
|
+
function hashInputs(changedFiles) {
|
|
16
|
+
const payload = [
|
|
17
|
+
`ownership-boundary:${OWNERSHIP_BOUNDARY_VERSION}`,
|
|
18
|
+
...(changedFiles ?? []).slice().sort(),
|
|
19
|
+
].join("\n");
|
|
20
|
+
return createHash("sha256").update(payload).digest("hex").slice(0, 16);
|
|
21
|
+
}
|
|
22
|
+
export async function loadTwinSnapshot(rootInput) {
|
|
23
|
+
const root = resolveRepoRoot(rootInput);
|
|
24
|
+
const parsed = await readJsonFile(snapshotPath(root), DEFAULT_MAX_FILE_SIZE_BYTES);
|
|
25
|
+
if (!parsed.ok)
|
|
26
|
+
return null;
|
|
27
|
+
if (parsed.data.meta.root !== root)
|
|
28
|
+
return null;
|
|
29
|
+
// Reject twins built under a prior ownership boundary (or any mismatched hash).
|
|
30
|
+
const expected = hashInputs(parsed.data.meta.changedFiles);
|
|
31
|
+
if (parsed.data.meta.invalidationHash !== expected)
|
|
32
|
+
return null;
|
|
33
|
+
return parsed.data;
|
|
34
|
+
}
|
|
35
|
+
export async function saveTwinSnapshot(rootInput, twin, meta) {
|
|
36
|
+
const root = resolveRepoRoot(rootInput);
|
|
37
|
+
const file = snapshotPath(root);
|
|
38
|
+
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
39
|
+
const stored = {
|
|
40
|
+
meta: {
|
|
41
|
+
root,
|
|
42
|
+
generatedAt: meta.generatedAt ?? new Date().toISOString(),
|
|
43
|
+
invalidationHash: meta.invalidationHash,
|
|
44
|
+
...(meta.changedFiles?.length ? { changedFiles: meta.changedFiles } : {}),
|
|
45
|
+
},
|
|
46
|
+
twin,
|
|
47
|
+
};
|
|
48
|
+
await atomicWriteTextFile(file, `${JSON.stringify(stored, null, 2)}\n`);
|
|
49
|
+
return file;
|
|
50
|
+
}
|
|
51
|
+
export async function invalidateTwinSnapshot(rootInput) {
|
|
52
|
+
const root = resolveRepoRoot(rootInput);
|
|
53
|
+
const file = snapshotPath(root);
|
|
54
|
+
if (await pathExists(file)) {
|
|
55
|
+
await fs.rm(file, { force: true });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
export async function updateTwin(rootInput, options) {
|
|
59
|
+
const root = resolveRepoRoot(rootInput);
|
|
60
|
+
const invalidationHash = hashInputs(options?.changedFiles);
|
|
61
|
+
const existing = options?.force ? null : await loadTwinSnapshot(root);
|
|
62
|
+
if (existing &&
|
|
63
|
+
existing.meta.invalidationHash === invalidationHash &&
|
|
64
|
+
existing.twin.root === root) {
|
|
65
|
+
return existing.twin;
|
|
66
|
+
}
|
|
67
|
+
await invalidateTwinSnapshot(root);
|
|
68
|
+
const twin = await buildSoftwareDigitalTwinSnapshot(root);
|
|
69
|
+
await saveTwinSnapshot(root, twin, {
|
|
70
|
+
root,
|
|
71
|
+
invalidationHash,
|
|
72
|
+
...(options?.changedFiles?.length ? { changedFiles: options.changedFiles } : {}),
|
|
73
|
+
});
|
|
74
|
+
return twin;
|
|
75
|
+
}
|
|
76
|
+
export async function buildSoftwareDigitalTwin(rootInput) {
|
|
77
|
+
return updateTwin(rootInput);
|
|
78
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { TruthLabel } from "../truth.js";
|
|
2
|
+
export interface WhatIfAffectedItem {
|
|
3
|
+
pathOrSymbol: string;
|
|
4
|
+
role: "changed" | "caller" | "callee" | "reverse-dependency";
|
|
5
|
+
truth: TruthLabel;
|
|
6
|
+
}
|
|
7
|
+
export interface WhatIfReport {
|
|
8
|
+
root: string;
|
|
9
|
+
target: string;
|
|
10
|
+
affected: WhatIfAffectedItem[];
|
|
11
|
+
limitations: string[];
|
|
12
|
+
}
|
|
13
|
+
export declare function analyzeWhatIf(rootInput: string, target: string, options?: {
|
|
14
|
+
graph?: {
|
|
15
|
+
nodes: Array<{
|
|
16
|
+
id: string;
|
|
17
|
+
kind: string;
|
|
18
|
+
label: string;
|
|
19
|
+
path?: string;
|
|
20
|
+
}>;
|
|
21
|
+
edges: Array<{
|
|
22
|
+
from: string;
|
|
23
|
+
to: string;
|
|
24
|
+
kind: string;
|
|
25
|
+
}>;
|
|
26
|
+
};
|
|
27
|
+
buildGraph?: boolean;
|
|
28
|
+
}): Promise<WhatIfReport>;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { deriveGraphChangeImpact } from "../../assurance/change.js";
|
|
2
|
+
import { buildIntelligenceGraph } from "../../intelligence/graph/build.js";
|
|
3
|
+
import { assertProjectOwnedRepoPath, classifyRelativePathOwnership, ProjectOwnershipError, } from "../../project/ownership.js";
|
|
4
|
+
import { PathEscapeError, resolveSafeRepoPath } from "../../security/paths.js";
|
|
5
|
+
import { resolveRepoRoot } from "../../utils/path.js";
|
|
6
|
+
import { minProductTruth } from "../truth.js";
|
|
7
|
+
function normalizeTarget(target) {
|
|
8
|
+
return target.replace(/\\/g, "/").replace(/^\.\//, "");
|
|
9
|
+
}
|
|
10
|
+
async function assertOwnedWhatIfTarget(root, target) {
|
|
11
|
+
const normalized = normalizeTarget(target);
|
|
12
|
+
const lexical = classifyRelativePathOwnership(normalized);
|
|
13
|
+
if (lexical !== "project_owned") {
|
|
14
|
+
throw new ProjectOwnershipError(lexical, `what-if target outside project ownership (${lexical})`);
|
|
15
|
+
}
|
|
16
|
+
try {
|
|
17
|
+
const abs = resolveSafeRepoPath(root, normalized);
|
|
18
|
+
await assertProjectOwnedRepoPath(root, abs, normalized);
|
|
19
|
+
}
|
|
20
|
+
catch (error) {
|
|
21
|
+
if (error instanceof PathEscapeError || error instanceof ProjectOwnershipError) {
|
|
22
|
+
throw error;
|
|
23
|
+
}
|
|
24
|
+
// Non-existent owned path remains a valid hypothetical change target.
|
|
25
|
+
}
|
|
26
|
+
return normalized;
|
|
27
|
+
}
|
|
28
|
+
export async function analyzeWhatIf(rootInput, target, options) {
|
|
29
|
+
const root = resolveRepoRoot(rootInput);
|
|
30
|
+
const normalized = await assertOwnedWhatIfTarget(root, target);
|
|
31
|
+
const limitations = [
|
|
32
|
+
"Impact analysis uses intelligence graph import/call edges when available.",
|
|
33
|
+
"Symbol-level precision depends on graph node coverage.",
|
|
34
|
+
"What-if targets must be project-owned paths (containment alone is not ownership).",
|
|
35
|
+
];
|
|
36
|
+
let graph = options?.graph;
|
|
37
|
+
if (!graph && options?.buildGraph !== false) {
|
|
38
|
+
try {
|
|
39
|
+
graph = await buildIntelligenceGraph({ root, mode: "auto" });
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
limitations.push("Graph build failed; impact may be empty.");
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
if (!graph) {
|
|
46
|
+
limitations.push("No graph provided or built; only the target file is listed.");
|
|
47
|
+
return {
|
|
48
|
+
root,
|
|
49
|
+
target: normalized,
|
|
50
|
+
affected: [
|
|
51
|
+
{
|
|
52
|
+
pathOrSymbol: normalized,
|
|
53
|
+
role: "changed",
|
|
54
|
+
truth: "VERIFIED",
|
|
55
|
+
},
|
|
56
|
+
],
|
|
57
|
+
limitations,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
const changedFiles = [{ path: normalized, kind: "modified" }];
|
|
61
|
+
const impact = deriveGraphChangeImpact(graph, changedFiles, []);
|
|
62
|
+
const affected = [
|
|
63
|
+
{
|
|
64
|
+
pathOrSymbol: normalized,
|
|
65
|
+
role: "changed",
|
|
66
|
+
truth: "VERIFIED",
|
|
67
|
+
},
|
|
68
|
+
];
|
|
69
|
+
for (const c of impact.callers) {
|
|
70
|
+
affected.push({
|
|
71
|
+
pathOrSymbol: c,
|
|
72
|
+
role: "caller",
|
|
73
|
+
truth: minProductTruth("INFERRED", "PARTIAL"),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
for (const c of impact.callees) {
|
|
77
|
+
affected.push({
|
|
78
|
+
pathOrSymbol: c,
|
|
79
|
+
role: "callee",
|
|
80
|
+
truth: minProductTruth("INFERRED", "PARTIAL"),
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
for (const c of impact.reverseDependencies) {
|
|
84
|
+
affected.push({
|
|
85
|
+
pathOrSymbol: c,
|
|
86
|
+
role: "reverse-dependency",
|
|
87
|
+
truth: minProductTruth("INFERRED", "PARTIAL"),
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
return { root, target: normalized, affected, limitations };
|
|
91
|
+
}
|