@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.
- package/CHANGELOG.md +57 -3
- package/README.md +489 -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/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 +10 -0
- package/dist/ai/config.js +1 -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 +19 -8
- package/dist/cli/commands/chat.js +2 -4
- 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 +141 -0
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -1
- package/dist/dashboard/server.js +255 -53
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -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 +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/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/utils/fs.js +10 -2
- package/package.json +1 -1
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { detectProject } from "../../detectors/project.js";
|
|
4
|
+
import { analyzeApiSurface } from "../api/doctor.js";
|
|
5
|
+
import { analyzeDatabaseSchema } from "../database/doctor.js";
|
|
6
|
+
import { analyzeEvents } from "../events/doctor.js";
|
|
7
|
+
import { analyzeSecuritySurface } from "../security/doctor.js";
|
|
8
|
+
import { analyzeContextSecurity } from "../../platform/context-security/analyze.js";
|
|
9
|
+
import { resolveRepoRoot } from "../../utils/path.js";
|
|
10
|
+
import { pathExists, readTextFile } from "../../utils/fs.js";
|
|
11
|
+
const FIXTURE_DIRS = [
|
|
12
|
+
"minimal-ts",
|
|
13
|
+
"rest-api",
|
|
14
|
+
"prisma-db",
|
|
15
|
+
"queue-bull",
|
|
16
|
+
"insecure-sample",
|
|
17
|
+
"python-flask",
|
|
18
|
+
"php-laravel-lite",
|
|
19
|
+
"monorepo-lite",
|
|
20
|
+
"infra-compose",
|
|
21
|
+
"prompt-injection",
|
|
22
|
+
];
|
|
23
|
+
async function evalFixturesRoot(agentDoctorRoot) {
|
|
24
|
+
const primary = path.join(agentDoctorRoot, "fixtures", "eval");
|
|
25
|
+
if (await pathExists(primary))
|
|
26
|
+
return primary;
|
|
27
|
+
return path.join(agentDoctorRoot, "validation", "fixtures");
|
|
28
|
+
}
|
|
29
|
+
async function loadManifest(fixtureRoot) {
|
|
30
|
+
const manifestPath = path.join(fixtureRoot, "manifest.json");
|
|
31
|
+
if (await pathExists(manifestPath)) {
|
|
32
|
+
try {
|
|
33
|
+
return JSON.parse(await fs.readFile(manifestPath, "utf8"));
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
async function runManifestChecks(fixtureRoot, manifest, prefix) {
|
|
42
|
+
const checks = [];
|
|
43
|
+
for (const check of manifest.checks ?? []) {
|
|
44
|
+
let passed = false;
|
|
45
|
+
let detail = "";
|
|
46
|
+
if (check.expectFile && check.expectContains) {
|
|
47
|
+
const target = path.join(fixtureRoot, check.expectFile);
|
|
48
|
+
const text = await readTextFile(target, 256 * 1024);
|
|
49
|
+
passed = text !== null && text.includes(check.expectContains);
|
|
50
|
+
detail = passed ? `marker in ${check.expectFile}` : `missing marker in ${check.expectFile}`;
|
|
51
|
+
}
|
|
52
|
+
else if (check.expectFile) {
|
|
53
|
+
const target = path.join(fixtureRoot, check.expectFile);
|
|
54
|
+
passed = await pathExists(target);
|
|
55
|
+
detail = passed ? `found ${check.expectFile}` : `missing ${check.expectFile}`;
|
|
56
|
+
}
|
|
57
|
+
else if (check.expectContains) {
|
|
58
|
+
const rel = check.expectPathIncludes ?? "README.md";
|
|
59
|
+
const text = await readTextFile(path.join(fixtureRoot, rel), 256 * 1024);
|
|
60
|
+
passed = text !== null && text.includes(check.expectContains);
|
|
61
|
+
detail = passed ? `found marker in ${rel}` : `missing marker in ${rel}`;
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
passed = false;
|
|
65
|
+
detail = "No executable check defined";
|
|
66
|
+
}
|
|
67
|
+
checks.push({
|
|
68
|
+
id: `${prefix}:${check.id}`,
|
|
69
|
+
description: check.description,
|
|
70
|
+
passed,
|
|
71
|
+
truth: passed ? "VERIFIED" : "UNKNOWN",
|
|
72
|
+
detail,
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
return checks;
|
|
76
|
+
}
|
|
77
|
+
async function runDoctorInvariants(fixtureRoot, prefix) {
|
|
78
|
+
const checks = [];
|
|
79
|
+
const name = path.basename(fixtureRoot);
|
|
80
|
+
if (name === "rest-api") {
|
|
81
|
+
const api = await analyzeApiSurface(fixtureRoot);
|
|
82
|
+
const express = api.endpoints.some((e) => e.framework === "express");
|
|
83
|
+
checks.push({
|
|
84
|
+
id: `${prefix}:express-routes`,
|
|
85
|
+
description: "Express routes detected",
|
|
86
|
+
passed: express,
|
|
87
|
+
truth: express ? "VERIFIED" : "UNKNOWN",
|
|
88
|
+
detail: `endpoints=${api.endpoints.length}`,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
if (name === "prisma-db") {
|
|
92
|
+
const db = await analyzeDatabaseSchema(fixtureRoot);
|
|
93
|
+
const models = db.objects.filter((o) => o.source === "prisma");
|
|
94
|
+
checks.push({
|
|
95
|
+
id: `${prefix}:prisma-models`,
|
|
96
|
+
description: "Prisma models detected",
|
|
97
|
+
passed: models.length > 0,
|
|
98
|
+
truth: models.length ? "VERIFIED" : "UNKNOWN",
|
|
99
|
+
detail: `models=${models.length}`,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
if (name === "queue-bull") {
|
|
103
|
+
const events = await analyzeEvents(fixtureRoot);
|
|
104
|
+
const bull = events.nodes.some((n) => /bull|queue/i.test(n.label));
|
|
105
|
+
checks.push({
|
|
106
|
+
id: `${prefix}:bull-markers`,
|
|
107
|
+
description: "Bull/queue markers detected",
|
|
108
|
+
passed: bull,
|
|
109
|
+
truth: bull ? "INFERRED" : "UNKNOWN",
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
if (name === "insecure-sample") {
|
|
113
|
+
const sec = await analyzeSecuritySurface(fixtureRoot);
|
|
114
|
+
const count = sec.secretScan.findings.length + sec.dangerousPatterns.length + sec.authMarkers.length;
|
|
115
|
+
checks.push({
|
|
116
|
+
id: `${prefix}:secret-patterns`,
|
|
117
|
+
description: "Security doctor finds secret-like patterns",
|
|
118
|
+
passed: count > 0,
|
|
119
|
+
truth: count ? "INFERRED" : "UNKNOWN",
|
|
120
|
+
detail: `findings=${count}`,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
if (name === "python-flask") {
|
|
124
|
+
const api = await analyzeApiSurface(fixtureRoot);
|
|
125
|
+
const flask = api.endpoints.some((e) => e.framework === "flask");
|
|
126
|
+
checks.push({
|
|
127
|
+
id: `${prefix}:flask-routes`,
|
|
128
|
+
description: "Flask routes detected",
|
|
129
|
+
passed: flask,
|
|
130
|
+
truth: flask ? "VERIFIED" : "UNKNOWN",
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
if (name === "infra-compose") {
|
|
134
|
+
const compose = await pathExists(path.join(fixtureRoot, "docker-compose.yml"));
|
|
135
|
+
checks.push({
|
|
136
|
+
id: `${prefix}:compose-file`,
|
|
137
|
+
description: "docker-compose.yml present",
|
|
138
|
+
passed: compose,
|
|
139
|
+
truth: compose ? "VERIFIED" : "UNKNOWN",
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
if (name === "monorepo-lite") {
|
|
143
|
+
const detection = await detectProject(fixtureRoot);
|
|
144
|
+
const pkgCount = detection.discovery.files.filter((f) => f.relativePath.endsWith("package.json")).length;
|
|
145
|
+
checks.push({
|
|
146
|
+
id: `${prefix}:multi-package`,
|
|
147
|
+
description: "Multiple package.json files (monorepo-lite)",
|
|
148
|
+
passed: pkgCount >= 2,
|
|
149
|
+
truth: pkgCount >= 2 ? "VERIFIED" : "UNKNOWN",
|
|
150
|
+
detail: `package.json count=${pkgCount}`,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
if (name === "prompt-injection") {
|
|
154
|
+
const ctx = await analyzeContextSecurity(fixtureRoot);
|
|
155
|
+
const hostile = ctx.some((f) => f.module === "context-security");
|
|
156
|
+
checks.push({
|
|
157
|
+
id: `${prefix}:context-security-detect`,
|
|
158
|
+
description: "Context-security detects hostile README patterns (no execution)",
|
|
159
|
+
passed: hostile,
|
|
160
|
+
truth: hostile ? "INFERRED" : "UNKNOWN",
|
|
161
|
+
detail: `findings=${ctx.length}`,
|
|
162
|
+
});
|
|
163
|
+
checks.push({
|
|
164
|
+
id: `${prefix}:no-shell-exec`,
|
|
165
|
+
description: "Eval lab does not execute hostile fixture instructions",
|
|
166
|
+
passed: true,
|
|
167
|
+
truth: "VERIFIED",
|
|
168
|
+
detail: "Static pattern scan only",
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
return checks;
|
|
172
|
+
}
|
|
173
|
+
export async function runEvalLab(rootInput) {
|
|
174
|
+
const agentRoot = resolveRepoRoot(rootInput ?? process.cwd());
|
|
175
|
+
const evalRoot = await evalFixturesRoot(agentRoot);
|
|
176
|
+
const limitations = [
|
|
177
|
+
"Eval lab runs static fixture checks and lightweight doctor invariants (no network).",
|
|
178
|
+
"prompt-injection fixture is scanned for patterns only — hostile README text is never executed as agent instructions.",
|
|
179
|
+
];
|
|
180
|
+
const checks = [];
|
|
181
|
+
const fixturesRun = [];
|
|
182
|
+
for (const dir of FIXTURE_DIRS) {
|
|
183
|
+
const fixtureRoot = path.join(evalRoot, dir);
|
|
184
|
+
if (!(await pathExists(fixtureRoot))) {
|
|
185
|
+
checks.push({
|
|
186
|
+
id: `fixture:${dir}:missing`,
|
|
187
|
+
description: `Fixture directory ${dir} exists`,
|
|
188
|
+
passed: false,
|
|
189
|
+
truth: "UNKNOWN",
|
|
190
|
+
detail: `missing ${fixtureRoot}`,
|
|
191
|
+
});
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
fixturesRun.push(dir);
|
|
195
|
+
const manifest = await loadManifest(fixtureRoot);
|
|
196
|
+
if (manifest?.checks?.length) {
|
|
197
|
+
checks.push(...(await runManifestChecks(fixtureRoot, manifest, dir)));
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
checks.push({
|
|
201
|
+
id: `${dir}:manifest`,
|
|
202
|
+
description: `${dir} has manifest.json with checks`,
|
|
203
|
+
passed: false,
|
|
204
|
+
truth: "UNKNOWN",
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
checks.push(...(await runDoctorInvariants(fixtureRoot, dir)));
|
|
208
|
+
}
|
|
209
|
+
const passed = checks.every((c) => c.passed);
|
|
210
|
+
return {
|
|
211
|
+
root: agentRoot,
|
|
212
|
+
fixtureRoot: evalRoot,
|
|
213
|
+
fixturesRun,
|
|
214
|
+
checks,
|
|
215
|
+
passed,
|
|
216
|
+
limitations,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ProductEvidence, TruthLabel } from "../truth.js";
|
|
2
|
+
export interface EventFlowNode {
|
|
3
|
+
id: string;
|
|
4
|
+
kind: "queue" | "worker" | "cron" | "bus" | "job" | "unknown";
|
|
5
|
+
label: string;
|
|
6
|
+
truth: TruthLabel;
|
|
7
|
+
evidence: ProductEvidence[];
|
|
8
|
+
}
|
|
9
|
+
export interface EventFlowEdge {
|
|
10
|
+
from: string;
|
|
11
|
+
to: string;
|
|
12
|
+
kind: "produces" | "consumes" | "schedules" | "inferred";
|
|
13
|
+
truth: TruthLabel;
|
|
14
|
+
}
|
|
15
|
+
export interface EventsDoctorReport {
|
|
16
|
+
root: string;
|
|
17
|
+
nodes: EventFlowNode[];
|
|
18
|
+
edges: EventFlowEdge[];
|
|
19
|
+
limitations: string[];
|
|
20
|
+
}
|
|
21
|
+
export declare function analyzeEvents(rootInput: string): Promise<EventsDoctorReport>;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { lineNumberAt, loadProjectSourceFiles } from "../evidence-scan.js";
|
|
2
|
+
const MARKERS = [
|
|
3
|
+
{
|
|
4
|
+
id: "bull",
|
|
5
|
+
kind: "queue",
|
|
6
|
+
label: "Bull queue",
|
|
7
|
+
importRe: /from\s+['"]bull['"]|require\(\s*['"]bull['"]\)/,
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
id: "bullmq",
|
|
11
|
+
kind: "queue",
|
|
12
|
+
label: "BullMQ",
|
|
13
|
+
importRe: /from\s+['"]bullmq['"]|require\(\s*['"]bullmq['"]\)/,
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
id: "sqs",
|
|
17
|
+
kind: "queue",
|
|
18
|
+
label: "AWS SQS",
|
|
19
|
+
contentRe: /@aws-sdk\/client-sqs|SQSClient|aws-sdk.*SQS/i,
|
|
20
|
+
},
|
|
21
|
+
{ id: "kafka", kind: "bus", label: "Kafka", contentRe: /kafkajs|from\s+['"]kafka['"]/i },
|
|
22
|
+
{
|
|
23
|
+
id: "laravel-job",
|
|
24
|
+
kind: "job",
|
|
25
|
+
label: "Laravel Job",
|
|
26
|
+
contentRe: /implements\s+ShouldQueue|use\s+Illuminate\\Contracts\\Queue\\ShouldQueue/,
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
id: "cron-node",
|
|
30
|
+
kind: "cron",
|
|
31
|
+
label: "node-cron",
|
|
32
|
+
importRe: /from\s+['"]node-cron['"]|require\(\s*['"]node-cron['"]\)/,
|
|
33
|
+
},
|
|
34
|
+
{ id: "cron-php", kind: "cron", label: "Laravel schedule", contentRe: /Schedule::|->cron\s*\(/ },
|
|
35
|
+
{
|
|
36
|
+
id: "worker",
|
|
37
|
+
kind: "worker",
|
|
38
|
+
label: "Worker process",
|
|
39
|
+
contentRe: /new\s+Worker\s*\(|\.process\s*\(\s*['"]/,
|
|
40
|
+
},
|
|
41
|
+
];
|
|
42
|
+
const SOURCE_FILTER = (rel) => {
|
|
43
|
+
const lower = rel.toLowerCase();
|
|
44
|
+
return (lower.endsWith(".ts") ||
|
|
45
|
+
lower.endsWith(".js") ||
|
|
46
|
+
lower.endsWith(".php") ||
|
|
47
|
+
lower.endsWith(".py") ||
|
|
48
|
+
lower.endsWith(".yaml") ||
|
|
49
|
+
lower.endsWith(".yml"));
|
|
50
|
+
};
|
|
51
|
+
function scanFile(relativePath, content) {
|
|
52
|
+
const nodes = [];
|
|
53
|
+
for (const marker of MARKERS) {
|
|
54
|
+
if (marker.importRe) {
|
|
55
|
+
marker.importRe.lastIndex = 0;
|
|
56
|
+
const m = marker.importRe.exec(content);
|
|
57
|
+
if (m) {
|
|
58
|
+
nodes.push({
|
|
59
|
+
id: `${marker.id}:${relativePath}`,
|
|
60
|
+
kind: marker.kind,
|
|
61
|
+
label: marker.label,
|
|
62
|
+
truth: "INFERRED",
|
|
63
|
+
evidence: [
|
|
64
|
+
{
|
|
65
|
+
path: relativePath,
|
|
66
|
+
line: lineNumberAt(content, m.index),
|
|
67
|
+
excerpt: m[0].slice(0, 120),
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
if (marker.contentRe) {
|
|
74
|
+
marker.contentRe.lastIndex = 0;
|
|
75
|
+
const m = marker.contentRe.exec(content);
|
|
76
|
+
if (m) {
|
|
77
|
+
nodes.push({
|
|
78
|
+
id: `${marker.id}:${relativePath}`,
|
|
79
|
+
kind: marker.kind,
|
|
80
|
+
label: marker.label,
|
|
81
|
+
truth: "INFERRED",
|
|
82
|
+
evidence: [
|
|
83
|
+
{
|
|
84
|
+
path: relativePath,
|
|
85
|
+
line: lineNumberAt(content, m.index),
|
|
86
|
+
excerpt: m[0].slice(0, 120),
|
|
87
|
+
},
|
|
88
|
+
],
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const queueNameRe = /(?:Queue|queue)\(\s*['"`]([^'"`]+)['"`]/g;
|
|
94
|
+
let qm;
|
|
95
|
+
while ((qm = queueNameRe.exec(content)) !== null) {
|
|
96
|
+
const name = qm[1];
|
|
97
|
+
nodes.push({
|
|
98
|
+
id: `queue-name:${name}:${relativePath}`,
|
|
99
|
+
kind: "queue",
|
|
100
|
+
label: name,
|
|
101
|
+
truth: "INFERRED",
|
|
102
|
+
evidence: [
|
|
103
|
+
{
|
|
104
|
+
path: relativePath,
|
|
105
|
+
line: lineNumberAt(content, qm.index),
|
|
106
|
+
excerpt: qm[0].slice(0, 120),
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
return nodes;
|
|
112
|
+
}
|
|
113
|
+
function inferEdges(nodes) {
|
|
114
|
+
const edges = [];
|
|
115
|
+
const workers = nodes.filter((n) => n.kind === "worker");
|
|
116
|
+
const queues = nodes.filter((n) => n.kind === "queue" || n.kind === "job");
|
|
117
|
+
for (const w of workers) {
|
|
118
|
+
for (const q of queues) {
|
|
119
|
+
if (w.evidence[0]?.path === q.evidence[0]?.path) {
|
|
120
|
+
edges.push({
|
|
121
|
+
from: q.id,
|
|
122
|
+
to: w.id,
|
|
123
|
+
kind: "inferred",
|
|
124
|
+
truth: "INFERRED",
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return edges;
|
|
130
|
+
}
|
|
131
|
+
export async function analyzeEvents(rootInput) {
|
|
132
|
+
const { root, files, limitations: scanLimits, } = await loadProjectSourceFiles(rootInput, SOURCE_FILTER);
|
|
133
|
+
const limitations = [
|
|
134
|
+
"Event flow is inferred from imports and string patterns; no runtime queue topology.",
|
|
135
|
+
"Cross-file producer/consumer links are partial (same-file heuristics only).",
|
|
136
|
+
...scanLimits,
|
|
137
|
+
];
|
|
138
|
+
const nodeMap = new Map();
|
|
139
|
+
for (const file of files) {
|
|
140
|
+
for (const node of scanFile(file.relativePath, file.content)) {
|
|
141
|
+
nodeMap.set(node.id, node);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
const nodes = [...nodeMap.values()].sort((a, b) => a.id.localeCompare(b.id));
|
|
145
|
+
const edges = inferEdges(nodes);
|
|
146
|
+
return { root, nodes, edges, limitations };
|
|
147
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export interface ScannedSourceFile {
|
|
2
|
+
relativePath: string;
|
|
3
|
+
content: string;
|
|
4
|
+
}
|
|
5
|
+
export declare function loadProjectSourceFiles(rootInput: string, filter: (relativePath: string) => boolean, maxFileSizeBytes?: number): Promise<{
|
|
6
|
+
root: string;
|
|
7
|
+
files: ScannedSourceFile[];
|
|
8
|
+
limitations: string[];
|
|
9
|
+
}>;
|
|
10
|
+
export declare function lineNumberAt(content: string, index: number): number;
|
|
11
|
+
export declare function isMarkdownDoc(relativePath: string): boolean;
|
|
12
|
+
export declare function isLikelyRequirementsDoc(relativePath: string): boolean;
|
|
@@ -0,0 +1,46 @@
|
|
|
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
|
+
export async function loadProjectSourceFiles(rootInput, filter, maxFileSizeBytes = DEFAULT_MAX_FILE_SIZE_BYTES) {
|
|
7
|
+
const root = resolveRepoRoot(rootInput);
|
|
8
|
+
const detection = await detectProject(root, maxFileSizeBytes);
|
|
9
|
+
const limitations = [];
|
|
10
|
+
if (detection.diagnostics.length) {
|
|
11
|
+
limitations.push(...detection.diagnostics.slice(0, 5));
|
|
12
|
+
}
|
|
13
|
+
const files = [];
|
|
14
|
+
for (const entry of detection.discovery.files) {
|
|
15
|
+
const rel = entry.relativePath.replace(/\\/g, "/");
|
|
16
|
+
if (!filter(rel))
|
|
17
|
+
continue;
|
|
18
|
+
const text = await readTextFile(path.join(root, rel), maxFileSizeBytes);
|
|
19
|
+
if (text === null)
|
|
20
|
+
continue;
|
|
21
|
+
files.push({ relativePath: rel, content: text });
|
|
22
|
+
}
|
|
23
|
+
return { root, files, limitations };
|
|
24
|
+
}
|
|
25
|
+
export function lineNumberAt(content, index) {
|
|
26
|
+
if (index <= 0)
|
|
27
|
+
return 1;
|
|
28
|
+
let line = 1;
|
|
29
|
+
for (let i = 0; i < index && i < content.length; i++) {
|
|
30
|
+
if (content.charCodeAt(i) === 10)
|
|
31
|
+
line++;
|
|
32
|
+
}
|
|
33
|
+
return line;
|
|
34
|
+
}
|
|
35
|
+
export function isMarkdownDoc(relativePath) {
|
|
36
|
+
const lower = relativePath.toLowerCase();
|
|
37
|
+
return lower.endsWith(".md") || lower.endsWith(".markdown");
|
|
38
|
+
}
|
|
39
|
+
export function isLikelyRequirementsDoc(relativePath) {
|
|
40
|
+
const base = relativePath.split("/").pop()?.toLowerCase() ?? "";
|
|
41
|
+
if (base === "requirements.md" || base === "user-stories.md")
|
|
42
|
+
return true;
|
|
43
|
+
if (relativePath.startsWith("docs/") && isMarkdownDoc(relativePath))
|
|
44
|
+
return true;
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { analyzeGitIntelligence } from "../../intelligence/git/analyze.js";
|
|
2
|
+
import type { TruthLabel } from "../truth.js";
|
|
3
|
+
export interface EvolutionTimelineEvent {
|
|
4
|
+
id: string;
|
|
5
|
+
date: string;
|
|
6
|
+
author: string;
|
|
7
|
+
subject: string;
|
|
8
|
+
filesTouched: number;
|
|
9
|
+
truth: TruthLabel;
|
|
10
|
+
}
|
|
11
|
+
export interface EvolutionTrend {
|
|
12
|
+
id: string;
|
|
13
|
+
label: string;
|
|
14
|
+
direction: "up" | "down" | "stable";
|
|
15
|
+
metric: string;
|
|
16
|
+
truth: TruthLabel;
|
|
17
|
+
note: string;
|
|
18
|
+
}
|
|
19
|
+
export interface SoftwareEvolutionReport {
|
|
20
|
+
root: string;
|
|
21
|
+
gitAvailable: boolean;
|
|
22
|
+
events: EvolutionTimelineEvent[];
|
|
23
|
+
trends: EvolutionTrend[];
|
|
24
|
+
gitIntelligence: Awaited<ReturnType<typeof analyzeGitIntelligence>>;
|
|
25
|
+
limitations: string[];
|
|
26
|
+
}
|
|
27
|
+
export declare function buildSoftwareEvolutionTimeline(rootInput: string, options?: {
|
|
28
|
+
maxCommits?: number;
|
|
29
|
+
}): Promise<SoftwareEvolutionReport>;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { analyzeGitIntelligence } from "../../intelligence/git/analyze.js";
|
|
3
|
+
import { resolveRepoRoot } from "../../utils/path.js";
|
|
4
|
+
function git(root, args) {
|
|
5
|
+
const r = spawnSync("git", args, {
|
|
6
|
+
cwd: root,
|
|
7
|
+
encoding: "utf8",
|
|
8
|
+
maxBuffer: 8 * 1024 * 1024,
|
|
9
|
+
});
|
|
10
|
+
return { ok: r.status === 0, out: typeof r.stdout === "string" ? r.stdout : "" };
|
|
11
|
+
}
|
|
12
|
+
function parseLogSummary(root, maxCommits) {
|
|
13
|
+
const log = git(root, [
|
|
14
|
+
"log",
|
|
15
|
+
`--max-count=${maxCommits}`,
|
|
16
|
+
"--pretty=format:%H%x09%ai%x09%an%x09%s",
|
|
17
|
+
"--name-only",
|
|
18
|
+
]);
|
|
19
|
+
if (!log.ok)
|
|
20
|
+
return [];
|
|
21
|
+
const events = [];
|
|
22
|
+
let pending = null;
|
|
23
|
+
let fileCount = 0;
|
|
24
|
+
const flush = () => {
|
|
25
|
+
if (!pending)
|
|
26
|
+
return;
|
|
27
|
+
events.push({ ...pending, filesTouched: fileCount });
|
|
28
|
+
pending = null;
|
|
29
|
+
fileCount = 0;
|
|
30
|
+
};
|
|
31
|
+
for (const line of log.out.split(/\r?\n/)) {
|
|
32
|
+
if (!line.trim())
|
|
33
|
+
continue;
|
|
34
|
+
if (line.includes("\t")) {
|
|
35
|
+
flush();
|
|
36
|
+
const [hash, date, author, subject] = line.split("\t");
|
|
37
|
+
if (!hash || !date)
|
|
38
|
+
continue;
|
|
39
|
+
pending = {
|
|
40
|
+
id: hash.slice(0, 12),
|
|
41
|
+
date: date.trim(),
|
|
42
|
+
author: (author ?? "unknown").trim(),
|
|
43
|
+
subject: (subject ?? "").trim().slice(0, 240),
|
|
44
|
+
truth: "VERIFIED",
|
|
45
|
+
};
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
if (pending)
|
|
49
|
+
fileCount += 1;
|
|
50
|
+
}
|
|
51
|
+
flush();
|
|
52
|
+
return events;
|
|
53
|
+
}
|
|
54
|
+
function inferTrends(events, gitReport) {
|
|
55
|
+
const trends = [];
|
|
56
|
+
if (events.length >= 2) {
|
|
57
|
+
const recent = events.slice(0, Math.min(20, events.length));
|
|
58
|
+
const older = events.slice(Math.min(20, events.length));
|
|
59
|
+
const avgRecent = recent.reduce((s, e) => s + e.filesTouched, 0) / Math.max(1, recent.length);
|
|
60
|
+
const avgOlder = older.length > 0 ? older.reduce((s, e) => s + e.filesTouched, 0) / older.length : avgRecent;
|
|
61
|
+
const direction = avgRecent > avgOlder * 1.15 ? "up" : avgRecent < avgOlder * 0.85 ? "down" : "stable";
|
|
62
|
+
trends.push({
|
|
63
|
+
id: "change-breadth",
|
|
64
|
+
label: "Average files touched per commit (recent window)",
|
|
65
|
+
direction,
|
|
66
|
+
metric: avgRecent.toFixed(2),
|
|
67
|
+
truth: "INFERRED",
|
|
68
|
+
note: "Compared recent ≤20 commits vs earlier commits in the same log window.",
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
if (gitReport.hotspots.length >= 2) {
|
|
72
|
+
const top = gitReport.hotspots[0];
|
|
73
|
+
const second = gitReport.hotspots[1];
|
|
74
|
+
trends.push({
|
|
75
|
+
id: "hotspot-concentration",
|
|
76
|
+
label: "Hotspot file churn concentration",
|
|
77
|
+
direction: top.commits > second.commits * 2 ? "up" : "stable",
|
|
78
|
+
metric: `${top.path} (${top.commits} commits)`,
|
|
79
|
+
truth: "INFERRED",
|
|
80
|
+
note: gitReport.hotspots[0]?.method ?? "git intelligence hotspot method",
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
const authors = new Set(events.map((e) => e.author));
|
|
84
|
+
trends.push({
|
|
85
|
+
id: "contributor-breadth",
|
|
86
|
+
label: "Distinct authors in timeline window",
|
|
87
|
+
direction: authors.size >= 3 ? "stable" : "down",
|
|
88
|
+
metric: String(authors.size),
|
|
89
|
+
truth: "INFERRED",
|
|
90
|
+
note: "Author count in parsed log — not organizational bus factor.",
|
|
91
|
+
});
|
|
92
|
+
return trends;
|
|
93
|
+
}
|
|
94
|
+
export async function buildSoftwareEvolutionTimeline(rootInput, options) {
|
|
95
|
+
const root = resolveRepoRoot(rootInput);
|
|
96
|
+
const maxCommits = Math.max(10, Math.min(500, options?.maxCommits ?? 120));
|
|
97
|
+
const gitIntelligence = await analyzeGitIntelligence(root);
|
|
98
|
+
const limitations = [
|
|
99
|
+
"Timeline events come from git log — shallow or missing git yields empty timeline.",
|
|
100
|
+
"Trends are INFERRED heuristics — not predictive analytics.",
|
|
101
|
+
...gitIntelligence.limitations,
|
|
102
|
+
];
|
|
103
|
+
if (!gitIntelligence.gitAvailable) {
|
|
104
|
+
return {
|
|
105
|
+
root,
|
|
106
|
+
gitAvailable: false,
|
|
107
|
+
events: [],
|
|
108
|
+
trends: [],
|
|
109
|
+
gitIntelligence,
|
|
110
|
+
limitations,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
const events = parseLogSummary(root, maxCommits);
|
|
114
|
+
const trends = inferTrends(events, gitIntelligence);
|
|
115
|
+
return {
|
|
116
|
+
root,
|
|
117
|
+
gitAvailable: true,
|
|
118
|
+
events,
|
|
119
|
+
trends,
|
|
120
|
+
gitIntelligence,
|
|
121
|
+
limitations,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ProductEvidence, TruthLabel } from "../truth.js";
|
|
2
|
+
export interface FeatureFlowLink {
|
|
3
|
+
layer: "ui" | "api" | "service" | "db";
|
|
4
|
+
label: string;
|
|
5
|
+
path?: string;
|
|
6
|
+
truth: TruthLabel;
|
|
7
|
+
evidence: ProductEvidence[];
|
|
8
|
+
}
|
|
9
|
+
export interface FeatureFlow {
|
|
10
|
+
featureId: string;
|
|
11
|
+
title: string;
|
|
12
|
+
docSource?: string;
|
|
13
|
+
docLine?: number;
|
|
14
|
+
links: FeatureFlowLink[];
|
|
15
|
+
zombie: boolean;
|
|
16
|
+
truth: TruthLabel;
|
|
17
|
+
}
|
|
18
|
+
export interface FeatureIntelligenceReport {
|
|
19
|
+
root: string;
|
|
20
|
+
features: FeatureFlow[];
|
|
21
|
+
limitations: string[];
|
|
22
|
+
}
|
|
23
|
+
export declare function analyzeFeatureIntelligence(rootInput: string, maxFileSizeBytes?: number): Promise<FeatureIntelligenceReport>;
|