@praneeth_54/agentdoctor 1.1.1 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (224) hide show
  1. package/CHANGELOG.md +164 -7
  2. package/README.md +263 -513
  3. package/dist/agents/aider/adapter.d.ts +1 -0
  4. package/dist/agents/aider/adapter.js +1 -0
  5. package/dist/agents/aider/detector.d.ts +14 -0
  6. package/dist/agents/aider/detector.js +120 -0
  7. package/dist/agents/copilot/adapter.d.ts +1 -0
  8. package/dist/agents/copilot/adapter.js +1 -0
  9. package/dist/agents/copilot/detector.d.ts +13 -0
  10. package/dist/agents/copilot/detector.js +113 -0
  11. package/dist/agents/gemini/adapter.d.ts +1 -0
  12. package/dist/agents/gemini/adapter.js +1 -0
  13. package/dist/agents/gemini/detector.d.ts +13 -0
  14. package/dist/agents/gemini/detector.js +151 -0
  15. package/dist/agents/registry.js +13 -1
  16. package/dist/agents/types.d.ts +1 -1
  17. package/dist/agents/windsurf/adapter.d.ts +1 -0
  18. package/dist/agents/windsurf/adapter.js +1 -0
  19. package/dist/agents/windsurf/detector.d.ts +14 -0
  20. package/dist/agents/windsurf/detector.js +119 -0
  21. package/dist/architecture/c4.d.ts +22 -0
  22. package/dist/architecture/c4.js +62 -0
  23. package/dist/architecture/contract.d.ts +64 -0
  24. package/dist/architecture/contract.js +373 -0
  25. package/dist/assurance/change.d.ts +211 -0
  26. package/dist/assurance/change.js +624 -0
  27. package/dist/assurance/proof.d.ts +136 -0
  28. package/dist/assurance/proof.js +338 -0
  29. package/dist/auth/index.d.ts +81 -0
  30. package/dist/auth/index.js +130 -0
  31. package/dist/auth/rbac.d.ts +9 -0
  32. package/dist/auth/rbac.js +26 -0
  33. package/dist/cli/commands/architecture.d.ts +6 -0
  34. package/dist/cli/commands/architecture.js +109 -0
  35. package/dist/cli/commands/assurance.d.ts +67 -0
  36. package/dist/cli/commands/assurance.js +210 -0
  37. package/dist/cli/commands/brain.d.ts +12 -0
  38. package/dist/cli/commands/brain.js +162 -0
  39. package/dist/cli/commands/complete.d.ts +23 -0
  40. package/dist/cli/commands/complete.js +232 -0
  41. package/dist/cli/commands/explain.js +1 -1
  42. package/dist/cli/commands/fix.js +22 -2
  43. package/dist/cli/commands/mcp.d.ts +8 -0
  44. package/dist/cli/commands/mcp.js +14 -0
  45. package/dist/cli/commands/platform.d.ts +17 -0
  46. package/dist/cli/commands/platform.js +195 -0
  47. package/dist/cli/commands/policy-graph-run.d.ts +40 -0
  48. package/dist/cli/commands/policy-graph-run.js +200 -0
  49. package/dist/cli/commands/v2.d.ts +55 -0
  50. package/dist/cli/commands/v2.js +278 -0
  51. package/dist/cli/commands/workspace.d.ts +9 -0
  52. package/dist/cli/commands/workspace.js +82 -0
  53. package/dist/cli/program.js +1328 -3
  54. package/dist/constants.d.ts +3 -2
  55. package/dist/constants.js +5 -1
  56. package/dist/contracts/adapters.d.ts +7 -0
  57. package/dist/contracts/adapters.js +60 -0
  58. package/dist/contracts/index.d.ts +162 -0
  59. package/dist/contracts/index.js +18 -0
  60. package/dist/core/baseline/store.d.ts +48 -0
  61. package/dist/core/baseline/store.js +163 -0
  62. package/dist/core/brain-cli/service.d.ts +26 -0
  63. package/dist/core/brain-cli/service.js +175 -0
  64. package/dist/core/brain-product/init.d.ts +45 -0
  65. package/dist/core/brain-product/init.js +235 -0
  66. package/dist/core/changes/analyze.d.ts +41 -0
  67. package/dist/core/changes/analyze.js +223 -0
  68. package/dist/core/context-health/analyze.d.ts +15 -0
  69. package/dist/core/context-health/analyze.js +234 -0
  70. package/dist/core/fix/apply.d.ts +21 -4
  71. package/dist/core/fix/apply.js +118 -18
  72. package/dist/core/fix/backup.d.ts +30 -0
  73. package/dist/core/fix/backup.js +193 -0
  74. package/dist/core/fix/plan.d.ts +6 -3
  75. package/dist/core/fix/plan.js +113 -9
  76. package/dist/core/fix/render.d.ts +2 -0
  77. package/dist/core/fix/render.js +36 -21
  78. package/dist/core/fix/run.js +5 -1
  79. package/dist/core/fix/safe-target.d.ts +14 -0
  80. package/dist/core/fix/safe-target.js +81 -0
  81. package/dist/core/fix/types.d.ts +12 -1
  82. package/dist/core/fix/types.js +2 -0
  83. package/dist/core/fix/writers/claude-settings.js +3 -1
  84. package/dist/core/fix/writers/codex-config.d.ts +3 -0
  85. package/dist/core/fix/writers/codex-config.js +24 -3
  86. package/dist/core/fix/writers/cursorignore.js +3 -1
  87. package/dist/core/fix/writers/simple-ignore.d.ts +16 -0
  88. package/dist/core/fix/writers/simple-ignore.js +68 -0
  89. package/dist/core/monorepo/detect.d.ts +13 -0
  90. package/dist/core/monorepo/detect.js +121 -0
  91. package/dist/core/rules/build-context.js +22 -3
  92. package/dist/core/rules/context/generated-directory.js +11 -1
  93. package/dist/core/rules/context/large-instruction-file.js +6 -0
  94. package/dist/core/rules/context/large-log-file.js +16 -1
  95. package/dist/core/rules/ignore.d.ts +11 -1
  96. package/dist/core/rules/ignore.js +22 -1
  97. package/dist/core/rules/instructions/empty-instructions.js +7 -0
  98. package/dist/core/rules/security/env-file-exposure.js +23 -2
  99. package/dist/core/rules/security/private-key-file.js +1 -1
  100. package/dist/core/scanner/scan.js +1 -1
  101. package/dist/core/schemas/validate.d.ts +6 -0
  102. package/dist/core/schemas/validate.js +62 -0
  103. package/dist/core/scoring/compute-scores.d.ts +1 -1
  104. package/dist/core/scoring/compute-scores.js +5 -1
  105. package/dist/core/scoring/placeholder.d.ts +1 -1
  106. package/dist/core/scoring/placeholder.js +5 -1
  107. package/dist/core/secrets/scan.d.ts +29 -0
  108. package/dist/core/secrets/scan.js +172 -0
  109. package/dist/coverage/cobertura.d.ts +6 -0
  110. package/dist/coverage/cobertura.js +52 -0
  111. package/dist/coverage/istanbul.d.ts +7 -0
  112. package/dist/coverage/istanbul.js +134 -0
  113. package/dist/coverage/lcov.d.ts +7 -0
  114. package/dist/coverage/lcov.js +59 -0
  115. package/dist/coverage/load.d.ts +9 -0
  116. package/dist/coverage/load.js +39 -0
  117. package/dist/coverage/types.d.ts +23 -0
  118. package/dist/coverage/types.js +5 -0
  119. package/dist/dashboard/server.d.ts +24 -0
  120. package/dist/dashboard/server.js +402 -0
  121. package/dist/enforcement/runner.d.ts +67 -0
  122. package/dist/enforcement/runner.js +460 -0
  123. package/dist/index.d.ts +34 -0
  124. package/dist/index.js +25 -0
  125. package/dist/integrations/github/pr-analyze.d.ts +28 -0
  126. package/dist/integrations/github/pr-analyze.js +98 -0
  127. package/dist/integrations/local-ai/provider.d.ts +45 -0
  128. package/dist/integrations/local-ai/provider.js +104 -0
  129. package/dist/intelligence/git/analyze.d.ts +34 -0
  130. package/dist/intelligence/git/analyze.js +136 -0
  131. package/dist/intelligence/graph/build.d.ts +15 -0
  132. package/dist/intelligence/graph/build.js +259 -0
  133. package/dist/intelligence/graph/incremental.d.ts +68 -0
  134. package/dist/intelligence/graph/incremental.js +234 -0
  135. package/dist/intelligence/resolve/imports.d.ts +36 -0
  136. package/dist/intelligence/resolve/imports.js +245 -0
  137. package/dist/knowledge/store.d.ts +25 -0
  138. package/dist/knowledge/store.js +91 -0
  139. package/dist/languages/go.d.ts +9 -0
  140. package/dist/languages/go.js +45 -0
  141. package/dist/languages/index.d.ts +11 -0
  142. package/dist/languages/index.js +76 -0
  143. package/dist/languages/php.d.ts +7 -0
  144. package/dist/languages/php.js +193 -0
  145. package/dist/languages/python.d.ts +7 -0
  146. package/dist/languages/python.js +150 -0
  147. package/dist/languages/types.d.ts +42 -0
  148. package/dist/languages/types.js +26 -0
  149. package/dist/languages/typescript.d.ts +3 -0
  150. package/dist/languages/typescript.js +95 -0
  151. package/dist/mcp/agentdoctor/server.d.ts +16 -0
  152. package/dist/mcp/agentdoctor/server.js +67 -0
  153. package/dist/mcp/brain/session.js +7 -7
  154. package/dist/mcp/intelligence/handlers.d.ts +16 -0
  155. package/dist/mcp/intelligence/handlers.js +364 -0
  156. package/dist/mcp/intelligence/path-safety.d.ts +9 -0
  157. package/dist/mcp/intelligence/path-safety.js +63 -0
  158. package/dist/mcp/intelligence/registry.d.ts +8 -0
  159. package/dist/mcp/intelligence/registry.js +228 -0
  160. package/dist/ops/health.d.ts +18 -0
  161. package/dist/ops/health.js +51 -0
  162. package/dist/platform/ai-quality/analyze.d.ts +9 -0
  163. package/dist/platform/ai-quality/analyze.js +65 -0
  164. package/dist/platform/architecture/drift.d.ts +17 -0
  165. package/dist/platform/architecture/drift.js +74 -0
  166. package/dist/platform/auth/local.d.ts +15 -0
  167. package/dist/platform/auth/local.js +58 -0
  168. package/dist/platform/context-security/analyze.d.ts +6 -0
  169. package/dist/platform/context-security/analyze.js +97 -0
  170. package/dist/platform/firewall/evaluate.d.ts +57 -0
  171. package/dist/platform/firewall/evaluate.js +359 -0
  172. package/dist/platform/graph/build.d.ts +6 -0
  173. package/dist/platform/graph/build.js +189 -0
  174. package/dist/platform/health/analyze.d.ts +7 -0
  175. package/dist/platform/health/analyze.js +192 -0
  176. package/dist/platform/index.d.ts +27 -0
  177. package/dist/platform/index.js +89 -0
  178. package/dist/platform/knowledge/analyze.d.ts +20 -0
  179. package/dist/platform/knowledge/analyze.js +96 -0
  180. package/dist/platform/provenance/build.d.ts +44 -0
  181. package/dist/platform/provenance/build.js +65 -0
  182. package/dist/platform/readiness/scorecard.d.ts +5 -0
  183. package/dist/platform/readiness/scorecard.js +98 -0
  184. package/dist/platform/refactor/impact.d.ts +20 -0
  185. package/dist/platform/refactor/impact.js +60 -0
  186. package/dist/platform/reports/export.d.ts +12 -0
  187. package/dist/platform/reports/export.js +92 -0
  188. package/dist/platform/security/redact.d.ts +15 -0
  189. package/dist/platform/security/redact.js +87 -0
  190. package/dist/platform/sessions/store.d.ts +32 -0
  191. package/dist/platform/sessions/store.js +97 -0
  192. package/dist/platform/store.d.ts +6 -0
  193. package/dist/platform/store.js +57 -0
  194. package/dist/platform/test-impact/analyze.d.ts +50 -0
  195. package/dist/platform/test-impact/analyze.js +350 -0
  196. package/dist/platform/time-machine/compare.d.ts +17 -0
  197. package/dist/platform/time-machine/compare.js +62 -0
  198. package/dist/platform/tokens/plan.d.ts +28 -0
  199. package/dist/platform/tokens/plan.js +108 -0
  200. package/dist/platform/types.d.ts +120 -0
  201. package/dist/platform/types.js +5 -0
  202. package/dist/plugins/runtime.d.ts +36 -0
  203. package/dist/plugins/runtime.js +101 -0
  204. package/dist/plugins/sdk.d.ts +29 -0
  205. package/dist/plugins/sdk.js +114 -0
  206. package/dist/policy/compose.d.ts +34 -0
  207. package/dist/policy/compose.js +118 -0
  208. package/dist/policy/packs.d.ts +9 -0
  209. package/dist/policy/packs.js +91 -0
  210. package/dist/reporters/terminal/report.js +38 -5
  211. package/dist/security/paths.d.ts +21 -0
  212. package/dist/security/paths.js +105 -0
  213. package/dist/storage/postgres.d.ts +26 -0
  214. package/dist/storage/postgres.js +90 -0
  215. package/dist/storage/provider.d.ts +30 -0
  216. package/dist/storage/provider.js +109 -0
  217. package/dist/storage/sqlite.d.ts +33 -0
  218. package/dist/storage/sqlite.js +77 -0
  219. package/dist/team/auth.d.ts +35 -0
  220. package/dist/team/auth.js +70 -0
  221. package/dist/types/index.d.ts +5 -1
  222. package/dist/workspace/index.d.ts +69 -0
  223. package/dist/workspace/index.js +220 -0
  224. package/package.json +15 -5
@@ -0,0 +1,350 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { analyzeChanges } from "../../core/changes/analyze.js";
4
+ import { loadCoverage } from "../../coverage/load.js";
5
+ import { normalizeCoveragePath } from "../../coverage/types.js";
6
+ import { resolveRepoRoot } from "../../utils/path.js";
7
+ import { writeJsonArtifact } from "../store.js";
8
+ function modulesForPath(file, attributed) {
9
+ const mods = new Set(attributed ?? []);
10
+ const parts = file.split("/");
11
+ if (parts[0] === "src" && parts[1])
12
+ mods.add(parts[1]);
13
+ if (parts[0] === "tests" && parts[1])
14
+ mods.add(parts[1]);
15
+ const base = path.basename(file, path.extname(file));
16
+ if (base && !base.startsWith("."))
17
+ mods.add(base);
18
+ return [...mods].sort();
19
+ }
20
+ async function fileExists(root, rel) {
21
+ try {
22
+ await fs.access(path.join(root, rel));
23
+ return true;
24
+ }
25
+ catch {
26
+ return false;
27
+ }
28
+ }
29
+ function isTestPath(file) {
30
+ return /\.(test|spec)\./i.test(file) || file.includes("__tests__");
31
+ }
32
+ function heuristicCandidates(file) {
33
+ const ext = path.extname(file);
34
+ const base = ext ? file.slice(0, -ext.length) : file;
35
+ return [
36
+ `${base}.test${ext || ".ts"}`,
37
+ `${base}.spec${ext || ".ts"}`,
38
+ file.replace(/\/src\//, "/tests/"),
39
+ file.replace(/^src\//, "tests/"),
40
+ ].filter((c, i, arr) => c !== file && arr.indexOf(c) === i);
41
+ }
42
+ function coverageFileFor(changed, coverage) {
43
+ const norm = normalizeCoveragePath(changed);
44
+ if (coverage.files[norm])
45
+ return norm;
46
+ // Match by suffix (absolute paths in LCOV vs repo-relative changed paths)
47
+ const keys = Object.keys(coverage.files);
48
+ const suffixHit = keys.find((k) => k === norm || k.endsWith(`/${norm}`) || norm.endsWith(`/${k}`));
49
+ return suffixHit ?? null;
50
+ }
51
+ function countLineHits(fileCov) {
52
+ if (!fileCov)
53
+ return { covered: 0, uncovered: 0, hits: 0 };
54
+ let covered = 0;
55
+ let uncovered = 0;
56
+ let hits = 0;
57
+ for (const h of Object.values(fileCov.lines)) {
58
+ if (h > 0) {
59
+ covered += 1;
60
+ hits += h;
61
+ }
62
+ else {
63
+ uncovered += 1;
64
+ }
65
+ }
66
+ return { covered, uncovered, hits };
67
+ }
68
+ function testsFromCoverageMap(coverage, sourceKey) {
69
+ const fileCov = coverage.files[sourceKey];
70
+ const fromFile = fileCov?.coveredByTests ?? [];
71
+ const fromMap = [];
72
+ if (coverage.testToFiles) {
73
+ for (const [test, sources] of Object.entries(coverage.testToFiles)) {
74
+ if (sources.some((s) => s === sourceKey || s.endsWith(`/${sourceKey}`) || sourceKey.endsWith(`/${s}`))) {
75
+ fromMap.push(test);
76
+ }
77
+ }
78
+ }
79
+ return [...new Set([...fromFile, ...fromMap].map(normalizeCoveragePath))].sort();
80
+ }
81
+ /**
82
+ * Module H — Test impact analysis.
83
+ * Heuristic by default; coverage-backed when a coverage file is loaded and used
84
+ * for changed-line evidence. Never claims tests passed.
85
+ */
86
+ export async function analyzeTestImpact(rootInput, maybeOptions) {
87
+ // Support both analyzeTestImpact(root) and analyzeTestImpact({ root, ... })
88
+ const options = typeof rootInput === "string" ? { ...(maybeOptions ?? {}), root: rootInput } : rootInput;
89
+ const root = resolveRepoRoot(options.root ?? process.cwd());
90
+ const changes = await analyzeChanges({
91
+ root,
92
+ impact: true,
93
+ ...(options.since ? { since: options.since } : {}),
94
+ });
95
+ const changed = changes.files.map((f) => f.path);
96
+ const recommended = [];
97
+ const uncovered = [];
98
+ const missingTestWarnings = [];
99
+ const allModules = new Set();
100
+ const limitations = [
101
+ "Does not execute tests; never claims pass/fail",
102
+ ...changes.limitations,
103
+ ];
104
+ let coverage = null;
105
+ let coverageSummary = null;
106
+ let mode = "heuristic";
107
+ let testAttribution = "none";
108
+ if (options.coveragePath) {
109
+ try {
110
+ coverage = await loadCoverage(options.coveragePath);
111
+ if (Object.keys(coverage.files).length === 0) {
112
+ limitations.push(`Coverage path provided but no file hits loaded (${coverage.format}): ${options.coveragePath}`);
113
+ limitations.push(...coverage.limitations);
114
+ coverage = null;
115
+ }
116
+ else {
117
+ limitations.push(...coverage.limitations);
118
+ }
119
+ }
120
+ catch (error) {
121
+ limitations.push(`Failed to load coverage from ${options.coveragePath}: ${error instanceof Error ? error.message : String(error)}`);
122
+ coverage = null;
123
+ }
124
+ }
125
+ if (!changes.gitAvailable) {
126
+ return {
127
+ root,
128
+ gitAvailable: false,
129
+ mode: "heuristic",
130
+ changedFiles: [],
131
+ recommendedTests: [],
132
+ relatedModules: [],
133
+ uncoveredAreas: [],
134
+ missingTestWarnings: [],
135
+ skipRisk: "Git not available — cannot infer changed files",
136
+ coverage: null,
137
+ limitations: ["Requires a git working tree to detect changes", ...limitations],
138
+ };
139
+ }
140
+ let changedCoveredLines = 0;
141
+ let changedUncoveredLines = 0;
142
+ let coverageHits = 0;
143
+ let filesWithHits = 0;
144
+ let usedCoverageForChangedLines = false;
145
+ let usedCoverageTestMap = false;
146
+ let usedHeuristicForTests = false;
147
+ for (const fileEntry of changes.files) {
148
+ const file = fileEntry.path;
149
+ const relatedModules = modulesForPath(file, fileEntry.modules);
150
+ for (const m of relatedModules)
151
+ allModules.add(m);
152
+ if (isTestPath(file)) {
153
+ recommended.push({
154
+ testPath: file,
155
+ reason: "Changed file is itself a test",
156
+ confidence: 0.95,
157
+ evidence: "verified",
158
+ exists: true,
159
+ relatedModules,
160
+ });
161
+ continue;
162
+ }
163
+ // Coverage-backed changed-line evidence
164
+ if (coverage) {
165
+ const covKey = coverageFileFor(file, coverage);
166
+ if (covKey) {
167
+ const counts = countLineHits(coverage.files[covKey]);
168
+ changedCoveredLines += counts.covered;
169
+ changedUncoveredLines += counts.uncovered;
170
+ coverageHits += counts.hits;
171
+ if (counts.covered + counts.uncovered > 0) {
172
+ usedCoverageForChangedLines = true;
173
+ filesWithHits += 1;
174
+ }
175
+ const mappedTests = testsFromCoverageMap(coverage, covKey);
176
+ if (mappedTests.length > 0) {
177
+ usedCoverageTestMap = true;
178
+ for (const testPath of mappedTests) {
179
+ const exists = await fileExists(root, testPath);
180
+ recommended.push({
181
+ testPath,
182
+ reason: `Coverage map links test to changed source ${file}`,
183
+ confidence: exists ? 0.9 : 0.75,
184
+ evidence: "verified",
185
+ exists,
186
+ relatedModules,
187
+ });
188
+ }
189
+ }
190
+ else {
191
+ // Hybrid: coverage proves line hits; tests from naming heuristic
192
+ usedHeuristicForTests = true;
193
+ const candidates = heuristicCandidates(file);
194
+ let anyExists = false;
195
+ for (const c of candidates) {
196
+ const exists = await fileExists(root, c);
197
+ if (exists)
198
+ anyExists = true;
199
+ recommended.push({
200
+ testPath: c,
201
+ reason: exists
202
+ ? `Heuristic test candidate for coverage-touched file ${file} (no test→source map)`
203
+ : `Suggested test path for coverage-touched file ${file} (not found; no test→source map)`,
204
+ confidence: exists ? 0.65 : 0.35,
205
+ evidence: "inferred",
206
+ exists,
207
+ relatedModules,
208
+ });
209
+ }
210
+ if (candidates.length === 0) {
211
+ uncovered.push(file);
212
+ }
213
+ else if (!anyExists) {
214
+ missingTestWarnings.push({
215
+ sourceFile: file,
216
+ message: `No co-located or tests/ counterpart found for ${file}`,
217
+ relatedModules,
218
+ });
219
+ }
220
+ }
221
+ continue;
222
+ }
223
+ }
224
+ // Pure heuristic path (no coverage for this file, or no coverage loaded)
225
+ const candidates = heuristicCandidates(file);
226
+ let anyExists = false;
227
+ for (const c of candidates) {
228
+ const exists = await fileExists(root, c);
229
+ if (exists)
230
+ anyExists = true;
231
+ recommended.push({
232
+ testPath: c,
233
+ reason: exists
234
+ ? `Existing test candidate for changed file ${file} (naming heuristic)`
235
+ : `Suggested test path for changed file ${file} (file not found; naming heuristic)`,
236
+ confidence: exists ? 0.7 : 0.4,
237
+ evidence: "inferred",
238
+ exists,
239
+ relatedModules,
240
+ });
241
+ }
242
+ if (candidates.length === 0) {
243
+ uncovered.push(file);
244
+ }
245
+ else if (!anyExists) {
246
+ missingTestWarnings.push({
247
+ sourceFile: file,
248
+ message: `No co-located or tests/ counterpart found for ${file}`,
249
+ relatedModules,
250
+ });
251
+ }
252
+ }
253
+ for (const rel of changes.impact?.relatedPaths ?? []) {
254
+ for (const m of modulesForPath(rel))
255
+ allModules.add(m);
256
+ }
257
+ if (usedCoverageForChangedLines) {
258
+ mode = "coverage-backed";
259
+ if (usedCoverageTestMap && usedHeuristicForTests) {
260
+ testAttribution = "hybrid-heuristic";
261
+ limitations.push("mode=coverage-backed: changed-line evidence from coverage; some tests attributed via naming heuristic (hybrid)");
262
+ }
263
+ else if (usedCoverageTestMap) {
264
+ testAttribution = "coverage-map";
265
+ limitations.push("mode=coverage-backed: test attribution from coverage test→source map");
266
+ }
267
+ else {
268
+ testAttribution = "hybrid-heuristic";
269
+ limitations.push("mode=coverage-backed: coverage used for changed-line hits; test list uses naming heuristics because coverage lacks test→source map");
270
+ }
271
+ coverageSummary = {
272
+ format: coverage.format,
273
+ filesWithHits,
274
+ changedCoveredLines,
275
+ changedUncoveredLines,
276
+ coverageHits,
277
+ testAttribution,
278
+ };
279
+ }
280
+ else {
281
+ mode = "heuristic";
282
+ limitations.push("Impact mapping is path/naming heuristic — not coverage-backed (no usable coverage intersection with changed files)");
283
+ if (coverage) {
284
+ limitations.push("Coverage file was loaded but did not intersect changed source paths for line evidence");
285
+ coverageSummary = {
286
+ format: coverage.format,
287
+ filesWithHits: Object.keys(coverage.files).length,
288
+ changedCoveredLines: 0,
289
+ changedUncoveredLines: 0,
290
+ coverageHits: 0,
291
+ testAttribution: "none",
292
+ };
293
+ }
294
+ }
295
+ const seen = new Set();
296
+ const deduped = recommended.filter((r) => {
297
+ if (seen.has(r.testPath))
298
+ return false;
299
+ seen.add(r.testPath);
300
+ return true;
301
+ });
302
+ return {
303
+ root,
304
+ gitAvailable: true,
305
+ mode,
306
+ changedFiles: changed,
307
+ recommendedTests: deduped,
308
+ relatedModules: [...allModules].sort(),
309
+ uncoveredAreas: uncovered,
310
+ missingTestWarnings,
311
+ skipRisk: changed.length === 0
312
+ ? "No changes detected"
313
+ : missingTestWarnings.length > 0
314
+ ? "Changed source files lack matching tests — skipping tests risks regressions"
315
+ : "Skipping recommended tests may miss regressions in changed modules",
316
+ coverage: coverageSummary,
317
+ limitations,
318
+ };
319
+ }
320
+ export async function persistTestImpactReport(root, report) {
321
+ return writeJsonArtifact(root, "reports/test-impact.json", report);
322
+ }
323
+ export function formatTestImpactHuman(report) {
324
+ const lines = [
325
+ "AgentDoctor 2.0 test-impact",
326
+ ` mode: ${report.mode}`,
327
+ ` git: ${report.gitAvailable ? "available" : "unavailable"}`,
328
+ ` changed files: ${report.changedFiles.length}`,
329
+ ` recommended tests: ${report.recommendedTests.length}`,
330
+ ` missing-test warnings: ${report.missingTestWarnings.length}`,
331
+ ` related modules: ${report.relatedModules.slice(0, 12).join(", ") || "(none)"}`,
332
+ ` skip risk: ${report.skipRisk}`,
333
+ ];
334
+ if (report.coverage) {
335
+ lines.push(` coverage: format=${report.coverage.format} filesWithHits=${report.coverage.filesWithHits} coveredLines=${report.coverage.changedCoveredLines} uncoveredLines=${report.coverage.changedUncoveredLines} hits=${report.coverage.coverageHits} testAttribution=${report.coverage.testAttribution}`);
336
+ }
337
+ if (report.recommendedTests.length) {
338
+ lines.push(" affected tests:");
339
+ for (const t of report.recommendedTests.slice(0, 20)) {
340
+ lines.push(` - ${t.testPath} (exists=${t.exists}, confidence=${t.confidence}, evidence=${t.evidence}, ${t.reason})`);
341
+ }
342
+ }
343
+ if (report.missingTestWarnings.length) {
344
+ lines.push(" missing tests:");
345
+ for (const w of report.missingTestWarnings.slice(0, 10)) {
346
+ lines.push(` - ${w.sourceFile}: ${w.message}`);
347
+ }
348
+ }
349
+ return `${lines.join("\n")}\n`;
350
+ }
@@ -0,0 +1,17 @@
1
+ export interface TimeMachineCompare {
2
+ root: string;
3
+ left: string;
4
+ right: string;
5
+ changedFiles: string[];
6
+ summary: string;
7
+ limitations: string[];
8
+ }
9
+ /**
10
+ * Module J — Repository time machine (git-backed).
11
+ */
12
+ export declare function compareCommits(options: {
13
+ root: string;
14
+ left: string;
15
+ right: string;
16
+ }): Promise<TimeMachineCompare>;
17
+ export declare function listRecentCommits(rootInput: string, limit?: number): Promise<string[]>;
@@ -0,0 +1,62 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { resolveRepoRoot } from "../../utils/path.js";
3
+ function git(root, args) {
4
+ const r = spawnSync("git", args, { cwd: root, encoding: "utf8", maxBuffer: 8 * 1024 * 1024 });
5
+ return { ok: r.status === 0, out: typeof r.stdout === "string" ? r.stdout : "" };
6
+ }
7
+ /**
8
+ * Module J — Repository time machine (git-backed).
9
+ */
10
+ export async function compareCommits(options) {
11
+ const root = resolveRepoRoot(options.root);
12
+ const left = options.left.trim();
13
+ const right = options.right.trim();
14
+ if (!isSafeGitRef(left) || !isSafeGitRef(right)) {
15
+ return {
16
+ root,
17
+ left,
18
+ right,
19
+ changedFiles: [],
20
+ summary: "Invalid git ref",
21
+ limitations: ["Refs must be non-empty and must not start with '-'"],
22
+ };
23
+ }
24
+ const probe = git(root, ["rev-parse", "--is-inside-work-tree"]);
25
+ if (!probe.ok || probe.out.trim() !== "true") {
26
+ return {
27
+ root,
28
+ left,
29
+ right,
30
+ changedFiles: [],
31
+ summary: "Git not available",
32
+ limitations: ["Requires a git repository"],
33
+ };
34
+ }
35
+ const diff = git(root, ["diff", "--name-only", `${left}...${right}`]);
36
+ const files = diff.ok
37
+ ? diff.out
38
+ .split(/\r?\n/)
39
+ .map((l) => l.trim())
40
+ .filter(Boolean)
41
+ : [];
42
+ return {
43
+ root,
44
+ left,
45
+ right,
46
+ changedFiles: files,
47
+ summary: `${files.length} path(s) differ between ${left} and ${right}`,
48
+ limitations: [
49
+ "Architecture/risk trend overlays are extension points; this MVP lists path diffs",
50
+ ],
51
+ };
52
+ }
53
+ function isSafeGitRef(ref) {
54
+ return Boolean(ref) && !ref.includes("\0") && !ref.startsWith("-") && !/[\r\n]/.test(ref);
55
+ }
56
+ export async function listRecentCommits(rootInput, limit = 20) {
57
+ const root = resolveRepoRoot(rootInput);
58
+ const log = git(root, ["log", `-${limit}`, "--pretty=format:%H %s"]);
59
+ if (!log.ok)
60
+ return [];
61
+ return log.out.split(/\r?\n/).filter(Boolean);
62
+ }
@@ -0,0 +1,28 @@
1
+ import type { RepositoryGraph } from "../types.js";
2
+ export interface ContextPlan {
3
+ root: string;
4
+ budgetTokens: number;
5
+ estimatedTokens: number;
6
+ selected: Array<{
7
+ path: string;
8
+ tokens: number;
9
+ relevance: number;
10
+ reason: string;
11
+ }>;
12
+ excluded: Array<{
13
+ path: string;
14
+ reason: string;
15
+ }>;
16
+ recommendations: string[];
17
+ limitations: string[];
18
+ }
19
+ /**
20
+ * Module L — Token / context optimization.
21
+ * Never includes secret-like paths.
22
+ */
23
+ export declare function planContext(options: {
24
+ root: string;
25
+ graph: RepositoryGraph;
26
+ query: string;
27
+ budgetTokens?: number;
28
+ }): Promise<ContextPlan>;
@@ -0,0 +1,108 @@
1
+ import { constants as fsConstants } from "node:fs";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { resolveRepoRoot, isPathInsideRoot } from "../../utils/path.js";
5
+ function estimateTokens(text) {
6
+ return Math.ceil(text.length / 4);
7
+ }
8
+ const SECRETISH = /\.env|id_rsa|credentials|secrets\//i;
9
+ /**
10
+ * Module L — Token / context optimization.
11
+ * Never includes secret-like paths.
12
+ */
13
+ export async function planContext(options) {
14
+ const root = resolveRepoRoot(options.root);
15
+ const budget = options.budgetTokens ?? 8_000;
16
+ const q = options.query.toLowerCase();
17
+ const selected = [];
18
+ const excluded = [];
19
+ let used = 0;
20
+ const candidates = options.graph.nodes
21
+ .filter((n) => (n.kind === "file" || n.kind === "doc" || n.kind === "config") && n.path)
22
+ .map((n) => n.path)
23
+ .sort();
24
+ for (const rel of candidates) {
25
+ if (rel.includes("\0") || path.isAbsolute(rel) || rel.split(/[/\\]/).includes("..")) {
26
+ excluded.push({ path: rel, reason: "Unsafe path rejected" });
27
+ continue;
28
+ }
29
+ if (SECRETISH.test(rel)) {
30
+ excluded.push({ path: rel, reason: "Secret-like path excluded" });
31
+ continue;
32
+ }
33
+ const relevance = (rel.toLowerCase().includes(q) ? 0.6 : 0) +
34
+ (/\.(ts|tsx|md)$/i.test(rel) ? 0.2 : 0.05) +
35
+ (rel.startsWith("src/") ? 0.15 : 0);
36
+ if (relevance < 0.25) {
37
+ excluded.push({ path: rel, reason: "Low relevance to query" });
38
+ continue;
39
+ }
40
+ try {
41
+ const absolute = path.resolve(root, rel);
42
+ if (!isPathInsideRoot(root, absolute)) {
43
+ excluded.push({ path: rel, reason: "Path escapes repository root" });
44
+ continue;
45
+ }
46
+ // Open first (O_NOFOLLOW rejects symlinks), then fstat+read from the same fd — no TOCTOU.
47
+ const openFlags = typeof fsConstants.O_NOFOLLOW === "number"
48
+ ? fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW
49
+ : fsConstants.O_RDONLY;
50
+ let handle;
51
+ try {
52
+ handle = await fs.open(absolute, openFlags);
53
+ }
54
+ catch (error) {
55
+ const code = error && typeof error === "object" && "code" in error
56
+ ? String(error.code)
57
+ : "";
58
+ if (code === "ELOOP") {
59
+ excluded.push({ path: rel, reason: "Symlink excluded" });
60
+ continue;
61
+ }
62
+ throw error;
63
+ }
64
+ try {
65
+ const st = await handle.stat();
66
+ if (st.size > 100_000) {
67
+ excluded.push({ path: rel, reason: "File too large for context budget" });
68
+ continue;
69
+ }
70
+ const text = await handle.readFile("utf8");
71
+ const tokens = estimateTokens(text);
72
+ if (used + tokens > budget) {
73
+ excluded.push({ path: rel, reason: "Would exceed token budget" });
74
+ continue;
75
+ }
76
+ selected.push({
77
+ path: rel,
78
+ tokens,
79
+ relevance,
80
+ reason: relevance >= 0.6 ? "Path matches query" : "Likely relevant source/doc",
81
+ });
82
+ used += tokens;
83
+ }
84
+ finally {
85
+ await handle.close();
86
+ }
87
+ }
88
+ catch {
89
+ excluded.push({ path: rel, reason: "Unreadable" });
90
+ }
91
+ }
92
+ selected.sort((a, b) => b.relevance - a.relevance || a.path.localeCompare(b.path));
93
+ return {
94
+ root,
95
+ budgetTokens: budget,
96
+ estimatedTokens: used,
97
+ selected,
98
+ excluded: excluded.slice(0, 200),
99
+ recommendations: [
100
+ used > budget * 0.9 ? "Increase budget or narrow query" : "Budget headroom available",
101
+ "Prefer Project Brain claims over dumping entire directories",
102
+ ],
103
+ limitations: [
104
+ "Token estimate is chars/4 heuristic",
105
+ "Relevance is lexical, not embedding-based",
106
+ ],
107
+ };
108
+ }
@@ -0,0 +1,120 @@
1
+ /**
2
+ * AgentDoctor 2.0 platform contracts.
3
+ * Local-first: no mandatory cloud, DB, or API keys.
4
+ */
5
+ export type EvidenceKind = "verified" | "inferred" | "unknown";
6
+ export interface EvidenceRef {
7
+ kind: EvidenceKind;
8
+ path?: string;
9
+ detail: string;
10
+ line?: number;
11
+ }
12
+ export type PlatformSeverity = "critical" | "high" | "medium" | "low" | "info";
13
+ export interface PlatformFinding {
14
+ id: string;
15
+ module: string;
16
+ severity: PlatformSeverity;
17
+ title: string;
18
+ message: string;
19
+ recommendation: string;
20
+ confidence: number;
21
+ evidence: EvidenceRef[];
22
+ falsePositiveWarning?: string;
23
+ }
24
+ export interface GraphNode {
25
+ id: string;
26
+ kind: "file" | "directory" | "function" | "class" | "module" | "api" | "test" | "dependency" | "doc" | "config" | "service" | "commit" | "owner";
27
+ label: string;
28
+ path?: string;
29
+ meta?: Record<string, string | number | boolean>;
30
+ }
31
+ export interface GraphEdge {
32
+ id: string;
33
+ from: string;
34
+ to: string;
35
+ kind: string;
36
+ evidence: EvidenceKind;
37
+ }
38
+ export interface RepositoryGraph {
39
+ root: string;
40
+ generatedAt: string;
41
+ nodes: GraphNode[];
42
+ edges: GraphEdge[];
43
+ limitations: string[];
44
+ }
45
+ export type FirewallDecision = "allow" | "block" | "require-approval" | "sandbox-only" | "deny-network" | "deny-secrets";
46
+ export type AgentActionType = "shell" | "file-create" | "file-modify" | "file-delete" | "git" | "package-install" | "database" | "network" | "docker" | "infra" | "secret-access" | "config-change" | "deploy";
47
+ export interface AgentActionRequest {
48
+ actionId: string;
49
+ agentId: string;
50
+ userId?: string;
51
+ timestamp: string;
52
+ type: AgentActionType;
53
+ params: Record<string, string>;
54
+ repositoryRoot: string;
55
+ }
56
+ export interface FirewallVerdict {
57
+ actionId: string;
58
+ decision: FirewallDecision;
59
+ reason: string;
60
+ riskLevel: PlatformSeverity;
61
+ policyId?: string;
62
+ approvalStatus: "not-required" | "pending" | "approved" | "denied";
63
+ executionResult: "not-executed";
64
+ }
65
+ export type LocalRole = "admin" | "developer" | "reviewer" | "auditor" | "readonly";
66
+ export interface PlatformSnapshot {
67
+ version: "2.0";
68
+ root: string;
69
+ generatedAt: string;
70
+ graph: RepositoryGraph;
71
+ findings: PlatformFinding[];
72
+ readiness: ReadinessReport;
73
+ testImpact?: TestImpactSnapshot;
74
+ limitations: string[];
75
+ }
76
+ /** Persisted subset of Module H test-impact analysis. */
77
+ export interface TestImpactSnapshot {
78
+ gitAvailable: boolean;
79
+ mode: "coverage-backed" | "heuristic";
80
+ changedFiles: string[];
81
+ recommendedTests: Array<{
82
+ testPath: string;
83
+ reason: string;
84
+ confidence: number;
85
+ evidence: EvidenceKind;
86
+ exists: boolean;
87
+ relatedModules: string[];
88
+ }>;
89
+ relatedModules: string[];
90
+ missingTestWarnings: Array<{
91
+ sourceFile: string;
92
+ message: string;
93
+ relatedModules: string[];
94
+ }>;
95
+ uncoveredAreas: string[];
96
+ skipRisk: string;
97
+ coverage: {
98
+ format: string;
99
+ filesWithHits: number;
100
+ changedCoveredLines: number;
101
+ changedUncoveredLines: number;
102
+ coverageHits: number;
103
+ testAttribution: "coverage-map" | "hybrid-heuristic" | "none";
104
+ } | null;
105
+ limitations: string[];
106
+ }
107
+ export interface ReadinessCategory {
108
+ id: string;
109
+ title: string;
110
+ score: number;
111
+ evidence: EvidenceRef[];
112
+ gaps: string[];
113
+ recommendations: string[];
114
+ }
115
+ export interface ReadinessReport {
116
+ root: string;
117
+ categories: ReadinessCategory[];
118
+ overallNote: string;
119
+ limitations: string[];
120
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * AgentDoctor 2.0 platform contracts.
3
+ * Local-first: no mandatory cloud, DB, or API keys.
4
+ */
5
+ export {};