@praneeth_54/agentdoctor 2.0.1 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (192) hide show
  1. package/CHANGELOG.md +123 -1
  2. package/README.md +510 -260
  3. package/dist/agent/approvals.d.ts +24 -0
  4. package/dist/agent/approvals.js +64 -0
  5. package/dist/agent/chat/deterministic.d.ts +6 -0
  6. package/dist/agent/chat/deterministic.js +180 -0
  7. package/dist/agent/chat/index.d.ts +7 -0
  8. package/dist/agent/chat/index.js +5 -0
  9. package/dist/agent/chat/memory.d.ts +44 -0
  10. package/dist/agent/chat/memory.js +103 -0
  11. package/dist/agent/chat/project-summary.d.ts +16 -0
  12. package/dist/agent/chat/project-summary.js +70 -0
  13. package/dist/agent/chat/prompts.d.ts +6 -0
  14. package/dist/agent/chat/prompts.js +39 -0
  15. package/dist/agent/chat/response.d.ts +19 -0
  16. package/dist/agent/chat/response.js +109 -0
  17. package/dist/agent/chat/service.d.ts +42 -0
  18. package/dist/agent/chat/service.js +246 -0
  19. package/dist/agent/chat/types.d.ts +48 -0
  20. package/dist/agent/chat/types.js +1 -0
  21. package/dist/agent/context/retrieve.d.ts +21 -0
  22. package/dist/agent/context/retrieve.js +117 -0
  23. package/dist/agent/context/truth.d.ts +6 -0
  24. package/dist/agent/context/truth.js +19 -0
  25. package/dist/agent/context/types.d.ts +23 -0
  26. package/dist/agent/context/types.js +4 -0
  27. package/dist/agent/index.d.ts +28 -0
  28. package/dist/agent/index.js +15 -0
  29. package/dist/agent/loop.d.ts +53 -0
  30. package/dist/agent/loop.js +253 -0
  31. package/dist/agent/modes.d.ts +18 -0
  32. package/dist/agent/modes.js +102 -0
  33. package/dist/agent/plan.d.ts +30 -0
  34. package/dist/agent/plan.js +121 -0
  35. package/dist/agent/roles.d.ts +12 -0
  36. package/dist/agent/roles.js +138 -0
  37. package/dist/agent/runtime.d.ts +83 -0
  38. package/dist/agent/runtime.js +293 -0
  39. package/dist/agent/state.d.ts +30 -0
  40. package/dist/agent/state.js +95 -0
  41. package/dist/agent/student.d.ts +56 -0
  42. package/dist/agent/student.js +230 -0
  43. package/dist/agent/tools/execute.d.ts +20 -0
  44. package/dist/agent/tools/execute.js +365 -0
  45. package/dist/agent/tools/index.d.ts +6 -0
  46. package/dist/agent/tools/index.js +5 -0
  47. package/dist/agent/tools/registry.d.ts +7 -0
  48. package/dist/agent/tools/registry.js +212 -0
  49. package/dist/agent/tools/run.d.ts +24 -0
  50. package/dist/agent/tools/run.js +44 -0
  51. package/dist/agent/tools/types.d.ts +32 -0
  52. package/dist/agent/tools/types.js +10 -0
  53. package/dist/agent/tools/write.d.ts +24 -0
  54. package/dist/agent/tools/write.js +121 -0
  55. package/dist/agent/verify.d.ts +29 -0
  56. package/dist/agent/verify.js +210 -0
  57. package/dist/ai/config.d.ts +22 -0
  58. package/dist/ai/config.js +69 -0
  59. package/dist/ai/index.d.ts +17 -0
  60. package/dist/ai/index.js +52 -0
  61. package/dist/ai/providers/mock.d.ts +20 -0
  62. package/dist/ai/providers/mock.js +84 -0
  63. package/dist/ai/providers/none.d.ts +6 -0
  64. package/dist/ai/providers/none.js +23 -0
  65. package/dist/ai/providers/openai-compatible.d.ts +21 -0
  66. package/dist/ai/providers/openai-compatible.js +151 -0
  67. package/dist/ai/redact.d.ts +9 -0
  68. package/dist/ai/redact.js +61 -0
  69. package/dist/ai/types.d.ts +71 -0
  70. package/dist/ai/types.js +6 -0
  71. package/dist/cli/commands/agent.d.ts +29 -0
  72. package/dist/cli/commands/agent.js +164 -0
  73. package/dist/cli/commands/chat.d.ts +14 -0
  74. package/dist/cli/commands/chat.js +153 -0
  75. package/dist/cli/commands/learn.d.ts +12 -0
  76. package/dist/cli/commands/learn.js +107 -0
  77. package/dist/cli/commands/product.d.ts +19 -0
  78. package/dist/cli/commands/product.js +147 -0
  79. package/dist/cli/commands/start.d.ts +15 -0
  80. package/dist/cli/commands/start.js +80 -0
  81. package/dist/cli/program.js +233 -0
  82. package/dist/constants.d.ts +1 -1
  83. package/dist/constants.js +1 -1
  84. package/dist/dashboard/server.d.ts +6 -0
  85. package/dist/dashboard/server.js +333 -43
  86. package/dist/enforcement/runner.d.ts +5 -0
  87. package/dist/enforcement/runner.js +51 -4
  88. package/dist/index.d.ts +17 -0
  89. package/dist/index.js +10 -0
  90. package/dist/intelligence/graph/build.js +36 -8
  91. package/dist/intelligence/resolve/imports.js +1 -1
  92. package/dist/languages/dart.d.ts +10 -0
  93. package/dist/languages/dart.js +99 -0
  94. package/dist/languages/go.d.ts +13 -7
  95. package/dist/languages/go.js +91 -26
  96. package/dist/languages/index.d.ts +5 -1
  97. package/dist/languages/index.js +13 -36
  98. package/dist/languages/java.d.ts +10 -0
  99. package/dist/languages/java.js +80 -0
  100. package/dist/languages/kotlin.d.ts +10 -0
  101. package/dist/languages/kotlin.js +85 -0
  102. package/dist/languages/php.d.ts +1 -0
  103. package/dist/languages/php.js +11 -2
  104. package/dist/languages/python.d.ts +6 -2
  105. package/dist/languages/python.js +39 -9
  106. package/dist/languages/rust.d.ts +10 -0
  107. package/dist/languages/rust.js +93 -0
  108. package/dist/languages/types.d.ts +4 -3
  109. package/dist/mcp/agent/registry.d.ts +13 -0
  110. package/dist/mcp/agent/registry.js +320 -0
  111. package/dist/mcp/agentdoctor/server.js +8 -1
  112. package/dist/mcp/intelligence/handlers.d.ts +3 -0
  113. package/dist/mcp/intelligence/handlers.js +28 -0
  114. package/dist/mcp/intelligence/registry.d.ts +1 -1
  115. package/dist/mcp/intelligence/registry.js +30 -1
  116. package/dist/product/api/doctor.d.ts +14 -0
  117. package/dist/product/api/doctor.js +185 -0
  118. package/dist/product/api/openapi.d.ts +7 -0
  119. package/dist/product/api/openapi.js +122 -0
  120. package/dist/product/approval/model.d.ts +30 -0
  121. package/dist/product/approval/model.js +64 -0
  122. package/dist/product/approval/session.d.ts +49 -0
  123. package/dist/product/approval/session.js +134 -0
  124. package/dist/product/database/doctor.d.ts +30 -0
  125. package/dist/product/database/doctor.js +184 -0
  126. package/dist/product/decisions/ledger.d.ts +24 -0
  127. package/dist/product/decisions/ledger.js +110 -0
  128. package/dist/product/deps/analyze.d.ts +46 -0
  129. package/dist/product/deps/analyze.js +137 -0
  130. package/dist/product/deps/lockfiles.d.ts +25 -0
  131. package/dist/product/deps/lockfiles.js +200 -0
  132. package/dist/product/discovery/roots.d.ts +42 -0
  133. package/dist/product/discovery/roots.js +215 -0
  134. package/dist/product/dna/build.d.ts +50 -0
  135. package/dist/product/dna/build.js +255 -0
  136. package/dist/product/eval/lab.d.ts +17 -0
  137. package/dist/product/eval/lab.js +218 -0
  138. package/dist/product/events/doctor.d.ts +21 -0
  139. package/dist/product/events/doctor.js +147 -0
  140. package/dist/product/evidence-scan.d.ts +12 -0
  141. package/dist/product/evidence-scan.js +46 -0
  142. package/dist/product/evolution/timeline.d.ts +29 -0
  143. package/dist/product/evolution/timeline.js +123 -0
  144. package/dist/product/features/intelligence.d.ts +23 -0
  145. package/dist/product/features/intelligence.js +158 -0
  146. package/dist/product/forensic/mode.d.ts +25 -0
  147. package/dist/product/forensic/mode.js +70 -0
  148. package/dist/product/graph/enrich-languages.d.ts +20 -0
  149. package/dist/product/graph/enrich-languages.js +193 -0
  150. package/dist/product/health/code-health.d.ts +20 -0
  151. package/dist/product/health/code-health.js +149 -0
  152. package/dist/product/index.d.ts +69 -0
  153. package/dist/product/index.js +35 -0
  154. package/dist/product/ledger/change-ledger.d.ts +23 -0
  155. package/dist/product/ledger/change-ledger.js +64 -0
  156. package/dist/product/map/software-map.d.ts +17 -0
  157. package/dist/product/map/software-map.js +95 -0
  158. package/dist/product/memory/institutional.d.ts +16 -0
  159. package/dist/product/memory/institutional.js +87 -0
  160. package/dist/product/ops/incident.d.ts +20 -0
  161. package/dist/product/ops/incident.js +61 -0
  162. package/dist/product/ops/infra.d.ts +15 -0
  163. package/dist/product/ops/infra.js +112 -0
  164. package/dist/product/org/model.d.ts +34 -0
  165. package/dist/product/org/model.js +195 -0
  166. package/dist/product/privacy/doctor.d.ts +13 -0
  167. package/dist/product/privacy/doctor.js +90 -0
  168. package/dist/product/requirements/trace.d.ts +21 -0
  169. package/dist/product/requirements/trace.js +166 -0
  170. package/dist/product/search/index.d.ts +29 -0
  171. package/dist/product/search/index.js +116 -0
  172. package/dist/product/search/software-search.d.ts +19 -0
  173. package/dist/product/search/software-search.js +100 -0
  174. package/dist/product/security/doctor.d.ts +23 -0
  175. package/dist/product/security/doctor.js +124 -0
  176. package/dist/product/self/diagnose.d.ts +15 -0
  177. package/dist/product/self/diagnose.js +82 -0
  178. package/dist/product/techdebt/roadmap.d.ts +20 -0
  179. package/dist/product/techdebt/roadmap.js +118 -0
  180. package/dist/product/testbrain/analyze.d.ts +29 -0
  181. package/dist/product/testbrain/analyze.js +129 -0
  182. package/dist/product/truth.d.ts +12 -0
  183. package/dist/product/truth.js +30 -0
  184. package/dist/product/twin/digital-twin.d.ts +23 -0
  185. package/dist/product/twin/digital-twin.js +52 -0
  186. package/dist/product/twin/store.d.ts +21 -0
  187. package/dist/product/twin/store.js +70 -0
  188. package/dist/product/whatif/engine.d.ts +28 -0
  189. package/dist/product/whatif/engine.js +70 -0
  190. package/dist/security/paths.js +43 -15
  191. package/dist/utils/fs.js +10 -2
  192. package/package.json +1 -1
@@ -0,0 +1,134 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { createHash, randomBytes } from "node:crypto";
4
+ import { resolveRepoRoot } from "../../utils/path.js";
5
+ import { atomicWriteTextFile } from "../../utils/fs.js";
6
+ function grantsPath(root) {
7
+ return path.join(resolveRepoRoot(root), ".agentdoctor", "approvals", "grants.json");
8
+ }
9
+ async function loadGrants(root) {
10
+ try {
11
+ const raw = await fs.readFile(grantsPath(root), "utf8");
12
+ const parsed = JSON.parse(raw);
13
+ if (!Array.isArray(parsed))
14
+ return [];
15
+ return parsed;
16
+ }
17
+ catch {
18
+ return [];
19
+ }
20
+ }
21
+ async function saveGrants(root, grants) {
22
+ const file = grantsPath(root);
23
+ await fs.mkdir(path.dirname(file), { recursive: true });
24
+ await atomicWriteTextFile(file, `${JSON.stringify(grants, null, 2)}\n`);
25
+ }
26
+ export function hashPlanPayload(payload) {
27
+ return createHash("sha256").update(payload).digest("hex").slice(0, 32);
28
+ }
29
+ /** Stable plan hash for a file write operation (path + content). */
30
+ export function hashFileWritePlan(action, relativePath, content) {
31
+ return hashPlanPayload(`${action}\n${relativePath}\n${content}`);
32
+ }
33
+ function normalizeResources(resources) {
34
+ return [...new Set(resources.map((r) => r.replace(/\\/g, "/").replace(/^\.\//, "")))].filter(Boolean);
35
+ }
36
+ function resourceCovers(granted, requested) {
37
+ if (granted === requested)
38
+ return true;
39
+ // Directory prefix grants (e.g. "src/" or "src") cover children — never bare "*".
40
+ const g = granted.endsWith("/") ? granted : `${granted}/`;
41
+ return requested.startsWith(g);
42
+ }
43
+ export async function issueApprovalGrant(options) {
44
+ const root = resolveRepoRoot(options.root);
45
+ const resources = normalizeResources(options.resources);
46
+ if (resources.length === 0) {
47
+ throw new Error("Approval grant requires at least one concrete resource path (wildcards alone rejected).");
48
+ }
49
+ if (resources.some((r) => r === "*" || r === "**")) {
50
+ throw new Error("Bare '*' resource grants are rejected — list concrete paths or directories.");
51
+ }
52
+ if (!options.planHash || options.planHash.length < 8) {
53
+ throw new Error("Approval grant requires a planHash binding the exact operation.");
54
+ }
55
+ const now = Date.now();
56
+ const grant = {
57
+ token: `agt_${randomBytes(24).toString("hex")}`,
58
+ root,
59
+ action: options.action,
60
+ resources,
61
+ risk: options.risk,
62
+ planHash: options.planHash,
63
+ actor: options.actor ?? "cli-human",
64
+ createdAt: new Date(now).toISOString(),
65
+ expiresAt: new Date(now + (options.ttlMs ?? 30 * 60_000)).toISOString(),
66
+ consumed: false,
67
+ scope: options.scope ?? "write+execute",
68
+ };
69
+ const grants = await loadGrants(root);
70
+ grants.push(grant);
71
+ await saveGrants(root, grants.slice(-200));
72
+ return grant;
73
+ }
74
+ /**
75
+ * Validate and optionally consume a grant token for a write/execute action.
76
+ * Bare `approved: true` without a valid token is NEVER sufficient.
77
+ */
78
+ export async function consumeApprovalGrant(options) {
79
+ const root = resolveRepoRoot(options.root);
80
+ if (!options.token || typeof options.token !== "string" || !options.token.startsWith("agt_")) {
81
+ return {
82
+ ok: false,
83
+ reason: "Trusted approval token required (issue via CLI --approve / issueApprovalGrant). Bare approved=true is rejected.",
84
+ };
85
+ }
86
+ const requested = normalizeResources(options.resources);
87
+ if (requested.length === 0) {
88
+ return { ok: false, reason: "Consume requires at least one concrete resource path." };
89
+ }
90
+ if (!options.requirePlanHash) {
91
+ return { ok: false, reason: "Consume requires requirePlanHash bound to the exact operation." };
92
+ }
93
+ const grants = await loadGrants(root);
94
+ const idx = grants.findIndex((g) => g.token === options.token);
95
+ if (idx < 0) {
96
+ return { ok: false, reason: "Approval token not found for this project." };
97
+ }
98
+ const grant = grants[idx];
99
+ if (grant.consumed) {
100
+ return { ok: false, reason: "Approval token already consumed." };
101
+ }
102
+ if (Date.parse(grant.expiresAt) < Date.now()) {
103
+ return { ok: false, reason: "Approval token expired." };
104
+ }
105
+ if (path.resolve(grant.root) !== path.resolve(root)) {
106
+ return { ok: false, reason: "Approval token root mismatch." };
107
+ }
108
+ if (grant.action !== options.action) {
109
+ return {
110
+ ok: false,
111
+ reason: `Approval token action mismatch (granted=${grant.action}, requested=${options.action}).`,
112
+ };
113
+ }
114
+ if (grant.planHash !== options.requirePlanHash) {
115
+ return {
116
+ ok: false,
117
+ reason: "Approval token planHash mismatch — operation changed after approval.",
118
+ };
119
+ }
120
+ for (const res of requested) {
121
+ const ok = grant.resources.some((g) => resourceCovers(g, res));
122
+ if (!ok) {
123
+ return {
124
+ ok: false,
125
+ reason: `Resource not covered by approval grant: ${res}`,
126
+ };
127
+ }
128
+ }
129
+ if (options.consume !== false) {
130
+ grants[idx] = { ...grant, consumed: true };
131
+ await saveGrants(root, grants);
132
+ }
133
+ return { ok: true, reason: "Approval grant accepted", grant };
134
+ }
@@ -0,0 +1,30 @@
1
+ import type { ProductEvidence, TruthLabel } from "../truth.js";
2
+ export interface SchemaObjectFinding {
3
+ name: string;
4
+ kind: "table" | "collection" | "model" | "unknown";
5
+ source: string;
6
+ truth: TruthLabel;
7
+ evidence: ProductEvidence[];
8
+ }
9
+ export interface SchemaDriftFinding {
10
+ name: string;
11
+ truth: TruthLabel;
12
+ note: string;
13
+ sources: string[];
14
+ }
15
+ export interface SchemaRelationFinding {
16
+ from: string;
17
+ to: string;
18
+ kind: "prisma-relation" | "sql-reference";
19
+ source: string;
20
+ truth: TruthLabel;
21
+ evidence: ProductEvidence[];
22
+ }
23
+ export interface DatabaseDoctorReport {
24
+ root: string;
25
+ objects: SchemaObjectFinding[];
26
+ relations: SchemaRelationFinding[];
27
+ drift: SchemaDriftFinding[];
28
+ limitations: string[];
29
+ }
30
+ export declare function analyzeDatabaseSchema(rootInput: string, maxFileSizeBytes?: number): Promise<DatabaseDoctorReport>;
@@ -0,0 +1,184 @@
1
+ import path from "node:path";
2
+ import { DEFAULT_MAX_FILE_SIZE_BYTES } from "../../constants.js";
3
+ import { detectProject } from "../../detectors/project.js";
4
+ import { readTextFile } from "../../utils/fs.js";
5
+ import { resolveRepoRoot } from "../../utils/path.js";
6
+ import { lineNumberAt } from "../evidence-scan.js";
7
+ function parseLaravelMigration(content, relativePath) {
8
+ const out = [];
9
+ const createRe = /Schema::create\s*\(\s*['"]([^'"]+)['"]/g;
10
+ let m;
11
+ while ((m = createRe.exec(content)) !== null) {
12
+ out.push({
13
+ name: m[1],
14
+ kind: "table",
15
+ source: "laravel-migration",
16
+ truth: "VERIFIED",
17
+ evidence: [
18
+ {
19
+ path: relativePath,
20
+ line: lineNumberAt(content, m.index),
21
+ excerpt: m[0].slice(0, 120),
22
+ },
23
+ ],
24
+ });
25
+ }
26
+ return out;
27
+ }
28
+ function parseSqlTables(content, relativePath) {
29
+ const out = [];
30
+ const re = /CREATE\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?[`"']?(\w+)[`"']?/gi;
31
+ let m;
32
+ while ((m = re.exec(content)) !== null) {
33
+ out.push({
34
+ name: m[1],
35
+ kind: "table",
36
+ source: "sql",
37
+ truth: "VERIFIED",
38
+ evidence: [
39
+ {
40
+ path: relativePath,
41
+ line: lineNumberAt(content, m.index),
42
+ excerpt: m[0].slice(0, 120),
43
+ },
44
+ ],
45
+ });
46
+ }
47
+ return out;
48
+ }
49
+ function parsePrismaRelations(content, relativePath) {
50
+ const out = [];
51
+ const modelBlocks = content.split(/^model\s+/gm).slice(1);
52
+ for (const block of modelBlocks) {
53
+ const modelName = /^(\w+)/.exec(block)?.[1];
54
+ if (!modelName)
55
+ continue;
56
+ const relRe = /^\s+(\w+)\s+\w+\??\s+@relation\s*\(\s*fields:\s*\[[^\]]+\]\s*,\s*references:\s*\[[^\]]+\]\s*,\s*[^)]*?\)/gm;
57
+ let m;
58
+ while ((m = relRe.exec(block)) !== null) {
59
+ const field = m[1];
60
+ out.push({
61
+ from: `${modelName}.${field}`,
62
+ to: "referenced-model",
63
+ kind: "prisma-relation",
64
+ source: "prisma",
65
+ truth: "VERIFIED",
66
+ evidence: [
67
+ {
68
+ path: relativePath,
69
+ line: lineNumberAt(content, content.indexOf(m[0])),
70
+ excerpt: m[0].trim().slice(0, 120),
71
+ },
72
+ ],
73
+ });
74
+ }
75
+ const shorthand = /^\s+(\w+)\s+(\w+)\s+@relation/gm;
76
+ while ((m = shorthand.exec(block)) !== null) {
77
+ out.push({
78
+ from: `${modelName}.${m[1]}`,
79
+ to: m[2],
80
+ kind: "prisma-relation",
81
+ source: "prisma",
82
+ truth: "INFERRED",
83
+ evidence: [{ path: relativePath, excerpt: m[0].trim() }],
84
+ });
85
+ }
86
+ }
87
+ return out;
88
+ }
89
+ function parseSqlReferences(content, relativePath) {
90
+ const out = [];
91
+ const re = /FOREIGN\s+KEY\s*\(\s*[`"']?(\w+)[`"']?\s*\)\s*REFERENCES\s+[`"']?(\w+)[`"']?\s*\(\s*[`"']?(\w+)[`"']?\s*\)/gi;
92
+ let m;
93
+ while ((m = re.exec(content)) !== null) {
94
+ out.push({
95
+ from: m[1],
96
+ to: `${m[2]}.${m[3]}`,
97
+ kind: "sql-reference",
98
+ source: "sql",
99
+ truth: "VERIFIED",
100
+ evidence: [
101
+ {
102
+ path: relativePath,
103
+ line: lineNumberAt(content, m.index),
104
+ excerpt: m[0].slice(0, 120),
105
+ },
106
+ ],
107
+ });
108
+ }
109
+ return out;
110
+ }
111
+ function parsePrismaModels(content, relativePath) {
112
+ const out = [];
113
+ const re = /^model\s+(\w+)\s*\{/gm;
114
+ let m;
115
+ while ((m = re.exec(content)) !== null) {
116
+ out.push({
117
+ name: m[1],
118
+ kind: "model",
119
+ source: "prisma",
120
+ truth: "VERIFIED",
121
+ evidence: [
122
+ {
123
+ path: relativePath,
124
+ line: lineNumberAt(content, m.index),
125
+ excerpt: m[0].trim(),
126
+ },
127
+ ],
128
+ });
129
+ }
130
+ return out;
131
+ }
132
+ function detectDrift(objects) {
133
+ const byName = new Map();
134
+ for (const o of objects) {
135
+ const key = o.name.toLowerCase();
136
+ if (!byName.has(key))
137
+ byName.set(key, []);
138
+ byName.get(key).push(o);
139
+ }
140
+ const drift = [];
141
+ for (const [name, group] of byName) {
142
+ const sources = [...new Set(group.map((g) => g.source))];
143
+ if (sources.length <= 1)
144
+ continue;
145
+ drift.push({
146
+ name,
147
+ truth: "UNKNOWN",
148
+ note: "Same object name appears in multiple schema sources; manual reconciliation required.",
149
+ sources,
150
+ });
151
+ }
152
+ return drift.sort((a, b) => a.name.localeCompare(b.name));
153
+ }
154
+ export async function analyzeDatabaseSchema(rootInput, maxFileSizeBytes = DEFAULT_MAX_FILE_SIZE_BYTES) {
155
+ const root = resolveRepoRoot(rootInput);
156
+ const detection = await detectProject(root, maxFileSizeBytes);
157
+ const limitations = [
158
+ "Schema extraction is pattern-based on migrations/SQL/Prisma; runtime DB state is not inspected.",
159
+ "Drift is reported only when the same object name appears in disagreeing sources.",
160
+ ];
161
+ const objects = [];
162
+ const relations = [];
163
+ for (const entry of detection.discovery.files) {
164
+ const rel = entry.relativePath.replace(/\\/g, "/");
165
+ const lower = rel.toLowerCase();
166
+ const abs = path.join(root, rel);
167
+ const text = await readTextFile(abs, maxFileSizeBytes);
168
+ if (text === null)
169
+ continue;
170
+ if (lower.endsWith(".sql")) {
171
+ objects.push(...parseSqlTables(text, rel));
172
+ relations.push(...parseSqlReferences(text, rel));
173
+ }
174
+ else if (lower.includes("database/migrations/") && lower.endsWith(".php")) {
175
+ objects.push(...parseLaravelMigration(text, rel));
176
+ }
177
+ else if (lower.endsWith("schema.prisma") || lower.endsWith(".prisma")) {
178
+ objects.push(...parsePrismaModels(text, rel));
179
+ relations.push(...parsePrismaRelations(text, rel));
180
+ }
181
+ }
182
+ const drift = detectDrift(objects);
183
+ return { root, objects, relations, drift, limitations };
184
+ }
@@ -0,0 +1,24 @@
1
+ import type { ProductEvidence, TruthLabel } from "../truth.js";
2
+ export interface DecisionRecord {
3
+ id: string;
4
+ title: string;
5
+ status?: string;
6
+ source: "adr-file" | "ledger";
7
+ truth: TruthLabel;
8
+ evidence: ProductEvidence[];
9
+ recordedAt?: string;
10
+ bodyExcerpt?: string;
11
+ }
12
+ export interface DecisionLedgerResult {
13
+ root: string;
14
+ decisions: DecisionRecord[];
15
+ ledgerPath: string;
16
+ limitations: string[];
17
+ }
18
+ export declare function parseAdrDecisions(rootInput: string, maxFileSizeBytes?: number): Promise<DecisionRecord[]>;
19
+ export declare function appendDecisionLedgerEntry(rootInput: string, record: Omit<DecisionRecord, "source" | "truth"> & {
20
+ source?: DecisionRecord["source"];
21
+ truth?: TruthLabel;
22
+ }): Promise<DecisionRecord>;
23
+ export declare function loadDecisionLedger(rootInput: string): Promise<DecisionLedgerResult>;
24
+ export declare function resolveDecisionPath(rootInput: string, candidate: string): string;
@@ -0,0 +1,110 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { DEFAULT_MAX_FILE_SIZE_BYTES } from "../../constants.js";
4
+ import { detectProject } from "../../detectors/project.js";
5
+ import { readTextFile } from "../../utils/fs.js";
6
+ import { resolveSafeRepoPath } from "../../security/paths.js";
7
+ import { resolveRepoRoot } from "../../utils/path.js";
8
+ import { pathExists } from "../../utils/fs.js";
9
+ function ledgerFile(root) {
10
+ return path.join(resolveRepoRoot(root), ".agentdoctor", "decisions", "entries.jsonl");
11
+ }
12
+ function isAdrPath(relativePath) {
13
+ const norm = relativePath.replace(/\\/g, "/").toLowerCase();
14
+ return (norm.startsWith("docs/adr/") ||
15
+ norm.startsWith("adr/") ||
16
+ norm.includes("/adr/") ||
17
+ /^adr-\d+/i.test(path.basename(relativePath)));
18
+ }
19
+ function parseAdr(relativePath, content) {
20
+ const lines = content.split(/\r?\n/);
21
+ let title = "";
22
+ let status;
23
+ for (const line of lines) {
24
+ if (!title && /^#\s+/.test(line)) {
25
+ title = line.replace(/^#\s+/, "").trim();
26
+ }
27
+ const statusMatch = /^\*\*Status\*\*:\s*(.+)$/i.exec(line.trim());
28
+ if (statusMatch)
29
+ status = statusMatch[1].trim();
30
+ }
31
+ if (!title)
32
+ return null;
33
+ const id = relativePath.replace(/[^\w./-]/g, "_");
34
+ return {
35
+ id,
36
+ title,
37
+ ...(status !== undefined ? { status } : {}),
38
+ source: "adr-file",
39
+ truth: "VERIFIED",
40
+ evidence: [{ path: relativePath, line: 1, excerpt: title.slice(0, 120) }],
41
+ bodyExcerpt: content.slice(0, 400),
42
+ };
43
+ }
44
+ export async function parseAdrDecisions(rootInput, maxFileSizeBytes = DEFAULT_MAX_FILE_SIZE_BYTES) {
45
+ const root = resolveRepoRoot(rootInput);
46
+ const detection = await detectProject(root, maxFileSizeBytes);
47
+ const decisions = [];
48
+ for (const entry of detection.discovery.files) {
49
+ const rel = entry.relativePath.replace(/\\/g, "/");
50
+ if (!isAdrPath(rel) || !rel.toLowerCase().endsWith(".md"))
51
+ continue;
52
+ const text = await readTextFile(path.join(root, rel), maxFileSizeBytes);
53
+ if (!text)
54
+ continue;
55
+ const parsed = parseAdr(rel, text);
56
+ if (parsed)
57
+ decisions.push(parsed);
58
+ }
59
+ decisions.sort((a, b) => a.id.localeCompare(b.id));
60
+ return decisions;
61
+ }
62
+ export async function appendDecisionLedgerEntry(rootInput, record) {
63
+ const root = resolveRepoRoot(rootInput);
64
+ const dir = path.dirname(ledgerFile(root));
65
+ await fs.mkdir(dir, { recursive: true });
66
+ const full = {
67
+ ...record,
68
+ source: record.source ?? "ledger",
69
+ truth: record.truth ?? "VERIFIED",
70
+ recordedAt: record.recordedAt ?? new Date().toISOString(),
71
+ };
72
+ await fs.appendFile(ledgerFile(root), `${JSON.stringify(full)}\n`, "utf8");
73
+ return full;
74
+ }
75
+ export async function loadDecisionLedger(rootInput) {
76
+ const root = resolveRepoRoot(rootInput);
77
+ const limitations = [
78
+ "ADR parsing reads markdown titles and Status lines only.",
79
+ "Ledger JSONL entries are merged with discovered ADR files (deduped by id).",
80
+ ];
81
+ const fromAdr = await parseAdrDecisions(root);
82
+ const fromLedger = [];
83
+ const file = ledgerFile(root);
84
+ if (await pathExists(file)) {
85
+ const text = await fs.readFile(file, "utf8");
86
+ for (const line of text.split(/\r?\n/)) {
87
+ if (!line.trim())
88
+ continue;
89
+ try {
90
+ fromLedger.push(JSON.parse(line));
91
+ }
92
+ catch {
93
+ limitations.push("Skipped malformed decision ledger line.");
94
+ }
95
+ }
96
+ }
97
+ const byId = new Map();
98
+ for (const d of [...fromAdr, ...fromLedger]) {
99
+ byId.set(d.id, d);
100
+ }
101
+ return {
102
+ root,
103
+ decisions: [...byId.values()].sort((a, b) => a.title.localeCompare(b.title)),
104
+ ledgerPath: ledgerFile(root),
105
+ limitations,
106
+ };
107
+ }
108
+ export function resolveDecisionPath(rootInput, candidate) {
109
+ return resolveSafeRepoPath(rootInput, candidate);
110
+ }
@@ -0,0 +1,46 @@
1
+ import type { ProductEvidence, TruthLabel } from "../truth.js";
2
+ import { type LockfilePackageVersion } from "./lockfiles.js";
3
+ export interface DirectDependency {
4
+ name: string;
5
+ versionRange: string;
6
+ packageJsonPath: string;
7
+ truth: TruthLabel;
8
+ evidence: ProductEvidence[];
9
+ }
10
+ export interface DuplicatePackageName {
11
+ name: string;
12
+ paths: string[];
13
+ truth: TruthLabel;
14
+ }
15
+ export interface UpgradeImpactHint {
16
+ dependency: string;
17
+ importers: string[];
18
+ truth: TruthLabel;
19
+ }
20
+ export interface DependencyAnalysisReport {
21
+ root: string;
22
+ monorepo: string;
23
+ directDependencies: DirectDependency[];
24
+ duplicateNames: DuplicatePackageName[];
25
+ lockfilesPresent: string[];
26
+ lockfilesParsed: string[];
27
+ transitiveFromLockfile: LockfilePackageVersion[];
28
+ upgradeImpact: UpgradeImpactHint[];
29
+ limitations: string[];
30
+ }
31
+ export declare function analyzeDependencies(rootInput: string, options?: {
32
+ graph?: {
33
+ nodes: Array<{
34
+ id: string;
35
+ kind: string;
36
+ label: string;
37
+ path?: string;
38
+ }>;
39
+ edges: Array<{
40
+ from: string;
41
+ to: string;
42
+ kind: string;
43
+ }>;
44
+ };
45
+ maxFileSizeBytes?: number;
46
+ }): Promise<DependencyAnalysisReport>;
@@ -0,0 +1,137 @@
1
+ import path from "node:path";
2
+ import { DEFAULT_MAX_FILE_SIZE_BYTES } from "../../constants.js";
3
+ import { detectProject } from "../../detectors/project.js";
4
+ import { detectMonorepo } from "../../detectors/monorepo.js";
5
+ import { readJsonFile } from "../../utils/fs.js";
6
+ import { resolveRepoRoot } from "../../utils/path.js";
7
+ import { parseAllLockfiles } from "./lockfiles.js";
8
+ function basename(rel) {
9
+ const parts = rel.split("/");
10
+ return parts[parts.length - 1] ?? rel;
11
+ }
12
+ function lockfilesFromPaths(relativePaths) {
13
+ const names = new Set();
14
+ for (const p of relativePaths) {
15
+ const base = basename(p).toLowerCase();
16
+ if (base === "package-lock.json" ||
17
+ base === "yarn.lock" ||
18
+ base === "pnpm-lock.yaml" ||
19
+ base === "bun.lockb") {
20
+ names.add(p.replace(/\\/g, "/"));
21
+ }
22
+ }
23
+ return [...names].sort();
24
+ }
25
+ function importersForPackage(depName, graph) {
26
+ if (!graph)
27
+ return [];
28
+ const importers = new Set();
29
+ for (const edge of graph.edges) {
30
+ if (edge.kind !== "imports" && edge.kind !== "depends-on")
31
+ continue;
32
+ const target = graph.nodes.find((n) => n.id === edge.to);
33
+ const source = graph.nodes.find((n) => n.id === edge.from);
34
+ if (!target || !source)
35
+ continue;
36
+ const label = target.label ?? target.id;
37
+ if (label === depName || label.endsWith(`/${depName}`) || label.includes(depName)) {
38
+ if (source.path)
39
+ importers.add(source.path.replace(/\\/g, "/"));
40
+ else if (source.label)
41
+ importers.add(source.label);
42
+ }
43
+ }
44
+ return [...importers].sort();
45
+ }
46
+ export async function analyzeDependencies(rootInput, options) {
47
+ const maxFileSizeBytes = options?.maxFileSizeBytes ?? DEFAULT_MAX_FILE_SIZE_BYTES;
48
+ const root = resolveRepoRoot(rootInput);
49
+ const detection = await detectProject(root, maxFileSizeBytes);
50
+ const relativePaths = detection.discovery.files.map((f) => f.relativePath.replace(/\\/g, "/"));
51
+ let rootWorkspaces;
52
+ const rootPkg = await readJsonFile(path.join(root, "package.json"), maxFileSizeBytes);
53
+ if (rootPkg.ok)
54
+ rootWorkspaces = rootPkg.data.workspaces;
55
+ const monorepo = detectMonorepo({
56
+ relativePaths,
57
+ packageJsonWorkspaces: rootWorkspaces,
58
+ }).monorepo;
59
+ const lockfilesPresent = lockfilesFromPaths(relativePaths);
60
+ const limitations = [
61
+ "Direct dependencies are read from package.json files only.",
62
+ "Transitive versions are VERIFIED only when a lockfile was parsed successfully.",
63
+ ];
64
+ if (!lockfilesPresent.length) {
65
+ limitations.push("No lockfile detected; transitive upgrade impact is UNKNOWN.");
66
+ }
67
+ const lockParse = await parseAllLockfiles(root, lockfilesPresent, maxFileSizeBytes);
68
+ limitations.push(...lockParse.limitations);
69
+ const packageJsonPaths = relativePaths.filter((p) => basename(p).toLowerCase() === "package.json");
70
+ const directDependencies = [];
71
+ const nameToPaths = new Map();
72
+ for (const rel of packageJsonPaths) {
73
+ const parsed = await readJsonFile(path.join(root, rel), maxFileSizeBytes);
74
+ if (!parsed.ok)
75
+ continue;
76
+ const pkgName = parsed.data.name?.trim();
77
+ if (pkgName) {
78
+ const list = nameToPaths.get(pkgName) ?? [];
79
+ list.push(rel);
80
+ nameToPaths.set(pkgName, list);
81
+ }
82
+ const sections = [
83
+ ["dependencies", parsed.data.dependencies],
84
+ ["devDependencies", parsed.data.devDependencies],
85
+ ["peerDependencies", parsed.data.peerDependencies],
86
+ ];
87
+ for (const [section, deps] of sections) {
88
+ if (!deps)
89
+ continue;
90
+ for (const [name, versionRange] of Object.entries(deps)) {
91
+ directDependencies.push({
92
+ name,
93
+ versionRange,
94
+ packageJsonPath: rel,
95
+ truth: "VERIFIED",
96
+ evidence: [{ path: rel, excerpt: `${section}.${name}` }],
97
+ });
98
+ }
99
+ }
100
+ }
101
+ directDependencies.sort((a, b) => a.name === b.name
102
+ ? a.packageJsonPath.localeCompare(b.packageJsonPath)
103
+ : a.name.localeCompare(b.name));
104
+ const duplicateNames = [];
105
+ for (const [name, paths] of nameToPaths) {
106
+ if (paths.length > 1) {
107
+ duplicateNames.push({
108
+ name,
109
+ paths: [...paths].sort(),
110
+ truth: "VERIFIED",
111
+ });
112
+ }
113
+ }
114
+ const upgradeImpact = [];
115
+ const uniqueDeps = [...new Set(directDependencies.map((d) => d.name))];
116
+ for (const dep of uniqueDeps) {
117
+ const importers = importersForPackage(dep, options?.graph);
118
+ if (importers.length) {
119
+ upgradeImpact.push({
120
+ dependency: dep,
121
+ importers,
122
+ truth: options?.graph ? "INFERRED" : "UNKNOWN",
123
+ });
124
+ }
125
+ }
126
+ return {
127
+ root,
128
+ monorepo,
129
+ directDependencies,
130
+ duplicateNames,
131
+ lockfilesPresent,
132
+ lockfilesParsed: lockParse.parsedLockfiles,
133
+ transitiveFromLockfile: lockParse.packages,
134
+ upgradeImpact,
135
+ limitations,
136
+ };
137
+ }
@@ -0,0 +1,25 @@
1
+ import type { ProductEvidence, TruthLabel } from "../truth.js";
2
+ export interface LockfilePackageVersion {
3
+ name: string;
4
+ version: string;
5
+ lockfile: string;
6
+ truth: TruthLabel;
7
+ evidence: ProductEvidence[];
8
+ }
9
+ export interface ParsedLockfile {
10
+ lockfilePath: string;
11
+ format: "npm" | "yarn" | "pnpm" | "unknown";
12
+ packages: LockfilePackageVersion[];
13
+ limitations: string[];
14
+ }
15
+ export declare function parsePackageLockJson(content: string, lockfilePath: string): ParsedLockfile;
16
+ export declare function parseYarnLock(content: string, lockfilePath: string): ParsedLockfile;
17
+ export declare function parsePnpmLockYaml(content: string, lockfilePath: string): ParsedLockfile;
18
+ export declare function parseLockfileAtPath(root: string, relativePath: string, maxBytes: number): Promise<ParsedLockfile | null>;
19
+ export declare function parseAllLockfiles(root: string, lockfilePaths: string[], maxBytes: number): Promise<{
20
+ packages: LockfilePackageVersion[];
21
+ parsedLockfiles: string[];
22
+ limitations: string[];
23
+ }>;
24
+ /** Test helper: read lockfile from disk */
25
+ export declare function readLockfileFile(filePath: string): Promise<string>;