@praneeth_54/agentdoctor 1.1.1 → 2.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 (170) hide show
  1. package/CHANGELOG.md +95 -7
  2. package/README.md +163 -538
  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/cli/commands/brain.d.ts +12 -0
  24. package/dist/cli/commands/brain.js +162 -0
  25. package/dist/cli/commands/complete.d.ts +23 -0
  26. package/dist/cli/commands/complete.js +232 -0
  27. package/dist/cli/commands/explain.js +1 -1
  28. package/dist/cli/commands/fix.js +22 -2
  29. package/dist/cli/commands/mcp.d.ts +8 -0
  30. package/dist/cli/commands/mcp.js +14 -0
  31. package/dist/cli/commands/platform.d.ts +15 -0
  32. package/dist/cli/commands/platform.js +191 -0
  33. package/dist/cli/commands/v2.d.ts +55 -0
  34. package/dist/cli/commands/v2.js +278 -0
  35. package/dist/cli/program.js +809 -2
  36. package/dist/constants.d.ts +3 -2
  37. package/dist/constants.js +5 -1
  38. package/dist/contracts/adapters.d.ts +7 -0
  39. package/dist/contracts/adapters.js +60 -0
  40. package/dist/contracts/index.d.ts +162 -0
  41. package/dist/contracts/index.js +18 -0
  42. package/dist/core/baseline/store.d.ts +48 -0
  43. package/dist/core/baseline/store.js +163 -0
  44. package/dist/core/brain-cli/service.d.ts +26 -0
  45. package/dist/core/brain-cli/service.js +175 -0
  46. package/dist/core/brain-product/init.d.ts +45 -0
  47. package/dist/core/brain-product/init.js +232 -0
  48. package/dist/core/changes/analyze.d.ts +41 -0
  49. package/dist/core/changes/analyze.js +223 -0
  50. package/dist/core/context-health/analyze.d.ts +15 -0
  51. package/dist/core/context-health/analyze.js +234 -0
  52. package/dist/core/fix/apply.d.ts +21 -4
  53. package/dist/core/fix/apply.js +118 -18
  54. package/dist/core/fix/backup.d.ts +30 -0
  55. package/dist/core/fix/backup.js +193 -0
  56. package/dist/core/fix/plan.d.ts +6 -3
  57. package/dist/core/fix/plan.js +113 -9
  58. package/dist/core/fix/render.d.ts +2 -0
  59. package/dist/core/fix/render.js +36 -21
  60. package/dist/core/fix/run.js +5 -1
  61. package/dist/core/fix/safe-target.d.ts +14 -0
  62. package/dist/core/fix/safe-target.js +81 -0
  63. package/dist/core/fix/types.d.ts +12 -1
  64. package/dist/core/fix/types.js +2 -0
  65. package/dist/core/fix/writers/claude-settings.js +3 -1
  66. package/dist/core/fix/writers/codex-config.d.ts +3 -0
  67. package/dist/core/fix/writers/codex-config.js +24 -3
  68. package/dist/core/fix/writers/cursorignore.js +3 -1
  69. package/dist/core/fix/writers/simple-ignore.d.ts +16 -0
  70. package/dist/core/fix/writers/simple-ignore.js +68 -0
  71. package/dist/core/monorepo/detect.d.ts +13 -0
  72. package/dist/core/monorepo/detect.js +121 -0
  73. package/dist/core/rules/build-context.js +22 -3
  74. package/dist/core/rules/context/generated-directory.js +11 -1
  75. package/dist/core/rules/context/large-instruction-file.js +6 -0
  76. package/dist/core/rules/context/large-log-file.js +16 -1
  77. package/dist/core/rules/ignore.d.ts +11 -1
  78. package/dist/core/rules/ignore.js +22 -1
  79. package/dist/core/rules/instructions/empty-instructions.js +7 -0
  80. package/dist/core/rules/security/env-file-exposure.js +23 -2
  81. package/dist/core/rules/security/private-key-file.js +1 -1
  82. package/dist/core/scanner/scan.js +1 -1
  83. package/dist/core/schemas/validate.d.ts +6 -0
  84. package/dist/core/schemas/validate.js +62 -0
  85. package/dist/core/scoring/compute-scores.d.ts +1 -1
  86. package/dist/core/scoring/compute-scores.js +5 -1
  87. package/dist/core/scoring/placeholder.d.ts +1 -1
  88. package/dist/core/scoring/placeholder.js +5 -1
  89. package/dist/core/secrets/scan.d.ts +23 -0
  90. package/dist/core/secrets/scan.js +172 -0
  91. package/dist/dashboard/server.d.ts +24 -0
  92. package/dist/dashboard/server.js +386 -0
  93. package/dist/enforcement/runner.d.ts +23 -0
  94. package/dist/enforcement/runner.js +56 -0
  95. package/dist/index.d.ts +13 -0
  96. package/dist/index.js +12 -0
  97. package/dist/integrations/github/pr-analyze.d.ts +28 -0
  98. package/dist/integrations/github/pr-analyze.js +98 -0
  99. package/dist/integrations/local-ai/provider.d.ts +45 -0
  100. package/dist/integrations/local-ai/provider.js +104 -0
  101. package/dist/intelligence/git/analyze.d.ts +34 -0
  102. package/dist/intelligence/git/analyze.js +136 -0
  103. package/dist/intelligence/graph/build.d.ts +13 -0
  104. package/dist/intelligence/graph/build.js +215 -0
  105. package/dist/knowledge/store.d.ts +25 -0
  106. package/dist/knowledge/store.js +91 -0
  107. package/dist/mcp/agentdoctor/server.d.ts +16 -0
  108. package/dist/mcp/agentdoctor/server.js +67 -0
  109. package/dist/mcp/brain/session.js +7 -7
  110. package/dist/mcp/intelligence/handlers.d.ts +11 -0
  111. package/dist/mcp/intelligence/handlers.js +175 -0
  112. package/dist/mcp/intelligence/path-safety.d.ts +11 -0
  113. package/dist/mcp/intelligence/path-safety.js +82 -0
  114. package/dist/mcp/intelligence/registry.d.ts +8 -0
  115. package/dist/mcp/intelligence/registry.js +160 -0
  116. package/dist/ops/health.d.ts +18 -0
  117. package/dist/ops/health.js +51 -0
  118. package/dist/platform/ai-quality/analyze.d.ts +9 -0
  119. package/dist/platform/ai-quality/analyze.js +65 -0
  120. package/dist/platform/architecture/drift.d.ts +17 -0
  121. package/dist/platform/architecture/drift.js +74 -0
  122. package/dist/platform/auth/local.d.ts +15 -0
  123. package/dist/platform/auth/local.js +58 -0
  124. package/dist/platform/context-security/analyze.d.ts +6 -0
  125. package/dist/platform/context-security/analyze.js +97 -0
  126. package/dist/platform/firewall/evaluate.d.ts +54 -0
  127. package/dist/platform/firewall/evaluate.js +308 -0
  128. package/dist/platform/graph/build.d.ts +6 -0
  129. package/dist/platform/graph/build.js +193 -0
  130. package/dist/platform/health/analyze.d.ts +7 -0
  131. package/dist/platform/health/analyze.js +186 -0
  132. package/dist/platform/index.d.ts +27 -0
  133. package/dist/platform/index.js +87 -0
  134. package/dist/platform/knowledge/analyze.d.ts +20 -0
  135. package/dist/platform/knowledge/analyze.js +96 -0
  136. package/dist/platform/provenance/build.d.ts +44 -0
  137. package/dist/platform/provenance/build.js +65 -0
  138. package/dist/platform/readiness/scorecard.d.ts +5 -0
  139. package/dist/platform/readiness/scorecard.js +98 -0
  140. package/dist/platform/refactor/impact.d.ts +20 -0
  141. package/dist/platform/refactor/impact.js +60 -0
  142. package/dist/platform/reports/export.d.ts +12 -0
  143. package/dist/platform/reports/export.js +92 -0
  144. package/dist/platform/security/redact.d.ts +15 -0
  145. package/dist/platform/security/redact.js +87 -0
  146. package/dist/platform/sessions/store.d.ts +32 -0
  147. package/dist/platform/sessions/store.js +97 -0
  148. package/dist/platform/store.d.ts +6 -0
  149. package/dist/platform/store.js +57 -0
  150. package/dist/platform/test-impact/analyze.d.ts +32 -0
  151. package/dist/platform/test-impact/analyze.js +164 -0
  152. package/dist/platform/time-machine/compare.d.ts +17 -0
  153. package/dist/platform/time-machine/compare.js +62 -0
  154. package/dist/platform/tokens/plan.d.ts +28 -0
  155. package/dist/platform/tokens/plan.js +89 -0
  156. package/dist/platform/types.d.ts +111 -0
  157. package/dist/platform/types.js +5 -0
  158. package/dist/plugins/runtime.d.ts +36 -0
  159. package/dist/plugins/runtime.js +101 -0
  160. package/dist/plugins/sdk.d.ts +29 -0
  161. package/dist/plugins/sdk.js +114 -0
  162. package/dist/policy/packs.d.ts +9 -0
  163. package/dist/policy/packs.js +59 -0
  164. package/dist/reporters/terminal/report.js +38 -5
  165. package/dist/storage/provider.d.ts +35 -0
  166. package/dist/storage/provider.js +103 -0
  167. package/dist/team/auth.d.ts +35 -0
  168. package/dist/team/auth.js +70 -0
  169. package/dist/types/index.d.ts +5 -1
  170. package/package.json +8 -4
@@ -0,0 +1,60 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { resolveRepoRoot } from "../../utils/path.js";
4
+ /**
5
+ * Module K — Graph-aware refactoring analysis (rename impact MVP).
6
+ * Does not apply refactors.
7
+ */
8
+ export async function analyzeRenameImpact(options) {
9
+ const root = resolveRepoRoot(options.root);
10
+ const symbol = options.symbol.trim();
11
+ const affected = [];
12
+ const depPaths = [];
13
+ for (const node of options.graph.nodes) {
14
+ if ((node.kind === "function" || node.kind === "class") && node.label === symbol && node.path) {
15
+ affected.push(node.path);
16
+ }
17
+ }
18
+ for (const node of options.graph.nodes.filter((n) => n.kind === "file" && n.path)) {
19
+ const rel = node.path;
20
+ if (!/\.(ts|tsx|js|jsx)$/i.test(rel))
21
+ continue;
22
+ try {
23
+ const text = await fs.readFile(path.join(root, rel), "utf8");
24
+ if (new RegExp(`\\b${escapeReg(symbol)}\\b`).test(text)) {
25
+ if (!affected.includes(rel))
26
+ affected.push(rel);
27
+ }
28
+ }
29
+ catch {
30
+ // skip
31
+ }
32
+ }
33
+ for (const edge of options.graph.edges) {
34
+ const from = options.graph.nodes.find((n) => n.id === edge.from);
35
+ const to = options.graph.nodes.find((n) => n.id === edge.to);
36
+ if (from?.path && affected.includes(from.path)) {
37
+ depPaths.push(`${from.path} -> ${to?.label ?? edge.to}`);
38
+ }
39
+ }
40
+ const requiredTests = affected.map((f) => f.replace(/(\.\w+)$/, ".test$1"));
41
+ const risk = affected.length > 10 ? "high" : affected.length > 3 ? "medium" : "low";
42
+ return {
43
+ root,
44
+ symbol,
45
+ affectedFiles: affected.sort(),
46
+ dependencyPaths: depPaths.slice(0, 50),
47
+ risk,
48
+ requiredTests: [...new Set(requiredTests)].sort(),
49
+ breakingChangeRisk: risk === "high"
50
+ ? "Likely public surface impact — require review and tests"
51
+ : "Localized rename likely; still verify exports",
52
+ limitations: [
53
+ "Does not apply edits",
54
+ "Text search may include false positives for common names",
55
+ ],
56
+ };
57
+ }
58
+ function escapeReg(value) {
59
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
60
+ }
@@ -0,0 +1,12 @@
1
+ import type { PlatformFinding, PlatformSnapshot, ReadinessReport } from "../types.js";
2
+ /**
3
+ * Module O — Security & compliance reporting exports.
4
+ * Findings are secret-redacted before any format is written.
5
+ */
6
+ export declare function exportReports(options: {
7
+ root: string;
8
+ findings: PlatformFinding[];
9
+ readiness: ReadinessReport;
10
+ snapshot?: PlatformSnapshot;
11
+ }): Promise<Record<string, string>>;
12
+ export declare function escapeHtml(value: string): string;
@@ -0,0 +1,92 @@
1
+ import { ensurePlatformDir, writeJsonArtifact, writeTextArtifact } from "../store.js";
2
+ import { resolveRepoRoot } from "../../utils/path.js";
3
+ import { sanitizeFindingsForExport } from "../security/redact.js";
4
+ /**
5
+ * Module O — Security & compliance reporting exports.
6
+ * Findings are secret-redacted before any format is written.
7
+ */
8
+ export async function exportReports(options) {
9
+ const root = resolveRepoRoot(options.root);
10
+ await ensurePlatformDir(root);
11
+ const findings = sanitizeFindingsForExport(options.findings);
12
+ const snapshot = options.snapshot
13
+ ? { ...options.snapshot, findings: sanitizeFindingsForExport(options.snapshot.findings) }
14
+ : null;
15
+ const outputs = {};
16
+ outputs.json = await writeJsonArtifact(root, "reports/findings.json", {
17
+ findings,
18
+ readiness: options.readiness,
19
+ snapshot,
20
+ });
21
+ const md = [
22
+ "# AgentDoctor 2.0 Report",
23
+ "",
24
+ "## Executive summary",
25
+ "",
26
+ `Findings: ${findings.length}`,
27
+ `Critical/High: ${findings.filter((f) => f.severity === "critical" || f.severity === "high").length}`,
28
+ "",
29
+ options.readiness.overallNote,
30
+ "",
31
+ "## Findings",
32
+ "",
33
+ ...findings.map((f) => `### ${f.title}\n- Severity: ${f.severity}\n- Module: ${f.module}\n- ${f.message}\n- Recommendation: ${f.recommendation}\n- Evidence: ${f.evidence.map((e) => e.detail).join(" | ")}\n`),
34
+ ].join("\n");
35
+ outputs.markdown = await writeTextArtifact(root, "reports/findings.md", md);
36
+ const csv = [
37
+ "id,module,severity,title,confidence,evidence",
38
+ ...findings.map((f) => `${csvEscape(f.id)},${csvEscape(f.module)},${f.severity},${csvEscape(f.title)},${f.confidence},${csvEscape(f.evidence.map((e) => e.detail).join("; "))}`),
39
+ ].join("\n");
40
+ outputs.csv = await writeTextArtifact(root, "reports/findings.csv", `${csv}\n`);
41
+ const html = `<!doctype html><html><head><meta charset="utf-8"/><title>AgentDoctor Report</title></head><body><h1>AgentDoctor 2.0</h1><pre>${escapeHtml(md)}</pre></body></html>`;
42
+ outputs.html = await writeTextArtifact(root, "reports/findings.html", html);
43
+ const sarif = {
44
+ $schema: "https://json.schemastore.org/sarif-2.1.0.json",
45
+ version: "2.1.0",
46
+ runs: [
47
+ {
48
+ tool: {
49
+ driver: {
50
+ name: "AgentDoctor",
51
+ informationUri: "https://github.com/pranee54/AgentDoctor",
52
+ },
53
+ },
54
+ results: findings.map((f) => ({
55
+ ruleId: f.module,
56
+ level: f.severity === "critical" || f.severity === "high" ? "error" : "warning",
57
+ message: { text: f.message },
58
+ properties: {
59
+ evidence: f.evidence.map((e) => e.detail),
60
+ },
61
+ locations: f.evidence[0]?.path
62
+ ? [
63
+ {
64
+ physicalLocation: {
65
+ artifactLocation: { uri: f.evidence[0].path },
66
+ ...(typeof f.evidence[0].line === "number"
67
+ ? { region: { startLine: f.evidence[0].line } }
68
+ : {}),
69
+ },
70
+ },
71
+ ]
72
+ : [],
73
+ })),
74
+ },
75
+ ],
76
+ };
77
+ outputs.sarif = await writeJsonArtifact(root, "reports/findings.sarif.json", sarif);
78
+ return outputs;
79
+ }
80
+ export function escapeHtml(value) {
81
+ return value
82
+ .replace(/&/g, "&amp;")
83
+ .replace(/</g, "&lt;")
84
+ .replace(/>/g, "&gt;")
85
+ .replace(/"/g, "&quot;")
86
+ .replace(/'/g, "&#39;");
87
+ }
88
+ function csvEscape(value) {
89
+ if (/[",\n]/.test(value))
90
+ return `"${value.replace(/"/g, '""')}"`;
91
+ return value;
92
+ }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Redact likely secrets from evidence / report text while preserving security context.
3
+ */
4
+ export declare function redactSecrets(input: string): {
5
+ text: string;
6
+ redacted: boolean;
7
+ patterns: string[];
8
+ };
9
+ export declare function redactEvidenceDetail(detail: string, maxLen?: number): string;
10
+ export declare function sanitizeFindingsForExport<T extends {
11
+ evidence: Array<{
12
+ detail: string;
13
+ }>;
14
+ }>(findings: T[]): T[];
15
+ export declare const SECRET_REDACTION_MARKER = "[REDACTED]";
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Redact likely secrets from evidence / report text while preserving security context.
3
+ */
4
+ const REDACTED = "[REDACTED]";
5
+ const SECRET_PATTERNS = [
6
+ {
7
+ id: "aws-access-key",
8
+ re: /\b(AKIA[0-9A-Z]{16})\b/g,
9
+ },
10
+ {
11
+ id: "bearer-token",
12
+ re: /\b(Bearer\s+)[A-Za-z0-9\-._~+/]+=*/gi,
13
+ },
14
+ {
15
+ id: "jwt",
16
+ re: /\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g,
17
+ },
18
+ {
19
+ id: "private-key-block",
20
+ re: /-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/g,
21
+ },
22
+ {
23
+ id: "connection-string",
24
+ re: /\b((?:postgres|postgresql|mysql|mongodb(?:\+srv)?|redis|amqp):\/\/)[^\s'"]+/gi,
25
+ },
26
+ {
27
+ id: "password-assignment",
28
+ re: /\b((?:password|passwd|pwd|secret|api[_-]?key|access[_-]?token|auth[_-]?token)\s*[=:]\s*)(['"]?)[^\s'"]+\2/gi,
29
+ },
30
+ {
31
+ id: "env-export",
32
+ re: /\b((?:export\s+)?(?:AWS_SECRET_ACCESS_KEY|GITHUB_TOKEN|NPM_TOKEN|OPENAI_API_KEY|STRIPE_SECRET_KEY)\s*=\s*)(['"]?)[^\s'"]+\2/gi,
33
+ },
34
+ {
35
+ id: "generic-api-key",
36
+ re: /\b((?:api[_-]?key|apikey|client_secret)\s*[=:]\s*)(['"]?)[A-Za-z0-9_\-/+=]{16,}\2/gi,
37
+ },
38
+ {
39
+ id: "cloud-cred-json",
40
+ re: /("(?:private_key|client_secret|refresh_token)"\s*:\s*")[^"]+(")/gi,
41
+ },
42
+ ];
43
+ export function redactSecrets(input) {
44
+ let text = input;
45
+ const patterns = [];
46
+ for (const pattern of SECRET_PATTERNS) {
47
+ const before = text;
48
+ if (pattern.id === "bearer-token") {
49
+ text = text.replace(pattern.re, `$1${REDACTED}`);
50
+ }
51
+ else if (pattern.id === "password-assignment" ||
52
+ pattern.id === "env-export" ||
53
+ pattern.id === "generic-api-key") {
54
+ text = text.replace(pattern.re, `$1$2${REDACTED}$2`);
55
+ }
56
+ else if (pattern.id === "connection-string") {
57
+ text = text.replace(pattern.re, `$1${REDACTED}`);
58
+ }
59
+ else if (pattern.id === "cloud-cred-json") {
60
+ text = text.replace(pattern.re, `$1${REDACTED}$2`);
61
+ }
62
+ else if (pattern.id === "private-key-block") {
63
+ text = text.replace(pattern.re, `-----BEGIN PRIVATE KEY-----\n${REDACTED}\n-----END PRIVATE KEY-----`);
64
+ }
65
+ else {
66
+ text = text.replace(pattern.re, REDACTED);
67
+ }
68
+ if (text !== before)
69
+ patterns.push(pattern.id);
70
+ }
71
+ return { text, redacted: patterns.length > 0, patterns: [...new Set(patterns)] };
72
+ }
73
+ export function redactEvidenceDetail(detail, maxLen = 160) {
74
+ const { text, redacted } = redactSecrets(detail);
75
+ const clipped = text.length > maxLen ? `${text.slice(0, maxLen)}…` : text;
76
+ return redacted ? `${clipped} (secrets redacted)` : clipped;
77
+ }
78
+ export function sanitizeFindingsForExport(findings) {
79
+ return findings.map((f) => ({
80
+ ...f,
81
+ evidence: f.evidence.map((e) => ({
82
+ ...e,
83
+ detail: redactEvidenceDetail(e.detail),
84
+ })),
85
+ }));
86
+ }
87
+ export const SECRET_REDACTION_MARKER = REDACTED;
@@ -0,0 +1,32 @@
1
+ export interface SessionEvent {
2
+ id: string;
3
+ sessionId: string;
4
+ timestamp: string;
5
+ type: "session-start" | "prompt" | "tool-call" | "tool-result" | "file-read" | "file-change" | "command" | "policy" | "approval" | "error" | "session-end";
6
+ summary: string;
7
+ detail?: Record<string, string>;
8
+ risk?: "none" | "low" | "medium" | "high" | "critical";
9
+ }
10
+ export interface AgentSession {
11
+ id: string;
12
+ root: string;
13
+ agentId: string;
14
+ userId?: string;
15
+ model?: string;
16
+ startedAt: string;
17
+ endedAt?: string;
18
+ events: SessionEvent[];
19
+ }
20
+ export declare function createSession(options: {
21
+ root: string;
22
+ agentId: string;
23
+ userId?: string;
24
+ model?: string;
25
+ }): Promise<AgentSession>;
26
+ export declare function appendSessionEvent(session: AgentSession, event: Omit<SessionEvent, "id" | "sessionId" | "timestamp"> & {
27
+ timestamp?: string;
28
+ }): Promise<AgentSession>;
29
+ export declare function endSession(session: AgentSession): Promise<AgentSession>;
30
+ export declare function loadSession(root: string, sessionId: string): Promise<AgentSession | null>;
31
+ export declare function listSessions(root: string): Promise<string[]>;
32
+ export declare function exportSessionMarkdown(session: AgentSession): string;
@@ -0,0 +1,97 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { randomUUID } from "node:crypto";
4
+ import { appendJsonl, ensurePlatformDir, platformDir, writeJsonArtifact } from "../store.js";
5
+ import { resolveRepoRoot } from "../../utils/path.js";
6
+ export async function createSession(options) {
7
+ const root = resolveRepoRoot(options.root);
8
+ await ensurePlatformDir(root);
9
+ const session = {
10
+ id: randomUUID(),
11
+ root,
12
+ agentId: options.agentId,
13
+ startedAt: new Date().toISOString(),
14
+ events: [],
15
+ ...(options.userId ? { userId: options.userId } : {}),
16
+ ...(options.model ? { model: options.model } : {}),
17
+ };
18
+ session.events.push({
19
+ id: randomUUID(),
20
+ sessionId: session.id,
21
+ timestamp: session.startedAt,
22
+ type: "session-start",
23
+ summary: `Session started for agent ${options.agentId}`,
24
+ risk: "none",
25
+ });
26
+ await persistSession(session);
27
+ return session;
28
+ }
29
+ export async function appendSessionEvent(session, event) {
30
+ const full = {
31
+ id: randomUUID(),
32
+ sessionId: session.id,
33
+ timestamp: event.timestamp ?? new Date().toISOString(),
34
+ type: event.type,
35
+ summary: event.summary,
36
+ ...(event.detail ? { detail: event.detail } : {}),
37
+ ...(event.risk ? { risk: event.risk } : {}),
38
+ };
39
+ session.events.push(full);
40
+ await appendJsonl(session.root, path.join("sessions", `${session.id}.jsonl`), full);
41
+ await persistSession(session);
42
+ return session;
43
+ }
44
+ export async function endSession(session) {
45
+ session.endedAt = new Date().toISOString();
46
+ await appendSessionEvent(session, {
47
+ type: "session-end",
48
+ summary: "Session ended",
49
+ risk: "none",
50
+ });
51
+ return session;
52
+ }
53
+ async function persistSession(session) {
54
+ await writeJsonArtifact(session.root, path.join("sessions", `${session.id}.json`), session);
55
+ }
56
+ export async function loadSession(root, sessionId) {
57
+ if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(sessionId)) {
58
+ throw new Error("invalid session id");
59
+ }
60
+ const file = path.join(platformDir(root), "sessions", `${sessionId}.json`);
61
+ try {
62
+ return JSON.parse(await fs.readFile(file, "utf8"));
63
+ }
64
+ catch {
65
+ return null;
66
+ }
67
+ }
68
+ export async function listSessions(root) {
69
+ const dir = path.join(platformDir(root), "sessions");
70
+ try {
71
+ return (await fs.readdir(dir))
72
+ .filter((n) => n.endsWith(".json"))
73
+ .map((n) => n.replace(/\.json$/, ""))
74
+ .sort();
75
+ }
76
+ catch {
77
+ return [];
78
+ }
79
+ }
80
+ export function exportSessionMarkdown(session) {
81
+ const lines = [
82
+ `# Agent session ${session.id}`,
83
+ "",
84
+ `- Agent: ${session.agentId}`,
85
+ `- User: ${session.userId ?? "unknown"}`,
86
+ `- Model: ${session.model ?? "unknown"}`,
87
+ `- Started: ${session.startedAt}`,
88
+ `- Ended: ${session.endedAt ?? "(open)"}`,
89
+ "",
90
+ "## Timeline",
91
+ "",
92
+ ];
93
+ for (const e of session.events) {
94
+ lines.push(`- ${e.timestamp} **${e.type}** ${e.summary}${e.risk ? ` _(risk=${e.risk})_` : ""}`);
95
+ }
96
+ return `${lines.join("\n")}\n`;
97
+ }
@@ -0,0 +1,6 @@
1
+ export declare function platformDir(root: string): string;
2
+ export declare function ensurePlatformDir(root: string): Promise<string>;
3
+ export declare function writeJsonArtifact(root: string, relativeUnderPlatform: string, value: unknown): Promise<string>;
4
+ export declare function writeTextArtifact(root: string, relativeUnderPlatform: string, body: string): Promise<string>;
5
+ export declare function appendJsonl(root: string, relativeUnderPlatform: string, record: unknown): Promise<void>;
6
+ export declare function readJsonIfExists<T>(absolute: string): Promise<T | null>;
@@ -0,0 +1,57 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { atomicWriteTextFile } from "../utils/fs.js";
4
+ import { isPathInsideRoot, resolveRepoRoot } from "../utils/path.js";
5
+ export function platformDir(root) {
6
+ return path.join(resolveRepoRoot(root), ".agentdoctor", "platform");
7
+ }
8
+ export async function ensurePlatformDir(root) {
9
+ const dir = platformDir(root);
10
+ await fs.mkdir(path.join(dir, "sessions"), { recursive: true });
11
+ await fs.mkdir(path.join(dir, "provenance"), { recursive: true });
12
+ await fs.mkdir(path.join(dir, "reports"), { recursive: true });
13
+ await fs.mkdir(path.join(dir, "snapshots"), { recursive: true });
14
+ return dir;
15
+ }
16
+ function resolveSafePlatformPath(root, relativeUnderPlatform) {
17
+ const dir = platformDir(root);
18
+ if (relativeUnderPlatform.includes("\0") ||
19
+ path.isAbsolute(relativeUnderPlatform) ||
20
+ relativeUnderPlatform.split(/[/\\]/).includes("..")) {
21
+ throw new Error(`refusing unsafe platform path: ${relativeUnderPlatform}`);
22
+ }
23
+ const target = path.resolve(dir, relativeUnderPlatform);
24
+ if (!isPathInsideRoot(dir, target)) {
25
+ throw new Error(`platform path escapes store: ${relativeUnderPlatform}`);
26
+ }
27
+ return target;
28
+ }
29
+ export async function writeJsonArtifact(root, relativeUnderPlatform, value) {
30
+ await ensurePlatformDir(root);
31
+ const target = resolveSafePlatformPath(root, relativeUnderPlatform);
32
+ await fs.mkdir(path.dirname(target), { recursive: true });
33
+ await atomicWriteTextFile(target, `${JSON.stringify(value, null, 2)}\n`);
34
+ return target;
35
+ }
36
+ export async function writeTextArtifact(root, relativeUnderPlatform, body) {
37
+ await ensurePlatformDir(root);
38
+ const target = resolveSafePlatformPath(root, relativeUnderPlatform);
39
+ await fs.mkdir(path.dirname(target), { recursive: true });
40
+ await atomicWriteTextFile(target, body);
41
+ return target;
42
+ }
43
+ export async function appendJsonl(root, relativeUnderPlatform, record) {
44
+ await ensurePlatformDir(root);
45
+ const target = resolveSafePlatformPath(root, relativeUnderPlatform);
46
+ await fs.mkdir(path.dirname(target), { recursive: true });
47
+ await fs.appendFile(target, `${JSON.stringify(record)}\n`, "utf8");
48
+ }
49
+ export async function readJsonIfExists(absolute) {
50
+ try {
51
+ const raw = await fs.readFile(absolute, "utf8");
52
+ return JSON.parse(raw);
53
+ }
54
+ catch {
55
+ return null;
56
+ }
57
+ }
@@ -0,0 +1,32 @@
1
+ import type { EvidenceKind } from "../types.js";
2
+ export interface TestImpactItem {
3
+ testPath: string;
4
+ reason: string;
5
+ confidence: number;
6
+ evidence: EvidenceKind;
7
+ exists: boolean;
8
+ relatedModules: string[];
9
+ }
10
+ export interface MissingTestWarning {
11
+ sourceFile: string;
12
+ message: string;
13
+ relatedModules: string[];
14
+ }
15
+ export interface TestImpactReport {
16
+ root: string;
17
+ gitAvailable: boolean;
18
+ changedFiles: string[];
19
+ recommendedTests: TestImpactItem[];
20
+ relatedModules: string[];
21
+ uncoveredAreas: string[];
22
+ missingTestWarnings: MissingTestWarning[];
23
+ skipRisk: string;
24
+ limitations: string[];
25
+ }
26
+ /**
27
+ * Module H — Test impact analysis (naming + path heuristics).
28
+ * Never claims tests passed unless actually executed elsewhere.
29
+ */
30
+ export declare function analyzeTestImpact(rootInput: string): Promise<TestImpactReport>;
31
+ export declare function persistTestImpactReport(root: string, report: TestImpactReport): Promise<string>;
32
+ export declare function formatTestImpactHuman(report: TestImpactReport): string;
@@ -0,0 +1,164 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { analyzeChanges } from "../../core/changes/analyze.js";
4
+ import { resolveRepoRoot } from "../../utils/path.js";
5
+ import { writeJsonArtifact } from "../store.js";
6
+ function modulesForPath(file, attributed) {
7
+ const mods = new Set(attributed ?? []);
8
+ const parts = file.split("/");
9
+ if (parts[0] === "src" && parts[1])
10
+ mods.add(parts[1]);
11
+ if (parts[0] === "tests" && parts[1])
12
+ mods.add(parts[1]);
13
+ const base = path.basename(file, path.extname(file));
14
+ if (base && !base.startsWith("."))
15
+ mods.add(base);
16
+ return [...mods].sort();
17
+ }
18
+ async function fileExists(root, rel) {
19
+ try {
20
+ await fs.access(path.join(root, rel));
21
+ return true;
22
+ }
23
+ catch {
24
+ return false;
25
+ }
26
+ }
27
+ /**
28
+ * Module H — Test impact analysis (naming + path heuristics).
29
+ * Never claims tests passed unless actually executed elsewhere.
30
+ */
31
+ export async function analyzeTestImpact(rootInput) {
32
+ const root = resolveRepoRoot(rootInput);
33
+ const changes = await analyzeChanges({ root, impact: true });
34
+ const changed = changes.files.map((f) => f.path);
35
+ const recommended = [];
36
+ const uncovered = [];
37
+ const missingTestWarnings = [];
38
+ const allModules = new Set();
39
+ if (!changes.gitAvailable) {
40
+ return {
41
+ root,
42
+ gitAvailable: false,
43
+ changedFiles: [],
44
+ recommendedTests: [],
45
+ relatedModules: [],
46
+ uncoveredAreas: [],
47
+ missingTestWarnings: [],
48
+ skipRisk: "Git not available — cannot infer changed files",
49
+ limitations: [
50
+ "Requires a git working tree to detect changes",
51
+ "Does not execute tests; never claims pass/fail",
52
+ ...changes.limitations,
53
+ ],
54
+ };
55
+ }
56
+ for (const fileEntry of changes.files) {
57
+ const file = fileEntry.path;
58
+ const relatedModules = modulesForPath(file, fileEntry.modules);
59
+ for (const m of relatedModules)
60
+ allModules.add(m);
61
+ if (/\.(test|spec)\./i.test(file) || file.includes("__tests__")) {
62
+ recommended.push({
63
+ testPath: file,
64
+ reason: "Changed file is itself a test",
65
+ confidence: 0.95,
66
+ evidence: "verified",
67
+ exists: true,
68
+ relatedModules,
69
+ });
70
+ continue;
71
+ }
72
+ const ext = path.extname(file);
73
+ const base = ext ? file.slice(0, -ext.length) : file;
74
+ const candidates = [
75
+ `${base}.test${ext || ".ts"}`,
76
+ `${base}.spec${ext || ".ts"}`,
77
+ file.replace(/\/src\//, "/tests/"),
78
+ file.replace(/^src\//, "tests/"),
79
+ ].filter((c, i, arr) => c !== file && arr.indexOf(c) === i);
80
+ let anyExists = false;
81
+ for (const c of candidates) {
82
+ const exists = await fileExists(root, c);
83
+ if (exists)
84
+ anyExists = true;
85
+ recommended.push({
86
+ testPath: c,
87
+ reason: exists
88
+ ? `Existing test candidate for changed file ${file}`
89
+ : `Suggested test path for changed file ${file} (file not found)`,
90
+ confidence: exists ? 0.7 : 0.4,
91
+ evidence: exists ? "verified" : "inferred",
92
+ exists,
93
+ relatedModules,
94
+ });
95
+ }
96
+ if (candidates.length === 0) {
97
+ uncovered.push(file);
98
+ }
99
+ else if (!anyExists) {
100
+ missingTestWarnings.push({
101
+ sourceFile: file,
102
+ message: `No co-located or tests/ counterpart found for ${file}`,
103
+ relatedModules,
104
+ });
105
+ }
106
+ }
107
+ for (const rel of changes.impact?.relatedPaths ?? []) {
108
+ for (const m of modulesForPath(rel))
109
+ allModules.add(m);
110
+ }
111
+ const seen = new Set();
112
+ const deduped = recommended.filter((r) => {
113
+ if (seen.has(r.testPath))
114
+ return false;
115
+ seen.add(r.testPath);
116
+ return true;
117
+ });
118
+ return {
119
+ root,
120
+ gitAvailable: true,
121
+ changedFiles: changed,
122
+ recommendedTests: deduped,
123
+ relatedModules: [...allModules].sort(),
124
+ uncoveredAreas: uncovered,
125
+ missingTestWarnings,
126
+ skipRisk: changed.length === 0
127
+ ? "No changes detected"
128
+ : missingTestWarnings.length > 0
129
+ ? "Changed source files lack matching tests — skipping tests risks regressions"
130
+ : "Skipping recommended tests may miss regressions in changed modules",
131
+ limitations: [
132
+ "Impact mapping is path/naming heuristic — not a full coverage DB",
133
+ "Does not execute tests; never claims pass/fail",
134
+ ...changes.limitations,
135
+ ],
136
+ };
137
+ }
138
+ export async function persistTestImpactReport(root, report) {
139
+ return writeJsonArtifact(root, "reports/test-impact.json", report);
140
+ }
141
+ export function formatTestImpactHuman(report) {
142
+ const lines = [
143
+ "AgentDoctor 2.0 test-impact",
144
+ ` git: ${report.gitAvailable ? "available" : "unavailable"}`,
145
+ ` changed files: ${report.changedFiles.length}`,
146
+ ` recommended tests: ${report.recommendedTests.length}`,
147
+ ` missing-test warnings: ${report.missingTestWarnings.length}`,
148
+ ` related modules: ${report.relatedModules.slice(0, 12).join(", ") || "(none)"}`,
149
+ ` skip risk: ${report.skipRisk}`,
150
+ ];
151
+ if (report.recommendedTests.length) {
152
+ lines.push(" affected tests:");
153
+ for (const t of report.recommendedTests.slice(0, 20)) {
154
+ lines.push(` - ${t.testPath} (exists=${t.exists}, confidence=${t.confidence}, ${t.reason})`);
155
+ }
156
+ }
157
+ if (report.missingTestWarnings.length) {
158
+ lines.push(" missing tests:");
159
+ for (const w of report.missingTestWarnings.slice(0, 10)) {
160
+ lines.push(` - ${w.sourceFile}: ${w.message}`);
161
+ }
162
+ }
163
+ return `${lines.join("\n")}\n`;
164
+ }
@@ -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[]>;