@praneeth_54/agentdoctor 2.1.0 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (129) hide show
  1. package/CHANGELOG.md +57 -3
  2. package/README.md +489 -290
  3. package/dist/agent/chat/deterministic.d.ts +6 -0
  4. package/dist/agent/chat/deterministic.js +180 -0
  5. package/dist/agent/chat/service.js +8 -14
  6. package/dist/agent/index.d.ts +2 -0
  7. package/dist/agent/index.js +1 -0
  8. package/dist/agent/loop.d.ts +2 -0
  9. package/dist/agent/loop.js +31 -0
  10. package/dist/agent/roles.d.ts +12 -0
  11. package/dist/agent/roles.js +138 -0
  12. package/dist/agent/runtime.d.ts +21 -5
  13. package/dist/agent/runtime.js +141 -28
  14. package/dist/agent/tools/execute.d.ts +2 -0
  15. package/dist/agent/tools/execute.js +10 -0
  16. package/dist/ai/config.js +1 -0
  17. package/dist/ai/redact.d.ts +1 -0
  18. package/dist/ai/redact.js +47 -7
  19. package/dist/ai/types.d.ts +1 -1
  20. package/dist/cli/commands/agent.d.ts +2 -0
  21. package/dist/cli/commands/agent.js +19 -8
  22. package/dist/cli/commands/chat.js +2 -4
  23. package/dist/cli/commands/product.d.ts +19 -0
  24. package/dist/cli/commands/product.js +147 -0
  25. package/dist/cli/commands/start.d.ts +15 -0
  26. package/dist/cli/commands/start.js +80 -0
  27. package/dist/cli/program.js +141 -0
  28. package/dist/constants.d.ts +1 -1
  29. package/dist/constants.js +1 -1
  30. package/dist/dashboard/server.js +255 -53
  31. package/dist/index.d.ts +4 -0
  32. package/dist/index.js +2 -0
  33. package/dist/intelligence/graph/build.js +36 -8
  34. package/dist/intelligence/resolve/imports.js +1 -1
  35. package/dist/languages/dart.d.ts +10 -0
  36. package/dist/languages/dart.js +99 -0
  37. package/dist/languages/go.d.ts +13 -7
  38. package/dist/languages/go.js +84 -24
  39. package/dist/languages/index.d.ts +5 -1
  40. package/dist/languages/index.js +13 -36
  41. package/dist/languages/java.d.ts +10 -0
  42. package/dist/languages/java.js +80 -0
  43. package/dist/languages/kotlin.d.ts +10 -0
  44. package/dist/languages/kotlin.js +85 -0
  45. package/dist/languages/rust.d.ts +10 -0
  46. package/dist/languages/rust.js +93 -0
  47. package/dist/languages/types.d.ts +4 -3
  48. package/dist/mcp/agent/registry.d.ts +1 -1
  49. package/dist/mcp/agent/registry.js +109 -23
  50. package/dist/mcp/intelligence/handlers.d.ts +3 -0
  51. package/dist/mcp/intelligence/handlers.js +28 -0
  52. package/dist/mcp/intelligence/registry.d.ts +1 -1
  53. package/dist/mcp/intelligence/registry.js +30 -1
  54. package/dist/product/api/doctor.d.ts +14 -0
  55. package/dist/product/api/doctor.js +185 -0
  56. package/dist/product/api/openapi.d.ts +7 -0
  57. package/dist/product/api/openapi.js +122 -0
  58. package/dist/product/approval/model.d.ts +30 -0
  59. package/dist/product/approval/model.js +64 -0
  60. package/dist/product/approval/session.d.ts +49 -0
  61. package/dist/product/approval/session.js +134 -0
  62. package/dist/product/database/doctor.d.ts +30 -0
  63. package/dist/product/database/doctor.js +184 -0
  64. package/dist/product/decisions/ledger.d.ts +24 -0
  65. package/dist/product/decisions/ledger.js +110 -0
  66. package/dist/product/deps/analyze.d.ts +46 -0
  67. package/dist/product/deps/analyze.js +137 -0
  68. package/dist/product/deps/lockfiles.d.ts +25 -0
  69. package/dist/product/deps/lockfiles.js +200 -0
  70. package/dist/product/discovery/roots.d.ts +42 -0
  71. package/dist/product/discovery/roots.js +215 -0
  72. package/dist/product/dna/build.d.ts +50 -0
  73. package/dist/product/dna/build.js +255 -0
  74. package/dist/product/eval/lab.d.ts +17 -0
  75. package/dist/product/eval/lab.js +218 -0
  76. package/dist/product/events/doctor.d.ts +21 -0
  77. package/dist/product/events/doctor.js +147 -0
  78. package/dist/product/evidence-scan.d.ts +12 -0
  79. package/dist/product/evidence-scan.js +46 -0
  80. package/dist/product/evolution/timeline.d.ts +29 -0
  81. package/dist/product/evolution/timeline.js +123 -0
  82. package/dist/product/features/intelligence.d.ts +23 -0
  83. package/dist/product/features/intelligence.js +158 -0
  84. package/dist/product/forensic/mode.d.ts +25 -0
  85. package/dist/product/forensic/mode.js +70 -0
  86. package/dist/product/graph/enrich-languages.d.ts +20 -0
  87. package/dist/product/graph/enrich-languages.js +193 -0
  88. package/dist/product/health/code-health.d.ts +20 -0
  89. package/dist/product/health/code-health.js +149 -0
  90. package/dist/product/index.d.ts +69 -0
  91. package/dist/product/index.js +35 -0
  92. package/dist/product/ledger/change-ledger.d.ts +23 -0
  93. package/dist/product/ledger/change-ledger.js +64 -0
  94. package/dist/product/map/software-map.d.ts +17 -0
  95. package/dist/product/map/software-map.js +95 -0
  96. package/dist/product/memory/institutional.d.ts +16 -0
  97. package/dist/product/memory/institutional.js +87 -0
  98. package/dist/product/ops/incident.d.ts +20 -0
  99. package/dist/product/ops/incident.js +61 -0
  100. package/dist/product/ops/infra.d.ts +15 -0
  101. package/dist/product/ops/infra.js +112 -0
  102. package/dist/product/org/model.d.ts +34 -0
  103. package/dist/product/org/model.js +195 -0
  104. package/dist/product/privacy/doctor.d.ts +13 -0
  105. package/dist/product/privacy/doctor.js +90 -0
  106. package/dist/product/requirements/trace.d.ts +21 -0
  107. package/dist/product/requirements/trace.js +166 -0
  108. package/dist/product/search/index.d.ts +29 -0
  109. package/dist/product/search/index.js +116 -0
  110. package/dist/product/search/software-search.d.ts +19 -0
  111. package/dist/product/search/software-search.js +100 -0
  112. package/dist/product/security/doctor.d.ts +23 -0
  113. package/dist/product/security/doctor.js +124 -0
  114. package/dist/product/self/diagnose.d.ts +15 -0
  115. package/dist/product/self/diagnose.js +82 -0
  116. package/dist/product/techdebt/roadmap.d.ts +20 -0
  117. package/dist/product/techdebt/roadmap.js +118 -0
  118. package/dist/product/testbrain/analyze.d.ts +29 -0
  119. package/dist/product/testbrain/analyze.js +129 -0
  120. package/dist/product/truth.d.ts +12 -0
  121. package/dist/product/truth.js +30 -0
  122. package/dist/product/twin/digital-twin.d.ts +23 -0
  123. package/dist/product/twin/digital-twin.js +52 -0
  124. package/dist/product/twin/store.d.ts +21 -0
  125. package/dist/product/twin/store.js +70 -0
  126. package/dist/product/whatif/engine.d.ts +28 -0
  127. package/dist/product/whatif/engine.js +70 -0
  128. package/dist/utils/fs.js +10 -2
  129. package/package.json +1 -1
@@ -0,0 +1,100 @@
1
+ import path from "node:path";
2
+ import { loadLatestBrain, searchBrain } from "../../core/brain-cli/service.js";
3
+ import { detectProject } from "../../detectors/project.js";
4
+ import { readTextFile } from "../../utils/fs.js";
5
+ import { resolveRepoRoot } from "../../utils/path.js";
6
+ import { buildIndex, documentId, searchHybrid } from "./index.js";
7
+ export async function searchSymbolsAndConcepts(rootInput, query, options) {
8
+ const root = resolveRepoRoot(rootInput);
9
+ const q = query.trim();
10
+ const maxHits = options?.maxHits ?? 40;
11
+ const maxFiles = options?.maxFiles ?? 300;
12
+ const limitations = [
13
+ "Search combines TF-IDF keyword index + brain claims + bounded filename/content scan.",
14
+ "TF-IDF is lexical — not neural embeddings or semantic code search.",
15
+ "Content substring hits are INFERRED.",
16
+ ];
17
+ if (!q) {
18
+ return { root, query: q, hits: [], limitations: [...limitations, "Empty query"] };
19
+ }
20
+ const hits = [];
21
+ const qLower = q.toLowerCase();
22
+ const brain = await loadLatestBrain(root);
23
+ if (brain) {
24
+ for (const bh of searchBrain(brain, q)) {
25
+ hits.push({
26
+ kind: "brain",
27
+ path: ".agentdoctor/project-brain",
28
+ excerpt: bh.text,
29
+ score: bh.score + 0.2,
30
+ truth: "INFERRED",
31
+ });
32
+ }
33
+ }
34
+ else {
35
+ limitations.push("No project brain snapshot; brain search skipped.");
36
+ }
37
+ const detection = await detectProject(root);
38
+ let filesScanned = 0;
39
+ const indexDocs = [];
40
+ const substringHits = [];
41
+ for (const entry of detection.discovery.files) {
42
+ if (filesScanned >= maxFiles)
43
+ break;
44
+ const rel = entry.relativePath.replace(/\\/g, "/");
45
+ filesScanned += 1;
46
+ const base = path.basename(rel).toLowerCase();
47
+ if (base.includes(qLower)) {
48
+ hits.push({
49
+ kind: "filename",
50
+ path: rel,
51
+ excerpt: rel,
52
+ score: 1.1,
53
+ truth: "VERIFIED",
54
+ });
55
+ }
56
+ const text = await readTextFile(path.join(root, rel), 256 * 1024);
57
+ if (text === null)
58
+ continue;
59
+ indexDocs.push({
60
+ id: documentId(rel, text),
61
+ path: rel,
62
+ text: `${rel}\n${text.slice(0, 8_000)}`,
63
+ });
64
+ const idx = text.toLowerCase().indexOf(qLower);
65
+ if (idx >= 0) {
66
+ const line = text.slice(0, idx).split("\n").length;
67
+ const lineText = text.split(/\r?\n/)[line - 1]?.trim() ?? "";
68
+ substringHits.push({
69
+ path: rel,
70
+ line,
71
+ excerpt: lineText.slice(0, 140),
72
+ score: 1,
73
+ });
74
+ }
75
+ }
76
+ if (filesScanned >= maxFiles) {
77
+ limitations.push(`Content scan capped at ${maxFiles} files`);
78
+ }
79
+ if (indexDocs.length) {
80
+ const index = buildIndex(indexDocs);
81
+ limitations.push(...index.limitations);
82
+ for (const h of searchHybrid(index, q, substringHits, maxHits)) {
83
+ hits.push({
84
+ kind: h.kind === "tfidf" ? "tfidf" : "content",
85
+ path: h.path,
86
+ ...(h.line !== undefined ? { line: h.line } : {}),
87
+ excerpt: h.excerpt,
88
+ score: h.score,
89
+ truth: h.kind === "tfidf" ? "INFERRED" : "INFERRED",
90
+ });
91
+ }
92
+ }
93
+ hits.sort((a, b) => b.score - a.score || a.path.localeCompare(b.path));
94
+ return {
95
+ root,
96
+ query: q,
97
+ hits: hits.slice(0, maxHits),
98
+ limitations,
99
+ };
100
+ }
@@ -0,0 +1,23 @@
1
+ import { type SecretFinding } from "../../core/secrets/scan.js";
2
+ import type { ProductEvidence, TruthLabel } from "../truth.js";
3
+ export interface SecurityPatternFinding {
4
+ id: string;
5
+ title: string;
6
+ truth: TruthLabel;
7
+ evidence: ProductEvidence[];
8
+ }
9
+ export interface SecurityDoctorReport {
10
+ root: string;
11
+ /** Local heuristic rules only — not enterprise SAST. */
12
+ staticAnalysisLabel: "TECHNICAL_STATIC_ANALYSIS_SUPPORTED";
13
+ secretScan: {
14
+ enabled: boolean;
15
+ filesScanned: number;
16
+ findings: SecretFinding[];
17
+ limitations: string[];
18
+ };
19
+ authMarkers: SecurityPatternFinding[];
20
+ dangerousPatterns: SecurityPatternFinding[];
21
+ limitations: string[];
22
+ }
23
+ export declare function analyzeSecuritySurface(rootInput: string): Promise<SecurityDoctorReport>;
@@ -0,0 +1,124 @@
1
+ import { scanSecrets } from "../../core/secrets/scan.js";
2
+ import { resolveRepoRoot } from "../../utils/path.js";
3
+ import { lineNumberAt, loadProjectSourceFiles } from "../evidence-scan.js";
4
+ const AUTH_MARKERS = [
5
+ {
6
+ id: "middleware-auth",
7
+ title: "Auth middleware marker",
8
+ re: /\b(?:auth|authenticate|authorize)\s*\(/i,
9
+ },
10
+ { id: "laravel-gate", title: "Laravel Gate/Policy marker", re: /\bGate::|\bPolicy\b|\b@can\b/i },
11
+ { id: "nestjs-guard", title: "NestJS guard/decorator marker", re: /@UseGuards\b|AuthGuard\b/i },
12
+ {
13
+ id: "rbac-role",
14
+ title: "RBAC role/permission marker",
15
+ re: /\b(?:hasPermission|hasRole|@PreAuthorize|@Roles)\b/i,
16
+ },
17
+ ];
18
+ const DANGEROUS_PATTERNS = [
19
+ { id: "eval-call", title: "Dynamic eval() (CWE-95)", re: /\beval\s*\(/g },
20
+ {
21
+ id: "child-process-exec-string",
22
+ title: "child_process exec with string literal (CWE-78)",
23
+ re: /\b(?:exec|execSync)\s*\(\s*['"`]/g,
24
+ },
25
+ {
26
+ id: "child-process-exec-concat",
27
+ title: "child_process exec with string concatenation (CWE-78)",
28
+ re: /\b(?:exec|execSync)\s*\(\s*[^)]*\+/g,
29
+ },
30
+ {
31
+ id: "innerhtml-assign",
32
+ title: "DOM innerHTML assignment (CWE-79)",
33
+ re: /\.innerHTML\s*=/g,
34
+ },
35
+ {
36
+ id: "path-join-user-input",
37
+ title: "Path join with request/user input pattern (CWE-22)",
38
+ re: /\bpath\.(?:join|resolve)\s*\([^)]*(?:req\.|request\.|params\.|query\.|user[A-Z_a-z]*|input)/g,
39
+ },
40
+ { id: "shell-exec", title: "Shell invocation helper", re: /\bshell\s*:\s*true\b/g },
41
+ ];
42
+ function redactExcerpt(line) {
43
+ return line
44
+ .trim()
45
+ .slice(0, 120)
46
+ .replace(/['"][^'"]{8,}['"]/g, "'[REDACTED]'");
47
+ }
48
+ export async function analyzeSecuritySurface(rootInput) {
49
+ const root = resolveRepoRoot(rootInput);
50
+ const limitations = [
51
+ "TECHNICAL STATIC ANALYSIS SUPPORTED locally (regex/heuristic rules with file evidence — not full SAST).",
52
+ "Security doctor uses heuristic pattern scans — not a substitute for SAST/DAST or manual review.",
53
+ "Secret values are never included; only redacted snippets and rule ids.",
54
+ ];
55
+ const secretScan = await scanSecrets({ root, enabled: true, maxFiles: 400 });
56
+ const { files } = await loadProjectSourceFiles(root, (rel) => /\.(ts|tsx|js|jsx|mjs|cjs|py|php|go)$/i.test(rel));
57
+ const authMarkers = [];
58
+ const dangerousPatterns = [];
59
+ for (const file of files) {
60
+ for (const marker of AUTH_MARKERS) {
61
+ marker.re.lastIndex = 0;
62
+ if (marker.re.test(file.content)) {
63
+ const idx = file.content.search(marker.re);
64
+ authMarkers.push({
65
+ id: marker.id,
66
+ title: marker.title,
67
+ truth: "INFERRED",
68
+ evidence: [
69
+ {
70
+ path: file.relativePath,
71
+ ...(idx >= 0 ? { line: lineNumberAt(file.content, idx) } : {}),
72
+ excerpt: redactExcerpt(file.content.split(/\r?\n/)[Math.max(0, lineNumberAt(file.content, Math.max(idx, 0)) - 1)] ?? ""),
73
+ },
74
+ ],
75
+ });
76
+ break;
77
+ }
78
+ }
79
+ for (const pattern of DANGEROUS_PATTERNS) {
80
+ pattern.re.lastIndex = 0;
81
+ let match;
82
+ while ((match = pattern.re.exec(file.content)) !== null) {
83
+ const line = lineNumberAt(file.content, match.index);
84
+ const lineText = file.content.split(/\r?\n/)[line - 1] ?? "";
85
+ dangerousPatterns.push({
86
+ id: pattern.id,
87
+ title: pattern.title,
88
+ truth: "INFERRED",
89
+ evidence: [
90
+ {
91
+ path: file.relativePath,
92
+ line,
93
+ excerpt: redactExcerpt(lineText),
94
+ },
95
+ ],
96
+ });
97
+ if (dangerousPatterns.length >= 80)
98
+ break;
99
+ }
100
+ if (dangerousPatterns.length >= 80)
101
+ break;
102
+ }
103
+ if (dangerousPatterns.length >= 80) {
104
+ limitations.push("Dangerous pattern finding cap reached (80)");
105
+ break;
106
+ }
107
+ }
108
+ if (authMarkers.length === 0) {
109
+ limitations.push("No auth/permission file markers matched heuristics (absence is not proof).");
110
+ }
111
+ return {
112
+ root,
113
+ staticAnalysisLabel: "TECHNICAL_STATIC_ANALYSIS_SUPPORTED",
114
+ secretScan: {
115
+ enabled: secretScan.enabled,
116
+ filesScanned: secretScan.filesScanned,
117
+ findings: secretScan.findings,
118
+ limitations: secretScan.limitations,
119
+ },
120
+ authMarkers: authMarkers.slice(0, 40),
121
+ dangerousPatterns,
122
+ limitations,
123
+ };
124
+ }
@@ -0,0 +1,15 @@
1
+ import type { TruthLabel } from "../truth.js";
2
+ export interface SelfDiagnoseFinding {
3
+ id: string;
4
+ label: string;
5
+ ok: boolean;
6
+ truth: TruthLabel;
7
+ evidence: string;
8
+ }
9
+ export interface SelfDiagnoseReport {
10
+ root: string;
11
+ ok: boolean;
12
+ findings: SelfDiagnoseFinding[];
13
+ limitations: string[];
14
+ }
15
+ export declare function diagnoseAgentDoctorSelf(rootInput?: string): Promise<SelfDiagnoseReport>;
@@ -0,0 +1,82 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { detectProject } from "../../detectors/project.js";
4
+ import { resolveRepoRoot } from "../../utils/path.js";
5
+ import { pathExists } from "../../utils/fs.js";
6
+ const REQUIRED_MODULES = [
7
+ "src/security/paths.ts",
8
+ "src/enforcement/runner.ts",
9
+ "src/assurance/change.ts",
10
+ "src/intelligence/graph/build.ts",
11
+ ];
12
+ export async function diagnoseAgentDoctorSelf(rootInput) {
13
+ const root = resolveRepoRoot(rootInput ?? process.cwd());
14
+ const limitations = [
15
+ "Self-check reports VERIFIED findings only when files/signals are present on disk.",
16
+ "Does not validate npm publish or CI status.",
17
+ ];
18
+ const findings = [];
19
+ try {
20
+ const detection = await detectProject(root);
21
+ const langs = detection.repository.languages ?? [];
22
+ findings.push({
23
+ id: "detect-project",
24
+ label: "detectProject completes",
25
+ ok: true,
26
+ truth: "VERIFIED",
27
+ evidence: `languages=${langs.join(",") || "none"}`,
28
+ });
29
+ }
30
+ catch (error) {
31
+ findings.push({
32
+ id: "detect-project",
33
+ label: "detectProject completes",
34
+ ok: false,
35
+ truth: "VERIFIED",
36
+ evidence: error instanceof Error ? error.message : String(error),
37
+ });
38
+ }
39
+ const testsDir = path.join(root, "tests");
40
+ const testsExists = await pathExists(testsDir);
41
+ findings.push({
42
+ id: "tests-dir",
43
+ label: "tests/ directory exists",
44
+ ok: testsExists,
45
+ truth: testsExists ? "VERIFIED" : "VERIFIED",
46
+ evidence: testsExists ? "tests/" : "missing tests/",
47
+ });
48
+ const pkgPath = path.join(root, "package.json");
49
+ let scriptsOk = false;
50
+ let scriptEvidence = "no package.json";
51
+ if (await pathExists(pkgPath)) {
52
+ try {
53
+ const pkg = JSON.parse(await fs.readFile(pkgPath, "utf8"));
54
+ const scripts = pkg.scripts ?? {};
55
+ scriptsOk = typeof scripts.test === "string";
56
+ scriptEvidence = scriptsOk ? `test script present` : "test script missing";
57
+ }
58
+ catch {
59
+ scriptEvidence = "package.json unreadable";
60
+ }
61
+ }
62
+ findings.push({
63
+ id: "package-test-script",
64
+ label: "package.json defines test script",
65
+ ok: scriptsOk,
66
+ truth: "VERIFIED",
67
+ evidence: scriptEvidence,
68
+ });
69
+ for (const rel of REQUIRED_MODULES) {
70
+ const abs = path.join(root, rel);
71
+ const ok = await pathExists(abs);
72
+ findings.push({
73
+ id: `module-${rel.replace(/[^\w]/g, "-")}`,
74
+ label: `Required module ${rel}`,
75
+ ok,
76
+ truth: "VERIFIED",
77
+ evidence: ok ? "present" : "missing",
78
+ });
79
+ }
80
+ const ok = findings.every((f) => f.ok);
81
+ return { root, ok, findings, limitations };
82
+ }
@@ -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;