sentinelayer-cli 0.8.0 → 0.8.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 (153) hide show
  1. package/README.md +13 -0
  2. package/package.json +4 -4
  3. package/src/agents/ai-governance/index.js +12 -0
  4. package/src/agents/ai-governance/tools/base.js +171 -0
  5. package/src/agents/ai-governance/tools/eval-regression.js +47 -0
  6. package/src/agents/ai-governance/tools/hitl-audit.js +81 -0
  7. package/src/agents/ai-governance/tools/index.js +52 -0
  8. package/src/agents/ai-governance/tools/prompt-drift.js +42 -0
  9. package/src/agents/ai-governance/tools/provenance-check.js +69 -0
  10. package/src/agents/backend/index.js +12 -0
  11. package/src/agents/backend/tools/base.js +189 -0
  12. package/src/agents/backend/tools/circuit-breaker-check.js +123 -0
  13. package/src/agents/backend/tools/idempotency-audit.js +105 -0
  14. package/src/agents/backend/tools/index.js +87 -0
  15. package/src/agents/backend/tools/retry-audit.js +132 -0
  16. package/src/agents/backend/tools/timeout-audit.js +144 -0
  17. package/src/agents/code-quality/index.js +12 -0
  18. package/src/agents/code-quality/tools/base.js +159 -0
  19. package/src/agents/code-quality/tools/complexity-measure.js +197 -0
  20. package/src/agents/code-quality/tools/coupling-analysis.js +81 -0
  21. package/src/agents/code-quality/tools/cycle-detect.js +49 -0
  22. package/src/agents/code-quality/tools/dep-graph.js +196 -0
  23. package/src/agents/code-quality/tools/index.js +89 -0
  24. package/src/agents/data-layer/index.js +12 -0
  25. package/src/agents/data-layer/tools/base.js +181 -0
  26. package/src/agents/data-layer/tools/index-audit.js +165 -0
  27. package/src/agents/data-layer/tools/index.js +83 -0
  28. package/src/agents/data-layer/tools/migration-scan.js +135 -0
  29. package/src/agents/data-layer/tools/query-explain.js +120 -0
  30. package/src/agents/data-layer/tools/tenancy-scan.js +166 -0
  31. package/src/agents/documentation/index.js +12 -0
  32. package/src/agents/documentation/tools/api-diff.js +91 -0
  33. package/src/agents/documentation/tools/base.js +151 -0
  34. package/src/agents/documentation/tools/dead-link-check.js +58 -0
  35. package/src/agents/documentation/tools/docstring-coverage.js +78 -0
  36. package/src/agents/documentation/tools/index.js +52 -0
  37. package/src/agents/documentation/tools/readme-freshness.js +61 -0
  38. package/src/agents/envelope/fix-cycle.js +45 -0
  39. package/src/agents/envelope/index.js +31 -0
  40. package/src/agents/envelope/loop.js +150 -0
  41. package/src/agents/envelope/pulse.js +18 -0
  42. package/src/agents/envelope/stream.js +40 -0
  43. package/src/agents/infrastructure/index.js +12 -0
  44. package/src/agents/infrastructure/tools/base.js +171 -0
  45. package/src/agents/infrastructure/tools/checkov-run.js +32 -0
  46. package/src/agents/infrastructure/tools/drift-detect.js +59 -0
  47. package/src/agents/infrastructure/tools/iam-least-priv-check.js +78 -0
  48. package/src/agents/infrastructure/tools/index.js +52 -0
  49. package/src/agents/infrastructure/tools/tflint-run.js +31 -0
  50. package/src/agents/jules/loop.js +7 -4
  51. package/src/agents/jules/swarm/sub-agent.js +5 -1
  52. package/src/agents/jules/tools/auth-audit.js +10 -1
  53. package/src/agents/mode.js +113 -0
  54. package/src/agents/observability/index.js +12 -0
  55. package/src/agents/observability/tools/alert-audit.js +39 -0
  56. package/src/agents/observability/tools/base.js +181 -0
  57. package/src/agents/observability/tools/dashboard-gap.js +42 -0
  58. package/src/agents/observability/tools/index.js +54 -0
  59. package/src/agents/observability/tools/log-schema-check.js +74 -0
  60. package/src/agents/observability/tools/span-coverage.js +74 -0
  61. package/src/agents/persona-visuals.js +38 -0
  62. package/src/agents/release/index.js +12 -0
  63. package/src/agents/release/tools/base.js +181 -0
  64. package/src/agents/release/tools/changelog-diff.js +86 -0
  65. package/src/agents/release/tools/feature-flag-audit.js +126 -0
  66. package/src/agents/release/tools/index.js +61 -0
  67. package/src/agents/release/tools/rollback-verify.js +129 -0
  68. package/src/agents/release/tools/semver-check.js +109 -0
  69. package/src/agents/reliability/index.js +12 -0
  70. package/src/agents/reliability/tools/backpressure-check.js +129 -0
  71. package/src/agents/reliability/tools/base.js +181 -0
  72. package/src/agents/reliability/tools/chaos-probe.js +109 -0
  73. package/src/agents/reliability/tools/graceful-degradation-check.js +114 -0
  74. package/src/agents/reliability/tools/health-check-audit.js +111 -0
  75. package/src/agents/reliability/tools/index.js +87 -0
  76. package/src/agents/run-persona.js +109 -0
  77. package/src/agents/security/index.js +12 -0
  78. package/src/agents/security/tools/authz-audit.js +134 -0
  79. package/src/agents/security/tools/base.js +190 -0
  80. package/src/agents/security/tools/crypto-review.js +175 -0
  81. package/src/agents/security/tools/index.js +97 -0
  82. package/src/agents/security/tools/sast-scan.js +175 -0
  83. package/src/agents/security/tools/secrets-scan.js +216 -0
  84. package/src/agents/supply-chain/index.js +12 -0
  85. package/src/agents/supply-chain/tools/attestation-check.js +42 -0
  86. package/src/agents/supply-chain/tools/base.js +151 -0
  87. package/src/agents/supply-chain/tools/index.js +52 -0
  88. package/src/agents/supply-chain/tools/lockfile-integrity.js +73 -0
  89. package/src/agents/supply-chain/tools/package-verify.js +56 -0
  90. package/src/agents/supply-chain/tools/sbom-diff.js +34 -0
  91. package/src/agents/testing/index.js +12 -0
  92. package/src/agents/testing/tools/base.js +202 -0
  93. package/src/agents/testing/tools/coverage-gap.js +144 -0
  94. package/src/agents/testing/tools/flake-detect.js +125 -0
  95. package/src/agents/testing/tools/index.js +85 -0
  96. package/src/agents/testing/tools/mutation-test.js +143 -0
  97. package/src/agents/testing/tools/snapshot-diff.js +103 -0
  98. package/src/auth/gate.js +65 -37
  99. package/src/cli.js +1 -1
  100. package/src/commands/chat.js +3 -10
  101. package/src/commands/legacy-args.js +10 -0
  102. package/src/commands/omargate.js +36 -2
  103. package/src/commands/persona.js +46 -1
  104. package/src/commands/scan.js +3 -10
  105. package/src/commands/session.js +654 -6
  106. package/src/commands/spec.js +3 -10
  107. package/src/coord/events-log.js +141 -0
  108. package/src/coord/handshake.js +719 -0
  109. package/src/coord/index.js +35 -0
  110. package/src/coord/paths.js +84 -0
  111. package/src/coord/priority.js +62 -0
  112. package/src/coord/tarjan.js +157 -0
  113. package/src/cost/tokenizer.js +160 -0
  114. package/src/cost/tracker.js +61 -0
  115. package/src/daemon/artifact-lineage.js +362 -0
  116. package/src/daemon/assignment-ledger.js +117 -0
  117. package/src/daemon/ast-drift.js +496 -0
  118. package/src/daemon/ingest-refresh.js +69 -2
  119. package/src/ingest/engine.js +15 -0
  120. package/src/ingest/ownership.js +380 -0
  121. package/src/legacy-cli.js +68 -1
  122. package/src/orchestrator/kai-chen.js +126 -0
  123. package/src/review/ai-review.js +3 -10
  124. package/src/review/compliance-pack.js +389 -0
  125. package/src/review/investor-dd-config.js +54 -0
  126. package/src/review/investor-dd-file-loop.js +303 -0
  127. package/src/review/investor-dd-file-router.js +406 -0
  128. package/src/review/investor-dd-html-report.js +233 -0
  129. package/src/review/investor-dd-notification.js +120 -0
  130. package/src/review/investor-dd-orchestrator.js +405 -0
  131. package/src/review/investor-dd-persona-runner.js +275 -0
  132. package/src/review/live-validator.js +253 -0
  133. package/src/review/omargate-orchestrator.js +90 -2
  134. package/src/review/persona-prompts.js +244 -56
  135. package/src/review/reconciliation-rules.js +329 -0
  136. package/src/review/reproducibility-chain.js +136 -0
  137. package/src/review/scan-modes.js +102 -3
  138. package/src/session/agent-registry.js +7 -0
  139. package/src/session/analytics.js +479 -0
  140. package/src/session/daemon.js +609 -14
  141. package/src/session/file-locks.js +666 -0
  142. package/src/session/paths.js +4 -0
  143. package/src/session/recap.js +567 -0
  144. package/src/session/redact.js +82 -0
  145. package/src/session/runtime-bridge.js +24 -1
  146. package/src/session/scoring.js +406 -0
  147. package/src/session/setup-guides.js +304 -0
  148. package/src/session/store.js +318 -2
  149. package/src/session/stream.js +9 -1
  150. package/src/session/sync.js +753 -0
  151. package/src/session/tasks.js +1054 -0
  152. package/src/session/templates.js +188 -0
  153. package/src/swarm/runtime.js +1 -8
@@ -0,0 +1,303 @@
1
+ /**
2
+ * Per-file agentic review loop for investor-DD.
3
+ *
4
+ * Given a persona config, a list of files in scope, and an LLM-backed client,
5
+ * iterate file-by-file, running a bounded multi-turn tool-using loop per file.
6
+ * Emits structured events for session streaming and accumulates structured
7
+ * findings plus coverage proof (every file visited, how many turns, which
8
+ * tools were invoked).
9
+ *
10
+ * No fix-cycle path — review personas never mutate source. Tools are
11
+ * constrained to the caller-supplied list; the library does not import any
12
+ * edit/write tools.
13
+ *
14
+ * Budget: caller supplies a shared budget accumulator that is decremented
15
+ * on every tool call and LLM call. When the budget trips, the loop stops
16
+ * cleanly at the current file boundary so a partial-report generator can
17
+ * still emit what was finished.
18
+ */
19
+
20
+ import { runEnvelopeLoop } from "../agents/envelope/index.js";
21
+
22
+ export const INVESTOR_DD_DEFAULT_MAX_TURNS_PER_FILE = 6;
23
+ export const INVESTOR_DD_DEFAULT_STUCK_THRESHOLD = 2;
24
+
25
+ /**
26
+ * @typedef {object} InvestorDdBudgetState
27
+ * @property {number} spentUsd - Running USD spend.
28
+ * @property {number} maxUsd - Hard cap.
29
+ * @property {number} startedAtMs - Epoch ms when the run began.
30
+ * @property {number} maxRuntimeMs - Hard cap on runtime.
31
+ * @property {number} toolCalls - Running count of tool invocations.
32
+ * @property {number} llmCalls - Running count of LLM invocations.
33
+ */
34
+
35
+ /**
36
+ * @typedef {object} InvestorDdFileLoopEvent
37
+ * @property {string} type
38
+ * @property {string} personaId
39
+ * @property {string} file
40
+ * @property {number} [turn]
41
+ * @property {string} [tool]
42
+ * @property {object} [finding]
43
+ * @property {string} [stopReason]
44
+ * @property {number} [turnsUsed]
45
+ */
46
+
47
+ /**
48
+ * @typedef {object} InvestorDdFileLoopResult
49
+ * @property {string} personaId
50
+ * @property {Array<{file: string, findings: Array<object>, turnsUsed: number, stopReason: string|null, toolInvocations: Array<object>}>} perFile
51
+ * @property {Array<object>} findings - Flat list of all findings.
52
+ * @property {Array<string>} visited - Files the loop actually visited.
53
+ * @property {Array<string>} skipped - Files skipped because budget was exhausted.
54
+ * @property {"ok"|"budget-cost-exhausted"|"budget-runtime-exhausted"|"client-error"} terminationReason
55
+ */
56
+
57
+ /**
58
+ * Check whether the shared budget still permits further work. When false,
59
+ * the loop stops at the current file boundary and reports the remaining
60
+ * files as `skipped` so the caller can emit a partial report.
61
+ *
62
+ * @param {InvestorDdBudgetState} budget
63
+ * @returns {{ ok: true } | { ok: false, reason: "budget-cost-exhausted" | "budget-runtime-exhausted" }}
64
+ */
65
+ export function checkBudget(budget) {
66
+ if (!budget) return { ok: true };
67
+ if (Number.isFinite(budget.maxUsd) && budget.spentUsd >= budget.maxUsd) {
68
+ return { ok: false, reason: "budget-cost-exhausted" };
69
+ }
70
+ if (Number.isFinite(budget.maxRuntimeMs) && Number.isFinite(budget.startedAtMs)) {
71
+ const elapsed = Date.now() - budget.startedAtMs;
72
+ if (elapsed >= budget.maxRuntimeMs) {
73
+ return { ok: false, reason: "budget-runtime-exhausted" };
74
+ }
75
+ }
76
+ return { ok: true };
77
+ }
78
+
79
+ /**
80
+ * Instantiate a fresh budget state from caller-supplied caps.
81
+ *
82
+ * @param {object} [opts]
83
+ * @param {number} [opts.maxUsd]
84
+ * @param {number} [opts.maxRuntimeMs]
85
+ * @returns {InvestorDdBudgetState}
86
+ */
87
+ export function createBudgetState({ maxUsd = Infinity, maxRuntimeMs = Infinity } = {}) {
88
+ return {
89
+ spentUsd: 0,
90
+ maxUsd,
91
+ startedAtMs: Date.now(),
92
+ maxRuntimeMs,
93
+ toolCalls: 0,
94
+ llmCalls: 0,
95
+ };
96
+ }
97
+
98
+ /**
99
+ * Wrap the caller's tool array so every invocation increments the shared
100
+ * budget counters. The wrapping does not alter tool contract; it only
101
+ * observes + accounts.
102
+ *
103
+ * @param {Array<{name: string, invoke: Function, costUsd?: number}>} tools
104
+ * @param {InvestorDdBudgetState} budget
105
+ * @param {Function} onToolCall - (name, input) => void
106
+ */
107
+ function meterTools(tools, budget, onToolCall) {
108
+ return tools.map((tool) => ({
109
+ ...tool,
110
+ invoke: async (input) => {
111
+ budget.toolCalls += 1;
112
+ if (Number.isFinite(tool.costUsd)) {
113
+ budget.spentUsd += tool.costUsd;
114
+ }
115
+ try {
116
+ onToolCall(tool.name, input);
117
+ } catch {
118
+ // observer errors never break review
119
+ }
120
+ return tool.invoke(input);
121
+ },
122
+ }));
123
+ }
124
+
125
+ /**
126
+ * Wrap the caller's LLM client so every generatePlan call increments the
127
+ * llmCalls counter. Cost accounting for LLM calls is the client's
128
+ * responsibility (it knows the model and tokens), so the client adds to
129
+ * `budget.spentUsd` directly.
130
+ *
131
+ * @param {object} client
132
+ * @param {InvestorDdBudgetState} budget
133
+ */
134
+ function meterClient(client, budget) {
135
+ return {
136
+ ...client,
137
+ generatePlan: async (messages, options) => {
138
+ budget.llmCalls += 1;
139
+ return client.generatePlan(messages, options);
140
+ },
141
+ };
142
+ }
143
+
144
+ /**
145
+ * Run the per-file agentic review loop for a single persona.
146
+ *
147
+ * @param {object} params
148
+ * @param {string} params.personaId
149
+ * @param {Array<string>} params.files - Files in scope for this persona.
150
+ * @param {object} params.client - Must implement generatePlan().
151
+ * @param {(file: string) => Array<{name: string, invoke: Function, costUsd?: number}>} params.buildTools
152
+ * Factory that returns the tool list scoped to a single file. Called once per file.
153
+ * @param {(file: string) => Array<object>} params.buildInitialMessages
154
+ * Factory that returns the LLM messages to seed the loop for this file.
155
+ * @param {InvestorDdBudgetState} params.budget - Shared budget state.
156
+ * @param {(event: InvestorDdFileLoopEvent) => void} [params.onEvent] - Event sink.
157
+ * @param {object} [params.options]
158
+ * @param {number} [params.options.maxTurnsPerFile]
159
+ * @param {number} [params.options.stuckThreshold]
160
+ * @returns {Promise<InvestorDdFileLoopResult>}
161
+ */
162
+ export async function runPerFileReviewLoop({
163
+ personaId,
164
+ files,
165
+ client,
166
+ buildTools,
167
+ buildInitialMessages,
168
+ budget,
169
+ onEvent = () => {},
170
+ options = {},
171
+ } = {}) {
172
+ if (!personaId || typeof personaId !== "string") {
173
+ throw new TypeError("runPerFileReviewLoop requires a personaId string");
174
+ }
175
+ if (!Array.isArray(files)) {
176
+ throw new TypeError("runPerFileReviewLoop requires a files array");
177
+ }
178
+ if (typeof buildTools !== "function") {
179
+ throw new TypeError("runPerFileReviewLoop requires buildTools(file) factory");
180
+ }
181
+ if (typeof buildInitialMessages !== "function") {
182
+ throw new TypeError("runPerFileReviewLoop requires buildInitialMessages(file) factory");
183
+ }
184
+ if (!client || typeof client.generatePlan !== "function") {
185
+ throw new TypeError("runPerFileReviewLoop requires a client with generatePlan()");
186
+ }
187
+
188
+ const maxTurns = Number.isInteger(options.maxTurnsPerFile)
189
+ ? options.maxTurnsPerFile
190
+ : INVESTOR_DD_DEFAULT_MAX_TURNS_PER_FILE;
191
+ const stuckThreshold = Number.isInteger(options.stuckThreshold)
192
+ ? options.stuckThreshold
193
+ : INVESTOR_DD_DEFAULT_STUCK_THRESHOLD;
194
+
195
+ const safeBudget = budget || createBudgetState();
196
+ const meteredClient = meterClient(client, safeBudget);
197
+
198
+ const perFile = [];
199
+ const allFindings = [];
200
+ const visited = [];
201
+ const skipped = [];
202
+ let terminationReason = "ok";
203
+
204
+ const emit = (event) => {
205
+ try {
206
+ onEvent(event);
207
+ } catch {
208
+ // sinks never break review
209
+ }
210
+ };
211
+
212
+ for (const file of files) {
213
+ const budgetCheck = checkBudget(safeBudget);
214
+ if (!budgetCheck.ok) {
215
+ terminationReason = budgetCheck.reason;
216
+ skipped.push(file);
217
+ emit({ type: "persona_file_skipped", personaId, file, stopReason: budgetCheck.reason });
218
+ continue;
219
+ }
220
+
221
+ emit({ type: "persona_file_start", personaId, file });
222
+
223
+ const fileTools = buildTools(file);
224
+ const meteredTools = meterTools(fileTools, safeBudget, (tool, input) => {
225
+ emit({ type: "persona_file_tool_call", personaId, file, tool, input });
226
+ });
227
+ const initialMessages = buildInitialMessages(file);
228
+
229
+ let loopResult;
230
+ try {
231
+ loopResult = await runEnvelopeLoop({
232
+ client: meteredClient,
233
+ initialMessages,
234
+ tools: meteredTools,
235
+ options: {
236
+ maxTurns,
237
+ stuckThreshold,
238
+ shouldAllowCall: () => {
239
+ const check = checkBudget(safeBudget);
240
+ return { allow: check.ok };
241
+ },
242
+ onTurn: ({ turn, plan }) => {
243
+ emit({
244
+ type: "persona_file_turn",
245
+ personaId,
246
+ file,
247
+ turn,
248
+ stopReason: plan?.stopReason ?? null,
249
+ });
250
+ const findings = Array.isArray(plan?.findings) ? plan.findings : [];
251
+ for (const f of findings) {
252
+ const decorated = { ...f, personaId, file };
253
+ allFindings.push(decorated);
254
+ emit({ type: "persona_finding", personaId, file, finding: decorated });
255
+ }
256
+ },
257
+ },
258
+ });
259
+ } catch (err) {
260
+ terminationReason = "client-error";
261
+ emit({
262
+ type: "persona_file_error",
263
+ personaId,
264
+ file,
265
+ stopReason: err instanceof Error ? err.message : String(err),
266
+ });
267
+ perFile.push({
268
+ file,
269
+ findings: [],
270
+ turnsUsed: 0,
271
+ stopReason: "client-error",
272
+ toolInvocations: [],
273
+ });
274
+ continue;
275
+ }
276
+
277
+ visited.push(file);
278
+ perFile.push({
279
+ file,
280
+ findings: Array.isArray(loopResult.findings) ? loopResult.findings : [],
281
+ turnsUsed: loopResult.turnsUsed ?? 0,
282
+ stopReason: loopResult.stuckReason ?? null,
283
+ toolInvocations: Array.isArray(loopResult.toolInvocations) ? loopResult.toolInvocations : [],
284
+ });
285
+
286
+ emit({
287
+ type: "persona_file_complete",
288
+ personaId,
289
+ file,
290
+ turnsUsed: loopResult.turnsUsed ?? 0,
291
+ stopReason: loopResult.stuckReason ?? null,
292
+ });
293
+ }
294
+
295
+ return {
296
+ personaId,
297
+ perFile,
298
+ findings: allFindings,
299
+ visited,
300
+ skipped,
301
+ terminationReason,
302
+ };
303
+ }
@@ -0,0 +1,406 @@
1
+ /**
2
+ * Deterministic file routing engine for investor-DD.
3
+ *
4
+ * Given the full list of files in a target repo and the persona roster,
5
+ * produce a routing table `{ personaId: filesInScope[] }` based on
6
+ * domain-specific include/exclude patterns. The router is deterministic
7
+ * (same inputs → same routing) so a run is replayable, and overlap is
8
+ * allowed — a single file can land in multiple persona queues if the
9
+ * patterns match.
10
+ *
11
+ * When a persona has NO matches after pattern filtering, the router
12
+ * falls back to a capped subset of "risk-surface" files (entry points,
13
+ * config, routes) so the persona still has something to look at rather
14
+ * than silently reporting empty coverage.
15
+ */
16
+
17
+ const POSIX_SEP = "/";
18
+
19
+ /**
20
+ * Per-persona include/exclude rules. Rules are substring checks against
21
+ * POSIX-normalized relative paths (so they survive Windows/linux). Glob
22
+ * matchers are intentionally avoided here — every rule is a simple
23
+ * includes() check so the routing is easy to reason about and unit-test.
24
+ */
25
+ export const INVESTOR_DD_PERSONA_RULES = Object.freeze({
26
+ security: {
27
+ include: [
28
+ "/auth",
29
+ "/security",
30
+ "/crypto",
31
+ "/token",
32
+ "/password",
33
+ "/session",
34
+ "/login",
35
+ "/oauth",
36
+ "/permission",
37
+ "/role",
38
+ "/sanitiz",
39
+ "/escape",
40
+ "/middleware",
41
+ ],
42
+ extensions: [".js", ".ts", ".tsx", ".jsx", ".py", ".go", ".rs", ".java"],
43
+ exclude: ["/__fixtures__/", "/test-data/"],
44
+ },
45
+ backend: {
46
+ include: [
47
+ "/server",
48
+ "/api",
49
+ "/handler",
50
+ "/route",
51
+ "/controller",
52
+ "/service",
53
+ "/worker",
54
+ "/queue",
55
+ "/job",
56
+ "/middleware",
57
+ ],
58
+ extensions: [".js", ".ts", ".py", ".go", ".rs", ".java"],
59
+ exclude: ["/__fixtures__/", "/test-data/", "/web/", "/frontend/"],
60
+ },
61
+ "code-quality": {
62
+ include: [
63
+ "/src/",
64
+ "/lib/",
65
+ "/app/",
66
+ "/packages/",
67
+ ],
68
+ extensions: [".js", ".ts", ".tsx", ".jsx", ".py", ".go", ".rs", ".java"],
69
+ exclude: ["/node_modules/", "/dist/", "/build/", "/__snapshots__/", "/vendor/"],
70
+ },
71
+ testing: {
72
+ include: [
73
+ "/test/",
74
+ "/tests/",
75
+ "/__tests__/",
76
+ ".test.",
77
+ ".spec.",
78
+ "_test.",
79
+ "/conftest.py",
80
+ ],
81
+ extensions: [".js", ".ts", ".tsx", ".jsx", ".mjs", ".py", ".go", ".rs", ".java"],
82
+ exclude: ["/node_modules/", "/dist/"],
83
+ },
84
+ "data-layer": {
85
+ include: [
86
+ "/db/",
87
+ "/database/",
88
+ "/models/",
89
+ "/schema",
90
+ "/migration",
91
+ "/query",
92
+ "/repository",
93
+ "/repositories/",
94
+ "/dao",
95
+ ".sql",
96
+ "/prisma/",
97
+ "/sequelize/",
98
+ "/orm/",
99
+ ],
100
+ extensions: [".js", ".ts", ".py", ".sql", ".prisma"],
101
+ exclude: ["/node_modules/"],
102
+ },
103
+ reliability: {
104
+ include: [
105
+ "/health",
106
+ "/readiness",
107
+ "/liveness",
108
+ "/retry",
109
+ "/circuit",
110
+ "/fallback",
111
+ "/backpressure",
112
+ "/rate-limit",
113
+ "/degradation",
114
+ ],
115
+ extensions: [".js", ".ts", ".py", ".go"],
116
+ exclude: ["/node_modules/"],
117
+ },
118
+ release: {
119
+ include: [
120
+ ".github/workflows/",
121
+ "/ci/",
122
+ "CHANGELOG",
123
+ "/release",
124
+ "/deploy",
125
+ "/rollout",
126
+ "/version",
127
+ "/feature-flag",
128
+ "/feature_flag",
129
+ "/flags",
130
+ ],
131
+ extensions: [".yml", ".yaml", ".js", ".ts", ".py", ".md"],
132
+ exclude: [],
133
+ },
134
+ observability: {
135
+ include: [
136
+ "/logger",
137
+ "/logging",
138
+ "/metric",
139
+ "/trace",
140
+ "/telemetry",
141
+ "/span",
142
+ "/dashboard",
143
+ "/grafana",
144
+ "/alert",
145
+ "/monitor",
146
+ ],
147
+ extensions: [".js", ".ts", ".py", ".go", ".yml", ".yaml", ".json"],
148
+ exclude: [],
149
+ },
150
+ infrastructure: {
151
+ include: [
152
+ "/terraform/",
153
+ ".tf",
154
+ ".tfvars",
155
+ "/kubernetes/",
156
+ "/k8s/",
157
+ "/manifests/",
158
+ "/helm/",
159
+ "/docker",
160
+ "Dockerfile",
161
+ ".github/workflows/",
162
+ "/cdk/",
163
+ "/pulumi/",
164
+ "/serverless",
165
+ ],
166
+ extensions: [".tf", ".yaml", ".yml", ".json", ".hcl"],
167
+ exclude: [],
168
+ },
169
+ "supply-chain": {
170
+ include: [
171
+ "package.json",
172
+ "package-lock.json",
173
+ "yarn.lock",
174
+ "pnpm-lock.yaml",
175
+ "requirements.txt",
176
+ "pyproject.toml",
177
+ "Pipfile.lock",
178
+ "go.mod",
179
+ "go.sum",
180
+ "Cargo.toml",
181
+ "Cargo.lock",
182
+ "Gemfile",
183
+ "Gemfile.lock",
184
+ ".github/workflows/",
185
+ "SBOM",
186
+ "sbom",
187
+ ],
188
+ extensions: [],
189
+ exclude: ["/node_modules/"],
190
+ },
191
+ frontend: {
192
+ include: [
193
+ "/frontend/",
194
+ "/web/",
195
+ "/client/",
196
+ "/pages/",
197
+ "/components/",
198
+ "/app/",
199
+ "/ui/",
200
+ "/views/",
201
+ "/templates/",
202
+ ".vue",
203
+ ".svelte",
204
+ ".astro",
205
+ ],
206
+ extensions: [".tsx", ".jsx", ".ts", ".js", ".vue", ".svelte", ".html", ".css", ".scss"],
207
+ exclude: ["/server/", "/api/", "/node_modules/"],
208
+ },
209
+ documentation: {
210
+ include: [
211
+ "README",
212
+ "CHANGELOG",
213
+ "CONTRIBUTING",
214
+ "SECURITY",
215
+ "CODE_OF_CONDUCT",
216
+ "/docs/",
217
+ ".md",
218
+ ".mdx",
219
+ ".rst",
220
+ ],
221
+ extensions: [".md", ".mdx", ".rst", ".txt"],
222
+ exclude: ["/node_modules/"],
223
+ },
224
+ "ai-governance": {
225
+ include: [
226
+ "/prompt",
227
+ "/prompts/",
228
+ "/llm/",
229
+ "/ai/",
230
+ "/agent",
231
+ "/eval",
232
+ "/evals/",
233
+ "/guardrail",
234
+ "/safety",
235
+ "/completion",
236
+ "/inference",
237
+ "/embeddings",
238
+ "/rag",
239
+ ],
240
+ extensions: [".py", ".js", ".ts", ".yaml", ".yml", ".json", ".md"],
241
+ exclude: [],
242
+ },
243
+ });
244
+
245
+ const DEFAULT_FALLBACK_CAP = 20;
246
+
247
+ const RISK_SURFACE_HINTS = Object.freeze([
248
+ "/server",
249
+ "/api",
250
+ "/main",
251
+ "/index",
252
+ "/app",
253
+ "/route",
254
+ "/handler",
255
+ "/auth",
256
+ "Dockerfile",
257
+ "/.github/workflows/",
258
+ ]);
259
+
260
+ /**
261
+ * Normalize a path to POSIX separators so the substring rules are
262
+ * portable across OSes.
263
+ *
264
+ * @param {string} p
265
+ * @returns {string}
266
+ */
267
+ export function toPosix(p) {
268
+ return String(p || "").split(/[\\/]/).join(POSIX_SEP);
269
+ }
270
+
271
+ /**
272
+ * Return true if `file` matches the persona rule: includes any of the
273
+ * include substrings AND (extensions empty OR matches an extension) AND
274
+ * excludes none of the exclude substrings.
275
+ *
276
+ * @param {string} file - POSIX-normalized path.
277
+ * @param {object} rule
278
+ * @param {string[]} rule.include
279
+ * @param {string[]} rule.exclude
280
+ * @param {string[]} rule.extensions
281
+ * @returns {boolean}
282
+ */
283
+ export function matchesRule(file, rule) {
284
+ if (!rule) return false;
285
+ const include = rule.include || [];
286
+ const exclude = rule.exclude || [];
287
+ const extensions = rule.extensions || [];
288
+
289
+ for (const pattern of exclude) {
290
+ if (file.includes(pattern)) return false;
291
+ }
292
+
293
+ let includeMatch = false;
294
+ for (const pattern of include) {
295
+ if (file.includes(pattern)) {
296
+ includeMatch = true;
297
+ break;
298
+ }
299
+ }
300
+ if (!includeMatch) return false;
301
+
302
+ if (extensions.length === 0) return true;
303
+
304
+ for (const ext of extensions) {
305
+ if (file.endsWith(ext)) return true;
306
+ }
307
+
308
+ // Files with no extension (e.g. Dockerfile, Makefile, Procfile) should
309
+ // still match when the rule's include pattern explicitly names them —
310
+ // the include hit already proved domain relevance.
311
+ const basename = file.split(POSIX_SEP).pop() || "";
312
+ if (!basename.includes(".")) {
313
+ for (const pattern of include) {
314
+ if (basename === pattern || basename.includes(pattern)) return true;
315
+ }
316
+ }
317
+ return false;
318
+ }
319
+
320
+ /**
321
+ * Simple heuristic scoring for fallback ranking. Higher = more risk-
322
+ * surface-like. Path segments known to harbor crosscutting concerns
323
+ * (auth/server/api/middleware/index/main) score higher.
324
+ *
325
+ * @param {string} file
326
+ * @returns {number}
327
+ */
328
+ export function scoreRiskSurface(file) {
329
+ let score = 0;
330
+ for (const hint of RISK_SURFACE_HINTS) {
331
+ if (file.includes(hint)) score += 10;
332
+ }
333
+ // Prefer shallower files (closer to repo root → more likely entry points)
334
+ const depth = file.split(POSIX_SEP).length;
335
+ score += Math.max(0, 10 - depth);
336
+ return score;
337
+ }
338
+
339
+ /**
340
+ * Route the full file list to each persona's queue.
341
+ *
342
+ * @param {object} params
343
+ * @param {string[]} params.files - All candidate files (relative POSIX paths or raw).
344
+ * @param {string[]} params.personas - Persona IDs to route to.
345
+ * @param {object} [params.rules] - Custom rule map (defaults to INVESTOR_DD_PERSONA_RULES).
346
+ * @param {number} [params.fallbackCap] - Max fallback files when rule yields 0 (default 20).
347
+ * @returns {Record<string, string[]>} - { personaId: filesInScope[] }
348
+ */
349
+ export function routeFilesToPersonas({
350
+ files = [],
351
+ personas = [],
352
+ rules = INVESTOR_DD_PERSONA_RULES,
353
+ fallbackCap = DEFAULT_FALLBACK_CAP,
354
+ } = {}) {
355
+ const normalized = files.map(toPosix);
356
+ const routing = {};
357
+
358
+ // Pre-compute fallback list once: risk-surface-sorted, capped.
359
+ const fallbackPool = [...normalized]
360
+ .sort((a, b) => scoreRiskSurface(b) - scoreRiskSurface(a))
361
+ .slice(0, fallbackCap);
362
+
363
+ for (const personaId of personas) {
364
+ const rule = rules[personaId];
365
+ if (!rule) {
366
+ routing[personaId] = [];
367
+ continue;
368
+ }
369
+
370
+ const matched = normalized.filter((f) => matchesRule(f, rule));
371
+ routing[personaId] = matched.length > 0 ? matched : [...fallbackPool];
372
+ }
373
+
374
+ return routing;
375
+ }
376
+
377
+ /**
378
+ * Produce a dense coverage summary suitable for persisting as part of a
379
+ * run's `plan.json` artifact.
380
+ *
381
+ * @param {Record<string, string[]>} routing
382
+ * @returns {{ totalFilesByPersona: Record<string, number>, uniqueFiles: number, dedupIndex: Record<string, string[]> }}
383
+ */
384
+ export function summarizeRouting(routing) {
385
+ const totalFilesByPersona = {};
386
+ const fileToPersonas = new Map();
387
+
388
+ for (const [personaId, files] of Object.entries(routing || {})) {
389
+ totalFilesByPersona[personaId] = files.length;
390
+ for (const file of files) {
391
+ if (!fileToPersonas.has(file)) fileToPersonas.set(file, []);
392
+ fileToPersonas.get(file).push(personaId);
393
+ }
394
+ }
395
+
396
+ const dedupIndex = {};
397
+ for (const [file, personas] of fileToPersonas.entries()) {
398
+ dedupIndex[file] = [...personas];
399
+ }
400
+
401
+ return {
402
+ totalFilesByPersona,
403
+ uniqueFiles: fileToPersonas.size,
404
+ dedupIndex,
405
+ };
406
+ }