@praneeth_54/agentdoctor 1.0.0 → 1.1.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 (208) hide show
  1. package/CHANGELOG.md +34 -5
  2. package/README.md +15 -12
  3. package/dist/cli/commands/brain-mcp.d.ts +9 -0
  4. package/dist/cli/commands/brain-mcp.js +38 -0
  5. package/dist/cli/program.js +13 -0
  6. package/dist/constants.d.ts +1 -1
  7. package/dist/constants.js +1 -1
  8. package/dist/core/understanding/architecture/index.d.ts +4 -0
  9. package/dist/core/understanding/architecture/index.js +3 -0
  10. package/dist/core/understanding/architecture/infer.d.ts +6 -0
  11. package/dist/core/understanding/architecture/infer.js +39 -0
  12. package/dist/core/understanding/architecture/models.d.ts +23 -0
  13. package/dist/core/understanding/architecture/models.js +127 -0
  14. package/dist/core/understanding/architecture/patterns/distribution.d.ts +2 -0
  15. package/dist/core/understanding/architecture/patterns/distribution.js +115 -0
  16. package/dist/core/understanding/architecture/patterns/index.d.ts +2 -0
  17. package/dist/core/understanding/architecture/patterns/index.js +8 -0
  18. package/dist/core/understanding/architecture/patterns/layered.d.ts +2 -0
  19. package/dist/core/understanding/architecture/patterns/layered.js +279 -0
  20. package/dist/core/understanding/architecture/patterns/structural.d.ts +2 -0
  21. package/dist/core/understanding/architecture/patterns/structural.js +197 -0
  22. package/dist/core/understanding/architecture/rules.d.ts +4 -0
  23. package/dist/core/understanding/architecture/rules.js +6 -0
  24. package/dist/core/understanding/architecture/types.d.ts +52 -0
  25. package/dist/core/understanding/architecture/types.js +1 -0
  26. package/dist/core/understanding/brain/build.d.ts +13 -0
  27. package/dist/core/understanding/brain/build.js +158 -0
  28. package/dist/core/understanding/brain/claims/index.d.ts +3 -0
  29. package/dist/core/understanding/brain/claims/index.js +2 -0
  30. package/dist/core/understanding/brain/claims/lifecycle.d.ts +16 -0
  31. package/dist/core/understanding/brain/claims/lifecycle.js +78 -0
  32. package/dist/core/understanding/brain/claims/types.d.ts +32 -0
  33. package/dist/core/understanding/brain/claims/types.js +25 -0
  34. package/dist/core/understanding/brain/components/index.d.ts +17 -0
  35. package/dist/core/understanding/brain/components/index.js +97 -0
  36. package/dist/core/understanding/brain/components/types.d.ts +16 -0
  37. package/dist/core/understanding/brain/components/types.js +8 -0
  38. package/dist/core/understanding/brain/confidence.d.ts +19 -0
  39. package/dist/core/understanding/brain/confidence.js +39 -0
  40. package/dist/core/understanding/brain/contract.d.ts +17 -0
  41. package/dist/core/understanding/brain/contract.js +100 -0
  42. package/dist/core/understanding/brain/contradictions/index.d.ts +12 -0
  43. package/dist/core/understanding/brain/contradictions/index.js +57 -0
  44. package/dist/core/understanding/brain/contradictions/types.d.ts +12 -0
  45. package/dist/core/understanding/brain/contradictions/types.js +8 -0
  46. package/dist/core/understanding/brain/delta.d.ts +25 -0
  47. package/dist/core/understanding/brain/delta.js +116 -0
  48. package/dist/core/understanding/brain/evidence/index.d.ts +3 -0
  49. package/dist/core/understanding/brain/evidence/index.js +2 -0
  50. package/dist/core/understanding/brain/evidence/redact.d.ts +4 -0
  51. package/dist/core/understanding/brain/evidence/redact.js +36 -0
  52. package/dist/core/understanding/brain/evidence/types.d.ts +34 -0
  53. package/dist/core/understanding/brain/evidence/types.js +35 -0
  54. package/dist/core/understanding/brain/explain.d.ts +17 -0
  55. package/dist/core/understanding/brain/explain.js +33 -0
  56. package/dist/core/understanding/brain/index.d.ts +28 -0
  57. package/dist/core/understanding/brain/index.js +16 -0
  58. package/dist/core/understanding/brain/migrate.d.ts +21 -0
  59. package/dist/core/understanding/brain/migrate.js +47 -0
  60. package/dist/core/understanding/brain/query.d.ts +60 -0
  61. package/dist/core/understanding/brain/query.js +92 -0
  62. package/dist/core/understanding/brain/security.d.ts +3 -0
  63. package/dist/core/understanding/brain/security.js +43 -0
  64. package/dist/core/understanding/brain/storage/index.d.ts +2 -0
  65. package/dist/core/understanding/brain/storage/index.js +1 -0
  66. package/dist/core/understanding/brain/storage/store.d.ts +53 -0
  67. package/dist/core/understanding/brain/storage/store.js +305 -0
  68. package/dist/core/understanding/brain/trace.d.ts +24 -0
  69. package/dist/core/understanding/brain/trace.js +207 -0
  70. package/dist/core/understanding/brain/types.d.ts +32 -0
  71. package/dist/core/understanding/brain/types.js +8 -0
  72. package/dist/core/understanding/brain/version.d.ts +13 -0
  73. package/dist/core/understanding/brain/version.js +7 -0
  74. package/dist/core/understanding/delta/compare.d.ts +26 -0
  75. package/dist/core/understanding/delta/compare.js +74 -0
  76. package/dist/core/understanding/delta/index.d.ts +2 -0
  77. package/dist/core/understanding/delta/index.js +1 -0
  78. package/dist/core/understanding/dependencies/discover.d.ts +7 -0
  79. package/dist/core/understanding/dependencies/discover.js +550 -0
  80. package/dist/core/understanding/dependencies/extract.d.ts +10 -0
  81. package/dist/core/understanding/dependencies/extract.js +189 -0
  82. package/dist/core/understanding/dependencies/index.d.ts +4 -0
  83. package/dist/core/understanding/dependencies/index.js +3 -0
  84. package/dist/core/understanding/dependencies/models.d.ts +12 -0
  85. package/dist/core/understanding/dependencies/models.js +125 -0
  86. package/dist/core/understanding/dependencies/types.d.ts +27 -0
  87. package/dist/core/understanding/dependencies/types.js +1 -0
  88. package/dist/core/understanding/domain/discover.d.ts +14 -0
  89. package/dist/core/understanding/domain/discover.js +90 -0
  90. package/dist/core/understanding/domain/index.d.ts +2 -0
  91. package/dist/core/understanding/domain/index.js +1 -0
  92. package/dist/core/understanding/entrypoints/discover.d.ts +6 -0
  93. package/dist/core/understanding/entrypoints/discover.js +62 -0
  94. package/dist/core/understanding/entrypoints/extract.d.ts +19 -0
  95. package/dist/core/understanding/entrypoints/extract.js +43 -0
  96. package/dist/core/understanding/entrypoints/index.d.ts +4 -0
  97. package/dist/core/understanding/entrypoints/index.js +3 -0
  98. package/dist/core/understanding/entrypoints/models.d.ts +26 -0
  99. package/dist/core/understanding/entrypoints/models.js +216 -0
  100. package/dist/core/understanding/entrypoints/types.d.ts +20 -0
  101. package/dist/core/understanding/entrypoints/types.js +1 -0
  102. package/dist/core/understanding/index.d.ts +32 -0
  103. package/dist/core/understanding/index.js +18 -0
  104. package/dist/core/understanding/mind/build.d.ts +16 -0
  105. package/dist/core/understanding/mind/build.js +134 -0
  106. package/dist/core/understanding/mind/index.d.ts +6 -0
  107. package/dist/core/understanding/mind/index.js +3 -0
  108. package/dist/core/understanding/mind/query.d.ts +33 -0
  109. package/dist/core/understanding/mind/query.js +39 -0
  110. package/dist/core/understanding/mind/types.d.ts +24 -0
  111. package/dist/core/understanding/mind/types.js +7 -0
  112. package/dist/core/understanding/model/builder.d.ts +9 -0
  113. package/dist/core/understanding/model/builder.js +209 -0
  114. package/dist/core/understanding/model/ids.d.ts +7 -0
  115. package/dist/core/understanding/model/ids.js +48 -0
  116. package/dist/core/understanding/model/index.d.ts +7 -0
  117. package/dist/core/understanding/model/index.js +6 -0
  118. package/dist/core/understanding/model/schema.d.ts +12 -0
  119. package/dist/core/understanding/model/schema.js +33 -0
  120. package/dist/core/understanding/model/serializer.d.ts +9 -0
  121. package/dist/core/understanding/model/serializer.js +46 -0
  122. package/dist/core/understanding/model/types.d.ts +110 -0
  123. package/dist/core/understanding/model/types.js +1 -0
  124. package/dist/core/understanding/model/validator.d.ts +5 -0
  125. package/dist/core/understanding/model/validator.js +126 -0
  126. package/dist/core/understanding/model/version.d.ts +9 -0
  127. package/dist/core/understanding/model/version.js +7 -0
  128. package/dist/core/understanding/ownership/discover.d.ts +12 -0
  129. package/dist/core/understanding/ownership/discover.js +242 -0
  130. package/dist/core/understanding/ownership/index.d.ts +2 -0
  131. package/dist/core/understanding/ownership/index.js +1 -0
  132. package/dist/core/understanding/ownership/types.d.ts +23 -0
  133. package/dist/core/understanding/ownership/types.js +1 -0
  134. package/dist/core/understanding/query/engine.d.ts +24 -0
  135. package/dist/core/understanding/query/engine.js +35 -0
  136. package/dist/core/understanding/query/errors.d.ts +13 -0
  137. package/dist/core/understanding/query/errors.js +26 -0
  138. package/dist/core/understanding/query/executor.d.ts +8 -0
  139. package/dist/core/understanding/query/executor.js +34 -0
  140. package/dist/core/understanding/query/index.d.ts +7 -0
  141. package/dist/core/understanding/query/index.js +5 -0
  142. package/dist/core/understanding/query/models.d.ts +102 -0
  143. package/dist/core/understanding/query/models.js +1 -0
  144. package/dist/core/understanding/query/query.d.ts +68 -0
  145. package/dist/core/understanding/query/query.js +145 -0
  146. package/dist/core/understanding/query/registry.d.ts +8 -0
  147. package/dist/core/understanding/query/registry.js +340 -0
  148. package/dist/core/understanding/query/types.d.ts +63 -0
  149. package/dist/core/understanding/query/types.js +1 -0
  150. package/dist/core/understanding/relationships/discover.d.ts +11 -0
  151. package/dist/core/understanding/relationships/discover.js +517 -0
  152. package/dist/core/understanding/relationships/extract.d.ts +14 -0
  153. package/dist/core/understanding/relationships/extract.js +165 -0
  154. package/dist/core/understanding/relationships/index.d.ts +4 -0
  155. package/dist/core/understanding/relationships/index.js +3 -0
  156. package/dist/core/understanding/relationships/models.d.ts +21 -0
  157. package/dist/core/understanding/relationships/models.js +185 -0
  158. package/dist/core/understanding/relationships/types.d.ts +42 -0
  159. package/dist/core/understanding/relationships/types.js +1 -0
  160. package/dist/core/understanding/risks/discover.d.ts +8 -0
  161. package/dist/core/understanding/risks/discover.js +155 -0
  162. package/dist/core/understanding/risks/index.d.ts +2 -0
  163. package/dist/core/understanding/risks/index.js +1 -0
  164. package/dist/core/understanding/risks/types.d.ts +26 -0
  165. package/dist/core/understanding/risks/types.js +1 -0
  166. package/dist/core/understanding/shared/domain-lexicon.d.ts +9 -0
  167. package/dist/core/understanding/shared/domain-lexicon.js +107 -0
  168. package/dist/core/understanding/shared/index.d.ts +2 -0
  169. package/dist/core/understanding/shared/index.js +2 -0
  170. package/dist/core/understanding/shared/tokens.d.ts +7 -0
  171. package/dist/core/understanding/shared/tokens.js +141 -0
  172. package/dist/core/understanding/snapshot/identity.d.ts +24 -0
  173. package/dist/core/understanding/snapshot/identity.js +79 -0
  174. package/dist/core/understanding/snapshot/index.d.ts +2 -0
  175. package/dist/core/understanding/snapshot/index.js +1 -0
  176. package/dist/core/understanding/types/index.d.ts +22 -0
  177. package/dist/core/understanding/types/index.js +1 -0
  178. package/dist/core/understanding/understand/formatter.d.ts +10 -0
  179. package/dist/core/understanding/understand/formatter.js +115 -0
  180. package/dist/core/understanding/understand/index.d.ts +4 -0
  181. package/dist/core/understanding/understand/index.js +3 -0
  182. package/dist/core/understanding/understand/service.d.ts +17 -0
  183. package/dist/core/understanding/understand/service.js +76 -0
  184. package/dist/core/understanding/understand/summary.d.ts +14 -0
  185. package/dist/core/understanding/understand/summary.js +39 -0
  186. package/dist/core/understanding/understand/types.d.ts +31 -0
  187. package/dist/core/understanding/understand/types.js +1 -0
  188. package/dist/mcp/brain/compile.d.ts +9 -0
  189. package/dist/mcp/brain/compile.js +42 -0
  190. package/dist/mcp/brain/errors.d.ts +12 -0
  191. package/dist/mcp/brain/errors.js +23 -0
  192. package/dist/mcp/brain/index.d.ts +11 -0
  193. package/dist/mcp/brain/index.js +8 -0
  194. package/dist/mcp/brain/provenance.d.ts +29 -0
  195. package/dist/mcp/brain/provenance.js +59 -0
  196. package/dist/mcp/brain/schemas.d.ts +13 -0
  197. package/dist/mcp/brain/schemas.js +105 -0
  198. package/dist/mcp/brain/security/root.d.ts +6 -0
  199. package/dist/mcp/brain/security/root.js +66 -0
  200. package/dist/mcp/brain/server.d.ts +12 -0
  201. package/dist/mcp/brain/server.js +46 -0
  202. package/dist/mcp/brain/session.d.ts +33 -0
  203. package/dist/mcp/brain/session.js +117 -0
  204. package/dist/mcp/brain/tools/handlers.d.ts +13 -0
  205. package/dist/mcp/brain/tools/handlers.js +483 -0
  206. package/dist/mcp/brain/tools/registry.d.ts +9 -0
  207. package/dist/mcp/brain/tools/registry.js +191 -0
  208. package/package.json +5 -1
@@ -0,0 +1,92 @@
1
+ import { averageBrainConfidence } from "./confidence.js";
2
+ import { buildBrainDelta } from "./delta.js";
3
+ function meta(brain, queryType) {
4
+ return {
5
+ queryType,
6
+ brainId: brain.metadata.brainId,
7
+ schemaVersion: brain.metadata.schemaVersion,
8
+ projectName: brain.metadata.projectName,
9
+ };
10
+ }
11
+ function envelope(brain, queryType, result, evidenceIds, confidence, started) {
12
+ return {
13
+ result,
14
+ evidenceIds,
15
+ confidence,
16
+ snapshotId: brain.snapshot.id,
17
+ metadata: meta(brain, queryType),
18
+ executionTimeMs: Math.max(0, Math.round(performance.now() - started)),
19
+ };
20
+ }
21
+ export class BrainQueryEngine {
22
+ brain;
23
+ constructor(brain) {
24
+ this.brain = brain;
25
+ }
26
+ execute(query) {
27
+ const started = performance.now();
28
+ const brain = this.brain;
29
+ switch (query.type) {
30
+ case "ProjectSummary":
31
+ return envelope(brain, query.type, {
32
+ projectName: brain.metadata.projectName,
33
+ snapshotId: brain.snapshot.id,
34
+ domainCount: brain.model.domains.length,
35
+ componentCount: brain.components.length,
36
+ claimCount: brain.claims.filter((c) => c.status === "ACTIVE").length,
37
+ riskCount: brain.risks.risks.length,
38
+ unknownCount: brain.unknowns.length,
39
+ topDomains: brain.model.summary.topDomains,
40
+ }, [], averageBrainConfidence(brain.claims.map((c) => c.confidence)), started);
41
+ case "ListDomains":
42
+ return envelope(brain, query.type, brain.model.domains, brain.model.domains.flatMap((d) => brain.evidence.filter((e) => d.evidence.includes(e.locator)).map((e) => e.id)), averageBrainConfidence(brain.model.domains.map((d) => d.confidence)), started);
43
+ case "ListComponents":
44
+ return envelope(brain, query.type, brain.components, brain.components.flatMap((c) => c.evidenceIds), averageBrainConfidence(brain.components.map((c) => c.confidence)), started);
45
+ case "ListEntrypoints":
46
+ return envelope(brain, query.type, brain.model.entrypoints, [], averageBrainConfidence(brain.model.entrypoints.map((e) => e.confidence)), started);
47
+ case "ListDependencies":
48
+ return envelope(brain, query.type, brain.model.dependencies, [], averageBrainConfidence(brain.model.dependencies.map((d) => d.confidence)), started);
49
+ case "ListRelationships":
50
+ return envelope(brain, query.type, brain.model.relationships, [], averageBrainConfidence(brain.model.relationships.map((r) => r.confidence)), started);
51
+ case "ListArchitectures":
52
+ return envelope(brain, query.type, brain.model.architectures, [], averageBrainConfidence(brain.model.architectures.map((a) => a.confidence)), started);
53
+ case "ListOwnership":
54
+ return envelope(brain, query.type, brain.ownership, [], averageBrainConfidence(brain.ownership.ownerships.map((o) => o.confidence)), started);
55
+ case "ListRisks":
56
+ return envelope(brain, query.type, brain.risks, [], averageBrainConfidence(brain.risks.risks.map((r) => r.confidence)), started);
57
+ case "ListClaims": {
58
+ const filtered = query.status
59
+ ? brain.claims.filter((c) => c.status === query.status)
60
+ : brain.claims.filter((c) => c.status === "ACTIVE" || c.status === "CONTRADICTED");
61
+ return envelope(brain, query.type, filtered, filtered.flatMap((c) => c.evidenceIds), averageBrainConfidence(filtered.map((c) => c.confidence)), started);
62
+ }
63
+ case "ListEvidence":
64
+ return envelope(brain, query.type, brain.evidence, brain.evidence.map((e) => e.id), 1, started);
65
+ case "ListContradictions":
66
+ return envelope(brain, query.type, brain.contradictions, brain.contradictions.flatMap((c) => c.evidenceIds), averageBrainConfidence(brain.contradictions.map((c) => c.confidence)), started);
67
+ case "ListUnknowns":
68
+ return envelope(brain, query.type, brain.unknowns, [], 1, started);
69
+ case "ListChanges": {
70
+ if (!query.previous) {
71
+ return envelope(brain, query.type, { message: "previous brain required for changes" }, [], 0, started);
72
+ }
73
+ const delta = buildBrainDelta(query.previous, brain);
74
+ return envelope(brain, query.type, delta, [], 1, started);
75
+ }
76
+ case "ListInvalidations":
77
+ return envelope(brain, query.type, brain.claims.filter((c) => c.status === "INVALIDATED"), [], 1, started);
78
+ case "Impact":
79
+ case "BlastRadius": {
80
+ const target = query.target.toLowerCase();
81
+ const deps = brain.model.dependencies.filter((d) => d.from.toLowerCase().includes(target) || d.to.toLowerCase().includes(target));
82
+ const comps = brain.components.filter((c) => c.path.toLowerCase().includes(target) || c.name.toLowerCase().includes(target));
83
+ return envelope(brain, query.type, { target: query.target, dependencies: deps, components: comps }, comps.flatMap((c) => c.evidenceIds), averageBrainConfidence(deps.map((d) => d.confidence)), started);
84
+ }
85
+ default:
86
+ throw new Error(`unsupported brain query`);
87
+ }
88
+ }
89
+ }
90
+ export function createBrainQueryEngine(brain) {
91
+ return new BrainQueryEngine(brain);
92
+ }
@@ -0,0 +1,3 @@
1
+ import type { ProjectBrain } from "./types.js";
2
+ /** Deep-safe Brain for disk/export: redact evidence and secret-like claim strings. */
3
+ export declare function redactBrainForStorage(brain: ProjectBrain): ProjectBrain;
@@ -0,0 +1,43 @@
1
+ import { looksLikeSensitiveFilename, redactSecretValue } from "../../../security/redaction.js";
2
+ import { redactEvidenceList } from "./evidence/index.js";
3
+ const SECRETISH = /(password|secret|token|api[_-]?key|private[_-]?key|BEGIN [A-Z ]*PRIVATE KEY|credential)/i;
4
+ function scrubString(value) {
5
+ if (SECRETISH.test(value)) {
6
+ return redactSecretValue(value);
7
+ }
8
+ return value;
9
+ }
10
+ function scrubClaim(claim) {
11
+ return {
12
+ ...claim,
13
+ subject: looksLikeSensitiveFilename(claim.subject)
14
+ ? "[REDACTED_PATH]"
15
+ : scrubString(claim.subject),
16
+ object: scrubString(claim.object),
17
+ };
18
+ }
19
+ /** Deep-safe Brain for disk/export: redact evidence and secret-like claim strings. */
20
+ export function redactBrainForStorage(brain) {
21
+ return {
22
+ ...brain,
23
+ claims: brain.claims.map(scrubClaim),
24
+ evidence: redactEvidenceList(brain.evidence),
25
+ ownership: {
26
+ ...brain.ownership,
27
+ // timingMs is wall-clock and must not affect durable identity.
28
+ timingMs: 0,
29
+ ownerships: brain.ownership.ownerships.map((o) => ({
30
+ ...o,
31
+ evidence: o.evidence.map((e) => looksLikeSensitiveFilename(e) ? e.replace(/[^/\\]+$/, "[REDACTED_NAME]") : e),
32
+ })),
33
+ },
34
+ risks: {
35
+ ...brain.risks,
36
+ timingMs: 0,
37
+ risks: brain.risks.risks.map((r) => ({
38
+ ...r,
39
+ evidence: r.evidence.map((e) => scrubString(e)),
40
+ })),
41
+ },
42
+ };
43
+ }
@@ -0,0 +1,2 @@
1
+ export { LocalBrainStore, BrainStorageError, serializeBrain, checksumPayload } from "./store.js";
2
+ export type { BrainStoreMeta, SnapshotMeta, SnapshotComparison } from "./store.js";
@@ -0,0 +1 @@
1
+ export { LocalBrainStore, BrainStorageError, serializeBrain, checksumPayload } from "./store.js";
@@ -0,0 +1,53 @@
1
+ import type { BrainDelta } from "../delta.js";
2
+ import type { ProjectBrain } from "../types.js";
3
+ import { type BrainStorageFormatVersion, type ProjectBrainSchemaVersion, type SnapshotMetaSchemaVersion } from "../version.js";
4
+ export interface SnapshotMeta {
5
+ schemaVersion: SnapshotMetaSchemaVersion;
6
+ id: string;
7
+ contentHash: string;
8
+ createdAt: string;
9
+ projectName: string;
10
+ brainId: string;
11
+ checksum: string;
12
+ }
13
+ export interface BrainStoreMeta {
14
+ storageFormatVersion: BrainStorageFormatVersion;
15
+ schemaVersion: ProjectBrainSchemaVersion;
16
+ projectName: string;
17
+ latestSnapshotId: string | null;
18
+ snapshots: SnapshotMeta[];
19
+ }
20
+ export interface SnapshotComparison {
21
+ leftId: string;
22
+ rightId: string;
23
+ sameContentHash: boolean;
24
+ sameChecksum: boolean;
25
+ left: SnapshotMeta;
26
+ right: SnapshotMeta;
27
+ }
28
+ export declare class BrainStorageError extends Error {
29
+ constructor(message: string);
30
+ }
31
+ export declare function serializeBrain(brain: ProjectBrain): string;
32
+ export declare function checksumPayload(payload: string): string;
33
+ export declare class LocalBrainStore {
34
+ readonly root: string;
35
+ constructor(rootDir: string);
36
+ static underRepo(repoRoot: string): LocalBrainStore;
37
+ private metaPath;
38
+ private snapshotDir;
39
+ private brainPath;
40
+ private deltaPath;
41
+ ensureRoot(): Promise<void>;
42
+ readMeta(): Promise<BrainStoreMeta>;
43
+ private assertMetaCompatible;
44
+ writeMeta(meta: BrainStoreMeta): Promise<void>;
45
+ saveSnapshot(brain: ProjectBrain): Promise<SnapshotMeta>;
46
+ loadSnapshot(snapshotId: string): Promise<ProjectBrain>;
47
+ listSnapshots(): Promise<SnapshotMeta[]>;
48
+ latestSnapshotId(): Promise<string | null>;
49
+ loadLatest(): Promise<ProjectBrain | null>;
50
+ compareSnapshots(leftId: string, rightId: string): Promise<SnapshotComparison>;
51
+ saveDelta(delta: BrainDelta): Promise<string>;
52
+ loadDelta(beforeId: string, afterId: string): Promise<BrainDelta>;
53
+ }
@@ -0,0 +1,305 @@
1
+ import { createHash } from "node:crypto";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { atomicWriteTextFile } from "../../../../utils/fs.js";
5
+ import { parseBrainDelta, serializeBrainDelta } from "../delta.js";
6
+ import { checkBrainCompatibility, migrateBrain } from "../migrate.js";
7
+ import { assertBrainContract } from "../contract.js";
8
+ import { redactBrainForStorage } from "../security.js";
9
+ import { BRAIN_STORAGE_FORMAT_VERSION, PROJECT_BRAIN_SCHEMA_VERSION, SNAPSHOT_META_SCHEMA_VERSION, } from "../version.js";
10
+ export class BrainStorageError extends Error {
11
+ constructor(message) {
12
+ super(message);
13
+ this.name = "BrainStorageError";
14
+ }
15
+ }
16
+ function sortKeysDeep(value) {
17
+ if (Array.isArray(value)) {
18
+ return value.map(sortKeysDeep);
19
+ }
20
+ if (value && typeof value === "object") {
21
+ const record = value;
22
+ const sorted = {};
23
+ for (const key of Object.keys(record).sort((a, b) => a.localeCompare(b))) {
24
+ sorted[key] = sortKeysDeep(record[key]);
25
+ }
26
+ return sorted;
27
+ }
28
+ return value;
29
+ }
30
+ export function serializeBrain(brain) {
31
+ const safe = redactBrainForStorage(brain);
32
+ return `${JSON.stringify(sortKeysDeep(safe), null, 2)}\n`;
33
+ }
34
+ export function checksumPayload(payload) {
35
+ return createHash("sha256").update(payload).digest("hex");
36
+ }
37
+ function assertInsideRoot(root, candidate) {
38
+ const resolvedRoot = path.resolve(root);
39
+ const resolved = path.resolve(candidate);
40
+ const rel = path.relative(resolvedRoot, resolved);
41
+ if (rel.startsWith("..") || path.isAbsolute(rel)) {
42
+ throw new BrainStorageError("path escapes brain storage root");
43
+ }
44
+ return resolved;
45
+ }
46
+ async function assertInsideRootResolved(root, candidate) {
47
+ const lexical = assertInsideRoot(root, candidate);
48
+ try {
49
+ const realRoot = await fs.realpath(root);
50
+ // Walk up until an existing ancestor can be realpath'd (catches symlink dirs).
51
+ let probe = lexical;
52
+ for (;;) {
53
+ try {
54
+ const realProbe = await fs.realpath(probe);
55
+ const rel = path.relative(realRoot, realProbe);
56
+ if (rel.startsWith("..") || path.isAbsolute(rel)) {
57
+ throw new BrainStorageError("path escapes brain storage root via symlink");
58
+ }
59
+ // Existing path is inside root; return lexical target for create/read of leaf.
60
+ return lexical;
61
+ }
62
+ catch (error) {
63
+ if (error instanceof BrainStorageError) {
64
+ throw error;
65
+ }
66
+ if (error.code !== "ENOENT") {
67
+ throw error;
68
+ }
69
+ const parent = path.dirname(probe);
70
+ if (parent === probe) {
71
+ return lexical;
72
+ }
73
+ probe = parent;
74
+ }
75
+ }
76
+ }
77
+ catch (error) {
78
+ if (error instanceof BrainStorageError) {
79
+ throw error;
80
+ }
81
+ if (error.code === "ENOENT") {
82
+ return lexical;
83
+ }
84
+ throw error;
85
+ }
86
+ }
87
+ export class LocalBrainStore {
88
+ root;
89
+ constructor(rootDir) {
90
+ this.root = path.resolve(rootDir);
91
+ }
92
+ static underRepo(repoRoot) {
93
+ return new LocalBrainStore(path.join(repoRoot, ".agentdoctor", "project-brain"));
94
+ }
95
+ metaPath() {
96
+ return path.join(this.root, "store.json");
97
+ }
98
+ snapshotDir(snapshotId) {
99
+ if (!/^snap_[a-f0-9]+$/i.test(snapshotId) && !/^snap_[a-z0-9_]+$/i.test(snapshotId)) {
100
+ // Allow our snap_ hex ids; reject path tricks.
101
+ if (snapshotId.includes("..") || snapshotId.includes("/") || snapshotId.includes("\\")) {
102
+ throw new BrainStorageError("invalid snapshot id");
103
+ }
104
+ }
105
+ if (snapshotId.includes("..") || snapshotId.includes("/") || snapshotId.includes("\\")) {
106
+ throw new BrainStorageError("invalid snapshot id");
107
+ }
108
+ return assertInsideRoot(this.root, path.join(this.root, "snapshots", snapshotId));
109
+ }
110
+ brainPath(snapshotId) {
111
+ return path.join(this.snapshotDir(snapshotId), "brain.json");
112
+ }
113
+ deltaPath(beforeId, afterId) {
114
+ if (beforeId.includes("..") ||
115
+ afterId.includes("..") ||
116
+ beforeId.includes("/") ||
117
+ afterId.includes("/") ||
118
+ beforeId.includes("\\") ||
119
+ afterId.includes("\\")) {
120
+ throw new BrainStorageError("invalid delta snapshot ids");
121
+ }
122
+ const name = `delta_${beforeId}__${afterId}.json`;
123
+ return assertInsideRoot(this.root, path.join(this.root, "deltas", name));
124
+ }
125
+ async ensureRoot() {
126
+ await fs.mkdir(path.join(this.root, "snapshots"), { recursive: true });
127
+ await fs.mkdir(path.join(this.root, "deltas"), { recursive: true });
128
+ }
129
+ async readMeta() {
130
+ await this.ensureRoot();
131
+ try {
132
+ const raw = await fs.readFile(this.metaPath(), "utf8");
133
+ const parsed = JSON.parse(raw);
134
+ this.assertMetaCompatible(parsed);
135
+ return parsed;
136
+ }
137
+ catch (error) {
138
+ if (error.code === "ENOENT") {
139
+ return {
140
+ storageFormatVersion: BRAIN_STORAGE_FORMAT_VERSION,
141
+ schemaVersion: PROJECT_BRAIN_SCHEMA_VERSION,
142
+ projectName: "",
143
+ latestSnapshotId: null,
144
+ snapshots: [],
145
+ };
146
+ }
147
+ throw error;
148
+ }
149
+ }
150
+ assertMetaCompatible(meta) {
151
+ const check = checkBrainCompatibility({
152
+ schemaVersion: meta.schemaVersion,
153
+ storageFormatVersion: meta.storageFormatVersion,
154
+ });
155
+ if (!check.compatible) {
156
+ throw new BrainStorageError(check.reason ?? "incompatible store meta");
157
+ }
158
+ }
159
+ async writeMeta(meta) {
160
+ this.assertMetaCompatible(meta);
161
+ await this.ensureRoot();
162
+ await atomicWriteTextFile(this.metaPath(), `${JSON.stringify(sortKeysDeep(meta), null, 2)}\n`);
163
+ }
164
+ async saveSnapshot(brain) {
165
+ await this.ensureRoot();
166
+ const snapshotId = brain.snapshot.id;
167
+ const dir = await assertInsideRootResolved(this.root, this.snapshotDir(snapshotId));
168
+ await fs.mkdir(dir, { recursive: true });
169
+ const target = await assertInsideRootResolved(this.root, path.join(dir, "brain.json"));
170
+ const payload = serializeBrain(brain);
171
+ const nextChecksum = checksumPayload(payload);
172
+ try {
173
+ const existing = await fs.readFile(target, "utf8");
174
+ const existingChecksum = checksumPayload(existing);
175
+ if (existingChecksum !== nextChecksum) {
176
+ throw new BrainStorageError(`snapshot ${snapshotId} already exists with different content; refusing overwrite`);
177
+ }
178
+ const meta = await this.readMeta();
179
+ const found = meta.snapshots.find((s) => s.id === snapshotId);
180
+ if (found) {
181
+ return found;
182
+ }
183
+ }
184
+ catch (error) {
185
+ if (error instanceof BrainStorageError) {
186
+ throw error;
187
+ }
188
+ if (error.code !== "ENOENT") {
189
+ throw error;
190
+ }
191
+ }
192
+ await atomicWriteTextFile(target, payload);
193
+ const snapMeta = {
194
+ schemaVersion: SNAPSHOT_META_SCHEMA_VERSION,
195
+ id: snapshotId,
196
+ contentHash: brain.snapshot.contentHash,
197
+ createdAt: brain.snapshot.generatedAt,
198
+ projectName: brain.metadata.projectName,
199
+ brainId: brain.metadata.brainId,
200
+ checksum: nextChecksum,
201
+ };
202
+ const meta = await this.readMeta();
203
+ const snapshots = meta.snapshots.filter((s) => s.id !== snapshotId);
204
+ snapshots.push(snapMeta);
205
+ snapshots.sort((a, b) => a.createdAt.localeCompare(b.createdAt) || a.id.localeCompare(b.id));
206
+ await this.writeMeta({
207
+ storageFormatVersion: BRAIN_STORAGE_FORMAT_VERSION,
208
+ schemaVersion: PROJECT_BRAIN_SCHEMA_VERSION,
209
+ projectName: brain.metadata.projectName,
210
+ latestSnapshotId: snapshotId,
211
+ snapshots,
212
+ });
213
+ return snapMeta;
214
+ }
215
+ async loadSnapshot(snapshotId) {
216
+ const target = await assertInsideRootResolved(this.root, this.brainPath(snapshotId));
217
+ let raw;
218
+ try {
219
+ raw = await fs.readFile(target, "utf8");
220
+ }
221
+ catch (error) {
222
+ if (error.code === "ENOENT") {
223
+ throw new BrainStorageError(`snapshot not found: ${snapshotId}`);
224
+ }
225
+ throw error;
226
+ }
227
+ const meta = await this.readMeta();
228
+ const snap = meta.snapshots.find((s) => s.id === snapshotId);
229
+ if (snap) {
230
+ const actual = checksumPayload(raw);
231
+ if (actual !== snap.checksum) {
232
+ throw new BrainStorageError(`checksum mismatch for snapshot ${snapshotId}`);
233
+ }
234
+ }
235
+ const parsed = JSON.parse(raw);
236
+ if (!parsed || typeof parsed !== "object") {
237
+ throw new BrainStorageError("corrupt brain JSON");
238
+ }
239
+ const version = String(parsed.metadata?.schemaVersion ?? "");
240
+ const check = checkBrainCompatibility({ schemaVersion: version });
241
+ let brain;
242
+ if (!check.compatible) {
243
+ brain = migrateBrain(parsed, version);
244
+ }
245
+ else {
246
+ try {
247
+ brain = assertBrainContract(parsed);
248
+ }
249
+ catch (error) {
250
+ throw new BrainStorageError(error instanceof Error ? error.message : "invalid brain contract");
251
+ }
252
+ }
253
+ return brain;
254
+ }
255
+ async listSnapshots() {
256
+ const meta = await this.readMeta();
257
+ return [...meta.snapshots];
258
+ }
259
+ async latestSnapshotId() {
260
+ const meta = await this.readMeta();
261
+ return meta.latestSnapshotId;
262
+ }
263
+ async loadLatest() {
264
+ const id = await this.latestSnapshotId();
265
+ if (!id) {
266
+ return null;
267
+ }
268
+ return this.loadSnapshot(id);
269
+ }
270
+ async compareSnapshots(leftId, rightId) {
271
+ const meta = await this.readMeta();
272
+ const left = meta.snapshots.find((s) => s.id === leftId);
273
+ const right = meta.snapshots.find((s) => s.id === rightId);
274
+ if (!left || !right) {
275
+ throw new BrainStorageError("cannot compare: snapshot missing from registry");
276
+ }
277
+ return {
278
+ leftId,
279
+ rightId,
280
+ sameContentHash: left.contentHash === right.contentHash,
281
+ sameChecksum: left.checksum === right.checksum,
282
+ left,
283
+ right,
284
+ };
285
+ }
286
+ async saveDelta(delta) {
287
+ await this.ensureRoot();
288
+ const target = this.deltaPath(delta.beforeSnapshotId, delta.afterSnapshotId);
289
+ await atomicWriteTextFile(target, serializeBrainDelta(delta));
290
+ return target;
291
+ }
292
+ async loadDelta(beforeId, afterId) {
293
+ const target = this.deltaPath(beforeId, afterId);
294
+ try {
295
+ const raw = await fs.readFile(target, "utf8");
296
+ return parseBrainDelta(raw);
297
+ }
298
+ catch (error) {
299
+ if (error.code === "ENOENT") {
300
+ throw new BrainStorageError(`delta not found: ${beforeId} → ${afterId}`);
301
+ }
302
+ throw error;
303
+ }
304
+ }
305
+ }
@@ -0,0 +1,24 @@
1
+ import type { ProjectBrain } from "./types.js";
2
+ export interface TraceNode {
3
+ id: string;
4
+ label: string;
5
+ kind: "component" | "dependency-endpoint" | "entrypoint" | "domain";
6
+ path?: string;
7
+ confidence: number;
8
+ }
9
+ export interface TraceEdge {
10
+ from: string;
11
+ to: string;
12
+ type: string;
13
+ evidence: readonly string[];
14
+ confidence: number;
15
+ }
16
+ export interface TraceResult {
17
+ root: string;
18
+ nodes: readonly TraceNode[];
19
+ edges: readonly TraceEdge[];
20
+ cycles: readonly string[];
21
+ confidence: number;
22
+ }
23
+ export type TraceMode = "dependencies" | "dependents" | "entrypoint-downstream" | "blast-radius" | "domain-modules";
24
+ export declare function traceBrain(brain: ProjectBrain, target: string, mode?: TraceMode): TraceResult;