@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
@@ -11,6 +11,9 @@ import { redactSecrets } from "../../platform/security/redact.js";
11
11
  import { resolveRepoRoot } from "../../utils/path.js";
12
12
  import { CONTRACTS_VERSION } from "../../contracts/index.js";
13
13
  import { assertSafeRepoTarget } from "./path-safety.js";
14
+ import { buildProjectDna } from "../../product/dna/build.js";
15
+ import { buildSoftwareMap } from "../../product/map/software-map.js";
16
+ import { analyzeWhatIf } from "../../product/whatif/engine.js";
14
17
  function requireString(args, key) {
15
18
  const value = args[key];
16
19
  if (typeof value !== "string")
@@ -362,3 +365,28 @@ export async function handleGraphQueryTool(rootInput, args) {
362
365
  ],
363
366
  });
364
367
  }
368
+ export async function handleProjectDnaTool(rootInput) {
369
+ const root = resolveRepoRoot(rootInput);
370
+ const dna = await buildProjectDna(root);
371
+ return redactDeep({ ok: true, dna, limitations: dna.limitations });
372
+ }
373
+ export async function handleSoftwareMapTool(rootInput) {
374
+ const root = resolveRepoRoot(rootInput);
375
+ const map = await buildSoftwareMap(root);
376
+ return redactDeep({ ok: true, map, limitations: map.limitations });
377
+ }
378
+ export async function handleWhatIfTool(rootInput, args) {
379
+ const root = resolveRepoRoot(rootInput);
380
+ const target = requireString(args, "target");
381
+ if (!target) {
382
+ return invalidArgument("target required (repo-relative path or symbol)");
383
+ }
384
+ try {
385
+ assertSafeRepoTarget(root, target);
386
+ }
387
+ catch {
388
+ return pathEscape();
389
+ }
390
+ const report = await analyzeWhatIf(root, target);
391
+ return redactDeep({ ok: true, report, limitations: report.limitations });
392
+ }
@@ -4,6 +4,11 @@ export { tryDecodeUriComponent };
4
4
  * Validate a user-supplied repository-relative or in-repo target.
5
5
  * Returns a normalized path relative to root (POSIX) when the target is path-like and safe,
6
6
  * or null when the target is treated as a non-path identifier (graph id / label).
7
- * Throws a safe Error (no outside path leakage) when a path-like target escapes the root.
7
+ * Throws a safe Error (no outside path leakage) when a path-like target escapes the root
8
+ * or is outside project ownership (.private, AgentDoctorOS, fixtures, validation).
8
9
  */
9
10
  export declare function assertSafeRepoTarget(root: string, rawTarget: string): string | null;
11
+ /**
12
+ * Async ownership gate including nested-repository ancestry (for MCP file-path tools).
13
+ */
14
+ export declare function assertSafeOwnedRepoTarget(root: string, rawTarget: string): Promise<string | null>;
@@ -1,4 +1,6 @@
1
1
  import path from "node:path";
2
+ import fs from "node:fs";
3
+ import { assertProjectOwnedRepoPath, classifyRelativePathOwnership, ProjectOwnershipError, } from "../../project/ownership.js";
2
4
  import { PathEscapeError, rejectHostilePathInput, resolveSafeRepoPath, tryDecodeUriComponent, } from "../../security/paths.js";
3
5
  import { sanitizeForOutput } from "../../utils/path.js";
4
6
  export { tryDecodeUriComponent };
@@ -6,7 +8,8 @@ export { tryDecodeUriComponent };
6
8
  * Validate a user-supplied repository-relative or in-repo target.
7
9
  * Returns a normalized path relative to root (POSIX) when the target is path-like and safe,
8
10
  * or null when the target is treated as a non-path identifier (graph id / label).
9
- * Throws a safe Error (no outside path leakage) when a path-like target escapes the root.
11
+ * Throws a safe Error (no outside path leakage) when a path-like target escapes the root
12
+ * or is outside project ownership (.private, AgentDoctorOS, fixtures, validation).
10
13
  */
11
14
  export function assertSafeRepoTarget(root, rawTarget) {
12
15
  if (typeof rawTarget !== "string" || rawTarget.includes("\0")) {
@@ -52,12 +55,62 @@ export function assertSafeRepoTarget(root, rawTarget) {
52
55
  const primary = candidates[candidates.length - 1];
53
56
  try {
54
57
  const abs = resolveSafeRepoPath(root, primary);
55
- return path.relative(root, abs).split(path.sep).join("/");
58
+ const lexicalRel = path.isAbsolute(primary)
59
+ ? path.relative(root, abs).split(path.sep).join("/")
60
+ : primary.replace(/\\/g, "/").replace(/^\.\//, "");
61
+ // Lexical non-owned prefixes always deny.
62
+ if (classifyRelativePathOwnership(lexicalRel) !== "project_owned") {
63
+ throw new Error("path outside project ownership");
64
+ }
65
+ // Realpath-relative classification catches symlink aliases into non-owned trees.
66
+ let realRoot = root;
67
+ let realAbs = abs;
68
+ try {
69
+ realRoot = fs.realpathSync(root);
70
+ }
71
+ catch {
72
+ /* keep */
73
+ }
74
+ try {
75
+ realAbs = fs.realpathSync(abs);
76
+ }
77
+ catch {
78
+ /* keep */
79
+ }
80
+ const realRel = path.relative(realRoot, realAbs).split(path.sep).join("/") || ".";
81
+ if (realRel.startsWith("..") ||
82
+ path.isAbsolute(realRel) ||
83
+ classifyRelativePathOwnership(realRel) !== "project_owned") {
84
+ throw new Error("path outside project ownership");
85
+ }
86
+ return realRel || lexicalRel;
56
87
  }
57
88
  catch (error) {
58
89
  if (error instanceof PathEscapeError) {
59
90
  throw new Error(error.message);
60
91
  }
92
+ if (error instanceof Error && error.message === "path outside project ownership") {
93
+ throw error;
94
+ }
61
95
  throw new Error("invalid target");
62
96
  }
63
97
  }
98
+ /**
99
+ * Async ownership gate including nested-repository ancestry (for MCP file-path tools).
100
+ */
101
+ export async function assertSafeOwnedRepoTarget(root, rawTarget) {
102
+ const rel = assertSafeRepoTarget(root, rawTarget);
103
+ if (rel === null)
104
+ return null;
105
+ try {
106
+ const abs = resolveSafeRepoPath(root, rel);
107
+ await assertProjectOwnedRepoPath(root, abs, rel);
108
+ }
109
+ catch (error) {
110
+ if (error instanceof PathEscapeError || error instanceof ProjectOwnershipError) {
111
+ throw new Error(error.message);
112
+ }
113
+ throw error;
114
+ }
115
+ return rel;
116
+ }
@@ -1,5 +1,5 @@
1
1
  import type { Tool } from "@modelcontextprotocol/sdk/types.js";
2
- export declare const INTELLIGENCE_MCP_TOOL_NAMES: readonly ["repo_overview", "codebase_search", "symbol_lookup", "dependency_lookup", "call_graph_lookup", "test_impact", "refactor_impact", "code_health", "architecture_info", "architecture_check", "knowledge_retrieve", "policy_evaluate", "change_analyze", "proof_inspect", "evidence_inspect", "graph_query"];
2
+ export declare const INTELLIGENCE_MCP_TOOL_NAMES: readonly ["repo_overview", "codebase_search", "symbol_lookup", "dependency_lookup", "call_graph_lookup", "test_impact", "refactor_impact", "code_health", "architecture_info", "architecture_check", "knowledge_retrieve", "policy_evaluate", "change_analyze", "proof_inspect", "evidence_inspect", "graph_query", "project_dna", "software_map", "what_if"];
3
3
  export type IntelligenceMcpToolName = (typeof INTELLIGENCE_MCP_TOOL_NAMES)[number];
4
4
  export declare function listIntelligenceMcpTools(): Tool[];
5
5
  export declare function invokeIntelligenceMcpTool(root: string, name: string, args: Record<string, unknown>): Promise<{
@@ -1,4 +1,4 @@
1
- import { handleArchitectureCheckTool, handleArchitectureTool, handleCallGraphLookup, handleChangeAnalyzeTool, handleCodeHealthTool, handleCodebaseSearch, handleDependencyLookup, handleEvidenceInspectTool, handleGraphQueryTool, handleKnowledgeTool, handlePolicyEvalTool, handleProofInspectTool, handleRefactorImpactTool, handleRepoOverview, handleSymbolLookup, handleTestImpactTool, } from "./handlers.js";
1
+ import { handleArchitectureCheckTool, handleArchitectureTool, handleCallGraphLookup, handleChangeAnalyzeTool, handleCodeHealthTool, handleCodebaseSearch, handleDependencyLookup, handleEvidenceInspectTool, handleGraphQueryTool, handleKnowledgeTool, handlePolicyEvalTool, handleProofInspectTool, handleRefactorImpactTool, handleRepoOverview, handleSymbolLookup, handleTestImpactTool, handleProjectDnaTool, handleSoftwareMapTool, handleWhatIfTool, } from "./handlers.js";
2
2
  export const INTELLIGENCE_MCP_TOOL_NAMES = [
3
3
  "repo_overview",
4
4
  "codebase_search",
@@ -16,6 +16,9 @@ export const INTELLIGENCE_MCP_TOOL_NAMES = [
16
16
  "proof_inspect",
17
17
  "evidence_inspect",
18
18
  "graph_query",
19
+ "project_dna",
20
+ "software_map",
21
+ "what_if",
19
22
  ];
20
23
  const emptyObjectSchema = {
21
24
  type: "object",
@@ -166,6 +169,26 @@ export function listIntelligenceMcpTools() {
166
169
  additionalProperties: false,
167
170
  },
168
171
  },
172
+ {
173
+ name: "project_dna",
174
+ description: "READ: Deterministic project DNA fingerprint (languages, frameworks, monorepo). No shell.",
175
+ inputSchema: emptyObjectSchema,
176
+ },
177
+ {
178
+ name: "software_map",
179
+ description: "READ: Navigable software map tree from discovery heuristics. No shell.",
180
+ inputSchema: emptyObjectSchema,
181
+ },
182
+ {
183
+ name: "what_if",
184
+ description: "READ: What-if impact analysis for a repo-relative path or symbol (graph-backed). Path validated.",
185
+ inputSchema: {
186
+ type: "object",
187
+ properties: { target: { type: "string" } },
188
+ required: ["target"],
189
+ additionalProperties: false,
190
+ },
191
+ },
169
192
  ];
170
193
  }
171
194
  export async function invokeIntelligenceMcpTool(root, name, args) {
@@ -203,6 +226,12 @@ export async function invokeIntelligenceMcpTool(root, name, args) {
203
226
  return { structured: await handleEvidenceInspectTool(root, args), isError: false };
204
227
  case "graph_query":
205
228
  return { structured: await handleGraphQueryTool(root, args), isError: false };
229
+ case "project_dna":
230
+ return { structured: await handleProjectDnaTool(root), isError: false };
231
+ case "software_map":
232
+ return { structured: await handleSoftwareMapTool(root), isError: false };
233
+ case "what_if":
234
+ return { structured: await handleWhatIfTool(root, args), isError: false };
206
235
  default:
207
236
  return {
208
237
  structured: {
@@ -1,6 +1,7 @@
1
1
  import fs from "node:fs/promises";
2
2
  import path from "node:path";
3
3
  import { createHash } from "node:crypto";
4
+ import { decideDirectoryTraversal } from "../../project/ownership.js";
4
5
  import { readTextFile } from "../../utils/fs.js";
5
6
  import { resolveRepoRoot, toPosixRelative } from "../../utils/path.js";
6
7
  const SKIP = new Set([
@@ -48,6 +49,14 @@ async function walk(root, dir, files, dirs, limit) {
48
49
  if (entry.isSymbolicLink())
49
50
  continue;
50
51
  if (entry.isDirectory()) {
52
+ const relativeDir = toPosixRelative(root, absolute);
53
+ const traversal = await decideDirectoryTraversal({
54
+ projectRoot: root,
55
+ absoluteDir: absolute,
56
+ relativeDir,
57
+ });
58
+ if (!traversal.traverse)
59
+ continue;
51
60
  dirs.push(absolute);
52
61
  await walk(root, absolute, files, dirs, limit);
53
62
  continue;
@@ -0,0 +1,14 @@
1
+ import type { ProductEvidence, TruthLabel } from "../truth.js";
2
+ export interface ApiEndpointFinding {
3
+ method: string;
4
+ pathPattern: string;
5
+ framework: string;
6
+ truth: TruthLabel;
7
+ evidence: ProductEvidence[];
8
+ }
9
+ export interface ApiDoctorReport {
10
+ root: string;
11
+ endpoints: ApiEndpointFinding[];
12
+ limitations: string[];
13
+ }
14
+ export declare function analyzeApiSurface(rootInput: string): Promise<ApiDoctorReport>;
@@ -0,0 +1,185 @@
1
+ import { lineNumberAt, loadProjectSourceFiles } from "../evidence-scan.js";
2
+ import { discoverOpenApiEndpoints } from "./openapi.js";
3
+ const ROUTE_PATTERNS = [
4
+ {
5
+ framework: "express",
6
+ regex: /\.(?:get|post|put|patch|delete|all)\(\s*['"`]([^'"`]+)['"`]/gi,
7
+ pathGroup: 1,
8
+ },
9
+ {
10
+ framework: "express-router",
11
+ regex: /router\.(?:get|post|put|patch|delete|all)\(\s*['"`]([^'"`]+)['"`]/gi,
12
+ pathGroup: 1,
13
+ },
14
+ {
15
+ framework: "fastify",
16
+ regex: /fastify\.(?:get|post|put|patch|delete)\(\s*['"`]([^'"`]+)['"`]/gi,
17
+ pathGroup: 1,
18
+ },
19
+ {
20
+ framework: "nestjs",
21
+ regex: /@(Get|Post|Put|Patch|Delete|All)\(\s*['"`]([^'"`]+)['"`]?\s*\)/g,
22
+ methodGroup: 1,
23
+ pathGroup: 2,
24
+ },
25
+ {
26
+ framework: "nestjs",
27
+ regex: /@(Get|Post|Put|Patch|Delete|All)\(\)/g,
28
+ methodGroup: 1,
29
+ pathGroup: 0,
30
+ },
31
+ {
32
+ framework: "laravel",
33
+ regex: /Route::(get|post|put|patch|delete|any)\(\s*['"]([^'"]+)['"]/gi,
34
+ methodGroup: 1,
35
+ pathGroup: 2,
36
+ },
37
+ {
38
+ framework: "flask",
39
+ regex: /@(?:app|bp|blueprint)\.(?:route|get|post|put|patch|delete)\(\s*['"]([^'"]+)['"]/gi,
40
+ pathGroup: 1,
41
+ },
42
+ {
43
+ framework: "flask",
44
+ regex: /@(?:app|bp)\.(get|post|put|patch|delete)\(\s*['"]([^'"]+)['"]/gi,
45
+ methodGroup: 1,
46
+ pathGroup: 2,
47
+ },
48
+ ];
49
+ function inferMethodFromMatch(full, pattern, groups) {
50
+ const methodRaw = pattern.methodGroup !== undefined ? groups[pattern.methodGroup] : undefined;
51
+ if (methodRaw) {
52
+ return methodRaw.toUpperCase();
53
+ }
54
+ const lower = full.toLowerCase();
55
+ for (const m of ["get", "post", "put", "patch", "delete", "all"]) {
56
+ if (lower.includes(`.${m}(`) || lower.includes(`@${m}`) || lower.includes(`::${m}(`)) {
57
+ return m === "all" ? "ALL" : m.toUpperCase();
58
+ }
59
+ }
60
+ return pattern.defaultMethod ?? "UNKNOWN";
61
+ }
62
+ function scanFile(relativePath, content) {
63
+ const findings = [];
64
+ for (const pattern of ROUTE_PATTERNS) {
65
+ pattern.regex.lastIndex = 0;
66
+ let match;
67
+ while ((match = pattern.regex.exec(content)) !== null) {
68
+ const pathGroup = pattern.pathGroup;
69
+ let pathPattern = pathGroup > 0 ? (match[pathGroup] ?? "") : "/";
70
+ if (!pathPattern && pattern.framework === "nestjs")
71
+ pathPattern = "/";
72
+ if (!pathPattern)
73
+ continue;
74
+ const method = inferMethodFromMatch(match[0], pattern, match);
75
+ const line = lineNumberAt(content, match.index);
76
+ findings.push({
77
+ method,
78
+ pathPattern,
79
+ framework: pattern.framework,
80
+ truth: "INFERRED",
81
+ evidence: [
82
+ {
83
+ path: relativePath,
84
+ line,
85
+ excerpt: match[0].trim().slice(0, 160),
86
+ },
87
+ ],
88
+ });
89
+ }
90
+ }
91
+ return findings;
92
+ }
93
+ function scanGraphqlSurface(relativePath, content) {
94
+ const findings = [];
95
+ const typeQueryRe = /type\s+Query\s*\{/g;
96
+ let match;
97
+ while ((match = typeQueryRe.exec(content)) !== null) {
98
+ findings.push({
99
+ method: "GRAPHQL",
100
+ pathPattern: "Query",
101
+ framework: "graphql-sdl",
102
+ truth: "INFERRED",
103
+ evidence: [
104
+ {
105
+ path: relativePath,
106
+ line: lineNumberAt(content, match.index),
107
+ excerpt: match[0].trim(),
108
+ },
109
+ ],
110
+ });
111
+ }
112
+ const gqlRe = /gql`\s*([\s\S]*?)`/g;
113
+ while ((match = gqlRe.exec(content)) !== null) {
114
+ const inner = match[1] ?? "";
115
+ const opMatch = /(?:query|mutation|subscription)\s+(\w+)/i.exec(inner);
116
+ const opName = opMatch?.[1] ?? "anonymous";
117
+ findings.push({
118
+ method: "GRAPHQL",
119
+ pathPattern: opName,
120
+ framework: "graphql-tag",
121
+ truth: "INFERRED",
122
+ evidence: [
123
+ {
124
+ path: relativePath,
125
+ line: lineNumberAt(content, match.index),
126
+ excerpt: match[0].trim().slice(0, 160),
127
+ },
128
+ ],
129
+ });
130
+ }
131
+ return findings;
132
+ }
133
+ const SOURCE_FILTER = (rel) => {
134
+ const lower = rel.toLowerCase();
135
+ if (lower.endsWith(".graphql") || lower.endsWith(".gql"))
136
+ return true;
137
+ if (lower.endsWith(".php") && (lower.includes("routes/") || lower.includes("route")))
138
+ return true;
139
+ if (lower.endsWith(".py") &&
140
+ (lower.includes("routes") || lower.includes("views") || lower.includes("app"))) {
141
+ return true;
142
+ }
143
+ if (lower.endsWith(".ts") ||
144
+ lower.endsWith(".tsx") ||
145
+ lower.endsWith(".js") ||
146
+ lower.endsWith(".jsx")) {
147
+ return (lower.includes("route") ||
148
+ lower.includes("controller") ||
149
+ lower.includes("api/") ||
150
+ lower.includes("server") ||
151
+ lower.includes("app.module") ||
152
+ lower.includes("graphql") ||
153
+ lower.includes("schema"));
154
+ }
155
+ return false;
156
+ };
157
+ export async function analyzeApiSurface(rootInput) {
158
+ const { root, files, limitations: scanLimits, } = await loadProjectSourceFiles(rootInput, SOURCE_FILTER);
159
+ const limitations = [
160
+ "Endpoint discovery uses regex on route declarations; middleware, prefixes, and global mounts are not resolved.",
161
+ "Dynamic route paths and framework-specific route files may be missed.",
162
+ "GraphQL schema extraction (PARTIAL): static SDL `type Query` and gql` template literals only — not live gateway introspection.",
163
+ ...scanLimits,
164
+ ];
165
+ const endpoints = [];
166
+ for (const file of files) {
167
+ endpoints.push(...scanFile(file.relativePath, file.content));
168
+ endpoints.push(...scanGraphqlSurface(file.relativePath, file.content));
169
+ }
170
+ const openApi = await discoverOpenApiEndpoints(root);
171
+ limitations.push(...openApi.limitations);
172
+ endpoints.push(...openApi.endpoints);
173
+ const seen = new Set();
174
+ const deduped = endpoints.filter((e) => {
175
+ const key = `${e.method}:${e.pathPattern}:${e.framework}:${e.evidence[0]?.path}:${e.evidence[0]?.line}`;
176
+ if (seen.has(key))
177
+ return false;
178
+ seen.add(key);
179
+ return true;
180
+ });
181
+ deduped.sort((a, b) => a.pathPattern === b.pathPattern
182
+ ? a.method.localeCompare(b.method)
183
+ : a.pathPattern.localeCompare(b.pathPattern));
184
+ return { root, endpoints: deduped, limitations };
185
+ }
@@ -0,0 +1,7 @@
1
+ import type { ProductEvidence, TruthLabel } from "../truth.js";
2
+ import type { ApiEndpointFinding } from "./doctor.js";
3
+ export declare function discoverOpenApiEndpoints(rootInput: string, maxFileSizeBytes?: number): Promise<{
4
+ endpoints: ApiEndpointFinding[];
5
+ limitations: string[];
6
+ }>;
7
+ export type { TruthLabel, ProductEvidence };
@@ -0,0 +1,122 @@
1
+ import path from "node:path";
2
+ import { detectProject } from "../../detectors/project.js";
3
+ import { readTextFile } from "../../utils/fs.js";
4
+ import { resolveRepoRoot } from "../../utils/path.js";
5
+ const OPENAPI_NAMES = new Set([
6
+ "openapi.json",
7
+ "openapi.yaml",
8
+ "openapi.yml",
9
+ "swagger.json",
10
+ "swagger.yaml",
11
+ "swagger.yml",
12
+ ]);
13
+ function basenameLower(rel) {
14
+ return path.basename(rel).toLowerCase();
15
+ }
16
+ function extractJsonOpenApiPaths(content, relativePath) {
17
+ let doc;
18
+ try {
19
+ doc = JSON.parse(content);
20
+ }
21
+ catch {
22
+ return [];
23
+ }
24
+ const paths = doc.paths;
25
+ if (!paths || typeof paths !== "object")
26
+ return [];
27
+ const out = [];
28
+ for (const [pathPattern, methods] of Object.entries(paths)) {
29
+ if (!methods || typeof methods !== "object")
30
+ continue;
31
+ for (const method of Object.keys(methods)) {
32
+ const lower = method.toLowerCase();
33
+ if (!["get", "post", "put", "patch", "delete", "head", "options", "trace"].includes(lower)) {
34
+ continue;
35
+ }
36
+ out.push({
37
+ method: lower.toUpperCase(),
38
+ pathPattern,
39
+ framework: "openapi",
40
+ truth: "VERIFIED",
41
+ evidence: [{ path: relativePath, excerpt: `paths.${pathPattern}.${lower}` }],
42
+ });
43
+ }
44
+ }
45
+ return out;
46
+ }
47
+ /** Minimal YAML path/method extraction without a YAML library */
48
+ function extractYamlOpenApiPaths(content, relativePath) {
49
+ const out = [];
50
+ const lines = content.split(/\r?\n/);
51
+ let inPaths = false;
52
+ let currentPath = null;
53
+ const methodRe = /^\s{2,6}(get|post|put|patch|delete|head|options|trace):\s*$/i;
54
+ for (let i = 0; i < lines.length; i++) {
55
+ const line = lines[i];
56
+ if (/^paths:\s*$/.test(line.trim())) {
57
+ inPaths = true;
58
+ currentPath = null;
59
+ continue;
60
+ }
61
+ if (!inPaths)
62
+ continue;
63
+ if (/^[a-zA-Z]/.test(line) && !line.startsWith(" ")) {
64
+ break;
65
+ }
66
+ const pathKey = /^\s{2}(\/[^\s:]+):\s*$/.exec(line);
67
+ if (pathKey) {
68
+ currentPath = pathKey[1];
69
+ continue;
70
+ }
71
+ const methodMatch = methodRe.exec(line);
72
+ if (methodMatch && currentPath) {
73
+ const method = methodMatch[1].toUpperCase();
74
+ out.push({
75
+ method,
76
+ pathPattern: currentPath,
77
+ framework: "openapi",
78
+ truth: "VERIFIED",
79
+ evidence: [{ path: relativePath, line: i + 1, excerpt: line.trim() }],
80
+ });
81
+ }
82
+ }
83
+ return out;
84
+ }
85
+ export async function discoverOpenApiEndpoints(rootInput, maxFileSizeBytes = 512 * 1024) {
86
+ const root = resolveRepoRoot(rootInput);
87
+ const detection = await detectProject(root, maxFileSizeBytes);
88
+ const limitations = [
89
+ "OpenAPI discovery reads static spec files only — mounted servers and merged specs are not resolved.",
90
+ ];
91
+ const endpoints = [];
92
+ for (const entry of detection.discovery.files) {
93
+ const rel = entry.relativePath.replace(/\\/g, "/");
94
+ if (!OPENAPI_NAMES.has(basenameLower(rel)))
95
+ continue;
96
+ const text = await readTextFile(path.join(root, rel), maxFileSizeBytes);
97
+ if (text === null)
98
+ continue;
99
+ const lower = basenameLower(rel);
100
+ if (lower.endsWith(".json")) {
101
+ endpoints.push(...extractJsonOpenApiPaths(text, rel));
102
+ }
103
+ else {
104
+ endpoints.push(...extractYamlOpenApiPaths(text, rel));
105
+ if (!endpoints.length) {
106
+ limitations.push(`YAML spec ${rel} had no paths block detected (minimal parser)`);
107
+ }
108
+ }
109
+ }
110
+ const seen = new Set();
111
+ const deduped = endpoints.filter((e) => {
112
+ const key = `${e.method}:${e.pathPattern}:${e.evidence[0]?.path}`;
113
+ if (seen.has(key))
114
+ return false;
115
+ seen.add(key);
116
+ return true;
117
+ });
118
+ deduped.sort((a, b) => a.pathPattern === b.pathPattern
119
+ ? a.method.localeCompare(b.method)
120
+ : a.pathPattern.localeCompare(b.pathPattern));
121
+ return { endpoints: deduped, limitations };
122
+ }
@@ -0,0 +1,30 @@
1
+ export type ApprovalState = "pending" | "approved" | "denied" | "expired";
2
+ export type ApprovalRisk = "LOW" | "MEDIUM" | "HIGH" | "CRITICAL";
3
+ export interface ApprovalRecord {
4
+ id: string;
5
+ action: string;
6
+ reason: string;
7
+ resources: string[];
8
+ risk: ApprovalRisk;
9
+ requirement?: string;
10
+ state: ApprovalState;
11
+ actor?: string;
12
+ createdAt: string;
13
+ decidedAt?: string;
14
+ /** Must be true from trusted CLI/MCP session layer — never accept caller approved=true alone */
15
+ approvedByHuman: boolean;
16
+ }
17
+ export interface ApprovalEvaluationInput {
18
+ record: Omit<ApprovalRecord, "id" | "createdAt" | "state" | "approvedByHuman">;
19
+ /** Explicit human gate from CLI `--approve` or authenticated session layer */
20
+ approvedByHuman?: boolean;
21
+ actor?: string;
22
+ }
23
+ export interface ApprovalEvaluationResult {
24
+ record: ApprovalRecord;
25
+ allowed: boolean;
26
+ message: string;
27
+ limitations: string[];
28
+ }
29
+ export declare function evaluateApprovalRecord(input: ApprovalEvaluationInput): ApprovalEvaluationResult;
30
+ export declare function formatApprovalRecordSummary(record: ApprovalRecord): string;
@@ -0,0 +1,64 @@
1
+ const LIMITATIONS = [
2
+ "ApprovalRecord is an explicit abstraction — MCP/model callers cannot set approvedByHuman=true without a trusted session.",
3
+ "CLI passes approvedByHuman via --approve; dashboard chat does not grant approval.",
4
+ "This module does not persist approvals — use change ledger / audit trails separately.",
5
+ ];
6
+ function newId() {
7
+ return `apr_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
8
+ }
9
+ export function evaluateApprovalRecord(input) {
10
+ const approvedByHuman = input.approvedByHuman === true;
11
+ const now = new Date().toISOString();
12
+ const base = {
13
+ id: newId(),
14
+ action: input.record.action,
15
+ reason: input.record.reason,
16
+ resources: [...input.record.resources],
17
+ risk: input.record.risk,
18
+ ...(input.record.requirement !== undefined ? { requirement: input.record.requirement } : {}),
19
+ state: "pending",
20
+ ...(input.actor !== undefined ? { actor: input.actor } : {}),
21
+ createdAt: now,
22
+ approvedByHuman,
23
+ };
24
+ if (!approvedByHuman) {
25
+ return {
26
+ record: { ...base, state: "pending", approvedByHuman: false },
27
+ allowed: false,
28
+ message: "Human approval required — approvedByHuman must be true from trusted CLI/MCP session (not from model output).",
29
+ limitations: LIMITATIONS,
30
+ };
31
+ }
32
+ if (input.record.risk === "CRITICAL" && !input.record.reason.trim()) {
33
+ return {
34
+ record: { ...base, state: "denied", approvedByHuman: true, decidedAt: now },
35
+ allowed: false,
36
+ message: "CRITICAL actions require a non-empty reason even when approvedByHuman is true.",
37
+ limitations: LIMITATIONS,
38
+ };
39
+ }
40
+ return {
41
+ record: {
42
+ ...base,
43
+ state: "approved",
44
+ approvedByHuman: true,
45
+ decidedAt: now,
46
+ },
47
+ allowed: true,
48
+ message: "Explicit human approval recorded.",
49
+ limitations: LIMITATIONS,
50
+ };
51
+ }
52
+ export function formatApprovalRecordSummary(record) {
53
+ return [
54
+ `Approval ${record.id}`,
55
+ `Action: ${record.action}`,
56
+ `Risk: ${record.risk}`,
57
+ `State: ${record.state}`,
58
+ `approvedByHuman: ${record.approvedByHuman}`,
59
+ record.requirement ? `Requirement: ${record.requirement}` : "",
60
+ record.resources.length ? `Resources: ${record.resources.join(", ")}` : "",
61
+ ]
62
+ .filter(Boolean)
63
+ .join("\n");
64
+ }