@praneeth_54/agentdoctor 2.1.0 → 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 (129) hide show
  1. package/CHANGELOG.md +57 -3
  2. package/README.md +489 -290
  3. package/dist/agent/chat/deterministic.d.ts +6 -0
  4. package/dist/agent/chat/deterministic.js +180 -0
  5. package/dist/agent/chat/service.js +8 -14
  6. package/dist/agent/index.d.ts +2 -0
  7. package/dist/agent/index.js +1 -0
  8. package/dist/agent/loop.d.ts +2 -0
  9. package/dist/agent/loop.js +31 -0
  10. package/dist/agent/roles.d.ts +12 -0
  11. package/dist/agent/roles.js +138 -0
  12. package/dist/agent/runtime.d.ts +21 -5
  13. package/dist/agent/runtime.js +141 -28
  14. package/dist/agent/tools/execute.d.ts +2 -0
  15. package/dist/agent/tools/execute.js +10 -0
  16. package/dist/ai/config.js +1 -0
  17. package/dist/ai/redact.d.ts +1 -0
  18. package/dist/ai/redact.js +47 -7
  19. package/dist/ai/types.d.ts +1 -1
  20. package/dist/cli/commands/agent.d.ts +2 -0
  21. package/dist/cli/commands/agent.js +19 -8
  22. package/dist/cli/commands/chat.js +2 -4
  23. package/dist/cli/commands/product.d.ts +19 -0
  24. package/dist/cli/commands/product.js +147 -0
  25. package/dist/cli/commands/start.d.ts +15 -0
  26. package/dist/cli/commands/start.js +80 -0
  27. package/dist/cli/program.js +141 -0
  28. package/dist/constants.d.ts +1 -1
  29. package/dist/constants.js +1 -1
  30. package/dist/dashboard/server.js +255 -53
  31. package/dist/index.d.ts +4 -0
  32. package/dist/index.js +2 -0
  33. package/dist/intelligence/graph/build.js +36 -8
  34. package/dist/intelligence/resolve/imports.js +1 -1
  35. package/dist/languages/dart.d.ts +10 -0
  36. package/dist/languages/dart.js +99 -0
  37. package/dist/languages/go.d.ts +13 -7
  38. package/dist/languages/go.js +84 -24
  39. package/dist/languages/index.d.ts +5 -1
  40. package/dist/languages/index.js +13 -36
  41. package/dist/languages/java.d.ts +10 -0
  42. package/dist/languages/java.js +80 -0
  43. package/dist/languages/kotlin.d.ts +10 -0
  44. package/dist/languages/kotlin.js +85 -0
  45. package/dist/languages/rust.d.ts +10 -0
  46. package/dist/languages/rust.js +93 -0
  47. package/dist/languages/types.d.ts +4 -3
  48. package/dist/mcp/agent/registry.d.ts +1 -1
  49. package/dist/mcp/agent/registry.js +109 -23
  50. package/dist/mcp/intelligence/handlers.d.ts +3 -0
  51. package/dist/mcp/intelligence/handlers.js +28 -0
  52. package/dist/mcp/intelligence/registry.d.ts +1 -1
  53. package/dist/mcp/intelligence/registry.js +30 -1
  54. package/dist/product/api/doctor.d.ts +14 -0
  55. package/dist/product/api/doctor.js +185 -0
  56. package/dist/product/api/openapi.d.ts +7 -0
  57. package/dist/product/api/openapi.js +122 -0
  58. package/dist/product/approval/model.d.ts +30 -0
  59. package/dist/product/approval/model.js +64 -0
  60. package/dist/product/approval/session.d.ts +49 -0
  61. package/dist/product/approval/session.js +134 -0
  62. package/dist/product/database/doctor.d.ts +30 -0
  63. package/dist/product/database/doctor.js +184 -0
  64. package/dist/product/decisions/ledger.d.ts +24 -0
  65. package/dist/product/decisions/ledger.js +110 -0
  66. package/dist/product/deps/analyze.d.ts +46 -0
  67. package/dist/product/deps/analyze.js +137 -0
  68. package/dist/product/deps/lockfiles.d.ts +25 -0
  69. package/dist/product/deps/lockfiles.js +200 -0
  70. package/dist/product/discovery/roots.d.ts +42 -0
  71. package/dist/product/discovery/roots.js +215 -0
  72. package/dist/product/dna/build.d.ts +50 -0
  73. package/dist/product/dna/build.js +255 -0
  74. package/dist/product/eval/lab.d.ts +17 -0
  75. package/dist/product/eval/lab.js +218 -0
  76. package/dist/product/events/doctor.d.ts +21 -0
  77. package/dist/product/events/doctor.js +147 -0
  78. package/dist/product/evidence-scan.d.ts +12 -0
  79. package/dist/product/evidence-scan.js +46 -0
  80. package/dist/product/evolution/timeline.d.ts +29 -0
  81. package/dist/product/evolution/timeline.js +123 -0
  82. package/dist/product/features/intelligence.d.ts +23 -0
  83. package/dist/product/features/intelligence.js +158 -0
  84. package/dist/product/forensic/mode.d.ts +25 -0
  85. package/dist/product/forensic/mode.js +70 -0
  86. package/dist/product/graph/enrich-languages.d.ts +20 -0
  87. package/dist/product/graph/enrich-languages.js +193 -0
  88. package/dist/product/health/code-health.d.ts +20 -0
  89. package/dist/product/health/code-health.js +149 -0
  90. package/dist/product/index.d.ts +69 -0
  91. package/dist/product/index.js +35 -0
  92. package/dist/product/ledger/change-ledger.d.ts +23 -0
  93. package/dist/product/ledger/change-ledger.js +64 -0
  94. package/dist/product/map/software-map.d.ts +17 -0
  95. package/dist/product/map/software-map.js +95 -0
  96. package/dist/product/memory/institutional.d.ts +16 -0
  97. package/dist/product/memory/institutional.js +87 -0
  98. package/dist/product/ops/incident.d.ts +20 -0
  99. package/dist/product/ops/incident.js +61 -0
  100. package/dist/product/ops/infra.d.ts +15 -0
  101. package/dist/product/ops/infra.js +112 -0
  102. package/dist/product/org/model.d.ts +34 -0
  103. package/dist/product/org/model.js +195 -0
  104. package/dist/product/privacy/doctor.d.ts +13 -0
  105. package/dist/product/privacy/doctor.js +90 -0
  106. package/dist/product/requirements/trace.d.ts +21 -0
  107. package/dist/product/requirements/trace.js +166 -0
  108. package/dist/product/search/index.d.ts +29 -0
  109. package/dist/product/search/index.js +116 -0
  110. package/dist/product/search/software-search.d.ts +19 -0
  111. package/dist/product/search/software-search.js +100 -0
  112. package/dist/product/security/doctor.d.ts +23 -0
  113. package/dist/product/security/doctor.js +124 -0
  114. package/dist/product/self/diagnose.d.ts +15 -0
  115. package/dist/product/self/diagnose.js +82 -0
  116. package/dist/product/techdebt/roadmap.d.ts +20 -0
  117. package/dist/product/techdebt/roadmap.js +118 -0
  118. package/dist/product/testbrain/analyze.d.ts +29 -0
  119. package/dist/product/testbrain/analyze.js +129 -0
  120. package/dist/product/truth.d.ts +12 -0
  121. package/dist/product/truth.js +30 -0
  122. package/dist/product/twin/digital-twin.d.ts +23 -0
  123. package/dist/product/twin/digital-twin.js +52 -0
  124. package/dist/product/twin/store.d.ts +21 -0
  125. package/dist/product/twin/store.js +70 -0
  126. package/dist/product/whatif/engine.d.ts +28 -0
  127. package/dist/product/whatif/engine.js +70 -0
  128. package/dist/utils/fs.js +10 -2
  129. package/package.json +1 -1
@@ -0,0 +1,122 @@
1
+ import path from "node:path";
2
+ import { detectProject } from "../../detectors/project.js";
3
+ import { readTextFile } from "../../utils/fs.js";
4
+ import { resolveRepoRoot } from "../../utils/path.js";
5
+ const OPENAPI_NAMES = new Set([
6
+ "openapi.json",
7
+ "openapi.yaml",
8
+ "openapi.yml",
9
+ "swagger.json",
10
+ "swagger.yaml",
11
+ "swagger.yml",
12
+ ]);
13
+ function basenameLower(rel) {
14
+ return path.basename(rel).toLowerCase();
15
+ }
16
+ function extractJsonOpenApiPaths(content, relativePath) {
17
+ let doc;
18
+ try {
19
+ doc = JSON.parse(content);
20
+ }
21
+ catch {
22
+ return [];
23
+ }
24
+ const paths = doc.paths;
25
+ if (!paths || typeof paths !== "object")
26
+ return [];
27
+ const out = [];
28
+ for (const [pathPattern, methods] of Object.entries(paths)) {
29
+ if (!methods || typeof methods !== "object")
30
+ continue;
31
+ for (const method of Object.keys(methods)) {
32
+ const lower = method.toLowerCase();
33
+ if (!["get", "post", "put", "patch", "delete", "head", "options", "trace"].includes(lower)) {
34
+ continue;
35
+ }
36
+ out.push({
37
+ method: lower.toUpperCase(),
38
+ pathPattern,
39
+ framework: "openapi",
40
+ truth: "VERIFIED",
41
+ evidence: [{ path: relativePath, excerpt: `paths.${pathPattern}.${lower}` }],
42
+ });
43
+ }
44
+ }
45
+ return out;
46
+ }
47
+ /** Minimal YAML path/method extraction without a YAML library */
48
+ function extractYamlOpenApiPaths(content, relativePath) {
49
+ const out = [];
50
+ const lines = content.split(/\r?\n/);
51
+ let inPaths = false;
52
+ let currentPath = null;
53
+ const methodRe = /^\s{2,6}(get|post|put|patch|delete|head|options|trace):\s*$/i;
54
+ for (let i = 0; i < lines.length; i++) {
55
+ const line = lines[i];
56
+ if (/^paths:\s*$/.test(line.trim())) {
57
+ inPaths = true;
58
+ currentPath = null;
59
+ continue;
60
+ }
61
+ if (!inPaths)
62
+ continue;
63
+ if (/^[a-zA-Z]/.test(line) && !line.startsWith(" ")) {
64
+ break;
65
+ }
66
+ const pathKey = /^\s{2}(\/[^\s:]+):\s*$/.exec(line);
67
+ if (pathKey) {
68
+ currentPath = pathKey[1];
69
+ continue;
70
+ }
71
+ const methodMatch = methodRe.exec(line);
72
+ if (methodMatch && currentPath) {
73
+ const method = methodMatch[1].toUpperCase();
74
+ out.push({
75
+ method,
76
+ pathPattern: currentPath,
77
+ framework: "openapi",
78
+ truth: "VERIFIED",
79
+ evidence: [{ path: relativePath, line: i + 1, excerpt: line.trim() }],
80
+ });
81
+ }
82
+ }
83
+ return out;
84
+ }
85
+ export async function discoverOpenApiEndpoints(rootInput, maxFileSizeBytes = 512 * 1024) {
86
+ const root = resolveRepoRoot(rootInput);
87
+ const detection = await detectProject(root, maxFileSizeBytes);
88
+ const limitations = [
89
+ "OpenAPI discovery reads static spec files only — mounted servers and merged specs are not resolved.",
90
+ ];
91
+ const endpoints = [];
92
+ for (const entry of detection.discovery.files) {
93
+ const rel = entry.relativePath.replace(/\\/g, "/");
94
+ if (!OPENAPI_NAMES.has(basenameLower(rel)))
95
+ continue;
96
+ const text = await readTextFile(path.join(root, rel), maxFileSizeBytes);
97
+ if (text === null)
98
+ continue;
99
+ const lower = basenameLower(rel);
100
+ if (lower.endsWith(".json")) {
101
+ endpoints.push(...extractJsonOpenApiPaths(text, rel));
102
+ }
103
+ else {
104
+ endpoints.push(...extractYamlOpenApiPaths(text, rel));
105
+ if (!endpoints.length) {
106
+ limitations.push(`YAML spec ${rel} had no paths block detected (minimal parser)`);
107
+ }
108
+ }
109
+ }
110
+ const seen = new Set();
111
+ const deduped = endpoints.filter((e) => {
112
+ const key = `${e.method}:${e.pathPattern}:${e.evidence[0]?.path}`;
113
+ if (seen.has(key))
114
+ return false;
115
+ seen.add(key);
116
+ return true;
117
+ });
118
+ deduped.sort((a, b) => a.pathPattern === b.pathPattern
119
+ ? a.method.localeCompare(b.method)
120
+ : a.pathPattern.localeCompare(b.pathPattern));
121
+ return { endpoints: deduped, limitations };
122
+ }
@@ -0,0 +1,30 @@
1
+ export type ApprovalState = "pending" | "approved" | "denied" | "expired";
2
+ export type ApprovalRisk = "LOW" | "MEDIUM" | "HIGH" | "CRITICAL";
3
+ export interface ApprovalRecord {
4
+ id: string;
5
+ action: string;
6
+ reason: string;
7
+ resources: string[];
8
+ risk: ApprovalRisk;
9
+ requirement?: string;
10
+ state: ApprovalState;
11
+ actor?: string;
12
+ createdAt: string;
13
+ decidedAt?: string;
14
+ /** Must be true from trusted CLI/MCP session layer — never accept caller approved=true alone */
15
+ approvedByHuman: boolean;
16
+ }
17
+ export interface ApprovalEvaluationInput {
18
+ record: Omit<ApprovalRecord, "id" | "createdAt" | "state" | "approvedByHuman">;
19
+ /** Explicit human gate from CLI `--approve` or authenticated session layer */
20
+ approvedByHuman?: boolean;
21
+ actor?: string;
22
+ }
23
+ export interface ApprovalEvaluationResult {
24
+ record: ApprovalRecord;
25
+ allowed: boolean;
26
+ message: string;
27
+ limitations: string[];
28
+ }
29
+ export declare function evaluateApprovalRecord(input: ApprovalEvaluationInput): ApprovalEvaluationResult;
30
+ export declare function formatApprovalRecordSummary(record: ApprovalRecord): string;
@@ -0,0 +1,64 @@
1
+ const LIMITATIONS = [
2
+ "ApprovalRecord is an explicit abstraction — MCP/model callers cannot set approvedByHuman=true without a trusted session.",
3
+ "CLI passes approvedByHuman via --approve; dashboard chat does not grant approval.",
4
+ "This module does not persist approvals — use change ledger / audit trails separately.",
5
+ ];
6
+ function newId() {
7
+ return `apr_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
8
+ }
9
+ export function evaluateApprovalRecord(input) {
10
+ const approvedByHuman = input.approvedByHuman === true;
11
+ const now = new Date().toISOString();
12
+ const base = {
13
+ id: newId(),
14
+ action: input.record.action,
15
+ reason: input.record.reason,
16
+ resources: [...input.record.resources],
17
+ risk: input.record.risk,
18
+ ...(input.record.requirement !== undefined ? { requirement: input.record.requirement } : {}),
19
+ state: "pending",
20
+ ...(input.actor !== undefined ? { actor: input.actor } : {}),
21
+ createdAt: now,
22
+ approvedByHuman,
23
+ };
24
+ if (!approvedByHuman) {
25
+ return {
26
+ record: { ...base, state: "pending", approvedByHuman: false },
27
+ allowed: false,
28
+ message: "Human approval required — approvedByHuman must be true from trusted CLI/MCP session (not from model output).",
29
+ limitations: LIMITATIONS,
30
+ };
31
+ }
32
+ if (input.record.risk === "CRITICAL" && !input.record.reason.trim()) {
33
+ return {
34
+ record: { ...base, state: "denied", approvedByHuman: true, decidedAt: now },
35
+ allowed: false,
36
+ message: "CRITICAL actions require a non-empty reason even when approvedByHuman is true.",
37
+ limitations: LIMITATIONS,
38
+ };
39
+ }
40
+ return {
41
+ record: {
42
+ ...base,
43
+ state: "approved",
44
+ approvedByHuman: true,
45
+ decidedAt: now,
46
+ },
47
+ allowed: true,
48
+ message: "Explicit human approval recorded.",
49
+ limitations: LIMITATIONS,
50
+ };
51
+ }
52
+ export function formatApprovalRecordSummary(record) {
53
+ return [
54
+ `Approval ${record.id}`,
55
+ `Action: ${record.action}`,
56
+ `Risk: ${record.risk}`,
57
+ `State: ${record.state}`,
58
+ `approvedByHuman: ${record.approvedByHuman}`,
59
+ record.requirement ? `Requirement: ${record.requirement}` : "",
60
+ record.resources.length ? `Resources: ${record.resources.join(", ")}` : "",
61
+ ]
62
+ .filter(Boolean)
63
+ .join("\n");
64
+ }
@@ -0,0 +1,49 @@
1
+ import type { ApprovalRisk } from "./model.js";
2
+ /**
3
+ * Trusted approval grants — MCP/agent writes must present a grant token
4
+ * issued by CLI `--approve` or an authenticated session, not a bare approved=true.
5
+ */
6
+ export interface ApprovalGrant {
7
+ token: string;
8
+ root: string;
9
+ action: string;
10
+ resources: string[];
11
+ risk: ApprovalRisk;
12
+ planHash: string;
13
+ actor: string;
14
+ createdAt: string;
15
+ expiresAt: string;
16
+ consumed: boolean;
17
+ scope: "write" | "execute" | "write+execute";
18
+ }
19
+ export declare function hashPlanPayload(payload: string): string;
20
+ /** Stable plan hash for a file write operation (path + content). */
21
+ export declare function hashFileWritePlan(action: string, relativePath: string, content: string): string;
22
+ export declare function issueApprovalGrant(options: {
23
+ root: string;
24
+ action: string;
25
+ resources: string[];
26
+ risk: ApprovalRisk;
27
+ planHash: string;
28
+ actor?: string;
29
+ scope?: ApprovalGrant["scope"];
30
+ ttlMs?: number;
31
+ }): Promise<ApprovalGrant>;
32
+ export interface ConsumeApprovalResult {
33
+ ok: boolean;
34
+ reason: string;
35
+ grant?: ApprovalGrant;
36
+ }
37
+ /**
38
+ * Validate and optionally consume a grant token for a write/execute action.
39
+ * Bare `approved: true` without a valid token is NEVER sufficient.
40
+ */
41
+ export declare function consumeApprovalGrant(options: {
42
+ root: string;
43
+ token: string | undefined;
44
+ action: string;
45
+ resources: string[];
46
+ /** Required for MCP/agent writes — binds token to exact operation payload */
47
+ requirePlanHash: string;
48
+ consume?: boolean;
49
+ }): Promise<ConsumeApprovalResult>;
@@ -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;