@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.
- package/CHANGELOG.md +123 -1
- package/README.md +510 -260
- package/dist/agent/approvals.d.ts +24 -0
- package/dist/agent/approvals.js +64 -0
- package/dist/agent/chat/deterministic.d.ts +6 -0
- package/dist/agent/chat/deterministic.js +180 -0
- package/dist/agent/chat/index.d.ts +7 -0
- package/dist/agent/chat/index.js +5 -0
- package/dist/agent/chat/memory.d.ts +44 -0
- package/dist/agent/chat/memory.js +103 -0
- package/dist/agent/chat/project-summary.d.ts +16 -0
- package/dist/agent/chat/project-summary.js +70 -0
- package/dist/agent/chat/prompts.d.ts +6 -0
- package/dist/agent/chat/prompts.js +39 -0
- package/dist/agent/chat/response.d.ts +19 -0
- package/dist/agent/chat/response.js +109 -0
- package/dist/agent/chat/service.d.ts +42 -0
- package/dist/agent/chat/service.js +246 -0
- package/dist/agent/chat/types.d.ts +48 -0
- package/dist/agent/chat/types.js +1 -0
- package/dist/agent/context/retrieve.d.ts +21 -0
- package/dist/agent/context/retrieve.js +117 -0
- package/dist/agent/context/truth.d.ts +6 -0
- package/dist/agent/context/truth.js +19 -0
- package/dist/agent/context/types.d.ts +23 -0
- package/dist/agent/context/types.js +4 -0
- package/dist/agent/index.d.ts +28 -0
- package/dist/agent/index.js +15 -0
- package/dist/agent/loop.d.ts +53 -0
- package/dist/agent/loop.js +253 -0
- package/dist/agent/modes.d.ts +18 -0
- package/dist/agent/modes.js +102 -0
- package/dist/agent/plan.d.ts +30 -0
- package/dist/agent/plan.js +121 -0
- package/dist/agent/roles.d.ts +12 -0
- package/dist/agent/roles.js +138 -0
- package/dist/agent/runtime.d.ts +83 -0
- package/dist/agent/runtime.js +293 -0
- package/dist/agent/state.d.ts +30 -0
- package/dist/agent/state.js +95 -0
- package/dist/agent/student.d.ts +56 -0
- package/dist/agent/student.js +230 -0
- package/dist/agent/tools/execute.d.ts +20 -0
- package/dist/agent/tools/execute.js +365 -0
- package/dist/agent/tools/index.d.ts +6 -0
- package/dist/agent/tools/index.js +5 -0
- package/dist/agent/tools/registry.d.ts +7 -0
- package/dist/agent/tools/registry.js +212 -0
- package/dist/agent/tools/run.d.ts +24 -0
- package/dist/agent/tools/run.js +44 -0
- package/dist/agent/tools/types.d.ts +32 -0
- package/dist/agent/tools/types.js +10 -0
- package/dist/agent/tools/write.d.ts +24 -0
- package/dist/agent/tools/write.js +121 -0
- package/dist/agent/verify.d.ts +29 -0
- package/dist/agent/verify.js +210 -0
- package/dist/ai/config.d.ts +22 -0
- package/dist/ai/config.js +69 -0
- package/dist/ai/index.d.ts +17 -0
- package/dist/ai/index.js +52 -0
- package/dist/ai/providers/mock.d.ts +20 -0
- package/dist/ai/providers/mock.js +84 -0
- package/dist/ai/providers/none.d.ts +6 -0
- package/dist/ai/providers/none.js +23 -0
- package/dist/ai/providers/openai-compatible.d.ts +21 -0
- package/dist/ai/providers/openai-compatible.js +151 -0
- package/dist/ai/redact.d.ts +9 -0
- package/dist/ai/redact.js +61 -0
- package/dist/ai/types.d.ts +71 -0
- package/dist/ai/types.js +6 -0
- package/dist/cli/commands/agent.d.ts +29 -0
- package/dist/cli/commands/agent.js +164 -0
- package/dist/cli/commands/chat.d.ts +14 -0
- package/dist/cli/commands/chat.js +153 -0
- package/dist/cli/commands/learn.d.ts +12 -0
- package/dist/cli/commands/learn.js +107 -0
- package/dist/cli/commands/product.d.ts +19 -0
- package/dist/cli/commands/product.js +147 -0
- package/dist/cli/commands/start.d.ts +15 -0
- package/dist/cli/commands/start.js +80 -0
- package/dist/cli/program.js +233 -0
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -1
- package/dist/dashboard/server.d.ts +6 -0
- package/dist/dashboard/server.js +333 -43
- package/dist/enforcement/runner.d.ts +5 -0
- package/dist/enforcement/runner.js +51 -4
- package/dist/index.d.ts +17 -0
- package/dist/index.js +10 -0
- package/dist/intelligence/graph/build.js +36 -8
- 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 +91 -26
- 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/php.d.ts +1 -0
- package/dist/languages/php.js +11 -2
- package/dist/languages/python.d.ts +6 -2
- package/dist/languages/python.js +39 -9
- 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 +13 -0
- package/dist/mcp/agent/registry.js +320 -0
- package/dist/mcp/agentdoctor/server.js +8 -1
- package/dist/mcp/intelligence/handlers.d.ts +3 -0
- package/dist/mcp/intelligence/handlers.js +28 -0
- package/dist/mcp/intelligence/registry.d.ts +1 -1
- package/dist/mcp/intelligence/registry.js +30 -1
- 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 +24 -0
- package/dist/product/decisions/ledger.js +110 -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 +42 -0
- package/dist/product/discovery/roots.js +215 -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 +193 -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 +69 -0
- package/dist/product/index.js +35 -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 +95 -0
- package/dist/product/memory/institutional.d.ts +16 -0
- package/dist/product/memory/institutional.js +87 -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 +100 -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 +70 -0
- package/dist/product/whatif/engine.d.ts +28 -0
- package/dist/product/whatif/engine.js +70 -0
- package/dist/security/paths.js +43 -15
- package/dist/utils/fs.js +10 -2
- package/package.json +1 -1
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { createHash } from "node:crypto";
|
|
4
|
+
import { getAdapterForFile, parseSourceFile } from "../../languages/index.js";
|
|
5
|
+
import { readTextFile } from "../../utils/fs.js";
|
|
6
|
+
import { resolveRepoRoot, toPosixRelative } from "../../utils/path.js";
|
|
7
|
+
function nodeId(kind, key) {
|
|
8
|
+
return `${kind}:${createHash("sha1").update(key).digest("hex").slice(0, 12)}`;
|
|
9
|
+
}
|
|
10
|
+
const LANG_FILE_RE = /\.(py|php|go|java|kt|kts|rs|dart)$/i;
|
|
11
|
+
async function listLanguageSourceFiles(root, limit = 120) {
|
|
12
|
+
const out = [];
|
|
13
|
+
const skip = new Set(["node_modules", ".git", "dist", "coverage", ".agentdoctor", "vendor"]);
|
|
14
|
+
async function walk(dir) {
|
|
15
|
+
if (out.length >= limit)
|
|
16
|
+
return;
|
|
17
|
+
let entries;
|
|
18
|
+
try {
|
|
19
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
for (const e of entries) {
|
|
25
|
+
if (out.length >= limit)
|
|
26
|
+
return;
|
|
27
|
+
if (skip.has(e.name))
|
|
28
|
+
continue;
|
|
29
|
+
const abs = path.join(dir, e.name);
|
|
30
|
+
if (e.isSymbolicLink())
|
|
31
|
+
continue;
|
|
32
|
+
if (e.isDirectory()) {
|
|
33
|
+
await walk(abs);
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
36
|
+
if (LANG_FILE_RE.test(e.name)) {
|
|
37
|
+
out.push(abs);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
await walk(root);
|
|
42
|
+
return out.sort();
|
|
43
|
+
}
|
|
44
|
+
function evidenceFromAdapter(kind) {
|
|
45
|
+
if (kind === "ast")
|
|
46
|
+
return "verified";
|
|
47
|
+
return "unknown";
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Walk a bounded set of Python/PHP/Go sources and merge adapter symbols/imports/calls into the graph.
|
|
51
|
+
*/
|
|
52
|
+
export async function enrichGraphWithLanguageAdapters(graph, rootInput, options) {
|
|
53
|
+
const root = resolveRepoRoot(rootInput);
|
|
54
|
+
const maxFiles = options?.maxFiles ?? 120;
|
|
55
|
+
const limitations = [
|
|
56
|
+
"Language enrichment covers .py/.php/.go/.java/.kt/.kts/.rs/.dart, capped file budget, skips unsupported adapters.",
|
|
57
|
+
"Symbol/call edges from language adapters may be partial — evidence follows adapter (ast→verified, else unknown).",
|
|
58
|
+
];
|
|
59
|
+
const existingNodeIds = new Set(graph.nodes.map((n) => n.id));
|
|
60
|
+
const existingEdgeIds = new Set(graph.edges.map((e) => e.id));
|
|
61
|
+
const nodes = [...graph.nodes];
|
|
62
|
+
const edges = [...graph.edges];
|
|
63
|
+
const pushNode = (n) => {
|
|
64
|
+
if (existingNodeIds.has(n.id))
|
|
65
|
+
return;
|
|
66
|
+
existingNodeIds.add(n.id);
|
|
67
|
+
nodes.push(n);
|
|
68
|
+
};
|
|
69
|
+
const pushEdge = (e) => {
|
|
70
|
+
if (existingEdgeIds.has(e.id))
|
|
71
|
+
return;
|
|
72
|
+
existingEdgeIds.add(e.id);
|
|
73
|
+
edges.push(e);
|
|
74
|
+
};
|
|
75
|
+
const nodesBefore = nodes.length;
|
|
76
|
+
const edgesBefore = edges.length;
|
|
77
|
+
const byLanguage = {};
|
|
78
|
+
let filesAttempted = 0;
|
|
79
|
+
let filesParsed = 0;
|
|
80
|
+
let filesSkippedUnsupported = 0;
|
|
81
|
+
const files = await listLanguageSourceFiles(root, maxFiles);
|
|
82
|
+
if (files.length >= maxFiles) {
|
|
83
|
+
limitations.push(`Language file walk capped at ${maxFiles} files`);
|
|
84
|
+
}
|
|
85
|
+
for (const absolute of files) {
|
|
86
|
+
filesAttempted += 1;
|
|
87
|
+
const rel = toPosixRelative(root, absolute);
|
|
88
|
+
const adapter = getAdapterForFile(rel);
|
|
89
|
+
if (!adapter) {
|
|
90
|
+
filesSkippedUnsupported += 1;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const caps = adapter.capabilities();
|
|
94
|
+
if (caps.parse !== "supported") {
|
|
95
|
+
filesSkippedUnsupported += 1;
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
const source = await readTextFile(absolute, 512 * 1024);
|
|
99
|
+
if (source === null)
|
|
100
|
+
continue;
|
|
101
|
+
const parsed = await parseSourceFile(rel, source);
|
|
102
|
+
if (!parsed.ok) {
|
|
103
|
+
limitations.push(`Parse failed for ${rel}: ${parsed.diagnostics[0] ?? "unknown"}`);
|
|
104
|
+
continue;
|
|
105
|
+
}
|
|
106
|
+
filesParsed += 1;
|
|
107
|
+
byLanguage[parsed.language] = (byLanguage[parsed.language] ?? 0) + 1;
|
|
108
|
+
const fileId = nodeId("file", rel);
|
|
109
|
+
pushNode({ id: fileId, kind: "file", label: path.basename(rel), path: rel });
|
|
110
|
+
for (const sym of parsed.symbols) {
|
|
111
|
+
if (sym.evidence === "unsupported")
|
|
112
|
+
continue;
|
|
113
|
+
const symId = nodeId("function", `${rel}:${sym.name}:${sym.kind}`);
|
|
114
|
+
pushNode({
|
|
115
|
+
id: symId,
|
|
116
|
+
kind: sym.kind === "class" ? "class" : "function",
|
|
117
|
+
label: sym.name,
|
|
118
|
+
path: rel,
|
|
119
|
+
meta: {
|
|
120
|
+
parser: `language-${parsed.language}`,
|
|
121
|
+
symbolKind: sym.kind,
|
|
122
|
+
},
|
|
123
|
+
});
|
|
124
|
+
pushEdge({
|
|
125
|
+
id: nodeId("edge", `${fileId}->${symId}`),
|
|
126
|
+
from: fileId,
|
|
127
|
+
to: symId,
|
|
128
|
+
kind: "defines",
|
|
129
|
+
evidence: evidenceFromAdapter(sym.evidence),
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
for (const imp of parsed.imports) {
|
|
133
|
+
if (imp.evidence === "unsupported")
|
|
134
|
+
continue;
|
|
135
|
+
const depId = nodeId("dependency", `${rel}->${imp.specifier}`);
|
|
136
|
+
pushNode({
|
|
137
|
+
id: depId,
|
|
138
|
+
kind: "dependency",
|
|
139
|
+
label: imp.specifier,
|
|
140
|
+
path: rel,
|
|
141
|
+
meta: { parser: `language-${parsed.language}`, specifier: imp.specifier },
|
|
142
|
+
});
|
|
143
|
+
pushEdge({
|
|
144
|
+
id: nodeId("edge", `${fileId}->${depId}`),
|
|
145
|
+
from: fileId,
|
|
146
|
+
to: depId,
|
|
147
|
+
kind: "imports",
|
|
148
|
+
evidence: evidenceFromAdapter(imp.evidence),
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
for (const call of parsed.calls) {
|
|
152
|
+
if (call.evidence === "unsupported")
|
|
153
|
+
continue;
|
|
154
|
+
const callId = nodeId("function", `call:${rel}:${call.callee}`);
|
|
155
|
+
pushNode({
|
|
156
|
+
id: callId,
|
|
157
|
+
kind: "function",
|
|
158
|
+
label: call.callee,
|
|
159
|
+
path: rel,
|
|
160
|
+
meta: { callSite: true, parser: `language-${parsed.language}` },
|
|
161
|
+
});
|
|
162
|
+
pushEdge({
|
|
163
|
+
id: nodeId("edge", `${fileId}->call:${call.callee}:${call.callee}`),
|
|
164
|
+
from: fileId,
|
|
165
|
+
to: callId,
|
|
166
|
+
kind: "calls",
|
|
167
|
+
evidence: "inferred",
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
nodes.sort((a, b) => a.id.localeCompare(b.id));
|
|
172
|
+
edges.sort((a, b) => a.id.localeCompare(b.id));
|
|
173
|
+
if (filesParsed === 0 && filesAttempted > 0 && filesSkippedUnsupported === filesAttempted) {
|
|
174
|
+
limitations.push("No supported language adapters available for discovered multi-language source files.");
|
|
175
|
+
}
|
|
176
|
+
return {
|
|
177
|
+
graph: {
|
|
178
|
+
...graph,
|
|
179
|
+
nodes,
|
|
180
|
+
edges,
|
|
181
|
+
limitations: [...graph.limitations, ...limitations],
|
|
182
|
+
},
|
|
183
|
+
stats: {
|
|
184
|
+
filesAttempted,
|
|
185
|
+
filesParsed,
|
|
186
|
+
filesSkippedUnsupported,
|
|
187
|
+
nodesAdded: nodes.length - nodesBefore,
|
|
188
|
+
edgesAdded: edges.length - edgesBefore,
|
|
189
|
+
byLanguage,
|
|
190
|
+
limitations,
|
|
191
|
+
},
|
|
192
|
+
};
|
|
193
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { GitIntelligenceReport } from "../../intelligence/git/analyze.js";
|
|
2
|
+
import type { ArchitectureCheckResult } from "../../architecture/contract.js";
|
|
3
|
+
import type { ProductEvidence, TruthLabel } from "../truth.js";
|
|
4
|
+
export interface CodeHealthIndicator {
|
|
5
|
+
id: string;
|
|
6
|
+
label: string;
|
|
7
|
+
value: number | string;
|
|
8
|
+
truth: TruthLabel;
|
|
9
|
+
evidence: ProductEvidence[];
|
|
10
|
+
}
|
|
11
|
+
export interface CodeHealthReport {
|
|
12
|
+
root: string;
|
|
13
|
+
indicators: CodeHealthIndicator[];
|
|
14
|
+
limitations: string[];
|
|
15
|
+
}
|
|
16
|
+
export declare function analyzeCodeHealth(rootInput: string, options?: {
|
|
17
|
+
gitReport?: GitIntelligenceReport;
|
|
18
|
+
architectureCheck?: ArchitectureCheckResult;
|
|
19
|
+
maxFileSizeBytes?: number;
|
|
20
|
+
}): Promise<CodeHealthReport>;
|
|
@@ -0,0 +1,149 @@
|
|
|
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
|
+
function isTestPath(relativePath) {
|
|
7
|
+
const p = relativePath.toLowerCase();
|
|
8
|
+
return (p.includes(".test.") ||
|
|
9
|
+
p.includes(".spec.") ||
|
|
10
|
+
p.startsWith("tests/") ||
|
|
11
|
+
p.startsWith("test/") ||
|
|
12
|
+
p.includes("__tests__/"));
|
|
13
|
+
}
|
|
14
|
+
function isSourcePath(relativePath) {
|
|
15
|
+
const p = relativePath.toLowerCase();
|
|
16
|
+
if (isTestPath(p))
|
|
17
|
+
return false;
|
|
18
|
+
return (p.endsWith(".ts") ||
|
|
19
|
+
p.endsWith(".tsx") ||
|
|
20
|
+
p.endsWith(".js") ||
|
|
21
|
+
p.endsWith(".jsx") ||
|
|
22
|
+
p.endsWith(".py") ||
|
|
23
|
+
p.endsWith(".php") ||
|
|
24
|
+
p.endsWith(".go") ||
|
|
25
|
+
p.endsWith(".rs"));
|
|
26
|
+
}
|
|
27
|
+
async function countTodoFixme(root, files, maxBytes) {
|
|
28
|
+
let todo = 0;
|
|
29
|
+
let fixme = 0;
|
|
30
|
+
const evidence = [];
|
|
31
|
+
const re = /\b(TODO|FIXME)\b/g;
|
|
32
|
+
for (const f of files) {
|
|
33
|
+
const text = await readTextFile(path.join(root, f.relativePath), maxBytes);
|
|
34
|
+
if (!text)
|
|
35
|
+
continue;
|
|
36
|
+
re.lastIndex = 0;
|
|
37
|
+
let m;
|
|
38
|
+
while ((m = re.exec(text)) !== null) {
|
|
39
|
+
if (m[1] === "TODO")
|
|
40
|
+
todo++;
|
|
41
|
+
else
|
|
42
|
+
fixme++;
|
|
43
|
+
if (evidence.length < 8) {
|
|
44
|
+
const line = text.slice(0, m.index).split("\n").length;
|
|
45
|
+
const excerpt = text.split(/\r?\n/)[line - 1]?.trim().slice(0, 100);
|
|
46
|
+
evidence.push({
|
|
47
|
+
path: f.relativePath.replace(/\\/g, "/"),
|
|
48
|
+
line,
|
|
49
|
+
...(excerpt !== undefined ? { excerpt } : {}),
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return { todo, fixme, evidence };
|
|
55
|
+
}
|
|
56
|
+
export async function analyzeCodeHealth(rootInput, options) {
|
|
57
|
+
const maxFileSizeBytes = options?.maxFileSizeBytes ?? DEFAULT_MAX_FILE_SIZE_BYTES;
|
|
58
|
+
const root = resolveRepoRoot(rootInput);
|
|
59
|
+
const detection = await detectProject(root, maxFileSizeBytes);
|
|
60
|
+
const paths = detection.discovery.files.map((f) => f.relativePath.replace(/\\/g, "/"));
|
|
61
|
+
const limitations = [
|
|
62
|
+
"Scores are descriptive indicators with evidence; not a single authoritative debt score.",
|
|
63
|
+
"Hotspot overlap requires git intelligence input.",
|
|
64
|
+
];
|
|
65
|
+
const sourceFiles = paths.filter(isSourcePath);
|
|
66
|
+
const testFiles = paths.filter(isTestPath);
|
|
67
|
+
const { todo, fixme, evidence: todoEvidence, } = await countTodoFixme(root, detection.discovery.files, maxFileSizeBytes);
|
|
68
|
+
const indicators = [
|
|
69
|
+
{
|
|
70
|
+
id: "file-count",
|
|
71
|
+
label: "Discovered files",
|
|
72
|
+
value: paths.length,
|
|
73
|
+
truth: "VERIFIED",
|
|
74
|
+
evidence: [{ path: ".", excerpt: `discovery.files.length=${paths.length}` }],
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
id: "source-file-count",
|
|
78
|
+
label: "Source-like files",
|
|
79
|
+
value: sourceFiles.length,
|
|
80
|
+
truth: "INFERRED",
|
|
81
|
+
evidence: [{ path: ".", excerpt: `extension/heuristic count=${sourceFiles.length}` }],
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
id: "test-file-count",
|
|
85
|
+
label: "Test-like files",
|
|
86
|
+
value: testFiles.length,
|
|
87
|
+
truth: "INFERRED",
|
|
88
|
+
evidence: [{ path: ".", excerpt: `test path heuristic count=${testFiles.length}` }],
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
id: "test-ratio",
|
|
92
|
+
label: "Test file ratio (test / (test+source))",
|
|
93
|
+
value: sourceFiles.length + testFiles.length === 0
|
|
94
|
+
? "n/a"
|
|
95
|
+
: (testFiles.length / (sourceFiles.length + testFiles.length)).toFixed(3),
|
|
96
|
+
truth: "INFERRED",
|
|
97
|
+
evidence: [
|
|
98
|
+
{
|
|
99
|
+
path: ".",
|
|
100
|
+
excerpt: `tests=${testFiles.length}, sources=${sourceFiles.length}`,
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
id: "todo-count",
|
|
106
|
+
label: "TODO markers",
|
|
107
|
+
value: todo,
|
|
108
|
+
truth: "VERIFIED",
|
|
109
|
+
evidence: todoEvidence.filter((e) => e.excerpt?.includes("TODO")),
|
|
110
|
+
},
|
|
111
|
+
{
|
|
112
|
+
id: "fixme-count",
|
|
113
|
+
label: "FIXME markers",
|
|
114
|
+
value: fixme,
|
|
115
|
+
truth: "VERIFIED",
|
|
116
|
+
evidence: todoEvidence.filter((e) => e.excerpt?.includes("FIXME")),
|
|
117
|
+
},
|
|
118
|
+
];
|
|
119
|
+
if (options?.architectureCheck) {
|
|
120
|
+
indicators.push({
|
|
121
|
+
id: "architecture-violations",
|
|
122
|
+
label: "Architecture contract violations",
|
|
123
|
+
value: options.architectureCheck.violations.length,
|
|
124
|
+
truth: "VERIFIED",
|
|
125
|
+
evidence: options.architectureCheck.violations.slice(0, 5).map((v) => ({
|
|
126
|
+
path: v.fromPath || v.toPath || ".",
|
|
127
|
+
excerpt: `${v.ruleId}: ${v.description}`.slice(0, 120),
|
|
128
|
+
})),
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
if (options?.gitReport?.gitAvailable) {
|
|
132
|
+
const hotspotPaths = new Set(options.gitReport.hotspots.map((h) => h.path.replace(/\\/g, "/")));
|
|
133
|
+
const overlap = sourceFiles.filter((p) => hotspotPaths.has(p)).length;
|
|
134
|
+
indicators.push({
|
|
135
|
+
id: "hotspot-source-overlap",
|
|
136
|
+
label: "Source files overlapping git hotspots",
|
|
137
|
+
value: overlap,
|
|
138
|
+
truth: "INFERRED",
|
|
139
|
+
evidence: options.gitReport.hotspots.slice(0, 5).map((h) => ({
|
|
140
|
+
path: h.path,
|
|
141
|
+
excerpt: `commits=${h.commits}`,
|
|
142
|
+
})),
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
limitations.push("Git hotspot overlap skipped (git unavailable or not provided).");
|
|
147
|
+
}
|
|
148
|
+
return { root, indicators, limitations };
|
|
149
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
export type { TruthLabel, ProductEvidence } from "./truth.js";
|
|
2
|
+
export { minProductTruth, truthLabelDescription } from "./truth.js";
|
|
3
|
+
export { traceRequirements } from "./requirements/trace.js";
|
|
4
|
+
export type { RequirementTrace, RequirementsTraceReport } from "./requirements/trace.js";
|
|
5
|
+
export { analyzeApiSurface } from "./api/doctor.js";
|
|
6
|
+
export type { ApiDoctorReport, ApiEndpointFinding } from "./api/doctor.js";
|
|
7
|
+
export { analyzeDatabaseSchema } from "./database/doctor.js";
|
|
8
|
+
export type { DatabaseDoctorReport, SchemaObjectFinding, SchemaDriftFinding, SchemaRelationFinding, } from "./database/doctor.js";
|
|
9
|
+
export { analyzeEvents } from "./events/doctor.js";
|
|
10
|
+
export type { EventsDoctorReport, EventFlowNode, EventFlowEdge } from "./events/doctor.js";
|
|
11
|
+
export { analyzeDependencies } from "./deps/analyze.js";
|
|
12
|
+
export type { DependencyAnalysisReport, DirectDependency, DuplicatePackageName, UpgradeImpactHint, } from "./deps/analyze.js";
|
|
13
|
+
export { analyzeCodeHealth } from "./health/code-health.js";
|
|
14
|
+
export type { CodeHealthReport, CodeHealthIndicator } from "./health/code-health.js";
|
|
15
|
+
export { buildSoftwareMap } from "./map/software-map.js";
|
|
16
|
+
export type { SoftwareMap, SoftwareMapNode, SoftwareMapNodeKind } from "./map/software-map.js";
|
|
17
|
+
export { appendChangeLedgerEntry, readChangeLedger, listChangeLedgerEntries, } from "./ledger/change-ledger.js";
|
|
18
|
+
export type { ChangeLedgerEntry, ChangeLedgerListResult } from "./ledger/change-ledger.js";
|
|
19
|
+
export { analyzeWhatIf } from "./whatif/engine.js";
|
|
20
|
+
export type { WhatIfReport, WhatIfAffectedItem } from "./whatif/engine.js";
|
|
21
|
+
export { loadDecisionLedger, appendDecisionLedgerEntry, parseAdrDecisions, resolveDecisionPath, } from "./decisions/ledger.js";
|
|
22
|
+
export type { DecisionRecord, DecisionLedgerResult } from "./decisions/ledger.js";
|
|
23
|
+
export { runForensicAnalysis, assertForensicReadOnly, ForensicWriteRefusedError, } from "./forensic/mode.js";
|
|
24
|
+
export type { ForensicAnalysisReport } from "./forensic/mode.js";
|
|
25
|
+
export { buildSoftwareDigitalTwinSnapshot } from "./twin/digital-twin.js";
|
|
26
|
+
export type { SoftwareDigitalTwin } from "./twin/digital-twin.js";
|
|
27
|
+
export { buildSoftwareDigitalTwin, updateTwin, loadTwinSnapshot, saveTwinSnapshot, invalidateTwinSnapshot, } from "./twin/store.js";
|
|
28
|
+
export type { StoredTwinSnapshot, TwinSnapshotMeta } from "./twin/store.js";
|
|
29
|
+
export { parsePackageLockJson, parseYarnLock, parsePnpmLockYaml, parseAllLockfiles, } from "./deps/lockfiles.js";
|
|
30
|
+
export type { LockfilePackageVersion, ParsedLockfile } from "./deps/lockfiles.js";
|
|
31
|
+
export { discoverOpenApiEndpoints } from "./api/openapi.js";
|
|
32
|
+
export { buildIndex, searchIndex, searchHybrid, documentId } from "./search/index.js";
|
|
33
|
+
export type { TfidfIndex, IndexDocument } from "./search/index.js";
|
|
34
|
+
export { runEvalLab } from "./eval/lab.js";
|
|
35
|
+
export type { EvalLabReport, EvalCheckResult } from "./eval/lab.js";
|
|
36
|
+
export { diagnoseAgentDoctorSelf } from "./self/diagnose.js";
|
|
37
|
+
export type { SelfDiagnoseReport, SelfDiagnoseFinding } from "./self/diagnose.js";
|
|
38
|
+
export { analyzeInfra } from "./ops/infra.js";
|
|
39
|
+
export type { InfraReport, InfraArtifact } from "./ops/infra.js";
|
|
40
|
+
export { buildIncidentHypotheses } from "./ops/incident.js";
|
|
41
|
+
export type { IncidentReport, IncidentTimelineItem } from "./ops/incident.js";
|
|
42
|
+
export { discoverProjectRoots, resolveStartedProjectRoot } from "./discovery/roots.js";
|
|
43
|
+
export type { ProjectDiscoveryReport, ProjectCandidate, DiscoverProjectOptions, } from "./discovery/roots.js";
|
|
44
|
+
export { buildProjectDna, persistProjectDna } from "./dna/build.js";
|
|
45
|
+
export type { ProjectDna } from "./dna/build.js";
|
|
46
|
+
export { enrichGraphWithLanguageAdapters } from "./graph/enrich-languages.js";
|
|
47
|
+
export type { LanguageGraphEnrichmentStats } from "./graph/enrich-languages.js";
|
|
48
|
+
export { analyzeSecuritySurface } from "./security/doctor.js";
|
|
49
|
+
export type { SecurityDoctorReport, SecurityPatternFinding } from "./security/doctor.js";
|
|
50
|
+
export { analyzeTestBrain, persistTestBrainReport, mapTestsFromGraph, } from "./testbrain/analyze.js";
|
|
51
|
+
export type { TestBrainReport, FileTestMapping, TestBrainGraphStats, FileTestMappingEvidence, } from "./testbrain/analyze.js";
|
|
52
|
+
export { analyzePrivacySurface } from "./privacy/doctor.js";
|
|
53
|
+
export type { PrivacyDoctorReport, PrivacyFinding } from "./privacy/doctor.js";
|
|
54
|
+
export { searchSymbolsAndConcepts } from "./search/software-search.js";
|
|
55
|
+
export type { SoftwareSearchReport, SoftwareSearchHit } from "./search/software-search.js";
|
|
56
|
+
export { buildTechnicalDebtRoadmap } from "./techdebt/roadmap.js";
|
|
57
|
+
export type { TechnicalDebtRoadmap, TechnicalDebtItem } from "./techdebt/roadmap.js";
|
|
58
|
+
export { loadOrganizationModel, saveOrganizationModel, linkProjectToOrgModel, resolveLinkedProjectRoot, } from "./org/model.js";
|
|
59
|
+
export type { OrganizationModel, OrgEntityRef, OrgProjectLink } from "./org/model.js";
|
|
60
|
+
export { analyzeFeatureIntelligence } from "./features/intelligence.js";
|
|
61
|
+
export type { FeatureIntelligenceReport, FeatureFlow, FeatureFlowLink, } from "./features/intelligence.js";
|
|
62
|
+
export { buildSoftwareEvolutionTimeline } from "./evolution/timeline.js";
|
|
63
|
+
export type { SoftwareEvolutionReport, EvolutionTimelineEvent, EvolutionTrend, } from "./evolution/timeline.js";
|
|
64
|
+
export { queryMemory } from "./memory/institutional.js";
|
|
65
|
+
export type { InstitutionalMemoryQueryResult, MemoryHit } from "./memory/institutional.js";
|
|
66
|
+
export { evaluateApprovalRecord, formatApprovalRecordSummary } from "./approval/model.js";
|
|
67
|
+
export type { ApprovalRecord, ApprovalState, ApprovalRisk, ApprovalEvaluationInput, ApprovalEvaluationResult, } from "./approval/model.js";
|
|
68
|
+
export { issueApprovalGrant, consumeApprovalGrant, hashPlanPayload, hashFileWritePlan, } from "./approval/session.js";
|
|
69
|
+
export type { ApprovalGrant, ConsumeApprovalResult } from "./approval/session.js";
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export { minProductTruth, truthLabelDescription } from "./truth.js";
|
|
2
|
+
export { traceRequirements } from "./requirements/trace.js";
|
|
3
|
+
export { analyzeApiSurface } from "./api/doctor.js";
|
|
4
|
+
export { analyzeDatabaseSchema } from "./database/doctor.js";
|
|
5
|
+
export { analyzeEvents } from "./events/doctor.js";
|
|
6
|
+
export { analyzeDependencies } from "./deps/analyze.js";
|
|
7
|
+
export { analyzeCodeHealth } from "./health/code-health.js";
|
|
8
|
+
export { buildSoftwareMap } from "./map/software-map.js";
|
|
9
|
+
export { appendChangeLedgerEntry, readChangeLedger, listChangeLedgerEntries, } from "./ledger/change-ledger.js";
|
|
10
|
+
export { analyzeWhatIf } from "./whatif/engine.js";
|
|
11
|
+
export { loadDecisionLedger, appendDecisionLedgerEntry, parseAdrDecisions, resolveDecisionPath, } from "./decisions/ledger.js";
|
|
12
|
+
export { runForensicAnalysis, assertForensicReadOnly, ForensicWriteRefusedError, } from "./forensic/mode.js";
|
|
13
|
+
export { buildSoftwareDigitalTwinSnapshot } from "./twin/digital-twin.js";
|
|
14
|
+
export { buildSoftwareDigitalTwin, updateTwin, loadTwinSnapshot, saveTwinSnapshot, invalidateTwinSnapshot, } from "./twin/store.js";
|
|
15
|
+
export { parsePackageLockJson, parseYarnLock, parsePnpmLockYaml, parseAllLockfiles, } from "./deps/lockfiles.js";
|
|
16
|
+
export { discoverOpenApiEndpoints } from "./api/openapi.js";
|
|
17
|
+
export { buildIndex, searchIndex, searchHybrid, documentId } from "./search/index.js";
|
|
18
|
+
export { runEvalLab } from "./eval/lab.js";
|
|
19
|
+
export { diagnoseAgentDoctorSelf } from "./self/diagnose.js";
|
|
20
|
+
export { analyzeInfra } from "./ops/infra.js";
|
|
21
|
+
export { buildIncidentHypotheses } from "./ops/incident.js";
|
|
22
|
+
export { discoverProjectRoots, resolveStartedProjectRoot } from "./discovery/roots.js";
|
|
23
|
+
export { buildProjectDna, persistProjectDna } from "./dna/build.js";
|
|
24
|
+
export { enrichGraphWithLanguageAdapters } from "./graph/enrich-languages.js";
|
|
25
|
+
export { analyzeSecuritySurface } from "./security/doctor.js";
|
|
26
|
+
export { analyzeTestBrain, persistTestBrainReport, mapTestsFromGraph, } from "./testbrain/analyze.js";
|
|
27
|
+
export { analyzePrivacySurface } from "./privacy/doctor.js";
|
|
28
|
+
export { searchSymbolsAndConcepts } from "./search/software-search.js";
|
|
29
|
+
export { buildTechnicalDebtRoadmap } from "./techdebt/roadmap.js";
|
|
30
|
+
export { loadOrganizationModel, saveOrganizationModel, linkProjectToOrgModel, resolveLinkedProjectRoot, } from "./org/model.js";
|
|
31
|
+
export { analyzeFeatureIntelligence } from "./features/intelligence.js";
|
|
32
|
+
export { buildSoftwareEvolutionTimeline } from "./evolution/timeline.js";
|
|
33
|
+
export { queryMemory } from "./memory/institutional.js";
|
|
34
|
+
export { evaluateApprovalRecord, formatApprovalRecordSummary } from "./approval/model.js";
|
|
35
|
+
export { issueApprovalGrant, consumeApprovalGrant, hashPlanPayload, hashFileWritePlan, } from "./approval/session.js";
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export interface ChangeLedgerEntry {
|
|
2
|
+
id: string;
|
|
3
|
+
recordedAt: string;
|
|
4
|
+
task?: string;
|
|
5
|
+
plan?: string;
|
|
6
|
+
approval?: string;
|
|
7
|
+
files?: string[];
|
|
8
|
+
tests?: string[];
|
|
9
|
+
proofIds?: string[];
|
|
10
|
+
note?: string;
|
|
11
|
+
}
|
|
12
|
+
export interface ChangeLedgerListResult {
|
|
13
|
+
root: string;
|
|
14
|
+
ledgerDir: string;
|
|
15
|
+
entries: ChangeLedgerEntry[];
|
|
16
|
+
limitations: string[];
|
|
17
|
+
}
|
|
18
|
+
export declare function appendChangeLedgerEntry(rootInput: string, entry: Omit<ChangeLedgerEntry, "id" | "recordedAt"> & {
|
|
19
|
+
id?: string;
|
|
20
|
+
recordedAt?: string;
|
|
21
|
+
}): Promise<ChangeLedgerEntry>;
|
|
22
|
+
export declare function readChangeLedger(rootInput: string): Promise<ChangeLedgerListResult>;
|
|
23
|
+
export declare function listChangeLedgerEntries(rootInput: string): Promise<ChangeLedgerEntry[]>;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { safeRelPath } from "../../security/paths.js";
|
|
4
|
+
import { resolveRepoRoot } from "../../utils/path.js";
|
|
5
|
+
import { pathExists } from "../../utils/fs.js";
|
|
6
|
+
function ledgerDirFor(root) {
|
|
7
|
+
return path.join(resolveRepoRoot(root), ".agentdoctor", "change-ledger");
|
|
8
|
+
}
|
|
9
|
+
function ledgerFileFor(root) {
|
|
10
|
+
return path.join(ledgerDirFor(root), "entries.jsonl");
|
|
11
|
+
}
|
|
12
|
+
async function ensureLedgerDir(root) {
|
|
13
|
+
const dir = ledgerDirFor(root);
|
|
14
|
+
await fs.mkdir(dir, { recursive: true });
|
|
15
|
+
return dir;
|
|
16
|
+
}
|
|
17
|
+
function newEntryId() {
|
|
18
|
+
return `cle_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
19
|
+
}
|
|
20
|
+
export async function appendChangeLedgerEntry(rootInput, entry) {
|
|
21
|
+
const root = resolveRepoRoot(rootInput);
|
|
22
|
+
await ensureLedgerDir(root);
|
|
23
|
+
const record = {
|
|
24
|
+
id: entry.id ?? newEntryId(),
|
|
25
|
+
recordedAt: entry.recordedAt ?? new Date().toISOString(),
|
|
26
|
+
...(entry.task !== undefined ? { task: entry.task } : {}),
|
|
27
|
+
...(entry.plan !== undefined ? { plan: entry.plan } : {}),
|
|
28
|
+
...(entry.approval !== undefined ? { approval: entry.approval } : {}),
|
|
29
|
+
...(entry.files !== undefined ? { files: entry.files.map((f) => safeRelPath(root, f)) } : {}),
|
|
30
|
+
...(entry.tests !== undefined ? { tests: entry.tests } : {}),
|
|
31
|
+
...(entry.proofIds !== undefined ? { proofIds: entry.proofIds } : {}),
|
|
32
|
+
...(entry.note !== undefined ? { note: entry.note } : {}),
|
|
33
|
+
};
|
|
34
|
+
const line = `${JSON.stringify(record)}\n`;
|
|
35
|
+
await fs.appendFile(ledgerFileFor(root), line, "utf8");
|
|
36
|
+
return record;
|
|
37
|
+
}
|
|
38
|
+
export async function readChangeLedger(rootInput) {
|
|
39
|
+
const root = resolveRepoRoot(rootInput);
|
|
40
|
+
const ledgerDir = ledgerDirFor(root);
|
|
41
|
+
const limitations = ["Append-only JSONL; entries are not cryptographically signed."];
|
|
42
|
+
const file = ledgerFileFor(root);
|
|
43
|
+
if (!(await pathExists(file))) {
|
|
44
|
+
return { root, ledgerDir, entries: [], limitations };
|
|
45
|
+
}
|
|
46
|
+
const text = await fs.readFile(file, "utf8");
|
|
47
|
+
const entries = [];
|
|
48
|
+
for (const line of text.split(/\r?\n/)) {
|
|
49
|
+
const trimmed = line.trim();
|
|
50
|
+
if (!trimmed)
|
|
51
|
+
continue;
|
|
52
|
+
try {
|
|
53
|
+
entries.push(JSON.parse(trimmed));
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
limitations.push("Skipped malformed JSONL line in change ledger.");
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return { root, ledgerDir, entries, limitations };
|
|
60
|
+
}
|
|
61
|
+
export async function listChangeLedgerEntries(rootInput) {
|
|
62
|
+
const result = await readChangeLedger(rootInput);
|
|
63
|
+
return result.entries;
|
|
64
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { ProductEvidence, TruthLabel } from "../truth.js";
|
|
2
|
+
export type SoftwareMapNodeKind = "root" | "package" | "directory" | "workspace" | "docs" | "tests" | "unknown";
|
|
3
|
+
export interface SoftwareMapNode {
|
|
4
|
+
id: string;
|
|
5
|
+
kind: SoftwareMapNodeKind;
|
|
6
|
+
label: string;
|
|
7
|
+
path?: string;
|
|
8
|
+
truth: TruthLabel;
|
|
9
|
+
evidence: ProductEvidence[];
|
|
10
|
+
children?: SoftwareMapNode[];
|
|
11
|
+
}
|
|
12
|
+
export interface SoftwareMap {
|
|
13
|
+
root: string;
|
|
14
|
+
tree: SoftwareMapNode;
|
|
15
|
+
limitations: string[];
|
|
16
|
+
}
|
|
17
|
+
export declare function buildSoftwareMap(rootInput: string, maxFileSizeBytes?: number): Promise<SoftwareMap>;
|
|
@@ -0,0 +1,95 @@
|
|
|
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 { detectMonorepo } from "../../detectors/monorepo.js";
|
|
6
|
+
import { resolveRepoRoot } from "../../utils/path.js";
|
|
7
|
+
function nodeId(prefix, p) {
|
|
8
|
+
return `${prefix}:${p.replace(/[^\w./-]/g, "_")}`;
|
|
9
|
+
}
|
|
10
|
+
async function topLevelEntries(root) {
|
|
11
|
+
try {
|
|
12
|
+
const entries = await fs.readdir(root, { withFileTypes: true });
|
|
13
|
+
return entries
|
|
14
|
+
.filter((e) => e.isDirectory() && !e.name.startsWith("."))
|
|
15
|
+
.map((e) => e.name)
|
|
16
|
+
.sort();
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export async function buildSoftwareMap(rootInput, maxFileSizeBytes = DEFAULT_MAX_FILE_SIZE_BYTES) {
|
|
23
|
+
const root = resolveRepoRoot(rootInput);
|
|
24
|
+
const detection = await detectProject(root, maxFileSizeBytes);
|
|
25
|
+
const relativePaths = detection.discovery.files.map((f) => f.relativePath.replace(/\\/g, "/"));
|
|
26
|
+
let rootWorkspaces;
|
|
27
|
+
const rootPkgPath = path.join(root, "package.json");
|
|
28
|
+
try {
|
|
29
|
+
const raw = await fs.readFile(rootPkgPath, "utf8");
|
|
30
|
+
rootWorkspaces = JSON.parse(raw).workspaces;
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
rootWorkspaces = undefined;
|
|
34
|
+
}
|
|
35
|
+
const monorepo = detectMonorepo({
|
|
36
|
+
relativePaths,
|
|
37
|
+
packageJsonWorkspaces: rootWorkspaces,
|
|
38
|
+
}).monorepo;
|
|
39
|
+
const limitations = [
|
|
40
|
+
"Map is structural (directories/packages), not a full module dependency graph.",
|
|
41
|
+
`Monorepo signal: ${monorepo}`,
|
|
42
|
+
];
|
|
43
|
+
const packageJsonPaths = relativePaths.filter((p) => p.endsWith("package.json"));
|
|
44
|
+
const packageNodes = packageJsonPaths.map((p) => ({
|
|
45
|
+
id: nodeId("pkg", p),
|
|
46
|
+
kind: p === "package.json" ? "root" : "package",
|
|
47
|
+
label: path.dirname(p) === "." ? "root package" : path.dirname(p),
|
|
48
|
+
path: p,
|
|
49
|
+
truth: "VERIFIED",
|
|
50
|
+
evidence: [{ path: p }],
|
|
51
|
+
}));
|
|
52
|
+
const dirs = await topLevelEntries(root);
|
|
53
|
+
const dirNodes = dirs.map((name) => {
|
|
54
|
+
let kind = "directory";
|
|
55
|
+
if (name === "docs")
|
|
56
|
+
kind = "docs";
|
|
57
|
+
if (name === "tests" || name === "test")
|
|
58
|
+
kind = "tests";
|
|
59
|
+
return {
|
|
60
|
+
id: nodeId("dir", name),
|
|
61
|
+
kind,
|
|
62
|
+
label: name,
|
|
63
|
+
path: name,
|
|
64
|
+
truth: "VERIFIED",
|
|
65
|
+
evidence: [{ path: name, excerpt: "top-level directory listing" }],
|
|
66
|
+
};
|
|
67
|
+
});
|
|
68
|
+
const tree = {
|
|
69
|
+
id: nodeId("root", root),
|
|
70
|
+
kind: "root",
|
|
71
|
+
label: path.basename(root) || root,
|
|
72
|
+
path: ".",
|
|
73
|
+
truth: "VERIFIED",
|
|
74
|
+
evidence: [{ path: ".", excerpt: "detectProject root" }],
|
|
75
|
+
children: [
|
|
76
|
+
{
|
|
77
|
+
id: "packages-section",
|
|
78
|
+
kind: "workspace",
|
|
79
|
+
label: "packages",
|
|
80
|
+
truth: packageNodes.length ? "VERIFIED" : "UNKNOWN",
|
|
81
|
+
evidence: packageNodes.slice(0, 3).map((n) => ({ path: n.path ?? "." })),
|
|
82
|
+
children: packageNodes,
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
id: "directories-section",
|
|
86
|
+
kind: "directory",
|
|
87
|
+
label: "top-level directories",
|
|
88
|
+
truth: dirNodes.length ? "VERIFIED" : "PARTIAL",
|
|
89
|
+
evidence: dirNodes.slice(0, 5).map((n) => ({ path: n.path ?? n.label })),
|
|
90
|
+
children: dirNodes,
|
|
91
|
+
},
|
|
92
|
+
],
|
|
93
|
+
};
|
|
94
|
+
return { root, tree, limitations };
|
|
95
|
+
}
|