@praneeth_54/agentdoctor 1.1.0 → 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 +119 -5
  2. package/README.md +165 -332
  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 +14 -7
@@ -1,5 +1,6 @@
1
- export declare const PACKAGE_VERSION = "1.1.0";
1
+ import type { AgentId } from "./types/index.js";
2
+ export declare const PACKAGE_VERSION = "2.0.0";
2
3
  export declare const DEFAULT_MAX_FILE_SIZE_BYTES: number;
3
4
  /** Directories skipped during normal discovery (unless a rule needs them later). */
4
5
  export declare const DEFAULT_IGNORE_DIRECTORIES: Set<string>;
5
- export declare const AGENT_DISPLAY_NAMES: Record<"cursor" | "claude-code" | "codex", string>;
6
+ export declare const AGENT_DISPLAY_NAMES: Record<AgentId, string>;
package/dist/constants.js CHANGED
@@ -1,4 +1,4 @@
1
- export const PACKAGE_VERSION = "1.1.0";
1
+ export const PACKAGE_VERSION = "2.0.0";
2
2
  export const DEFAULT_MAX_FILE_SIZE_BYTES = 2 * 1024 * 1024; // 2 MiB
3
3
  /** Directories skipped during normal discovery (unless a rule needs them later). */
4
4
  export const DEFAULT_IGNORE_DIRECTORIES = new Set([
@@ -35,4 +35,8 @@ export const AGENT_DISPLAY_NAMES = {
35
35
  cursor: "Cursor",
36
36
  "claude-code": "Claude Code",
37
37
  codex: "Codex",
38
+ copilot: "GitHub Copilot",
39
+ windsurf: "Windsurf",
40
+ "gemini-cli": "Gemini CLI",
41
+ aider: "Aider",
38
42
  };
@@ -0,0 +1,7 @@
1
+ import type { FindingContract, GraphEdgeContract } from "./index.js";
2
+ import type { PlatformFinding, GraphEdge } from "../platform/types.js";
3
+ export declare function platformFindingToContract(finding: PlatformFinding, opts?: {
4
+ repositoryId?: string;
5
+ snapshotId?: string;
6
+ }): FindingContract;
7
+ export declare function platformEdgeToContract(edge: GraphEdge): GraphEdgeContract;
@@ -0,0 +1,60 @@
1
+ import { CONTRACTS_VERSION, clampConfidence } from "./index.js";
2
+ function mapKind(kind) {
3
+ if (kind === "verified")
4
+ return "observed";
5
+ if (kind === "inferred")
6
+ return "inferred";
7
+ return "unknown";
8
+ }
9
+ export function platformFindingToContract(finding, opts) {
10
+ return {
11
+ id: finding.id,
12
+ module: finding.module,
13
+ severity: finding.severity,
14
+ title: finding.title,
15
+ message: finding.message,
16
+ recommendation: finding.recommendation,
17
+ confidence: clampConfidence(finding.confidence),
18
+ evidence: finding.evidence.map((e, i) => ({
19
+ id: `${finding.id}_ev_${i}`,
20
+ kind: mapKind(e.kind),
21
+ detail: e.detail,
22
+ ...(e.path
23
+ ? {
24
+ location: {
25
+ path: e.path,
26
+ ...(typeof e.line === "number" ? { startLine: e.line } : {}),
27
+ },
28
+ }
29
+ : {}),
30
+ ...(opts?.snapshotId ? { snapshotId: opts.snapshotId } : {}),
31
+ })),
32
+ analysisVersion: CONTRACTS_VERSION,
33
+ timestamp: new Date().toISOString(),
34
+ ...(opts?.repositoryId ? { repositoryId: opts.repositoryId } : {}),
35
+ ...(opts?.snapshotId ? { snapshotId: opts.snapshotId } : {}),
36
+ ...(finding.falsePositiveWarning
37
+ ? {
38
+ falsePositiveWarning: finding.falsePositiveWarning,
39
+ limitations: [finding.falsePositiveWarning],
40
+ }
41
+ : {}),
42
+ };
43
+ }
44
+ export function platformEdgeToContract(edge) {
45
+ return {
46
+ id: edge.id,
47
+ from: edge.from,
48
+ to: edge.to,
49
+ kind: edge.kind,
50
+ evidence: [
51
+ {
52
+ id: `${edge.id}_ev`,
53
+ kind: mapKind(edge.evidence),
54
+ detail: `edge kind=${edge.kind}`,
55
+ },
56
+ ],
57
+ confidence: edge.evidence === "verified" ? 0.85 : edge.evidence === "inferred" ? 0.5 : 0.2,
58
+ analysisVersion: CONTRACTS_VERSION,
59
+ };
60
+ }
@@ -0,0 +1,162 @@
1
+ /**
2
+ * AgentDoctor 2.0 shared core contracts.
3
+ * Unifies Safety / Brain / Platform without breaking existing module types.
4
+ */
5
+ export declare const CONTRACTS_VERSION: "2.0.0-contracts";
6
+ export type Confidence = number;
7
+ export type EpistemicKind = "observed" | "inferred" | "proposed" | "unknown";
8
+ export type Severity = "critical" | "high" | "medium" | "low" | "info";
9
+ export interface SourceLocation {
10
+ path: string;
11
+ startLine?: number;
12
+ endLine?: number;
13
+ startColumn?: number;
14
+ endColumn?: number;
15
+ }
16
+ export interface EvidenceRef {
17
+ id: string;
18
+ kind: EpistemicKind;
19
+ detail: string;
20
+ location?: SourceLocation;
21
+ snapshotId?: string;
22
+ redacted?: boolean;
23
+ }
24
+ export interface ProvenanceRef {
25
+ id: string;
26
+ repositoryId: string;
27
+ snapshotId?: string;
28
+ commit?: string | null;
29
+ branch?: string | null;
30
+ agentId?: string | null;
31
+ model?: string | null;
32
+ analysisVersion: string;
33
+ recordedAt: string;
34
+ fields: Record<string, {
35
+ value: string | null;
36
+ kind: EpistemicKind;
37
+ }>;
38
+ }
39
+ export interface GraphNodeContract {
40
+ id: string;
41
+ kind: string;
42
+ label: string;
43
+ path?: string;
44
+ meta?: Record<string, string | number | boolean>;
45
+ }
46
+ export interface GraphEdgeContract {
47
+ id: string;
48
+ from: string;
49
+ to: string;
50
+ kind: string;
51
+ evidence: EvidenceRef[];
52
+ confidence: Confidence;
53
+ analysisVersion: string;
54
+ sourceLocation?: SourceLocation;
55
+ }
56
+ export interface FindingContract {
57
+ id: string;
58
+ module: string;
59
+ severity: Severity;
60
+ title: string;
61
+ message: string;
62
+ recommendation: string;
63
+ confidence: Confidence;
64
+ evidence: EvidenceRef[];
65
+ location?: SourceLocation;
66
+ repositoryId?: string;
67
+ snapshotId?: string;
68
+ analysisVersion: string;
69
+ timestamp: string;
70
+ limitations?: string[];
71
+ falsePositiveWarning?: string;
72
+ }
73
+ export type KnowledgeStatus = "observed" | "proposed" | "draft" | "pending-review" | "approved" | "rejected" | "deprecated" | "unknown";
74
+ export interface KnowledgeRecordContract {
75
+ id: string;
76
+ title: string;
77
+ content: string;
78
+ status: KnowledgeStatus;
79
+ owner?: string | null;
80
+ approver?: string | null;
81
+ audience?: string[];
82
+ version: string;
83
+ effectiveAt?: string | null;
84
+ expiresAt?: string | null;
85
+ source?: string;
86
+ evidence: EvidenceRef[];
87
+ relatedRepositoryId?: string;
88
+ relatedModule?: string;
89
+ relatedPolicyId?: string;
90
+ changeHistory: Array<{
91
+ at: string;
92
+ by?: string;
93
+ note: string;
94
+ from?: KnowledgeStatus;
95
+ to?: KnowledgeStatus;
96
+ }>;
97
+ }
98
+ export type PolicyDecisionKind = "allow" | "block" | "require-approval" | "sandbox-only" | "deny-network" | "deny-secrets";
99
+ export interface PolicyDecisionContract {
100
+ actionId: string;
101
+ decision: PolicyDecisionKind;
102
+ reason: string;
103
+ policyId?: string;
104
+ policyVersion: string;
105
+ inputClassification: string;
106
+ evidence: EvidenceRef[];
107
+ riskLevel: Severity;
108
+ executionStatus: "not-executed" | "executed" | "blocked-by-enforcement";
109
+ approvalStatus: "not-required" | "pending" | "approved" | "denied";
110
+ timestamp: string;
111
+ }
112
+ export interface AgentEventContract {
113
+ id: string;
114
+ sessionId: string;
115
+ timestamp: string;
116
+ type: string;
117
+ summary: string;
118
+ detail?: Record<string, string>;
119
+ risk?: Severity | "none";
120
+ }
121
+ export interface OwnershipContract {
122
+ path: string;
123
+ owners: string[];
124
+ kind: EpistemicKind;
125
+ evidence: EvidenceRef[];
126
+ }
127
+ export interface RepositoryIdentity {
128
+ id: string;
129
+ root: string;
130
+ name?: string;
131
+ }
132
+ export interface WorkspaceIdentity {
133
+ id: string;
134
+ name: string;
135
+ repositoryIds: string[];
136
+ }
137
+ export interface SnapshotContract {
138
+ id: string;
139
+ repositoryId: string;
140
+ createdAt: string;
141
+ kind: "brain" | "platform" | "graph" | "workspace";
142
+ analysisVersion: string;
143
+ limitations: string[];
144
+ }
145
+ export interface StorageProvider {
146
+ readonly kind: "filesystem" | "sqlite" | "postgres" | "memory";
147
+ get(key: string): Promise<string | null>;
148
+ set(key: string, value: string): Promise<void>;
149
+ delete(key: string): Promise<void>;
150
+ list(prefix: string): Promise<string[]>;
151
+ }
152
+ /** Feature flags for experimental AgentDoctor 2.0 capabilities. */
153
+ export interface FeatureFlags {
154
+ typescriptAst: boolean;
155
+ sqliteStorage: boolean;
156
+ teamMode: boolean;
157
+ enforcementRunner: boolean;
158
+ vectorSearch: boolean;
159
+ architectureC4: boolean;
160
+ }
161
+ export declare const DEFAULT_FEATURE_FLAGS: FeatureFlags;
162
+ export declare function clampConfidence(value: number): Confidence;
@@ -0,0 +1,18 @@
1
+ /**
2
+ * AgentDoctor 2.0 shared core contracts.
3
+ * Unifies Safety / Brain / Platform without breaking existing module types.
4
+ */
5
+ export const CONTRACTS_VERSION = "2.0.0-contracts";
6
+ export const DEFAULT_FEATURE_FLAGS = {
7
+ typescriptAst: true,
8
+ sqliteStorage: false,
9
+ teamMode: false,
10
+ enforcementRunner: false,
11
+ vectorSearch: false,
12
+ architectureC4: true,
13
+ };
14
+ export function clampConfidence(value) {
15
+ if (!Number.isFinite(value))
16
+ return 0;
17
+ return Math.max(0, Math.min(1, value));
18
+ }
@@ -0,0 +1,48 @@
1
+ import type { Finding, ScanResult } from "../../types/index.js";
2
+ export interface NamedBaseline {
3
+ name: string;
4
+ createdAt: string;
5
+ root: string;
6
+ findingIds: string[];
7
+ findingFingerprints: string[];
8
+ counts: {
9
+ total: number;
10
+ critical: number;
11
+ warning: number;
12
+ info: number;
13
+ };
14
+ }
15
+ export declare function fingerprintFinding(finding: Finding): string;
16
+ export declare function baselineFromScan(name: string, scan: ScanResult): NamedBaseline;
17
+ export declare function saveNamedBaseline(rootInput: string, baseline: NamedBaseline): Promise<string>;
18
+ export declare function loadNamedBaseline(rootInput: string, name: string): Promise<NamedBaseline | null>;
19
+ export declare function listNamedBaselines(rootInput: string): Promise<string[]>;
20
+ export declare function deleteNamedBaseline(rootInput: string, name: string): Promise<void>;
21
+ export interface BaselineDiff {
22
+ name: string;
23
+ /** New findings vs baseline (fingerprints) */
24
+ added: string[];
25
+ /** Resolved findings vs baseline */
26
+ removed: string[];
27
+ /** Recurring / unchanged fingerprints */
28
+ recurring: string[];
29
+ unchanged: number;
30
+ }
31
+ export declare function compareToBaseline(baseline: NamedBaseline, findings: Finding[]): BaselineDiff;
32
+ export interface BaselineTrendPoint {
33
+ name: string;
34
+ createdAt: string;
35
+ counts: NamedBaseline["counts"];
36
+ }
37
+ export interface BaselineTrends {
38
+ root: string;
39
+ points: BaselineTrendPoint[];
40
+ /** Count deltas between chronologically adjacent baselines (no causality claimed). */
41
+ deltas: Array<{
42
+ from: string;
43
+ to: string;
44
+ totalDelta: number;
45
+ criticalDelta: number;
46
+ }>;
47
+ }
48
+ export declare function computeBaselineTrends(rootInput: string): Promise<BaselineTrends>;
@@ -0,0 +1,163 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { atomicWriteTextFile } from "../../utils/fs.js";
4
+ import { resolveRepoRoot } from "../../utils/path.js";
5
+ function baselineDir(root) {
6
+ return path.join(root, ".agentdoctor", "baselines");
7
+ }
8
+ function safeBaselineName(name) {
9
+ const safe = name.replace(/[^a-zA-Z0-9._-]/g, "_");
10
+ if (!safe || safe === "." || safe === "..") {
11
+ throw new Error("invalid baseline name");
12
+ }
13
+ return safe;
14
+ }
15
+ export function fingerprintFinding(finding) {
16
+ return [finding.ruleId, finding.severity, finding.evidence?.path ?? "", finding.title].join("|");
17
+ }
18
+ function isNamedBaseline(value) {
19
+ if (!value || typeof value !== "object")
20
+ return false;
21
+ const b = value;
22
+ return (typeof b.name === "string" &&
23
+ typeof b.createdAt === "string" &&
24
+ typeof b.root === "string" &&
25
+ Array.isArray(b.findingIds) &&
26
+ Array.isArray(b.findingFingerprints) &&
27
+ typeof b.counts === "object" &&
28
+ b.counts !== null);
29
+ }
30
+ export function baselineFromScan(name, scan) {
31
+ const fingerprints = scan.findings.map(fingerprintFinding).sort();
32
+ return {
33
+ name,
34
+ createdAt: new Date().toISOString(),
35
+ root: scan.repository.root,
36
+ findingIds: scan.findings.map((f) => f.id).sort(),
37
+ findingFingerprints: fingerprints,
38
+ counts: {
39
+ total: scan.findings.length,
40
+ critical: scan.findings.filter((f) => f.severity === "critical").length,
41
+ warning: scan.findings.filter((f) => f.severity === "warning").length,
42
+ info: scan.findings.filter((f) => f.severity === "info").length,
43
+ },
44
+ };
45
+ }
46
+ export async function saveNamedBaseline(rootInput, baseline) {
47
+ const root = resolveRepoRoot(rootInput);
48
+ const safeName = safeBaselineName(baseline.name);
49
+ const dir = baselineDir(root);
50
+ await fs.mkdir(dir, { recursive: true });
51
+ const file = path.join(dir, `${safeName}.json`);
52
+ await atomicWriteTextFile(file, `${JSON.stringify(baseline, null, 2)}\n`);
53
+ return file;
54
+ }
55
+ export async function loadNamedBaseline(rootInput, name) {
56
+ const root = resolveRepoRoot(rootInput);
57
+ const safeName = safeBaselineName(name);
58
+ try {
59
+ const raw = await fs.readFile(path.join(baselineDir(root), `${safeName}.json`), "utf8");
60
+ let parsed;
61
+ try {
62
+ parsed = JSON.parse(raw);
63
+ }
64
+ catch {
65
+ throw new Error(`corrupt baseline JSON: ${safeName}`);
66
+ }
67
+ if (!isNamedBaseline(parsed)) {
68
+ throw new Error(`corrupt baseline schema: ${safeName}`);
69
+ }
70
+ return parsed;
71
+ }
72
+ catch (error) {
73
+ const code = error.code;
74
+ if (code === "ENOENT")
75
+ return null;
76
+ throw error;
77
+ }
78
+ }
79
+ export async function listNamedBaselines(rootInput) {
80
+ const root = resolveRepoRoot(rootInput);
81
+ try {
82
+ const names = await fs.readdir(baselineDir(root));
83
+ return names
84
+ .filter((n) => n.endsWith(".json"))
85
+ .map((n) => n.replace(/\.json$/, ""))
86
+ .sort();
87
+ }
88
+ catch {
89
+ return [];
90
+ }
91
+ }
92
+ export async function deleteNamedBaseline(rootInput, name) {
93
+ const root = resolveRepoRoot(rootInput);
94
+ const safeName = safeBaselineName(name);
95
+ const file = path.join(baselineDir(root), `${safeName}.json`);
96
+ try {
97
+ await fs.unlink(file);
98
+ }
99
+ catch (error) {
100
+ const code = error.code;
101
+ if (code === "ENOENT") {
102
+ throw new Error(`baseline not found: ${name}`);
103
+ }
104
+ throw error;
105
+ }
106
+ }
107
+ export function compareToBaseline(baseline, findings) {
108
+ const current = new Set(findings.map(fingerprintFinding));
109
+ const previous = new Set(baseline.findingFingerprints);
110
+ const added = [];
111
+ const removed = [];
112
+ const recurring = [];
113
+ for (const fp of current) {
114
+ if (!previous.has(fp))
115
+ added.push(fp);
116
+ else
117
+ recurring.push(fp);
118
+ }
119
+ for (const fp of previous) {
120
+ if (!current.has(fp))
121
+ removed.push(fp);
122
+ }
123
+ return {
124
+ name: baseline.name,
125
+ added: added.sort(),
126
+ removed: removed.sort(),
127
+ recurring: recurring.sort(),
128
+ unchanged: recurring.length,
129
+ };
130
+ }
131
+ export async function computeBaselineTrends(rootInput) {
132
+ const root = resolveRepoRoot(rootInput);
133
+ const names = await listNamedBaselines(root);
134
+ const points = [];
135
+ for (const name of names) {
136
+ try {
137
+ const baseline = await loadNamedBaseline(root, name);
138
+ if (!baseline)
139
+ continue;
140
+ points.push({
141
+ name: baseline.name,
142
+ createdAt: baseline.createdAt,
143
+ counts: baseline.counts,
144
+ });
145
+ }
146
+ catch {
147
+ // skip corrupt for trends listing; load errors already fail-closed elsewhere
148
+ }
149
+ }
150
+ points.sort((a, b) => a.createdAt.localeCompare(b.createdAt) || a.name.localeCompare(b.name));
151
+ const deltas = [];
152
+ for (let i = 1; i < points.length; i += 1) {
153
+ const prev = points[i - 1];
154
+ const next = points[i];
155
+ deltas.push({
156
+ from: prev.name,
157
+ to: next.name,
158
+ totalDelta: next.counts.total - prev.counts.total,
159
+ criticalDelta: next.counts.critical - prev.counts.critical,
160
+ });
161
+ }
162
+ return { root, points, deltas };
163
+ }
@@ -0,0 +1,26 @@
1
+ import { type ProjectBrain, type SnapshotMeta } from "../understanding/brain/index.js";
2
+ export interface BrainStatus {
3
+ root: string;
4
+ storeRoot: string;
5
+ hasSnapshot: boolean;
6
+ latestSnapshotId: string | null;
7
+ snapshotCount: number;
8
+ projectName: string | null;
9
+ schemaVersion: string | null;
10
+ }
11
+ export interface BrainSearchHit {
12
+ kind: "claim" | "component" | "unknown" | "limitation";
13
+ id: string;
14
+ text: string;
15
+ score: number;
16
+ }
17
+ export declare function getBrainStatus(root: string): Promise<BrainStatus>;
18
+ export declare function initBrainStore(root: string): Promise<BrainStatus>;
19
+ export declare function loadLatestBrain(root: string): Promise<ProjectBrain | null>;
20
+ export declare function rebuildBrain(root: string): Promise<ProjectBrain>;
21
+ export declare function listBrainSnapshots(root: string): Promise<SnapshotMeta[]>;
22
+ export declare function showBrainSnapshot(root: string, snapshotId: string): Promise<ProjectBrain>;
23
+ export declare function searchBrain(brain: ProjectBrain, query: string): BrainSearchHit[];
24
+ export declare function exportBrain(root: string, outputPath: string, snapshotId?: string): Promise<string>;
25
+ export declare function importBrain(root: string, inputPath: string): Promise<SnapshotMeta>;
26
+ export declare function inspectBrainSummary(root: string): Promise<Record<string, unknown>>;
@@ -0,0 +1,175 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { LocalBrainStore, createBrainQueryEngine, redactBrainForStorage, } from "../understanding/brain/index.js";
4
+ import { compileProjectBrain } from "../../mcp/brain/compile.js";
5
+ import { resolveRepoRoot } from "../../utils/path.js";
6
+ import { isDirectory } from "../../utils/fs.js";
7
+ function storeFor(root) {
8
+ return LocalBrainStore.underRepo(resolveRepoRoot(root));
9
+ }
10
+ export async function getBrainStatus(root) {
11
+ const resolved = resolveRepoRoot(root);
12
+ if (!(await isDirectory(resolved))) {
13
+ throw new Error(`not a directory: ${resolved}`);
14
+ }
15
+ const store = storeFor(resolved);
16
+ const storeRoot = path.join(resolved, ".agentdoctor", "project-brain");
17
+ try {
18
+ const meta = await store.readMeta();
19
+ return {
20
+ root: resolved,
21
+ storeRoot,
22
+ hasSnapshot: meta.latestSnapshotId !== null,
23
+ latestSnapshotId: meta.latestSnapshotId,
24
+ snapshotCount: meta.snapshots.length,
25
+ projectName: meta.projectName || null,
26
+ schemaVersion: meta.schemaVersion || null,
27
+ };
28
+ }
29
+ catch {
30
+ return {
31
+ root: resolved,
32
+ storeRoot,
33
+ hasSnapshot: false,
34
+ latestSnapshotId: null,
35
+ snapshotCount: 0,
36
+ projectName: null,
37
+ schemaVersion: null,
38
+ };
39
+ }
40
+ }
41
+ export async function initBrainStore(root) {
42
+ const resolved = resolveRepoRoot(root);
43
+ const store = storeFor(resolved);
44
+ await store.ensureRoot();
45
+ return getBrainStatus(resolved);
46
+ }
47
+ export async function loadLatestBrain(root) {
48
+ const store = storeFor(root);
49
+ try {
50
+ const brain = await store.loadLatest();
51
+ return brain ? redactBrainForStorage(brain) : null;
52
+ }
53
+ catch {
54
+ return null;
55
+ }
56
+ }
57
+ export async function rebuildBrain(root) {
58
+ const resolved = resolveRepoRoot(root);
59
+ const store = storeFor(resolved);
60
+ let previous = null;
61
+ try {
62
+ previous = await store.loadLatest();
63
+ }
64
+ catch {
65
+ previous = null;
66
+ }
67
+ const brain = await compileProjectBrain(resolved, {
68
+ ...(previous ? { previousClaims: previous.claims } : {}),
69
+ });
70
+ try {
71
+ await store.saveSnapshot(brain);
72
+ }
73
+ catch (error) {
74
+ const message = error instanceof Error ? error.message : String(error);
75
+ if (!/refusing overwrite/i.test(message)) {
76
+ throw error;
77
+ }
78
+ }
79
+ return redactBrainForStorage(brain);
80
+ }
81
+ export async function listBrainSnapshots(root) {
82
+ const store = storeFor(root);
83
+ try {
84
+ return await store.listSnapshots();
85
+ }
86
+ catch {
87
+ return [];
88
+ }
89
+ }
90
+ export async function showBrainSnapshot(root, snapshotId) {
91
+ const store = storeFor(root);
92
+ const brain = await store.loadSnapshot(snapshotId);
93
+ return redactBrainForStorage(brain);
94
+ }
95
+ export function searchBrain(brain, query) {
96
+ const q = query.trim().toLowerCase();
97
+ if (!q) {
98
+ return [];
99
+ }
100
+ const hits = [];
101
+ for (const claim of brain.claims) {
102
+ const text = `${claim.subject} ${claim.predicate} ${claim.object}`;
103
+ if (text.toLowerCase().includes(q)) {
104
+ hits.push({ kind: "claim", id: claim.id, text: text.slice(0, 240), score: 1 });
105
+ }
106
+ }
107
+ for (const component of brain.components) {
108
+ const text = `${component.id} ${component.name ?? ""} ${component.type}`;
109
+ if (text.toLowerCase().includes(q)) {
110
+ hits.push({ kind: "component", id: component.id, text: text.slice(0, 240), score: 1 });
111
+ }
112
+ }
113
+ for (const [i, unknown] of brain.unknowns.entries()) {
114
+ if (unknown.toLowerCase().includes(q)) {
115
+ hits.push({ kind: "unknown", id: `unknown-${i}`, text: unknown.slice(0, 240), score: 0.5 });
116
+ }
117
+ }
118
+ for (const [i, limitation] of brain.limitations.entries()) {
119
+ if (limitation.toLowerCase().includes(q)) {
120
+ hits.push({
121
+ kind: "limitation",
122
+ id: `limitation-${i}`,
123
+ text: limitation.slice(0, 240),
124
+ score: 0.5,
125
+ });
126
+ }
127
+ }
128
+ return hits.sort((a, b) => b.score - a.score || a.id.localeCompare(b.id));
129
+ }
130
+ export async function exportBrain(root, outputPath, snapshotId) {
131
+ const store = storeFor(root);
132
+ const brain = snapshotId ? await store.loadSnapshot(snapshotId) : await store.loadLatest();
133
+ if (!brain) {
134
+ throw new Error("no Project Brain snapshot to export; run agentdoctor brain rebuild");
135
+ }
136
+ const safe = redactBrainForStorage(brain);
137
+ const absolute = path.resolve(outputPath);
138
+ await fs.mkdir(path.dirname(absolute), { recursive: true });
139
+ await fs.writeFile(absolute, `${JSON.stringify(safe, null, 2)}\n`, "utf8");
140
+ return absolute;
141
+ }
142
+ export async function importBrain(root, inputPath) {
143
+ const absolute = path.resolve(inputPath);
144
+ const raw = await fs.readFile(absolute, "utf8");
145
+ let parsed;
146
+ try {
147
+ parsed = JSON.parse(raw);
148
+ }
149
+ catch {
150
+ throw new Error(`import file is not valid JSON: ${absolute}`);
151
+ }
152
+ if (!parsed || typeof parsed !== "object") {
153
+ throw new Error("import JSON must be an object");
154
+ }
155
+ const store = storeFor(root);
156
+ // saveSnapshot redacts and validates via serialize path
157
+ return store.saveSnapshot(parsed);
158
+ }
159
+ export async function inspectBrainSummary(root) {
160
+ let brain = await loadLatestBrain(root);
161
+ if (!brain) {
162
+ brain = await rebuildBrain(root);
163
+ }
164
+ const engine = createBrainQueryEngine(brain);
165
+ const summary = engine.execute({ type: "ProjectSummary" });
166
+ return {
167
+ snapshotId: brain.snapshot.id,
168
+ projectName: brain.metadata.projectName,
169
+ claimCount: brain.claims.length,
170
+ componentCount: brain.components.length,
171
+ riskCount: brain.risks.risks.length,
172
+ unknownCount: brain.unknowns.length,
173
+ summary: summary.result,
174
+ };
175
+ }