@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.
Files changed (167) hide show
  1. package/CHANGELOG.md +86 -3
  2. package/README.md +490 -290
  3. package/dist/agent/chat/deterministic.d.ts +6 -0
  4. package/dist/agent/chat/deterministic.js +180 -0
  5. package/dist/agent/chat/service.js +8 -14
  6. package/dist/agent/context/retrieve.d.ts +2 -1
  7. package/dist/agent/context/retrieve.js +14 -1
  8. package/dist/agent/index.d.ts +2 -0
  9. package/dist/agent/index.js +1 -0
  10. package/dist/agent/loop.d.ts +2 -0
  11. package/dist/agent/loop.js +31 -0
  12. package/dist/agent/roles.d.ts +12 -0
  13. package/dist/agent/roles.js +138 -0
  14. package/dist/agent/runtime.d.ts +21 -5
  15. package/dist/agent/runtime.js +141 -28
  16. package/dist/agent/tools/execute.d.ts +2 -0
  17. package/dist/agent/tools/execute.js +49 -43
  18. package/dist/agent/tools/write.d.ts +1 -1
  19. package/dist/agent/tools/write.js +18 -4
  20. package/dist/ai/config.js +1 -0
  21. package/dist/ai/providers/adversarial-local.d.ts +7 -0
  22. package/dist/ai/providers/adversarial-local.js +146 -0
  23. package/dist/ai/redact.d.ts +1 -0
  24. package/dist/ai/redact.js +47 -7
  25. package/dist/ai/types.d.ts +1 -1
  26. package/dist/cli/commands/agent.d.ts +2 -0
  27. package/dist/cli/commands/agent.js +32 -11
  28. package/dist/cli/commands/architecture.js +7 -2
  29. package/dist/cli/commands/brain.js +7 -2
  30. package/dist/cli/commands/chat.js +15 -7
  31. package/dist/cli/commands/complete.js +13 -3
  32. package/dist/cli/commands/fix.js +6 -0
  33. package/dist/cli/commands/learn.js +7 -2
  34. package/dist/cli/commands/mcp.js +7 -3
  35. package/dist/cli/commands/platform.js +7 -2
  36. package/dist/cli/commands/policy-graph-run.js +7 -0
  37. package/dist/cli/commands/product.d.ts +19 -0
  38. package/dist/cli/commands/product.js +157 -0
  39. package/dist/cli/commands/scan.js +6 -0
  40. package/dist/cli/commands/start.d.ts +15 -0
  41. package/dist/cli/commands/start.js +86 -0
  42. package/dist/cli/commands/v2.js +61 -11
  43. package/dist/cli/commands/verify.js +6 -0
  44. package/dist/cli/program.js +141 -0
  45. package/dist/cli/safe-root.d.ts +17 -0
  46. package/dist/cli/safe-root.js +32 -0
  47. package/dist/constants.d.ts +1 -1
  48. package/dist/constants.js +1 -1
  49. package/dist/core/brain-cli/service.js +5 -2
  50. package/dist/core/monorepo/detect.js +10 -0
  51. package/dist/core/secrets/scan.js +9 -0
  52. package/dist/core/understanding/brain/storage/store.d.ts +4 -0
  53. package/dist/core/understanding/brain/storage/store.js +22 -2
  54. package/dist/dashboard/page.d.ts +2 -0
  55. package/dist/dashboard/page.js +53 -0
  56. package/dist/dashboard/server.js +117 -140
  57. package/dist/dashboard/ui/client.d.ts +2 -0
  58. package/dist/dashboard/ui/client.js +2 -0
  59. package/dist/dashboard/ui/styles.d.ts +2 -0
  60. package/dist/dashboard/ui/styles.js +138 -0
  61. package/dist/discovery/files.js +13 -0
  62. package/dist/index.d.ts +4 -0
  63. package/dist/index.js +2 -0
  64. package/dist/intelligence/graph/build.js +46 -38
  65. package/dist/intelligence/graph/incremental.d.ts +2 -0
  66. package/dist/intelligence/graph/incremental.js +9 -0
  67. package/dist/intelligence/resolve/imports.js +1 -1
  68. package/dist/languages/dart.d.ts +10 -0
  69. package/dist/languages/dart.js +99 -0
  70. package/dist/languages/go.d.ts +13 -7
  71. package/dist/languages/go.js +84 -24
  72. package/dist/languages/index.d.ts +5 -1
  73. package/dist/languages/index.js +13 -36
  74. package/dist/languages/java.d.ts +10 -0
  75. package/dist/languages/java.js +80 -0
  76. package/dist/languages/kotlin.d.ts +10 -0
  77. package/dist/languages/kotlin.js +85 -0
  78. package/dist/languages/rust.d.ts +10 -0
  79. package/dist/languages/rust.js +93 -0
  80. package/dist/languages/types.d.ts +4 -3
  81. package/dist/mcp/agent/registry.d.ts +1 -1
  82. package/dist/mcp/agent/registry.js +109 -23
  83. package/dist/mcp/intelligence/handlers.d.ts +3 -0
  84. package/dist/mcp/intelligence/handlers.js +28 -0
  85. package/dist/mcp/intelligence/path-safety.d.ts +6 -1
  86. package/dist/mcp/intelligence/path-safety.js +55 -2
  87. package/dist/mcp/intelligence/registry.d.ts +1 -1
  88. package/dist/mcp/intelligence/registry.js +30 -1
  89. package/dist/platform/graph/build.js +9 -0
  90. package/dist/product/api/doctor.d.ts +14 -0
  91. package/dist/product/api/doctor.js +185 -0
  92. package/dist/product/api/openapi.d.ts +7 -0
  93. package/dist/product/api/openapi.js +122 -0
  94. package/dist/product/approval/model.d.ts +30 -0
  95. package/dist/product/approval/model.js +64 -0
  96. package/dist/product/approval/session.d.ts +49 -0
  97. package/dist/product/approval/session.js +134 -0
  98. package/dist/product/database/doctor.d.ts +30 -0
  99. package/dist/product/database/doctor.js +184 -0
  100. package/dist/product/decisions/ledger.d.ts +36 -0
  101. package/dist/product/decisions/ledger.js +141 -0
  102. package/dist/product/deps/analyze.d.ts +46 -0
  103. package/dist/product/deps/analyze.js +137 -0
  104. package/dist/product/deps/lockfiles.d.ts +25 -0
  105. package/dist/product/deps/lockfiles.js +200 -0
  106. package/dist/product/discovery/roots.d.ts +52 -0
  107. package/dist/product/discovery/roots.js +231 -0
  108. package/dist/product/dna/build.d.ts +50 -0
  109. package/dist/product/dna/build.js +255 -0
  110. package/dist/product/eval/lab.d.ts +17 -0
  111. package/dist/product/eval/lab.js +218 -0
  112. package/dist/product/events/doctor.d.ts +21 -0
  113. package/dist/product/events/doctor.js +147 -0
  114. package/dist/product/evidence-scan.d.ts +12 -0
  115. package/dist/product/evidence-scan.js +46 -0
  116. package/dist/product/evolution/timeline.d.ts +29 -0
  117. package/dist/product/evolution/timeline.js +123 -0
  118. package/dist/product/features/intelligence.d.ts +23 -0
  119. package/dist/product/features/intelligence.js +158 -0
  120. package/dist/product/forensic/mode.d.ts +25 -0
  121. package/dist/product/forensic/mode.js +70 -0
  122. package/dist/product/graph/enrich-languages.d.ts +20 -0
  123. package/dist/product/graph/enrich-languages.js +168 -0
  124. package/dist/product/health/code-health.d.ts +20 -0
  125. package/dist/product/health/code-health.js +149 -0
  126. package/dist/product/index.d.ts +72 -0
  127. package/dist/product/index.js +36 -0
  128. package/dist/product/ledger/change-ledger.d.ts +23 -0
  129. package/dist/product/ledger/change-ledger.js +64 -0
  130. package/dist/product/map/software-map.d.ts +17 -0
  131. package/dist/product/map/software-map.js +96 -0
  132. package/dist/product/memory/institutional.d.ts +16 -0
  133. package/dist/product/memory/institutional.js +92 -0
  134. package/dist/product/ops/incident.d.ts +20 -0
  135. package/dist/product/ops/incident.js +61 -0
  136. package/dist/product/ops/infra.d.ts +15 -0
  137. package/dist/product/ops/infra.js +112 -0
  138. package/dist/product/org/model.d.ts +34 -0
  139. package/dist/product/org/model.js +195 -0
  140. package/dist/product/privacy/doctor.d.ts +13 -0
  141. package/dist/product/privacy/doctor.js +90 -0
  142. package/dist/product/requirements/trace.d.ts +21 -0
  143. package/dist/product/requirements/trace.js +166 -0
  144. package/dist/product/search/index.d.ts +29 -0
  145. package/dist/product/search/index.js +116 -0
  146. package/dist/product/search/software-search.d.ts +19 -0
  147. package/dist/product/search/software-search.js +105 -0
  148. package/dist/product/security/doctor.d.ts +23 -0
  149. package/dist/product/security/doctor.js +124 -0
  150. package/dist/product/self/diagnose.d.ts +15 -0
  151. package/dist/product/self/diagnose.js +82 -0
  152. package/dist/product/techdebt/roadmap.d.ts +20 -0
  153. package/dist/product/techdebt/roadmap.js +118 -0
  154. package/dist/product/testbrain/analyze.d.ts +29 -0
  155. package/dist/product/testbrain/analyze.js +129 -0
  156. package/dist/product/truth.d.ts +12 -0
  157. package/dist/product/truth.js +30 -0
  158. package/dist/product/twin/digital-twin.d.ts +23 -0
  159. package/dist/product/twin/digital-twin.js +52 -0
  160. package/dist/product/twin/store.d.ts +21 -0
  161. package/dist/product/twin/store.js +78 -0
  162. package/dist/product/whatif/engine.d.ts +28 -0
  163. package/dist/product/whatif/engine.js +91 -0
  164. package/dist/project/ownership.d.ts +65 -0
  165. package/dist/project/ownership.js +169 -0
  166. package/dist/utils/fs.js +10 -2
  167. package/package.json +1 -1
@@ -0,0 +1,255 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { createHash } from "node:crypto";
4
+ import { detectProject } from "../../detectors/project.js";
5
+ import { detectMonorepo } from "../../core/monorepo/detect.js";
6
+ import { resolveRepoRoot } from "../../utils/path.js";
7
+ import { pathExists, readJsonFile } from "../../utils/fs.js";
8
+ const FRONTEND_FW = new Set(["react", "nextjs", "vue", "nuxt", "angular", "svelte", "solid"]);
9
+ const BACKEND_FW = new Set([
10
+ "express",
11
+ "nestjs",
12
+ "fastapi",
13
+ "django",
14
+ "flask",
15
+ "laravel",
16
+ "rails",
17
+ "spring",
18
+ ]);
19
+ function classifyFramework(id) {
20
+ const lower = id.toLowerCase();
21
+ if (FRONTEND_FW.has(lower))
22
+ return "frontend";
23
+ if (BACKEND_FW.has(lower))
24
+ return "backend";
25
+ return "other";
26
+ }
27
+ function isSourcePath(p) {
28
+ return /\.(ts|tsx|js|jsx|mjs|cjs|py|php|go|rs|java|kt|kts|dart|rb|cs)$/i.test(p);
29
+ }
30
+ function isTestPath(p) {
31
+ const lower = p.toLowerCase();
32
+ return (lower.includes(".test.") ||
33
+ lower.includes(".spec.") ||
34
+ lower.startsWith("tests/") ||
35
+ lower.startsWith("test/") ||
36
+ lower.includes("/__tests__/"));
37
+ }
38
+ function isConfigPath(p) {
39
+ const base = path.posix.basename(p).toLowerCase();
40
+ return (base.endsWith(".json") ||
41
+ base.endsWith(".yml") ||
42
+ base.endsWith(".yaml") ||
43
+ base.endsWith(".toml") ||
44
+ base.endsWith(".ini") ||
45
+ base.startsWith(".env") ||
46
+ base.includes("config"));
47
+ }
48
+ /**
49
+ * Deterministic Project DNA from repository detectors + file inventory.
50
+ */
51
+ export async function buildProjectDna(rootInput) {
52
+ const root = resolveRepoRoot(rootInput);
53
+ const limitations = [
54
+ "DNA is derived from file markers and dependency manifests — not runtime introspection.",
55
+ "Database/cloud signals are heuristic (PARTIAL) unless schema/infra files are present.",
56
+ ];
57
+ const detection = await detectProject(root);
58
+ const mono = await detectMonorepo(root);
59
+ const repo = detection.repository;
60
+ const relativePaths = detection.discovery.files.map((f) => f.relativePath.replace(/\\/g, "/"));
61
+ const languages = (repo.languages ?? []).filter((l) => l !== "unknown");
62
+ const frameworks = (repo.frameworks ?? []).filter((f) => f !== "unknown");
63
+ const packageManagers = (repo.packageManagers ?? []).filter((p) => p !== "unknown");
64
+ const frontend = [];
65
+ const backend = [];
66
+ for (const fw of frameworks) {
67
+ const kind = classifyFramework(fw);
68
+ if (kind === "frontend")
69
+ frontend.push(fw);
70
+ else if (kind === "backend")
71
+ backend.push(fw);
72
+ }
73
+ const database = [];
74
+ const dbMarkers = [
75
+ ["prisma", /(^|\/)prisma\/schema\.prisma$/i],
76
+ ["sql-migrations", /(^|\/)migrations\/.+\.sql$/i],
77
+ ["laravel-migrations", /database\/migrations\/.+\.php$/i],
78
+ ["sequelize", /sequelize/i],
79
+ ["typeorm", /typeorm/i],
80
+ ["mongoose", /mongoose/i],
81
+ ];
82
+ for (const [label, re] of dbMarkers) {
83
+ if (relativePaths.some((p) => re.test(p)))
84
+ database.push(label);
85
+ }
86
+ const infrastructure = [];
87
+ const infraFiles = [
88
+ ["docker", "Dockerfile"],
89
+ ["compose", "docker-compose.yml"],
90
+ ["compose", "docker-compose.yaml"],
91
+ ["kubernetes", "k8s"],
92
+ ["terraform", ".tf"],
93
+ ["github-actions", ".github/workflows"],
94
+ ];
95
+ for (const [label, marker] of infraFiles) {
96
+ if (relativePaths.some((p) => p === marker ||
97
+ p.endsWith(`/${marker}`) ||
98
+ p.startsWith(`${marker}/`) ||
99
+ p.endsWith(marker))) {
100
+ if (!infrastructure.includes(label))
101
+ infrastructure.push(label);
102
+ }
103
+ }
104
+ const cloudIndicators = [];
105
+ for (const p of relativePaths) {
106
+ const lower = p.toLowerCase();
107
+ if (lower.includes("serverless") || lower.includes("sam.template"))
108
+ cloudIndicators.push("serverless");
109
+ if (lower.includes("vercel.json"))
110
+ cloudIndicators.push("vercel");
111
+ if (lower.includes("netlify.toml"))
112
+ cloudIndicators.push("netlify");
113
+ if (lower.includes("fly.toml"))
114
+ cloudIndicators.push("fly");
115
+ }
116
+ const buildSystems = [];
117
+ for (const [label, file] of [
118
+ ["tsc", "tsconfig.json"],
119
+ ["vite", "vite.config.ts"],
120
+ ["webpack", "webpack.config.js"],
121
+ ["gradle", "build.gradle"],
122
+ ["maven", "pom.xml"],
123
+ ["cargo", "Cargo.toml"],
124
+ ]) {
125
+ if (relativePaths.some((p) => p === file || p.endsWith(`/${file}`)))
126
+ buildSystems.push(label);
127
+ }
128
+ const testFrameworks = [];
129
+ const pkg = await readJsonFile(path.join(root, "package.json"), 512 * 1024);
130
+ const deps = {
131
+ ...(pkg.ok ? (pkg.data.dependencies ?? {}) : {}),
132
+ ...(pkg.ok ? (pkg.data.devDependencies ?? {}) : {}),
133
+ };
134
+ for (const name of ["vitest", "jest", "mocha", "pytest", "phpunit", "go test"]) {
135
+ if (name === "go test") {
136
+ if (await pathExists(path.join(root, "go.mod")))
137
+ testFrameworks.push("go-test");
138
+ continue;
139
+ }
140
+ if (name in deps || relativePaths.some((p) => p.includes(name))) {
141
+ if (!testFrameworks.includes(name))
142
+ testFrameworks.push(name);
143
+ }
144
+ }
145
+ // Marker scan for common AI coding tool configuration directories/files
146
+ const aiCodingTools = [];
147
+ for (const [id, marker] of [
148
+ ["cursor", ".cursor"],
149
+ ["claude-code", ".claude"],
150
+ ["codex", ".codex"],
151
+ ["copilot", ".github/copilot-instructions.md"],
152
+ ["windsurf", ".windsurfrules"],
153
+ ["gemini-cli", ".gemini"],
154
+ ["aider", ".aider"],
155
+ ]) {
156
+ if (relativePaths.some((p) => p === marker || p.startsWith(`${marker}/`) || p.endsWith(`/${marker}`)) ||
157
+ (await pathExists(path.join(root, marker)))) {
158
+ aiCodingTools.push(id);
159
+ }
160
+ }
161
+ const hasGit = await pathExists(path.join(root, ".git"));
162
+ const name = (pkg.ok && typeof pkg.data.name === "string" && pkg.data.name) || path.basename(root);
163
+ let projectType = "library-or-app";
164
+ if (mono.isMonorepo)
165
+ projectType = "monorepo";
166
+ else if (frontend.length && backend.length)
167
+ projectType = "fullstack-app";
168
+ else if (frontend.length)
169
+ projectType = "frontend-app";
170
+ else if (backend.length)
171
+ projectType = "backend-app";
172
+ else if (languages.includes("python"))
173
+ projectType = "python-project";
174
+ else if (languages.includes("php"))
175
+ projectType = "php-project";
176
+ const sourceFiles = relativePaths.filter(isSourcePath).length;
177
+ const testFiles = relativePaths.filter(isTestPath).length;
178
+ const configFiles = relativePaths.filter(isConfigPath).length;
179
+ const configurationStructure = [
180
+ ...new Set(relativePaths
181
+ .filter(isConfigPath)
182
+ .map((p) => p.split("/").slice(0, 2).join("/"))
183
+ .slice(0, 40)),
184
+ ].sort();
185
+ const packages = mono.packages.map((p) => p.name);
186
+ const applications = mono.packages
187
+ .filter((p) => /app|web|api|service/i.test(p.relativePath) || /app|web|api/i.test(p.name))
188
+ .map((p) => p.name);
189
+ const services = applications.slice();
190
+ const externalIntegrations = [];
191
+ for (const key of Object.keys(deps)) {
192
+ if (/stripe|twilio|sendgrid|aws-sdk|@aws-sdk|firebase|supabase|openai|anthropic/i.test(key)) {
193
+ externalIntegrations.push(key);
194
+ }
195
+ }
196
+ const fingerprintPayload = JSON.stringify({
197
+ name,
198
+ languages: [...languages].sort(),
199
+ frameworks: [...frameworks].sort(),
200
+ packageManagers: [...packageManagers].sort(),
201
+ monorepo: mono.isMonorepo,
202
+ packages: [...packages].sort(),
203
+ fileCount: relativePaths.length,
204
+ });
205
+ const fingerprint = createHash("sha256").update(fingerprintPayload).digest("hex").slice(0, 16);
206
+ return {
207
+ fingerprint,
208
+ name,
209
+ root,
210
+ projectType,
211
+ languages,
212
+ frameworks,
213
+ frontend,
214
+ backend,
215
+ database: [...new Set(database)],
216
+ packageManagers,
217
+ buildSystems: [...new Set(buildSystems)],
218
+ testFrameworks: [...new Set(testFrameworks)],
219
+ infrastructure: [...new Set(infrastructure)],
220
+ cloudIndicators: [...new Set(cloudIndicators)],
221
+ git: { present: hasGit, truth: hasGit ? "VERIFIED" : "UNKNOWN" },
222
+ monorepo: {
223
+ isMonorepo: mono.isMonorepo,
224
+ tool: mono.tool,
225
+ packages: mono.packages.map((p) => ({ name: p.name, path: p.relativePath })),
226
+ truth: mono.isMonorepo ? "VERIFIED" : "VERIFIED",
227
+ },
228
+ services,
229
+ packages,
230
+ applications,
231
+ externalIntegrations: [...new Set(externalIntegrations)].sort(),
232
+ aiCodingTools: [...new Set(aiCodingTools)].sort(),
233
+ counts: {
234
+ files: relativePaths.length,
235
+ sourceFiles,
236
+ testFiles,
237
+ configFiles,
238
+ },
239
+ configurationStructure,
240
+ truth: "VERIFIED",
241
+ limitations: [...limitations, ...mono.limitations],
242
+ generatedAt: new Date().toISOString(),
243
+ };
244
+ }
245
+ export async function persistProjectDna(rootInput, dna) {
246
+ const root = resolveRepoRoot(rootInput);
247
+ const built = dna ?? (await buildProjectDna(root));
248
+ const dir = path.join(root, ".agentdoctor", "dna");
249
+ await fs.mkdir(dir, { recursive: true });
250
+ await fs.writeFile(path.join(dir, "project-dna.json"), `${JSON.stringify(built, null, 2)}\n`, {
251
+ encoding: "utf8",
252
+ mode: 0o600,
253
+ });
254
+ return built;
255
+ }
@@ -0,0 +1,17 @@
1
+ import type { TruthLabel } from "../truth.js";
2
+ export interface EvalCheckResult {
3
+ id: string;
4
+ description: string;
5
+ passed: boolean;
6
+ truth: TruthLabel;
7
+ detail?: string;
8
+ }
9
+ export interface EvalLabReport {
10
+ root: string;
11
+ fixtureRoot: string;
12
+ fixturesRun: string[];
13
+ checks: EvalCheckResult[];
14
+ passed: boolean;
15
+ limitations: string[];
16
+ }
17
+ export declare function runEvalLab(rootInput?: string): Promise<EvalLabReport>;
@@ -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;