@praneeth_54/agentdoctor 2.1.0 → 3.0.1
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.
- package/CHANGELOG.md +86 -3
- package/README.md +490 -290
- package/dist/agent/chat/deterministic.d.ts +6 -0
- package/dist/agent/chat/deterministic.js +180 -0
- package/dist/agent/chat/service.js +8 -14
- package/dist/agent/context/retrieve.d.ts +2 -1
- package/dist/agent/context/retrieve.js +14 -1
- package/dist/agent/index.d.ts +2 -0
- package/dist/agent/index.js +1 -0
- package/dist/agent/loop.d.ts +2 -0
- package/dist/agent/loop.js +31 -0
- package/dist/agent/roles.d.ts +12 -0
- package/dist/agent/roles.js +138 -0
- package/dist/agent/runtime.d.ts +21 -5
- package/dist/agent/runtime.js +141 -28
- package/dist/agent/tools/execute.d.ts +2 -0
- package/dist/agent/tools/execute.js +49 -43
- package/dist/agent/tools/write.d.ts +1 -1
- package/dist/agent/tools/write.js +18 -4
- package/dist/ai/config.js +1 -0
- package/dist/ai/providers/adversarial-local.d.ts +7 -0
- package/dist/ai/providers/adversarial-local.js +146 -0
- package/dist/ai/redact.d.ts +1 -0
- package/dist/ai/redact.js +47 -7
- package/dist/ai/types.d.ts +1 -1
- package/dist/cli/commands/agent.d.ts +2 -0
- package/dist/cli/commands/agent.js +32 -11
- package/dist/cli/commands/architecture.js +7 -2
- package/dist/cli/commands/brain.js +7 -2
- package/dist/cli/commands/chat.js +15 -7
- package/dist/cli/commands/complete.js +13 -3
- package/dist/cli/commands/fix.js +6 -0
- package/dist/cli/commands/learn.js +7 -2
- package/dist/cli/commands/mcp.js +7 -3
- package/dist/cli/commands/platform.js +7 -2
- package/dist/cli/commands/policy-graph-run.js +7 -0
- package/dist/cli/commands/product.d.ts +19 -0
- package/dist/cli/commands/product.js +157 -0
- package/dist/cli/commands/scan.js +6 -0
- package/dist/cli/commands/start.d.ts +15 -0
- package/dist/cli/commands/start.js +86 -0
- package/dist/cli/commands/v2.js +61 -11
- package/dist/cli/commands/verify.js +6 -0
- package/dist/cli/program.js +141 -0
- package/dist/cli/safe-root.d.ts +17 -0
- package/dist/cli/safe-root.js +32 -0
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -1
- package/dist/core/brain-cli/service.js +5 -2
- package/dist/core/monorepo/detect.js +10 -0
- package/dist/core/secrets/scan.js +9 -0
- package/dist/core/understanding/brain/storage/store.d.ts +4 -0
- package/dist/core/understanding/brain/storage/store.js +22 -2
- package/dist/dashboard/page.d.ts +2 -0
- package/dist/dashboard/page.js +53 -0
- package/dist/dashboard/server.js +117 -140
- package/dist/dashboard/ui/client.d.ts +2 -0
- package/dist/dashboard/ui/client.js +2 -0
- package/dist/dashboard/ui/styles.d.ts +2 -0
- package/dist/dashboard/ui/styles.js +138 -0
- package/dist/discovery/files.js +13 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/dist/intelligence/graph/build.js +46 -38
- package/dist/intelligence/graph/incremental.d.ts +2 -0
- package/dist/intelligence/graph/incremental.js +9 -0
- package/dist/intelligence/resolve/imports.js +1 -1
- package/dist/languages/dart.d.ts +10 -0
- package/dist/languages/dart.js +99 -0
- package/dist/languages/go.d.ts +13 -7
- package/dist/languages/go.js +84 -24
- package/dist/languages/index.d.ts +5 -1
- package/dist/languages/index.js +13 -36
- package/dist/languages/java.d.ts +10 -0
- package/dist/languages/java.js +80 -0
- package/dist/languages/kotlin.d.ts +10 -0
- package/dist/languages/kotlin.js +85 -0
- package/dist/languages/rust.d.ts +10 -0
- package/dist/languages/rust.js +93 -0
- package/dist/languages/types.d.ts +4 -3
- package/dist/mcp/agent/registry.d.ts +1 -1
- package/dist/mcp/agent/registry.js +109 -23
- package/dist/mcp/intelligence/handlers.d.ts +3 -0
- package/dist/mcp/intelligence/handlers.js +28 -0
- package/dist/mcp/intelligence/path-safety.d.ts +6 -1
- package/dist/mcp/intelligence/path-safety.js +55 -2
- package/dist/mcp/intelligence/registry.d.ts +1 -1
- package/dist/mcp/intelligence/registry.js +30 -1
- package/dist/platform/graph/build.js +9 -0
- package/dist/product/api/doctor.d.ts +14 -0
- package/dist/product/api/doctor.js +185 -0
- package/dist/product/api/openapi.d.ts +7 -0
- package/dist/product/api/openapi.js +122 -0
- package/dist/product/approval/model.d.ts +30 -0
- package/dist/product/approval/model.js +64 -0
- package/dist/product/approval/session.d.ts +49 -0
- package/dist/product/approval/session.js +134 -0
- package/dist/product/database/doctor.d.ts +30 -0
- package/dist/product/database/doctor.js +184 -0
- package/dist/product/decisions/ledger.d.ts +36 -0
- package/dist/product/decisions/ledger.js +141 -0
- package/dist/product/deps/analyze.d.ts +46 -0
- package/dist/product/deps/analyze.js +137 -0
- package/dist/product/deps/lockfiles.d.ts +25 -0
- package/dist/product/deps/lockfiles.js +200 -0
- package/dist/product/discovery/roots.d.ts +52 -0
- package/dist/product/discovery/roots.js +231 -0
- package/dist/product/dna/build.d.ts +50 -0
- package/dist/product/dna/build.js +255 -0
- package/dist/product/eval/lab.d.ts +17 -0
- package/dist/product/eval/lab.js +218 -0
- package/dist/product/events/doctor.d.ts +21 -0
- package/dist/product/events/doctor.js +147 -0
- package/dist/product/evidence-scan.d.ts +12 -0
- package/dist/product/evidence-scan.js +46 -0
- package/dist/product/evolution/timeline.d.ts +29 -0
- package/dist/product/evolution/timeline.js +123 -0
- package/dist/product/features/intelligence.d.ts +23 -0
- package/dist/product/features/intelligence.js +158 -0
- package/dist/product/forensic/mode.d.ts +25 -0
- package/dist/product/forensic/mode.js +70 -0
- package/dist/product/graph/enrich-languages.d.ts +20 -0
- package/dist/product/graph/enrich-languages.js +168 -0
- package/dist/product/health/code-health.d.ts +20 -0
- package/dist/product/health/code-health.js +149 -0
- package/dist/product/index.d.ts +72 -0
- package/dist/product/index.js +36 -0
- package/dist/product/ledger/change-ledger.d.ts +23 -0
- package/dist/product/ledger/change-ledger.js +64 -0
- package/dist/product/map/software-map.d.ts +17 -0
- package/dist/product/map/software-map.js +96 -0
- package/dist/product/memory/institutional.d.ts +16 -0
- package/dist/product/memory/institutional.js +92 -0
- package/dist/product/ops/incident.d.ts +20 -0
- package/dist/product/ops/incident.js +61 -0
- package/dist/product/ops/infra.d.ts +15 -0
- package/dist/product/ops/infra.js +112 -0
- package/dist/product/org/model.d.ts +34 -0
- package/dist/product/org/model.js +195 -0
- package/dist/product/privacy/doctor.d.ts +13 -0
- package/dist/product/privacy/doctor.js +90 -0
- package/dist/product/requirements/trace.d.ts +21 -0
- package/dist/product/requirements/trace.js +166 -0
- package/dist/product/search/index.d.ts +29 -0
- package/dist/product/search/index.js +116 -0
- package/dist/product/search/software-search.d.ts +19 -0
- package/dist/product/search/software-search.js +105 -0
- package/dist/product/security/doctor.d.ts +23 -0
- package/dist/product/security/doctor.js +124 -0
- package/dist/product/self/diagnose.d.ts +15 -0
- package/dist/product/self/diagnose.js +82 -0
- package/dist/product/techdebt/roadmap.d.ts +20 -0
- package/dist/product/techdebt/roadmap.js +118 -0
- package/dist/product/testbrain/analyze.d.ts +29 -0
- package/dist/product/testbrain/analyze.js +129 -0
- package/dist/product/truth.d.ts +12 -0
- package/dist/product/truth.js +30 -0
- package/dist/product/twin/digital-twin.d.ts +23 -0
- package/dist/product/twin/digital-twin.js +52 -0
- package/dist/product/twin/store.d.ts +21 -0
- package/dist/product/twin/store.js +78 -0
- package/dist/product/whatif/engine.d.ts +28 -0
- package/dist/product/whatif/engine.js +91 -0
- package/dist/project/ownership.d.ts +65 -0
- package/dist/project/ownership.js +169 -0
- package/dist/utils/fs.js +10 -2
- package/package.json +1 -1
|
@@ -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,36 @@
|
|
|
1
|
+
import { type ProjectOwnershipClass } from "../../project/ownership.js";
|
|
2
|
+
import type { ProductEvidence, TruthLabel } from "../truth.js";
|
|
3
|
+
/** Why a decision document is attributed to the current project. */
|
|
4
|
+
export type DecisionSourceKind = "adr_file" | "decision_ledger" | "architecture_document" | "internal_note" | "unknown";
|
|
5
|
+
export interface DecisionRecord {
|
|
6
|
+
id: string;
|
|
7
|
+
title: string;
|
|
8
|
+
status?: string;
|
|
9
|
+
/** Wire-compat provenance channel. */
|
|
10
|
+
source: "adr-file" | "ledger";
|
|
11
|
+
sourceKind: DecisionSourceKind;
|
|
12
|
+
truth: TruthLabel;
|
|
13
|
+
/** Explicit meaning for UI — never “architecture proven correct”. */
|
|
14
|
+
truthMeaning: string;
|
|
15
|
+
projectRoot: string;
|
|
16
|
+
ownership: ProjectOwnershipClass;
|
|
17
|
+
evidence: ProductEvidence[];
|
|
18
|
+
recordedAt?: string;
|
|
19
|
+
bodyExcerpt?: string;
|
|
20
|
+
}
|
|
21
|
+
export interface DecisionLedgerResult {
|
|
22
|
+
root: string;
|
|
23
|
+
decisions: DecisionRecord[];
|
|
24
|
+
ledgerPath: string;
|
|
25
|
+
limitations: string[];
|
|
26
|
+
}
|
|
27
|
+
export declare function parseAdrDecisions(rootInput: string, maxFileSizeBytes?: number): Promise<DecisionRecord[]>;
|
|
28
|
+
export declare function appendDecisionLedgerEntry(rootInput: string, record: Omit<DecisionRecord, "source" | "truth" | "sourceKind" | "truthMeaning" | "projectRoot" | "ownership"> & {
|
|
29
|
+
source?: DecisionRecord["source"];
|
|
30
|
+
sourceKind?: DecisionSourceKind;
|
|
31
|
+
truth?: TruthLabel;
|
|
32
|
+
truthMeaning?: string;
|
|
33
|
+
ownership?: ProjectOwnershipClass;
|
|
34
|
+
}): Promise<DecisionRecord>;
|
|
35
|
+
export declare function loadDecisionLedger(rootInput: string): Promise<DecisionLedgerResult>;
|
|
36
|
+
export declare function resolveDecisionPath(rootInput: string, candidate: string): string;
|
|
@@ -0,0 +1,141 @@
|
|
|
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 { classifyRelativePathOwnership, isProjectOwnedRelativePath, verifiedDecisionTruthMeaning, } from "../../project/ownership.js";
|
|
6
|
+
import { resolveSafeRepoPath } from "../../security/paths.js";
|
|
7
|
+
import { pathExists, readTextFile } from "../../utils/fs.js";
|
|
8
|
+
import { resolveRepoRoot } from "../../utils/path.js";
|
|
9
|
+
function ledgerFile(root) {
|
|
10
|
+
return path.join(resolveRepoRoot(root), ".agentdoctor", "decisions", "entries.jsonl");
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* ADR-shaped paths under the current project-owned tree only.
|
|
14
|
+
* Nested foreign trees with an adr directory segment are excluded by ownership
|
|
15
|
+
* before this runs; this still refuses internal_docs / private paths as defense in depth.
|
|
16
|
+
*/
|
|
17
|
+
function isAdrPath(relativePath) {
|
|
18
|
+
if (!isProjectOwnedRelativePath(relativePath))
|
|
19
|
+
return false;
|
|
20
|
+
const norm = relativePath.replace(/\\/g, "/").toLowerCase();
|
|
21
|
+
const base = path.basename(relativePath);
|
|
22
|
+
return norm.startsWith("docs/adr/") || norm.startsWith("adr/") || /^adr[-_]\d+/i.test(base);
|
|
23
|
+
}
|
|
24
|
+
function parseAdr(projectRoot, relativePath, content) {
|
|
25
|
+
const lines = content.split(/\r?\n/);
|
|
26
|
+
let title = "";
|
|
27
|
+
let status;
|
|
28
|
+
for (const line of lines) {
|
|
29
|
+
if (!title && /^#\s+/.test(line)) {
|
|
30
|
+
title = line.replace(/^#\s+/, "").trim();
|
|
31
|
+
}
|
|
32
|
+
const statusMatch = /^\*\*Status\*\*:\s*(.+)$/i.exec(line.trim());
|
|
33
|
+
if (statusMatch)
|
|
34
|
+
status = statusMatch[1].trim();
|
|
35
|
+
}
|
|
36
|
+
if (!title)
|
|
37
|
+
return null;
|
|
38
|
+
const id = relativePath.replace(/[^\w./-]/g, "_");
|
|
39
|
+
const ownership = classifyRelativePathOwnership(relativePath);
|
|
40
|
+
return {
|
|
41
|
+
id,
|
|
42
|
+
title,
|
|
43
|
+
...(status !== undefined ? { status } : {}),
|
|
44
|
+
source: "adr-file",
|
|
45
|
+
sourceKind: "adr_file",
|
|
46
|
+
truth: "VERIFIED",
|
|
47
|
+
truthMeaning: verifiedDecisionTruthMeaning(),
|
|
48
|
+
projectRoot,
|
|
49
|
+
ownership,
|
|
50
|
+
evidence: [{ path: relativePath, line: 1, excerpt: title.slice(0, 120) }],
|
|
51
|
+
bodyExcerpt: content.slice(0, 400),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
export async function parseAdrDecisions(rootInput, maxFileSizeBytes = DEFAULT_MAX_FILE_SIZE_BYTES) {
|
|
55
|
+
const root = resolveRepoRoot(rootInput);
|
|
56
|
+
const detection = await detectProject(root, maxFileSizeBytes);
|
|
57
|
+
const decisions = [];
|
|
58
|
+
for (const entry of detection.discovery.files) {
|
|
59
|
+
const rel = entry.relativePath.replace(/\\/g, "/");
|
|
60
|
+
if (!isAdrPath(rel) || !rel.toLowerCase().endsWith(".md"))
|
|
61
|
+
continue;
|
|
62
|
+
const text = await readTextFile(path.join(root, rel), maxFileSizeBytes);
|
|
63
|
+
if (!text)
|
|
64
|
+
continue;
|
|
65
|
+
const parsed = parseAdr(root, rel, text);
|
|
66
|
+
if (parsed)
|
|
67
|
+
decisions.push(parsed);
|
|
68
|
+
}
|
|
69
|
+
decisions.sort((a, b) => a.id.localeCompare(b.id));
|
|
70
|
+
return decisions;
|
|
71
|
+
}
|
|
72
|
+
export async function appendDecisionLedgerEntry(rootInput, record) {
|
|
73
|
+
const root = resolveRepoRoot(rootInput);
|
|
74
|
+
const dir = path.dirname(ledgerFile(root));
|
|
75
|
+
await fs.mkdir(dir, { recursive: true });
|
|
76
|
+
const full = {
|
|
77
|
+
...record,
|
|
78
|
+
source: record.source ?? "ledger",
|
|
79
|
+
sourceKind: record.sourceKind ?? "decision_ledger",
|
|
80
|
+
truth: record.truth ?? "VERIFIED",
|
|
81
|
+
truthMeaning: record.truthMeaning ?? verifiedDecisionTruthMeaning(),
|
|
82
|
+
projectRoot: root,
|
|
83
|
+
ownership: record.ownership ?? "project_owned",
|
|
84
|
+
recordedAt: record.recordedAt ?? new Date().toISOString(),
|
|
85
|
+
};
|
|
86
|
+
await fs.appendFile(ledgerFile(root), `${JSON.stringify(full)}\n`, "utf8");
|
|
87
|
+
return full;
|
|
88
|
+
}
|
|
89
|
+
export async function loadDecisionLedger(rootInput) {
|
|
90
|
+
const root = resolveRepoRoot(rootInput);
|
|
91
|
+
const limitations = [
|
|
92
|
+
"ADR parsing reads markdown titles and Status lines only.",
|
|
93
|
+
"Only project-owned trees are scanned; nested repositories and private/validation checkouts are excluded.",
|
|
94
|
+
"VERIFIED means the ADR file was found and parsed — not that the architecture decision is independently proven correct.",
|
|
95
|
+
"Ledger JSONL entries are merged with discovered ADR files (deduped by id) when present and project-owned.",
|
|
96
|
+
];
|
|
97
|
+
const fromAdr = await parseAdrDecisions(root);
|
|
98
|
+
const fromLedger = [];
|
|
99
|
+
const file = ledgerFile(root);
|
|
100
|
+
if (await pathExists(file)) {
|
|
101
|
+
const text = await fs.readFile(file, "utf8");
|
|
102
|
+
for (const line of text.split(/\r?\n/)) {
|
|
103
|
+
if (!line.trim())
|
|
104
|
+
continue;
|
|
105
|
+
try {
|
|
106
|
+
const raw = JSON.parse(line);
|
|
107
|
+
const evidencePath = raw.evidence?.[0]?.path ?? raw.id;
|
|
108
|
+
if (!isProjectOwnedRelativePath(String(evidencePath)) &&
|
|
109
|
+
!isProjectOwnedRelativePath(String(raw.id))) {
|
|
110
|
+
limitations.push(`Skipped non-owned ledger entry: ${raw.id}`);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
fromLedger.push({
|
|
114
|
+
...raw,
|
|
115
|
+
source: raw.source ?? "ledger",
|
|
116
|
+
sourceKind: raw.sourceKind ?? "decision_ledger",
|
|
117
|
+
truth: raw.truth ?? "VERIFIED",
|
|
118
|
+
truthMeaning: raw.truthMeaning ?? verifiedDecisionTruthMeaning(),
|
|
119
|
+
projectRoot: raw.projectRoot ?? root,
|
|
120
|
+
ownership: raw.ownership ?? classifyRelativePathOwnership(String(evidencePath)),
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
limitations.push("Skipped malformed decision ledger line.");
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const byId = new Map();
|
|
129
|
+
for (const d of [...fromAdr, ...fromLedger]) {
|
|
130
|
+
byId.set(d.id, d);
|
|
131
|
+
}
|
|
132
|
+
return {
|
|
133
|
+
root,
|
|
134
|
+
decisions: [...byId.values()].sort((a, b) => a.title.localeCompare(b.title)),
|
|
135
|
+
ledgerPath: ledgerFile(root),
|
|
136
|
+
limitations,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
export function resolveDecisionPath(rootInput, candidate) {
|
|
140
|
+
return resolveSafeRepoPath(rootInput, candidate);
|
|
141
|
+
}
|
|
@@ -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>;
|