@praneeth_54/agentdoctor 1.1.1 → 2.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.
Files changed (170) hide show
  1. package/CHANGELOG.md +95 -7
  2. package/README.md +163 -538
  3. package/dist/agents/aider/adapter.d.ts +1 -0
  4. package/dist/agents/aider/adapter.js +1 -0
  5. package/dist/agents/aider/detector.d.ts +14 -0
  6. package/dist/agents/aider/detector.js +120 -0
  7. package/dist/agents/copilot/adapter.d.ts +1 -0
  8. package/dist/agents/copilot/adapter.js +1 -0
  9. package/dist/agents/copilot/detector.d.ts +13 -0
  10. package/dist/agents/copilot/detector.js +113 -0
  11. package/dist/agents/gemini/adapter.d.ts +1 -0
  12. package/dist/agents/gemini/adapter.js +1 -0
  13. package/dist/agents/gemini/detector.d.ts +13 -0
  14. package/dist/agents/gemini/detector.js +151 -0
  15. package/dist/agents/registry.js +13 -1
  16. package/dist/agents/types.d.ts +1 -1
  17. package/dist/agents/windsurf/adapter.d.ts +1 -0
  18. package/dist/agents/windsurf/adapter.js +1 -0
  19. package/dist/agents/windsurf/detector.d.ts +14 -0
  20. package/dist/agents/windsurf/detector.js +119 -0
  21. package/dist/architecture/c4.d.ts +22 -0
  22. package/dist/architecture/c4.js +62 -0
  23. package/dist/cli/commands/brain.d.ts +12 -0
  24. package/dist/cli/commands/brain.js +162 -0
  25. package/dist/cli/commands/complete.d.ts +23 -0
  26. package/dist/cli/commands/complete.js +232 -0
  27. package/dist/cli/commands/explain.js +1 -1
  28. package/dist/cli/commands/fix.js +22 -2
  29. package/dist/cli/commands/mcp.d.ts +8 -0
  30. package/dist/cli/commands/mcp.js +14 -0
  31. package/dist/cli/commands/platform.d.ts +15 -0
  32. package/dist/cli/commands/platform.js +191 -0
  33. package/dist/cli/commands/v2.d.ts +55 -0
  34. package/dist/cli/commands/v2.js +278 -0
  35. package/dist/cli/program.js +809 -2
  36. package/dist/constants.d.ts +3 -2
  37. package/dist/constants.js +5 -1
  38. package/dist/contracts/adapters.d.ts +7 -0
  39. package/dist/contracts/adapters.js +60 -0
  40. package/dist/contracts/index.d.ts +162 -0
  41. package/dist/contracts/index.js +18 -0
  42. package/dist/core/baseline/store.d.ts +48 -0
  43. package/dist/core/baseline/store.js +163 -0
  44. package/dist/core/brain-cli/service.d.ts +26 -0
  45. package/dist/core/brain-cli/service.js +175 -0
  46. package/dist/core/brain-product/init.d.ts +45 -0
  47. package/dist/core/brain-product/init.js +232 -0
  48. package/dist/core/changes/analyze.d.ts +41 -0
  49. package/dist/core/changes/analyze.js +223 -0
  50. package/dist/core/context-health/analyze.d.ts +15 -0
  51. package/dist/core/context-health/analyze.js +234 -0
  52. package/dist/core/fix/apply.d.ts +21 -4
  53. package/dist/core/fix/apply.js +118 -18
  54. package/dist/core/fix/backup.d.ts +30 -0
  55. package/dist/core/fix/backup.js +193 -0
  56. package/dist/core/fix/plan.d.ts +6 -3
  57. package/dist/core/fix/plan.js +113 -9
  58. package/dist/core/fix/render.d.ts +2 -0
  59. package/dist/core/fix/render.js +36 -21
  60. package/dist/core/fix/run.js +5 -1
  61. package/dist/core/fix/safe-target.d.ts +14 -0
  62. package/dist/core/fix/safe-target.js +81 -0
  63. package/dist/core/fix/types.d.ts +12 -1
  64. package/dist/core/fix/types.js +2 -0
  65. package/dist/core/fix/writers/claude-settings.js +3 -1
  66. package/dist/core/fix/writers/codex-config.d.ts +3 -0
  67. package/dist/core/fix/writers/codex-config.js +24 -3
  68. package/dist/core/fix/writers/cursorignore.js +3 -1
  69. package/dist/core/fix/writers/simple-ignore.d.ts +16 -0
  70. package/dist/core/fix/writers/simple-ignore.js +68 -0
  71. package/dist/core/monorepo/detect.d.ts +13 -0
  72. package/dist/core/monorepo/detect.js +121 -0
  73. package/dist/core/rules/build-context.js +22 -3
  74. package/dist/core/rules/context/generated-directory.js +11 -1
  75. package/dist/core/rules/context/large-instruction-file.js +6 -0
  76. package/dist/core/rules/context/large-log-file.js +16 -1
  77. package/dist/core/rules/ignore.d.ts +11 -1
  78. package/dist/core/rules/ignore.js +22 -1
  79. package/dist/core/rules/instructions/empty-instructions.js +7 -0
  80. package/dist/core/rules/security/env-file-exposure.js +23 -2
  81. package/dist/core/rules/security/private-key-file.js +1 -1
  82. package/dist/core/scanner/scan.js +1 -1
  83. package/dist/core/schemas/validate.d.ts +6 -0
  84. package/dist/core/schemas/validate.js +62 -0
  85. package/dist/core/scoring/compute-scores.d.ts +1 -1
  86. package/dist/core/scoring/compute-scores.js +5 -1
  87. package/dist/core/scoring/placeholder.d.ts +1 -1
  88. package/dist/core/scoring/placeholder.js +5 -1
  89. package/dist/core/secrets/scan.d.ts +23 -0
  90. package/dist/core/secrets/scan.js +172 -0
  91. package/dist/dashboard/server.d.ts +24 -0
  92. package/dist/dashboard/server.js +386 -0
  93. package/dist/enforcement/runner.d.ts +23 -0
  94. package/dist/enforcement/runner.js +56 -0
  95. package/dist/index.d.ts +13 -0
  96. package/dist/index.js +12 -0
  97. package/dist/integrations/github/pr-analyze.d.ts +28 -0
  98. package/dist/integrations/github/pr-analyze.js +98 -0
  99. package/dist/integrations/local-ai/provider.d.ts +45 -0
  100. package/dist/integrations/local-ai/provider.js +104 -0
  101. package/dist/intelligence/git/analyze.d.ts +34 -0
  102. package/dist/intelligence/git/analyze.js +136 -0
  103. package/dist/intelligence/graph/build.d.ts +13 -0
  104. package/dist/intelligence/graph/build.js +215 -0
  105. package/dist/knowledge/store.d.ts +25 -0
  106. package/dist/knowledge/store.js +91 -0
  107. package/dist/mcp/agentdoctor/server.d.ts +16 -0
  108. package/dist/mcp/agentdoctor/server.js +67 -0
  109. package/dist/mcp/brain/session.js +7 -7
  110. package/dist/mcp/intelligence/handlers.d.ts +11 -0
  111. package/dist/mcp/intelligence/handlers.js +175 -0
  112. package/dist/mcp/intelligence/path-safety.d.ts +11 -0
  113. package/dist/mcp/intelligence/path-safety.js +82 -0
  114. package/dist/mcp/intelligence/registry.d.ts +8 -0
  115. package/dist/mcp/intelligence/registry.js +160 -0
  116. package/dist/ops/health.d.ts +18 -0
  117. package/dist/ops/health.js +51 -0
  118. package/dist/platform/ai-quality/analyze.d.ts +9 -0
  119. package/dist/platform/ai-quality/analyze.js +65 -0
  120. package/dist/platform/architecture/drift.d.ts +17 -0
  121. package/dist/platform/architecture/drift.js +74 -0
  122. package/dist/platform/auth/local.d.ts +15 -0
  123. package/dist/platform/auth/local.js +58 -0
  124. package/dist/platform/context-security/analyze.d.ts +6 -0
  125. package/dist/platform/context-security/analyze.js +97 -0
  126. package/dist/platform/firewall/evaluate.d.ts +54 -0
  127. package/dist/platform/firewall/evaluate.js +308 -0
  128. package/dist/platform/graph/build.d.ts +6 -0
  129. package/dist/platform/graph/build.js +193 -0
  130. package/dist/platform/health/analyze.d.ts +7 -0
  131. package/dist/platform/health/analyze.js +186 -0
  132. package/dist/platform/index.d.ts +27 -0
  133. package/dist/platform/index.js +87 -0
  134. package/dist/platform/knowledge/analyze.d.ts +20 -0
  135. package/dist/platform/knowledge/analyze.js +96 -0
  136. package/dist/platform/provenance/build.d.ts +44 -0
  137. package/dist/platform/provenance/build.js +65 -0
  138. package/dist/platform/readiness/scorecard.d.ts +5 -0
  139. package/dist/platform/readiness/scorecard.js +98 -0
  140. package/dist/platform/refactor/impact.d.ts +20 -0
  141. package/dist/platform/refactor/impact.js +60 -0
  142. package/dist/platform/reports/export.d.ts +12 -0
  143. package/dist/platform/reports/export.js +92 -0
  144. package/dist/platform/security/redact.d.ts +15 -0
  145. package/dist/platform/security/redact.js +87 -0
  146. package/dist/platform/sessions/store.d.ts +32 -0
  147. package/dist/platform/sessions/store.js +97 -0
  148. package/dist/platform/store.d.ts +6 -0
  149. package/dist/platform/store.js +57 -0
  150. package/dist/platform/test-impact/analyze.d.ts +32 -0
  151. package/dist/platform/test-impact/analyze.js +164 -0
  152. package/dist/platform/time-machine/compare.d.ts +17 -0
  153. package/dist/platform/time-machine/compare.js +62 -0
  154. package/dist/platform/tokens/plan.d.ts +28 -0
  155. package/dist/platform/tokens/plan.js +89 -0
  156. package/dist/platform/types.d.ts +111 -0
  157. package/dist/platform/types.js +5 -0
  158. package/dist/plugins/runtime.d.ts +36 -0
  159. package/dist/plugins/runtime.js +101 -0
  160. package/dist/plugins/sdk.d.ts +29 -0
  161. package/dist/plugins/sdk.js +114 -0
  162. package/dist/policy/packs.d.ts +9 -0
  163. package/dist/policy/packs.js +59 -0
  164. package/dist/reporters/terminal/report.js +38 -5
  165. package/dist/storage/provider.d.ts +35 -0
  166. package/dist/storage/provider.js +103 -0
  167. package/dist/team/auth.d.ts +35 -0
  168. package/dist/team/auth.js +70 -0
  169. package/dist/types/index.d.ts +5 -1
  170. package/package.json +8 -4
@@ -0,0 +1,54 @@
1
+ import type { AgentActionRequest, FirewallDecision, FirewallVerdict, PlatformSeverity } from "../types.js";
2
+ export interface FirewallPolicyRule {
3
+ id: string;
4
+ actionTypes?: AgentActionRequest["type"][];
5
+ match?: {
6
+ commandContains?: string[];
7
+ commandPatterns?: string[];
8
+ pathPrefix?: string[];
9
+ pathContains?: string[];
10
+ };
11
+ decision: FirewallDecision;
12
+ reason: string;
13
+ riskLevel: PlatformSeverity;
14
+ }
15
+ export interface FirewallPolicyDocument {
16
+ version: "2.0";
17
+ defaultDecision: FirewallDecision;
18
+ /** When true and policy file is invalid, deny all actions instead of falling back to defaults. */
19
+ failClosed?: boolean;
20
+ /**
21
+ * When set, shell actions that are not already blocked must match at least one allowlist entry
22
+ * (substring match, case-insensitive) or they receive require-approval.
23
+ */
24
+ shellAllowlist?: string[];
25
+ rules: FirewallPolicyRule[];
26
+ }
27
+ export interface PolicyLoadResult {
28
+ policy: FirewallPolicyDocument;
29
+ source: "file" | "default" | "fail-closed-deny";
30
+ validationError?: string;
31
+ }
32
+ declare const EVALUATE_ONLY = "Evaluate-only: no commands are executed and no agents are intercepted.";
33
+ declare const DEFAULT_POLICY: FirewallPolicyDocument;
34
+ export declare function validateFirewallPolicy(raw: unknown): {
35
+ ok: boolean;
36
+ policy?: FirewallPolicyDocument;
37
+ error?: string;
38
+ };
39
+ /**
40
+ * Load local action-policy document.
41
+ * Invalid file + failClosed (file flag or option) → deny-all.
42
+ * Invalid file + default → documented fallback DEFAULT_POLICY.
43
+ */
44
+ export declare function loadFirewallPolicy(root: string, options?: {
45
+ failClosed?: boolean;
46
+ }): Promise<PolicyLoadResult>;
47
+ /**
48
+ * Module C — Action Policy Evaluator (compatibility: firewall).
49
+ * Evaluates policies only; never executes the requested action and does not intercept agents.
50
+ */
51
+ export declare function evaluateAgentAction(root: string, action: AgentActionRequest, options?: {
52
+ failClosed?: boolean;
53
+ }): Promise<FirewallVerdict>;
54
+ export { DEFAULT_POLICY, EVALUATE_ONLY };
@@ -0,0 +1,308 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { platformDir, writeJsonArtifact } from "../store.js";
4
+ import { resolveRepoRoot } from "../../utils/path.js";
5
+ const EVALUATE_ONLY = "Evaluate-only: no commands are executed and no agents are intercepted.";
6
+ const DEFAULT_POLICY = {
7
+ version: "2.0",
8
+ defaultDecision: "require-approval",
9
+ failClosed: false,
10
+ shellAllowlist: ["npm test", "npm run test", "vitest", "eslint", "tsc", "prettier"],
11
+ rules: [
12
+ {
13
+ id: "block-secret-paths",
14
+ actionTypes: ["file-modify", "file-delete", "file-create", "secret-access"],
15
+ match: {
16
+ pathContains: [
17
+ ".env",
18
+ "id_rsa",
19
+ "id_ed25519",
20
+ "credentials",
21
+ "secrets/",
22
+ ".pem",
23
+ "service-account",
24
+ ],
25
+ },
26
+ decision: "block",
27
+ reason: "Secret-sensitive path",
28
+ riskLevel: "critical",
29
+ },
30
+ {
31
+ id: "deny-prod-deploy",
32
+ actionTypes: ["deploy", "infra"],
33
+ decision: "block",
34
+ reason: "Production/infra changes require out-of-band process",
35
+ riskLevel: "critical",
36
+ },
37
+ {
38
+ id: "network-default-deny",
39
+ actionTypes: ["network"],
40
+ decision: "deny-network",
41
+ reason: "Network access denied by default",
42
+ riskLevel: "high",
43
+ },
44
+ {
45
+ id: "package-install-approval",
46
+ actionTypes: ["package-install"],
47
+ decision: "require-approval",
48
+ reason: "Package installs change supply chain",
49
+ riskLevel: "high",
50
+ },
51
+ {
52
+ id: "allow-safe-dev-paths",
53
+ actionTypes: ["file-modify", "file-create"],
54
+ match: { pathPrefix: ["src/", "tests/", "docs/"] },
55
+ decision: "allow",
56
+ reason: "Allowlisted development paths",
57
+ riskLevel: "low",
58
+ },
59
+ ],
60
+ };
61
+ const BUILTIN_SHELL_BLOCKS = [
62
+ {
63
+ id: "builtin-rm-recursive",
64
+ patterns: [
65
+ /\brm\s+(-[^\s]*f[^\s]*r|-[^\s]*r[^\s]*f|--recursive)\b/i,
66
+ /\brm\s+-rf\b/i,
67
+ /\brm\s+-fr\b/i,
68
+ ],
69
+ reason: "Recursive deletion pattern",
70
+ },
71
+ {
72
+ id: "builtin-disk-format",
73
+ patterns: [/\bmkfs(\.\w+)?\b/i, /\bdd\s+if=/i, /\bshred\b/i],
74
+ reason: "Disk-formatting / destructive device write pattern",
75
+ },
76
+ {
77
+ id: "builtin-fork-bomb",
78
+ patterns: [/:\(\)\s*\{\s*:\|:&\s*\}\s*;?\s*:/, /\bfork\s*\(\s*\)\s*while/i],
79
+ reason: "Fork-bomb pattern",
80
+ },
81
+ {
82
+ id: "builtin-deploy",
83
+ patterns: [
84
+ /\b(kubectl\s+apply|helm\s+upgrade|terraform\s+apply|pulumi\s+up)\b/i,
85
+ /\b(aws\s+s3\s+sync|gcloud\s+deploy|az\s+deployment)\b/i,
86
+ /\bdocker\s+push\b/i,
87
+ /\bnpm\s+publish\b/i,
88
+ ],
89
+ reason: "Deployment / publish command pattern",
90
+ },
91
+ {
92
+ id: "builtin-network-exfil",
93
+ patterns: [
94
+ /\bcurl\b.*\b(-d|--data|--upload-file|-T)\b/i,
95
+ /\bwget\b.*\b(--post-data|--body-data)\b/i,
96
+ /\bnc\s+-l\b/i,
97
+ /\bcurl\b.*\bhttps?:\/\/[^\s]*(webhook|exfil|pastebin|evil)/i,
98
+ ],
99
+ reason: "Network exfiltration-style pattern",
100
+ },
101
+ ];
102
+ function isFirewallDecision(value) {
103
+ return (value === "allow" ||
104
+ value === "block" ||
105
+ value === "require-approval" ||
106
+ value === "sandbox-only" ||
107
+ value === "deny-network" ||
108
+ value === "deny-secrets");
109
+ }
110
+ export function validateFirewallPolicy(raw) {
111
+ if (!raw || typeof raw !== "object")
112
+ return { ok: false, error: "policy must be an object" };
113
+ const doc = raw;
114
+ if (doc.version !== "2.0")
115
+ return { ok: false, error: 'policy.version must be "2.0"' };
116
+ if (!Array.isArray(doc.rules))
117
+ return { ok: false, error: "policy.rules must be an array" };
118
+ if (!isFirewallDecision(doc.defaultDecision)) {
119
+ return { ok: false, error: "policy.defaultDecision is invalid" };
120
+ }
121
+ for (const [i, rule] of doc.rules.entries()) {
122
+ if (!rule || typeof rule !== "object") {
123
+ return { ok: false, error: `rules[${i}] must be an object` };
124
+ }
125
+ const r = rule;
126
+ if (typeof r.id !== "string" || !r.id)
127
+ return { ok: false, error: `rules[${i}].id required` };
128
+ if (!isFirewallDecision(r.decision)) {
129
+ return { ok: false, error: `rules[${i}].decision invalid` };
130
+ }
131
+ }
132
+ if (doc.shellAllowlist !== undefined && !Array.isArray(doc.shellAllowlist)) {
133
+ return { ok: false, error: "shellAllowlist must be an array of strings" };
134
+ }
135
+ return {
136
+ ok: true,
137
+ policy: {
138
+ version: "2.0",
139
+ defaultDecision: doc.defaultDecision,
140
+ failClosed: Boolean(doc.failClosed),
141
+ ...(Array.isArray(doc.shellAllowlist)
142
+ ? { shellAllowlist: doc.shellAllowlist.filter((s) => typeof s === "string") }
143
+ : {}),
144
+ rules: doc.rules,
145
+ },
146
+ };
147
+ }
148
+ function denyAllPolicy(reason) {
149
+ return {
150
+ version: "2.0",
151
+ defaultDecision: "block",
152
+ failClosed: true,
153
+ rules: [
154
+ {
155
+ id: "fail-closed-deny-all",
156
+ decision: "block",
157
+ reason,
158
+ riskLevel: "critical",
159
+ },
160
+ ],
161
+ };
162
+ }
163
+ /**
164
+ * Load local action-policy document.
165
+ * Invalid file + failClosed (file flag or option) → deny-all.
166
+ * Invalid file + default → documented fallback DEFAULT_POLICY.
167
+ */
168
+ export async function loadFirewallPolicy(root, options) {
169
+ const resolved = resolveRepoRoot(root);
170
+ const file = path.join(platformDir(resolved), "firewall-policy.json");
171
+ try {
172
+ const raw = await fs.readFile(file, "utf8");
173
+ let parsed;
174
+ try {
175
+ parsed = JSON.parse(raw);
176
+ }
177
+ catch {
178
+ const failClosed = options?.failClosed === true;
179
+ if (failClosed) {
180
+ return {
181
+ policy: denyAllPolicy("Malformed policy JSON; fail-closed deny"),
182
+ source: "fail-closed-deny",
183
+ validationError: "Malformed JSON",
184
+ };
185
+ }
186
+ await writeJsonArtifact(resolved, "firewall-policy.json", DEFAULT_POLICY);
187
+ return {
188
+ policy: DEFAULT_POLICY,
189
+ source: "default",
190
+ validationError: "Malformed JSON; wrote default policy",
191
+ };
192
+ }
193
+ const validated = validateFirewallPolicy(parsed);
194
+ if (!validated.ok || !validated.policy) {
195
+ const fileWantsFailClosed = parsed &&
196
+ typeof parsed === "object" &&
197
+ parsed.failClosed === true;
198
+ const failClosed = options?.failClosed === true || fileWantsFailClosed;
199
+ if (failClosed) {
200
+ return {
201
+ policy: denyAllPolicy(`Invalid policy: ${validated.error}`),
202
+ source: "fail-closed-deny",
203
+ ...(validated.error ? { validationError: validated.error } : {}),
204
+ };
205
+ }
206
+ await writeJsonArtifact(resolved, "firewall-policy.json", DEFAULT_POLICY);
207
+ return {
208
+ policy: DEFAULT_POLICY,
209
+ source: "default",
210
+ ...(validated.error ? { validationError: validated.error } : {}),
211
+ };
212
+ }
213
+ return { policy: validated.policy, source: "file" };
214
+ }
215
+ catch {
216
+ await writeJsonArtifact(resolved, "firewall-policy.json", DEFAULT_POLICY);
217
+ return { policy: DEFAULT_POLICY, source: "default" };
218
+ }
219
+ }
220
+ function ruleMatches(rule, action) {
221
+ if (rule.actionTypes && !rule.actionTypes.includes(action.type))
222
+ return false;
223
+ const cmd = action.params.command ?? "";
224
+ const target = action.params.path ?? action.params.target ?? "";
225
+ const match = rule.match;
226
+ if (!match)
227
+ return true;
228
+ if (match.commandContains?.length) {
229
+ if (!match.commandContains.some((p) => cmd.toLowerCase().includes(p.toLowerCase()))) {
230
+ return false;
231
+ }
232
+ }
233
+ if (match.commandPatterns?.length) {
234
+ if (!match.commandPatterns.some((p) => {
235
+ try {
236
+ return new RegExp(p, "i").test(cmd);
237
+ }
238
+ catch {
239
+ return false;
240
+ }
241
+ })) {
242
+ return false;
243
+ }
244
+ }
245
+ if (match.pathPrefix?.length) {
246
+ if (!match.pathPrefix.some((p) => target.startsWith(p)))
247
+ return false;
248
+ }
249
+ if (match.pathContains?.length) {
250
+ if (!match.pathContains.some((p) => target.toLowerCase().includes(p.toLowerCase()))) {
251
+ return false;
252
+ }
253
+ }
254
+ return true;
255
+ }
256
+ function matchBuiltinShell(cmd) {
257
+ for (const rule of BUILTIN_SHELL_BLOCKS) {
258
+ if (rule.patterns.some((re) => re.test(cmd))) {
259
+ return { id: rule.id, reason: rule.reason };
260
+ }
261
+ }
262
+ return null;
263
+ }
264
+ function verdict(actionId, decision, reason, riskLevel, policyId) {
265
+ return {
266
+ actionId,
267
+ decision,
268
+ reason: `${reason}. ${EVALUATE_ONLY}`,
269
+ riskLevel,
270
+ ...(policyId ? { policyId } : {}),
271
+ approvalStatus: decision === "require-approval" ? "pending" : "not-required",
272
+ executionResult: "not-executed",
273
+ };
274
+ }
275
+ /**
276
+ * Module C — Action Policy Evaluator (compatibility: firewall).
277
+ * Evaluates policies only; never executes the requested action and does not intercept agents.
278
+ */
279
+ export async function evaluateAgentAction(root, action, options) {
280
+ const loaded = await loadFirewallPolicy(root, options);
281
+ if (action.type === "shell") {
282
+ const cmd = action.params.command ?? "";
283
+ const builtin = matchBuiltinShell(cmd);
284
+ if (builtin) {
285
+ return verdict(action.actionId, "block", builtin.reason, "critical", builtin.id);
286
+ }
287
+ }
288
+ for (const rule of loaded.policy.rules) {
289
+ if (!ruleMatches(rule, action))
290
+ continue;
291
+ return verdict(action.actionId, rule.decision, rule.reason, rule.riskLevel, rule.id);
292
+ }
293
+ if (action.type === "shell" && loaded.policy.shellAllowlist?.length) {
294
+ const cmd = (action.params.command ?? "").toLowerCase();
295
+ const allowed = loaded.policy.shellAllowlist.some((entry) => cmd.includes(entry.toLowerCase()));
296
+ if (!allowed) {
297
+ return verdict(action.actionId, "require-approval", "Shell command not on local allowlist", "medium", "shell-allowlist");
298
+ }
299
+ return verdict(action.actionId, "allow", "Shell command matches local allowlist", "low", "shell-allowlist");
300
+ }
301
+ const reason = loaded.source === "fail-closed-deny"
302
+ ? `Fail-closed deny (${loaded.validationError ?? "invalid policy"})`
303
+ : loaded.source === "default" && loaded.validationError
304
+ ? `No specific rule matched; default policy applied (fallback after: ${loaded.validationError})`
305
+ : "No specific rule matched; default policy applied";
306
+ return verdict(action.actionId, loaded.policy.defaultDecision, reason, loaded.source === "fail-closed-deny" ? "critical" : "medium", loaded.source === "fail-closed-deny" ? "fail-closed-deny-all" : "default");
307
+ }
308
+ export { DEFAULT_POLICY, EVALUATE_ONLY };
@@ -0,0 +1,6 @@
1
+ import type { RepositoryGraph } from "../types.js";
2
+ /**
3
+ * Module A — Repository Intelligence (local graph MVP).
4
+ * Symbol extraction is heuristic (regex), not a full language AST.
5
+ */
6
+ export declare function buildRepositoryGraph(rootInput: string): Promise<RepositoryGraph>;
@@ -0,0 +1,193 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { createHash } from "node:crypto";
4
+ import { resolveRepoRoot, toPosixRelative } from "../../utils/path.js";
5
+ const SKIP = new Set([
6
+ "node_modules",
7
+ ".git",
8
+ "dist",
9
+ "coverage",
10
+ ".agentdoctor",
11
+ "vendor",
12
+ ".next",
13
+ "build",
14
+ ]);
15
+ const CODE_EXT = new Set([
16
+ ".ts",
17
+ ".tsx",
18
+ ".js",
19
+ ".jsx",
20
+ ".mjs",
21
+ ".cjs",
22
+ ".py",
23
+ ".go",
24
+ ".rs",
25
+ ".java",
26
+ ".kt",
27
+ ".php",
28
+ ".rb",
29
+ ]);
30
+ function nodeId(kind, key) {
31
+ return `${kind}:${createHash("sha1").update(key).digest("hex").slice(0, 12)}`;
32
+ }
33
+ async function walk(root, dir, files, dirs, limit) {
34
+ if (files.length >= limit)
35
+ return;
36
+ let entries;
37
+ try {
38
+ entries = await fs.readdir(dir, { withFileTypes: true });
39
+ }
40
+ catch {
41
+ return;
42
+ }
43
+ for (const entry of entries) {
44
+ if (SKIP.has(entry.name))
45
+ continue;
46
+ const absolute = path.join(dir, entry.name);
47
+ if (entry.isSymbolicLink())
48
+ continue;
49
+ if (entry.isDirectory()) {
50
+ dirs.push(absolute);
51
+ await walk(root, absolute, files, dirs, limit);
52
+ continue;
53
+ }
54
+ if (entry.isFile())
55
+ files.push(absolute);
56
+ }
57
+ }
58
+ function extractSymbols(content, rel) {
59
+ const nodes = [];
60
+ const fnRe = /(?:export\s+)?(?:async\s+)?function\s+([A-Za-z_][A-Za-z0-9_]*)|([A-Za-z_][A-Za-z0-9_]*)\s*=\s*(?:async\s*)?\(/g;
61
+ const classRe = /(?:export\s+)?class\s+([A-Za-z_][A-Za-z0-9_]*)/g;
62
+ let m;
63
+ while ((m = fnRe.exec(content))) {
64
+ const name = m[1] ?? m[2];
65
+ if (!name || name.length < 2)
66
+ continue;
67
+ nodes.push({
68
+ id: nodeId("function", `${rel}:${name}`),
69
+ kind: "function",
70
+ label: name,
71
+ path: rel,
72
+ });
73
+ }
74
+ while ((m = classRe.exec(content))) {
75
+ const name = m[1];
76
+ nodes.push({
77
+ id: nodeId("class", `${rel}:${name}`),
78
+ kind: "class",
79
+ label: name,
80
+ path: rel,
81
+ });
82
+ }
83
+ return nodes;
84
+ }
85
+ function extractImports(content, rel) {
86
+ const edges = [];
87
+ const dependencyNodes = [];
88
+ const importRe = /(?:import\s+.*?from\s+|require\()\s*['"]([^'"]+)['"]|from\s+['"]([^'"]+)['"]/g;
89
+ let m;
90
+ const fromId = nodeId("file", rel);
91
+ const seen = new Set();
92
+ while ((m = importRe.exec(content))) {
93
+ const spec = m[1] ?? m[2];
94
+ if (!spec)
95
+ continue;
96
+ const toId = nodeId("dependency", `${rel}->${spec}`);
97
+ edges.push({
98
+ id: nodeId("edge", `${fromId}->${toId}`),
99
+ from: fromId,
100
+ to: toId,
101
+ kind: "imports",
102
+ evidence: spec.startsWith(".") ? "verified" : "inferred",
103
+ });
104
+ if (!seen.has(toId)) {
105
+ seen.add(toId);
106
+ dependencyNodes.push({
107
+ id: toId,
108
+ kind: "dependency",
109
+ label: spec,
110
+ path: rel,
111
+ meta: { specifier: spec },
112
+ });
113
+ }
114
+ }
115
+ return { edges, dependencyNodes };
116
+ }
117
+ /**
118
+ * Module A — Repository Intelligence (local graph MVP).
119
+ * Symbol extraction is heuristic (regex), not a full language AST.
120
+ */
121
+ export async function buildRepositoryGraph(rootInput) {
122
+ const root = resolveRepoRoot(rootInput);
123
+ const files = [];
124
+ const dirs = [];
125
+ await walk(root, root, files, dirs, 2_000);
126
+ const nodes = [];
127
+ const edges = [];
128
+ const limitations = [
129
+ "Symbol extraction is regex-heuristic for common languages; not a full AST",
130
+ "Call-graph and multi-repo federation are extension points (not fully built)",
131
+ ];
132
+ for (const dir of dirs) {
133
+ const rel = toPosixRelative(root, dir) || ".";
134
+ nodes.push({
135
+ id: nodeId("directory", rel),
136
+ kind: "directory",
137
+ label: path.basename(dir),
138
+ path: rel,
139
+ });
140
+ }
141
+ for (const absolute of files) {
142
+ const rel = toPosixRelative(root, absolute);
143
+ const ext = path.extname(rel).toLowerCase();
144
+ const base = path.basename(rel).toLowerCase();
145
+ let kind = "file";
146
+ if (/\.(test|spec)\./i.test(rel) || rel.includes("__tests__"))
147
+ kind = "test";
148
+ else if (/\.(md|rst|txt)$/i.test(ext))
149
+ kind = "doc";
150
+ else if (/\.(yml|yaml|toml|json|env)$/i.test(ext) ||
151
+ base.startsWith("dockerfile") ||
152
+ base === "makefile")
153
+ kind = "config";
154
+ const fileId = nodeId("file", rel);
155
+ nodes.push({ id: fileId, kind, label: path.basename(rel), path: rel });
156
+ if (CODE_EXT.has(ext)) {
157
+ try {
158
+ const st = await fs.stat(absolute);
159
+ if (st.size > 256 * 1024)
160
+ continue;
161
+ const content = await fs.readFile(absolute, "utf8");
162
+ nodes.push(...extractSymbols(content, rel));
163
+ const { edges: importEdges, dependencyNodes } = extractImports(content, rel);
164
+ edges.push(...importEdges);
165
+ for (const dep of dependencyNodes) {
166
+ if (!nodes.some((n) => n.id === dep.id))
167
+ nodes.push(dep);
168
+ }
169
+ if (/router\.|app\.(get|post|put|delete)|@app\.(get|post)/i.test(content)) {
170
+ nodes.push({
171
+ id: nodeId("api", rel),
172
+ kind: "api",
173
+ label: `api:${path.basename(rel)}`,
174
+ path: rel,
175
+ meta: { evidence: "inferred" },
176
+ });
177
+ }
178
+ }
179
+ catch {
180
+ // skip unreadable
181
+ }
182
+ }
183
+ }
184
+ nodes.sort((a, b) => a.id.localeCompare(b.id));
185
+ edges.sort((a, b) => a.id.localeCompare(b.id));
186
+ return {
187
+ root,
188
+ generatedAt: new Date().toISOString(),
189
+ nodes,
190
+ edges,
191
+ limitations,
192
+ };
193
+ }
@@ -0,0 +1,7 @@
1
+ import type { PlatformFinding } from "../types.js";
2
+ import type { RepositoryGraph } from "../types.js";
3
+ /**
4
+ * Module B — Code health & risk (deterministic heuristics with evidence).
5
+ */
6
+ export declare function analyzeCodeHealth(rootInput: string, graph: RepositoryGraph): Promise<PlatformFinding[]>;
7
+ export declare function scoreFileRisk(findings: PlatformFinding[], filePath: string): number;