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.
- package/README.md +13 -0
- package/package.json +4 -4
- package/src/agents/ai-governance/index.js +12 -0
- package/src/agents/ai-governance/tools/base.js +171 -0
- package/src/agents/ai-governance/tools/eval-regression.js +47 -0
- package/src/agents/ai-governance/tools/hitl-audit.js +81 -0
- package/src/agents/ai-governance/tools/index.js +52 -0
- package/src/agents/ai-governance/tools/prompt-drift.js +42 -0
- package/src/agents/ai-governance/tools/provenance-check.js +69 -0
- package/src/agents/backend/index.js +12 -0
- package/src/agents/backend/tools/base.js +189 -0
- package/src/agents/backend/tools/circuit-breaker-check.js +123 -0
- package/src/agents/backend/tools/idempotency-audit.js +105 -0
- package/src/agents/backend/tools/index.js +87 -0
- package/src/agents/backend/tools/retry-audit.js +132 -0
- package/src/agents/backend/tools/timeout-audit.js +144 -0
- package/src/agents/code-quality/index.js +12 -0
- package/src/agents/code-quality/tools/base.js +159 -0
- package/src/agents/code-quality/tools/complexity-measure.js +197 -0
- package/src/agents/code-quality/tools/coupling-analysis.js +81 -0
- package/src/agents/code-quality/tools/cycle-detect.js +49 -0
- package/src/agents/code-quality/tools/dep-graph.js +196 -0
- package/src/agents/code-quality/tools/index.js +89 -0
- package/src/agents/data-layer/index.js +12 -0
- package/src/agents/data-layer/tools/base.js +181 -0
- package/src/agents/data-layer/tools/index-audit.js +165 -0
- package/src/agents/data-layer/tools/index.js +83 -0
- package/src/agents/data-layer/tools/migration-scan.js +135 -0
- package/src/agents/data-layer/tools/query-explain.js +120 -0
- package/src/agents/data-layer/tools/tenancy-scan.js +166 -0
- package/src/agents/documentation/index.js +12 -0
- package/src/agents/documentation/tools/api-diff.js +91 -0
- package/src/agents/documentation/tools/base.js +151 -0
- package/src/agents/documentation/tools/dead-link-check.js +58 -0
- package/src/agents/documentation/tools/docstring-coverage.js +78 -0
- package/src/agents/documentation/tools/index.js +52 -0
- package/src/agents/documentation/tools/readme-freshness.js +61 -0
- package/src/agents/envelope/fix-cycle.js +45 -0
- package/src/agents/envelope/index.js +31 -0
- package/src/agents/envelope/loop.js +150 -0
- package/src/agents/envelope/pulse.js +18 -0
- package/src/agents/envelope/stream.js +40 -0
- package/src/agents/infrastructure/index.js +12 -0
- package/src/agents/infrastructure/tools/base.js +171 -0
- package/src/agents/infrastructure/tools/checkov-run.js +32 -0
- package/src/agents/infrastructure/tools/drift-detect.js +59 -0
- package/src/agents/infrastructure/tools/iam-least-priv-check.js +78 -0
- package/src/agents/infrastructure/tools/index.js +52 -0
- package/src/agents/infrastructure/tools/tflint-run.js +31 -0
- package/src/agents/jules/loop.js +7 -4
- package/src/agents/jules/swarm/sub-agent.js +5 -1
- package/src/agents/jules/tools/auth-audit.js +10 -1
- package/src/agents/mode.js +113 -0
- package/src/agents/observability/index.js +12 -0
- package/src/agents/observability/tools/alert-audit.js +39 -0
- package/src/agents/observability/tools/base.js +181 -0
- package/src/agents/observability/tools/dashboard-gap.js +42 -0
- package/src/agents/observability/tools/index.js +54 -0
- package/src/agents/observability/tools/log-schema-check.js +74 -0
- package/src/agents/observability/tools/span-coverage.js +74 -0
- package/src/agents/persona-visuals.js +38 -0
- package/src/agents/release/index.js +12 -0
- package/src/agents/release/tools/base.js +181 -0
- package/src/agents/release/tools/changelog-diff.js +86 -0
- package/src/agents/release/tools/feature-flag-audit.js +126 -0
- package/src/agents/release/tools/index.js +61 -0
- package/src/agents/release/tools/rollback-verify.js +129 -0
- package/src/agents/release/tools/semver-check.js +109 -0
- package/src/agents/reliability/index.js +12 -0
- package/src/agents/reliability/tools/backpressure-check.js +129 -0
- package/src/agents/reliability/tools/base.js +181 -0
- package/src/agents/reliability/tools/chaos-probe.js +109 -0
- package/src/agents/reliability/tools/graceful-degradation-check.js +114 -0
- package/src/agents/reliability/tools/health-check-audit.js +111 -0
- package/src/agents/reliability/tools/index.js +87 -0
- package/src/agents/run-persona.js +109 -0
- package/src/agents/security/index.js +12 -0
- package/src/agents/security/tools/authz-audit.js +134 -0
- package/src/agents/security/tools/base.js +190 -0
- package/src/agents/security/tools/crypto-review.js +175 -0
- package/src/agents/security/tools/index.js +97 -0
- package/src/agents/security/tools/sast-scan.js +175 -0
- package/src/agents/security/tools/secrets-scan.js +216 -0
- package/src/agents/supply-chain/index.js +12 -0
- package/src/agents/supply-chain/tools/attestation-check.js +42 -0
- package/src/agents/supply-chain/tools/base.js +151 -0
- package/src/agents/supply-chain/tools/index.js +52 -0
- package/src/agents/supply-chain/tools/lockfile-integrity.js +73 -0
- package/src/agents/supply-chain/tools/package-verify.js +56 -0
- package/src/agents/supply-chain/tools/sbom-diff.js +34 -0
- package/src/agents/testing/index.js +12 -0
- package/src/agents/testing/tools/base.js +202 -0
- package/src/agents/testing/tools/coverage-gap.js +144 -0
- package/src/agents/testing/tools/flake-detect.js +125 -0
- package/src/agents/testing/tools/index.js +85 -0
- package/src/agents/testing/tools/mutation-test.js +143 -0
- package/src/agents/testing/tools/snapshot-diff.js +103 -0
- package/src/auth/gate.js +65 -37
- package/src/cli.js +1 -1
- package/src/commands/chat.js +3 -10
- package/src/commands/legacy-args.js +10 -0
- package/src/commands/omargate.js +36 -2
- package/src/commands/persona.js +46 -1
- package/src/commands/scan.js +3 -10
- package/src/commands/session.js +654 -6
- package/src/commands/spec.js +3 -10
- package/src/coord/events-log.js +141 -0
- package/src/coord/handshake.js +719 -0
- package/src/coord/index.js +35 -0
- package/src/coord/paths.js +84 -0
- package/src/coord/priority.js +62 -0
- package/src/coord/tarjan.js +157 -0
- package/src/cost/tokenizer.js +160 -0
- package/src/cost/tracker.js +61 -0
- package/src/daemon/artifact-lineage.js +362 -0
- package/src/daemon/assignment-ledger.js +117 -0
- package/src/daemon/ast-drift.js +496 -0
- package/src/daemon/ingest-refresh.js +69 -2
- package/src/ingest/engine.js +15 -0
- package/src/ingest/ownership.js +380 -0
- package/src/legacy-cli.js +68 -1
- package/src/orchestrator/kai-chen.js +126 -0
- package/src/review/ai-review.js +3 -10
- package/src/review/compliance-pack.js +389 -0
- package/src/review/investor-dd-config.js +54 -0
- package/src/review/investor-dd-file-loop.js +303 -0
- package/src/review/investor-dd-file-router.js +406 -0
- package/src/review/investor-dd-html-report.js +233 -0
- package/src/review/investor-dd-notification.js +120 -0
- package/src/review/investor-dd-orchestrator.js +405 -0
- package/src/review/investor-dd-persona-runner.js +275 -0
- package/src/review/live-validator.js +253 -0
- package/src/review/omargate-orchestrator.js +90 -2
- package/src/review/persona-prompts.js +244 -56
- package/src/review/reconciliation-rules.js +329 -0
- package/src/review/reproducibility-chain.js +136 -0
- package/src/review/scan-modes.js +102 -3
- package/src/session/agent-registry.js +7 -0
- package/src/session/analytics.js +479 -0
- package/src/session/daemon.js +609 -14
- package/src/session/file-locks.js +666 -0
- package/src/session/paths.js +4 -0
- package/src/session/recap.js +567 -0
- package/src/session/redact.js +82 -0
- package/src/session/runtime-bridge.js +24 -1
- package/src/session/scoring.js +406 -0
- package/src/session/setup-guides.js +304 -0
- package/src/session/store.js +318 -2
- package/src/session/stream.js +9 -1
- package/src/session/sync.js +753 -0
- package/src/session/tasks.js +1054 -0
- package/src/session/templates.js +188 -0
- 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
|
+
}
|