sentinelayer-cli 0.8.0 → 0.8.2
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 +23 -2
- 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,405 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Investor-DD top-level orchestrator (#investor-dd-5 / -16 / -18).
|
|
3
|
+
*
|
|
4
|
+
* Wires the file router, per-persona runner, and streaming event sink
|
|
5
|
+
* into a single entry point that the CLI `investor-dd` subcommand can
|
|
6
|
+
* call. Produces an artifact bundle under `<outputDir>/investor-dd/`:
|
|
7
|
+
*
|
|
8
|
+
* plan.json — router output: { personaId: filesInScope[] }
|
|
9
|
+
* stream.ndjson — full event stream from the run
|
|
10
|
+
* persona-<id>.json — per-persona findings + coverage proof
|
|
11
|
+
* findings.json — flat list across all personas (dedup in PR-29)
|
|
12
|
+
* summary.json — run metadata (timings, cost, terminationReason)
|
|
13
|
+
* report.md — human-readable summary
|
|
14
|
+
* manifest.json — SHA-256 chain of every artifact
|
|
15
|
+
*
|
|
16
|
+
* The orchestrator is reproducible: given the same repo state + routing
|
|
17
|
+
* rules, it produces the same artifacts and finding IDs. The LLM-driven
|
|
18
|
+
* layer (PR-IDD-<TBD>) sits on top and only activates when --ai is passed.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import fsp from "node:fs/promises";
|
|
22
|
+
import path from "node:path";
|
|
23
|
+
import crypto from "node:crypto";
|
|
24
|
+
|
|
25
|
+
import { routeFilesToPersonas, summarizeRouting } from "./investor-dd-file-router.js";
|
|
26
|
+
import { runAllPersonas } from "./investor-dd-persona-runner.js";
|
|
27
|
+
import { createBudgetState } from "./investor-dd-file-loop.js";
|
|
28
|
+
import { resolveInvestorDdBudget, INVESTOR_DD_ARTIFACT_SUBDIR } from "./investor-dd-config.js";
|
|
29
|
+
import { runFullCompliancePack, COMPLIANCE_PACK_CATALOG } from "./compliance-pack.js";
|
|
30
|
+
import { reconcileFindings, applyReportPolicy } from "./reconciliation-rules.js";
|
|
31
|
+
import {
|
|
32
|
+
discoverInteractiveElements,
|
|
33
|
+
runLiveValidator,
|
|
34
|
+
buildObservationIndex,
|
|
35
|
+
createFindingObservationPair,
|
|
36
|
+
} from "./live-validator.js";
|
|
37
|
+
import { notifyRunCompleted } from "./investor-dd-notification.js";
|
|
38
|
+
import { attachReproducibilityChain } from "./reproducibility-chain.js";
|
|
39
|
+
import { renderInvestorDdHtml } from "./investor-dd-html-report.js";
|
|
40
|
+
|
|
41
|
+
const INVESTOR_DD_PERSONAS = Object.freeze([
|
|
42
|
+
"security",
|
|
43
|
+
"backend",
|
|
44
|
+
"code-quality",
|
|
45
|
+
"testing",
|
|
46
|
+
"data-layer",
|
|
47
|
+
"reliability",
|
|
48
|
+
"release",
|
|
49
|
+
"observability",
|
|
50
|
+
"infrastructure",
|
|
51
|
+
"supply-chain",
|
|
52
|
+
"documentation",
|
|
53
|
+
"ai-governance",
|
|
54
|
+
]);
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Walk the target repo and return a list of relative POSIX file paths.
|
|
58
|
+
* Skips common noise directories.
|
|
59
|
+
*/
|
|
60
|
+
async function walkRepoFiles(rootPath) {
|
|
61
|
+
const SKIP_DIRS = new Set([
|
|
62
|
+
"node_modules",
|
|
63
|
+
".git",
|
|
64
|
+
"dist",
|
|
65
|
+
"build",
|
|
66
|
+
"coverage",
|
|
67
|
+
".sentinelayer",
|
|
68
|
+
".next",
|
|
69
|
+
".turbo",
|
|
70
|
+
"__pycache__",
|
|
71
|
+
".pytest_cache",
|
|
72
|
+
".venv",
|
|
73
|
+
"venv",
|
|
74
|
+
]);
|
|
75
|
+
const SKIP_EXT = new Set([
|
|
76
|
+
".png",
|
|
77
|
+
".jpg",
|
|
78
|
+
".jpeg",
|
|
79
|
+
".gif",
|
|
80
|
+
".webp",
|
|
81
|
+
".ico",
|
|
82
|
+
".svg",
|
|
83
|
+
".pdf",
|
|
84
|
+
".zip",
|
|
85
|
+
".tar",
|
|
86
|
+
".gz",
|
|
87
|
+
".mp4",
|
|
88
|
+
".webm",
|
|
89
|
+
".woff",
|
|
90
|
+
".woff2",
|
|
91
|
+
".ttf",
|
|
92
|
+
]);
|
|
93
|
+
|
|
94
|
+
const results = [];
|
|
95
|
+
async function walk(absDir, relDir) {
|
|
96
|
+
let entries;
|
|
97
|
+
try {
|
|
98
|
+
entries = await fsp.readdir(absDir, { withFileTypes: true });
|
|
99
|
+
} catch {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
for (const entry of entries) {
|
|
103
|
+
if (entry.name.startsWith(".") && !entry.name.startsWith(".github")) {
|
|
104
|
+
if (entry.name !== ".gitignore" && entry.name !== ".env.example") continue;
|
|
105
|
+
}
|
|
106
|
+
const relPath = relDir ? `${relDir}/${entry.name}` : entry.name;
|
|
107
|
+
const absPath = path.join(absDir, entry.name);
|
|
108
|
+
if (entry.isDirectory()) {
|
|
109
|
+
if (SKIP_DIRS.has(entry.name)) continue;
|
|
110
|
+
await walk(absPath, relPath);
|
|
111
|
+
} else if (entry.isFile()) {
|
|
112
|
+
const ext = path.extname(entry.name).toLowerCase();
|
|
113
|
+
if (SKIP_EXT.has(ext)) continue;
|
|
114
|
+
results.push(relPath);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
await walk(rootPath, "");
|
|
119
|
+
return results;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function writeJson(filePath, obj) {
|
|
123
|
+
await fsp.mkdir(path.dirname(filePath), { recursive: true });
|
|
124
|
+
await fsp.writeFile(filePath, JSON.stringify(obj, null, 2), "utf-8");
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function sha256(text) {
|
|
128
|
+
return crypto.createHash("sha256").update(text).digest("hex");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Emit a summary markdown report. The detailed HTML/PDF variants are
|
|
133
|
+
* separate PRs (PR-IDD-18 follow-ups).
|
|
134
|
+
*/
|
|
135
|
+
function buildSummaryMarkdown({ runId, summary, routing, byPersona }) {
|
|
136
|
+
const lines = [];
|
|
137
|
+
lines.push(`# Investor-DD Report — ${runId}`);
|
|
138
|
+
lines.push("");
|
|
139
|
+
lines.push(`Generated: ${summary.startedAt}`);
|
|
140
|
+
lines.push(`Duration: ${summary.durationSeconds.toFixed(1)}s`);
|
|
141
|
+
lines.push(`Status: ${summary.terminationReason}`);
|
|
142
|
+
lines.push("");
|
|
143
|
+
lines.push("## Coverage");
|
|
144
|
+
lines.push("");
|
|
145
|
+
lines.push("| Persona | Files routed | Files visited | Findings |");
|
|
146
|
+
lines.push("|---|---:|---:|---:|");
|
|
147
|
+
for (const personaId of INVESTOR_DD_PERSONAS) {
|
|
148
|
+
const record = byPersona[personaId] || {};
|
|
149
|
+
const routed = (routing[personaId] || []).length;
|
|
150
|
+
const visited = Array.isArray(record.visited) ? record.visited.length : 0;
|
|
151
|
+
const findings = Array.isArray(record.findings) ? record.findings.length : 0;
|
|
152
|
+
lines.push(`| ${personaId} | ${routed} | ${visited} | ${findings} |`);
|
|
153
|
+
}
|
|
154
|
+
lines.push("");
|
|
155
|
+
lines.push("## Findings summary");
|
|
156
|
+
lines.push("");
|
|
157
|
+
const allFindings = Object.values(byPersona).flatMap((r) => r.findings || []);
|
|
158
|
+
const bySev = {};
|
|
159
|
+
for (const f of allFindings) {
|
|
160
|
+
const sev = f.severity || "UNKNOWN";
|
|
161
|
+
bySev[sev] = (bySev[sev] || 0) + 1;
|
|
162
|
+
}
|
|
163
|
+
for (const [sev, count] of Object.entries(bySev)) {
|
|
164
|
+
lines.push(`- **${sev}**: ${count}`);
|
|
165
|
+
}
|
|
166
|
+
lines.push("");
|
|
167
|
+
lines.push(`Total: ${allFindings.length}`);
|
|
168
|
+
return lines.join("\n");
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Run the investor-DD orchestration end to end.
|
|
173
|
+
*
|
|
174
|
+
* @param {object} params
|
|
175
|
+
* @param {string} params.rootPath
|
|
176
|
+
* @param {string} [params.outputDir] - Defaults to `<rootPath>/.sentinelayer/runs/<runId>`.
|
|
177
|
+
* @param {object} [params.budgetOptions] - Overrides from CLI: { maxUsd, maxRuntimeMinutes, maxParallel }.
|
|
178
|
+
* @param {string[]} [params.personas] - Override persona list; defaults to all 12.
|
|
179
|
+
* @param {Function} [params.onEvent] - Extra event sink (NDJSON stream is always written).
|
|
180
|
+
* @param {boolean} [params.dryRun] - If true, skip tool execution, emit plan.json + stub report only.
|
|
181
|
+
* @param {string[]|null} [params.compliancePacks] - Compliance pack IDs to run (default: all seven).
|
|
182
|
+
* @param {object} [params.liveValidator] - Optional live-web validator config.
|
|
183
|
+
* @param {object} [params.liveValidator.devTestBot] - DevTestBot client.
|
|
184
|
+
* @param {object} [params.liveValidator.aidenid] - AIdenID client.
|
|
185
|
+
* @param {number} [params.liveValidator.maxInteractions]
|
|
186
|
+
* @param {object} [params.notification] - Optional notification config.
|
|
187
|
+
* @param {string} [params.notification.notifyEmail]
|
|
188
|
+
* @param {object} [params.notification.emailClient]
|
|
189
|
+
* @param {object} [params.notification.dashboardClient]
|
|
190
|
+
* @returns {Promise<{runId: string, artifactDir: string, summary: object}>}
|
|
191
|
+
*/
|
|
192
|
+
export async function runInvestorDd({
|
|
193
|
+
rootPath,
|
|
194
|
+
outputDir = "",
|
|
195
|
+
budgetOptions = {},
|
|
196
|
+
personas = INVESTOR_DD_PERSONAS,
|
|
197
|
+
onEvent = () => {},
|
|
198
|
+
dryRun = false,
|
|
199
|
+
compliancePacks = COMPLIANCE_PACK_CATALOG,
|
|
200
|
+
liveValidator = null,
|
|
201
|
+
notification = null,
|
|
202
|
+
} = {}) {
|
|
203
|
+
if (!rootPath) throw new TypeError("runInvestorDd requires rootPath");
|
|
204
|
+
|
|
205
|
+
const runId = `investor-dd-${Date.now()}-${crypto.randomUUID().slice(0, 8)}`;
|
|
206
|
+
const resolvedBudget = resolveInvestorDdBudget(budgetOptions);
|
|
207
|
+
const artifactBase = outputDir
|
|
208
|
+
? path.resolve(outputDir, runId, INVESTOR_DD_ARTIFACT_SUBDIR)
|
|
209
|
+
: path.resolve(rootPath, ".sentinelayer", "runs", runId, INVESTOR_DD_ARTIFACT_SUBDIR);
|
|
210
|
+
await fsp.mkdir(artifactBase, { recursive: true });
|
|
211
|
+
|
|
212
|
+
const streamPath = path.join(artifactBase, "stream.ndjson");
|
|
213
|
+
const streamHandle = await fsp.open(streamPath, "w");
|
|
214
|
+
|
|
215
|
+
const emit = (event) => {
|
|
216
|
+
const enriched = { ...event, at: new Date().toISOString(), runId };
|
|
217
|
+
streamHandle.write(`${JSON.stringify(enriched)}\n`).catch(() => {});
|
|
218
|
+
try {
|
|
219
|
+
onEvent(enriched);
|
|
220
|
+
} catch {
|
|
221
|
+
// external sinks never break the run
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
|
|
225
|
+
const startTime = Date.now();
|
|
226
|
+
emit({ type: "investor_dd_start", rootPath, personas });
|
|
227
|
+
|
|
228
|
+
const files = await walkRepoFiles(rootPath);
|
|
229
|
+
emit({ type: "investor_dd_files_discovered", totalFiles: files.length });
|
|
230
|
+
|
|
231
|
+
const routing = routeFilesToPersonas({ files, personas });
|
|
232
|
+
const routingSummary = summarizeRouting(routing);
|
|
233
|
+
await writeJson(path.join(artifactBase, "plan.json"), {
|
|
234
|
+
runId,
|
|
235
|
+
rootPath,
|
|
236
|
+
personas,
|
|
237
|
+
routing,
|
|
238
|
+
routingSummary,
|
|
239
|
+
budget: resolvedBudget,
|
|
240
|
+
});
|
|
241
|
+
|
|
242
|
+
let byPersona = {};
|
|
243
|
+
let findings = [];
|
|
244
|
+
let terminationReason = "ok";
|
|
245
|
+
let reconciliationAvailable = false;
|
|
246
|
+
let compliance = null;
|
|
247
|
+
|
|
248
|
+
if (!dryRun) {
|
|
249
|
+
const budgetState = createBudgetState({
|
|
250
|
+
maxUsd: resolvedBudget.maxCostUsd,
|
|
251
|
+
maxRuntimeMs: resolvedBudget.maxRuntimeMinutes * 60_000,
|
|
252
|
+
});
|
|
253
|
+
const runResult = await runAllPersonas({
|
|
254
|
+
routing,
|
|
255
|
+
rootPath,
|
|
256
|
+
budget: budgetState,
|
|
257
|
+
onEvent: emit,
|
|
258
|
+
});
|
|
259
|
+
byPersona = runResult.byPersona;
|
|
260
|
+
findings = runResult.findings;
|
|
261
|
+
terminationReason = runResult.terminationReason;
|
|
262
|
+
|
|
263
|
+
// Compliance pack (Leila Farouk persona-adjacent dispatch). Deterministic,
|
|
264
|
+
// no LLM — an acquirer's auditor can re-run and get the same gap table.
|
|
265
|
+
emit({ type: "investor_dd_compliance_start" });
|
|
266
|
+
compliance = await runFullCompliancePack({
|
|
267
|
+
rootPath,
|
|
268
|
+
packs: Array.isArray(compliancePacks) ? compliancePacks : COMPLIANCE_PACK_CATALOG,
|
|
269
|
+
});
|
|
270
|
+
await writeJson(path.join(artifactBase, "compliance.json"), compliance);
|
|
271
|
+
emit({
|
|
272
|
+
type: "investor_dd_compliance_complete",
|
|
273
|
+
totalCovered: compliance.totalCovered,
|
|
274
|
+
totalGaps: compliance.totalGaps,
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
// Live-web validation (Jules): optional; only runs when both
|
|
278
|
+
// devTestBot + aidenid clients are supplied (pluggable contracts).
|
|
279
|
+
if (
|
|
280
|
+
liveValidator &&
|
|
281
|
+
liveValidator.devTestBot &&
|
|
282
|
+
liveValidator.aidenid
|
|
283
|
+
) {
|
|
284
|
+
emit({ type: "investor_dd_live_start" });
|
|
285
|
+
const elements = await discoverInteractiveElements(rootPath);
|
|
286
|
+
await writeJson(path.join(artifactBase, "interaction-plan.json"), elements);
|
|
287
|
+
const live = await runLiveValidator({
|
|
288
|
+
runId,
|
|
289
|
+
elements,
|
|
290
|
+
devTestBot: liveValidator.devTestBot,
|
|
291
|
+
aidenid: liveValidator.aidenid,
|
|
292
|
+
maxInteractions: liveValidator.maxInteractions,
|
|
293
|
+
onEvent: emit,
|
|
294
|
+
});
|
|
295
|
+
await writeJson(path.join(artifactBase, "live-observations.json"), live);
|
|
296
|
+
|
|
297
|
+
// Reconciliation — pair each finding with a live observation and emit
|
|
298
|
+
// a verdict per finding. FALSE_POSITIVE findings are suppressed in
|
|
299
|
+
// the final finding list unless the caller keeps them for HITL.
|
|
300
|
+
const observationIndex = buildObservationIndex(live.observations);
|
|
301
|
+
const pairFn = createFindingObservationPair(observationIndex);
|
|
302
|
+
findings = reconcileFindings(findings, pairFn);
|
|
303
|
+
findings = findings.filter(
|
|
304
|
+
(f) => applyReportPolicy(f) !== "suppress",
|
|
305
|
+
);
|
|
306
|
+
reconciliationAvailable = true;
|
|
307
|
+
emit({
|
|
308
|
+
type: "investor_dd_live_complete",
|
|
309
|
+
observations: live.observations.length,
|
|
310
|
+
verdicts: findings.reduce((acc, f) => {
|
|
311
|
+
const v = f.reconciliation?.verdict || "UNVERIFIABLE";
|
|
312
|
+
acc[v] = (acc[v] || 0) + 1;
|
|
313
|
+
return acc;
|
|
314
|
+
}, {}),
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Reproducibility chain — attach a per-finding replay block + file
|
|
319
|
+
// SHA at finding time so each line in the report is re-verifiable.
|
|
320
|
+
findings = await attachReproducibilityChain({
|
|
321
|
+
findings,
|
|
322
|
+
rootPath,
|
|
323
|
+
runId,
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
await writeJson(path.join(artifactBase, "findings.json"), findings);
|
|
327
|
+
for (const [personaId, record] of Object.entries(byPersona)) {
|
|
328
|
+
await writeJson(path.join(artifactBase, `persona-${personaId}.json`), record);
|
|
329
|
+
}
|
|
330
|
+
} else {
|
|
331
|
+
emit({ type: "investor_dd_dry_run" });
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const durationSeconds = (Date.now() - startTime) / 1000;
|
|
335
|
+
const summary = {
|
|
336
|
+
runId,
|
|
337
|
+
startedAt: new Date(startTime).toISOString(),
|
|
338
|
+
durationSeconds,
|
|
339
|
+
terminationReason,
|
|
340
|
+
totalFiles: files.length,
|
|
341
|
+
totalFindings: findings.length,
|
|
342
|
+
personas,
|
|
343
|
+
budget: resolvedBudget,
|
|
344
|
+
dryRun,
|
|
345
|
+
compliance: compliance
|
|
346
|
+
? { totalCovered: compliance.totalCovered, totalGaps: compliance.totalGaps }
|
|
347
|
+
: null,
|
|
348
|
+
reconciliation: reconciliationAvailable,
|
|
349
|
+
};
|
|
350
|
+
await writeJson(path.join(artifactBase, "summary.json"), summary);
|
|
351
|
+
|
|
352
|
+
const markdown = buildSummaryMarkdown({ runId, summary, routing, byPersona });
|
|
353
|
+
const reportPath = path.join(artifactBase, "report.md");
|
|
354
|
+
await fsp.writeFile(reportPath, markdown, "utf-8");
|
|
355
|
+
|
|
356
|
+
const htmlReport = renderInvestorDdHtml({
|
|
357
|
+
runId,
|
|
358
|
+
summary,
|
|
359
|
+
routing,
|
|
360
|
+
byPersona,
|
|
361
|
+
findings,
|
|
362
|
+
compliance: compliance ? compliance.packs : null,
|
|
363
|
+
});
|
|
364
|
+
await fsp.writeFile(path.join(artifactBase, "report.html"), htmlReport, "utf-8");
|
|
365
|
+
|
|
366
|
+
emit({
|
|
367
|
+
type: "investor_dd_complete",
|
|
368
|
+
totalFindings: findings.length,
|
|
369
|
+
durationSeconds,
|
|
370
|
+
terminationReason,
|
|
371
|
+
});
|
|
372
|
+
await streamHandle.close();
|
|
373
|
+
|
|
374
|
+
const artifactFiles = await fsp.readdir(artifactBase);
|
|
375
|
+
const manifest = {};
|
|
376
|
+
for (const file of artifactFiles) {
|
|
377
|
+
const abs = path.join(artifactBase, file);
|
|
378
|
+
const stat = await fsp.stat(abs);
|
|
379
|
+
if (!stat.isFile()) continue;
|
|
380
|
+
const contents = await fsp.readFile(abs);
|
|
381
|
+
manifest[file] = {
|
|
382
|
+
sha256: sha256(contents),
|
|
383
|
+
bytes: stat.size,
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
await writeJson(path.join(artifactBase, "manifest.json"), manifest);
|
|
387
|
+
|
|
388
|
+
const runResult = { runId, artifactDir: artifactBase, summary, findings };
|
|
389
|
+
|
|
390
|
+
// Fire-and-forget notification dispatch (email + dashboard). Failures
|
|
391
|
+
// are non-fatal — the report is already persisted to disk + manifest.
|
|
392
|
+
if (notification && (notification.emailClient || notification.dashboardClient)) {
|
|
393
|
+
await notifyRunCompleted({
|
|
394
|
+
run: runResult,
|
|
395
|
+
notifyEmail: notification.notifyEmail,
|
|
396
|
+
emailClient: notification.emailClient,
|
|
397
|
+
dashboardClient: notification.dashboardClient,
|
|
398
|
+
emailEnabled: notification.emailEnabled !== false,
|
|
399
|
+
dashboardEnabled: notification.dashboardEnabled !== false,
|
|
400
|
+
onEvent: emit,
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
return runResult;
|
|
405
|
+
}
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unified per-persona runner for investor-DD (#investor-dd-4..15).
|
|
3
|
+
*
|
|
4
|
+
* Wires the 12 domain personas (security, backend, code-quality, testing,
|
|
5
|
+
* data-layer, reliability, release, observability, infrastructure,
|
|
6
|
+
* supply-chain, documentation, ai-governance) into the per-file review
|
|
7
|
+
* loop. Each persona dispatches its declared domain tools against every
|
|
8
|
+
* file the router assigns to it, collects findings, and returns a
|
|
9
|
+
* per-persona coverage proof.
|
|
10
|
+
*
|
|
11
|
+
* This runner runs the tool registries DETERMINISTICALLY — the agentic
|
|
12
|
+
* (LLM-driven) layer sits on top in a later PR. Determinism is the
|
|
13
|
+
* investor-DD ground-truth floor; the LLM layer is additive.
|
|
14
|
+
*
|
|
15
|
+
* Frontend (Jules) is excluded here because Jules has its own bespoke
|
|
16
|
+
* envelope with live-web validation; PR-25 routes Jules into investor-DD
|
|
17
|
+
* via a dedicated live-validator dispatcher.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { AI_GOVERNANCE_TOOLS } from "../agents/ai-governance/tools/index.js";
|
|
21
|
+
import { BACKEND_TOOLS } from "../agents/backend/tools/index.js";
|
|
22
|
+
import { CODE_QUALITY_TOOLS } from "../agents/code-quality/tools/index.js";
|
|
23
|
+
import { DATA_LAYER_TOOLS } from "../agents/data-layer/tools/index.js";
|
|
24
|
+
import { DOCUMENTATION_TOOLS } from "../agents/documentation/tools/index.js";
|
|
25
|
+
import { INFRASTRUCTURE_TOOLS } from "../agents/infrastructure/tools/index.js";
|
|
26
|
+
import { OBSERVABILITY_TOOLS } from "../agents/observability/tools/index.js";
|
|
27
|
+
import { RELEASE_TOOLS } from "../agents/release/tools/index.js";
|
|
28
|
+
import { RELIABILITY_TOOLS } from "../agents/reliability/tools/index.js";
|
|
29
|
+
import { SECURITY_TOOLS } from "../agents/security/tools/index.js";
|
|
30
|
+
import { SUPPLY_CHAIN_TOOLS } from "../agents/supply-chain/tools/index.js";
|
|
31
|
+
import { TESTING_TOOLS } from "../agents/testing/tools/index.js";
|
|
32
|
+
|
|
33
|
+
import { checkBudget, createBudgetState } from "./investor-dd-file-loop.js";
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Registry of persona → tool map. Frontend handled separately via Jules.
|
|
37
|
+
*/
|
|
38
|
+
export const INVESTOR_DD_PERSONA_TOOL_REGISTRY = Object.freeze({
|
|
39
|
+
security: SECURITY_TOOLS,
|
|
40
|
+
backend: BACKEND_TOOLS,
|
|
41
|
+
"code-quality": CODE_QUALITY_TOOLS,
|
|
42
|
+
testing: TESTING_TOOLS,
|
|
43
|
+
"data-layer": DATA_LAYER_TOOLS,
|
|
44
|
+
reliability: RELIABILITY_TOOLS,
|
|
45
|
+
release: RELEASE_TOOLS,
|
|
46
|
+
observability: OBSERVABILITY_TOOLS,
|
|
47
|
+
infrastructure: INFRASTRUCTURE_TOOLS,
|
|
48
|
+
"supply-chain": SUPPLY_CHAIN_TOOLS,
|
|
49
|
+
documentation: DOCUMENTATION_TOOLS,
|
|
50
|
+
"ai-governance": AI_GOVERNANCE_TOOLS,
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
export const INVESTOR_DD_PERSONA_IDS = Object.freeze(
|
|
54
|
+
Object.keys(INVESTOR_DD_PERSONA_TOOL_REGISTRY),
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Resolve the tool list for a given persona.
|
|
59
|
+
*
|
|
60
|
+
* @param {string} personaId
|
|
61
|
+
* @returns {Array<{id: string, handler: Function, description: string}>}
|
|
62
|
+
*/
|
|
63
|
+
export function getPersonaTools(personaId) {
|
|
64
|
+
const map = INVESTOR_DD_PERSONA_TOOL_REGISTRY[personaId];
|
|
65
|
+
if (!map) return [];
|
|
66
|
+
return Object.values(map);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Dispatch every tool for the persona against a single file scope. Each
|
|
71
|
+
* tool handler is invoked with `{ rootPath, files: [file] }` because
|
|
72
|
+
* every persona's tool contract accepts this shape (see #A13-#A22).
|
|
73
|
+
*
|
|
74
|
+
* @param {object} params
|
|
75
|
+
* @param {string} params.personaId
|
|
76
|
+
* @param {string} params.file - Single file (relative to rootPath).
|
|
77
|
+
* @param {string} params.rootPath - Repo root.
|
|
78
|
+
* @param {object} params.budget - Shared budget state.
|
|
79
|
+
* @param {Function} [params.onEvent]
|
|
80
|
+
* @returns {Promise<{findings: Array, toolInvocations: Array, stoppedEarly: boolean}>}
|
|
81
|
+
*/
|
|
82
|
+
export async function runPersonaOnFile({
|
|
83
|
+
personaId,
|
|
84
|
+
file,
|
|
85
|
+
rootPath,
|
|
86
|
+
budget,
|
|
87
|
+
onEvent = () => {},
|
|
88
|
+
} = {}) {
|
|
89
|
+
if (!personaId) throw new TypeError("runPersonaOnFile requires personaId");
|
|
90
|
+
if (!file) throw new TypeError("runPersonaOnFile requires file");
|
|
91
|
+
if (!rootPath) throw new TypeError("runPersonaOnFile requires rootPath");
|
|
92
|
+
|
|
93
|
+
const tools = getPersonaTools(personaId);
|
|
94
|
+
const findings = [];
|
|
95
|
+
const toolInvocations = [];
|
|
96
|
+
let stoppedEarly = false;
|
|
97
|
+
|
|
98
|
+
for (const tool of tools) {
|
|
99
|
+
const budgetCheck = checkBudget(budget);
|
|
100
|
+
if (!budgetCheck.ok) {
|
|
101
|
+
stoppedEarly = true;
|
|
102
|
+
onEvent({
|
|
103
|
+
type: "persona_tool_skipped",
|
|
104
|
+
personaId,
|
|
105
|
+
file,
|
|
106
|
+
tool: tool.id,
|
|
107
|
+
stopReason: budgetCheck.reason,
|
|
108
|
+
});
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
onEvent({ type: "persona_file_tool_call", personaId, file, tool: tool.id });
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
const results = await tool.handler({ rootPath, files: [file] });
|
|
116
|
+
if (budget) budget.toolCalls = (budget.toolCalls || 0) + 1;
|
|
117
|
+
const normalized = Array.isArray(results) ? results : [];
|
|
118
|
+
for (const f of normalized) {
|
|
119
|
+
const decorated = {
|
|
120
|
+
...f,
|
|
121
|
+
personaId,
|
|
122
|
+
tool: tool.id,
|
|
123
|
+
file: f.file || file,
|
|
124
|
+
};
|
|
125
|
+
findings.push(decorated);
|
|
126
|
+
onEvent({ type: "persona_finding", personaId, file, tool: tool.id, finding: decorated });
|
|
127
|
+
}
|
|
128
|
+
toolInvocations.push({ tool: tool.id, findings: normalized.length });
|
|
129
|
+
} catch (err) {
|
|
130
|
+
const errorMessage = err instanceof Error ? err.message : String(err);
|
|
131
|
+
onEvent({
|
|
132
|
+
type: "persona_tool_error",
|
|
133
|
+
personaId,
|
|
134
|
+
file,
|
|
135
|
+
tool: tool.id,
|
|
136
|
+
stopReason: errorMessage,
|
|
137
|
+
});
|
|
138
|
+
toolInvocations.push({ tool: tool.id, error: errorMessage });
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return { findings, toolInvocations, stoppedEarly };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Run a single persona across its assigned files in the router output.
|
|
147
|
+
*
|
|
148
|
+
* @param {object} params
|
|
149
|
+
* @param {string} params.personaId
|
|
150
|
+
* @param {string[]} params.files
|
|
151
|
+
* @param {string} params.rootPath
|
|
152
|
+
* @param {object} params.budget
|
|
153
|
+
* @param {Function} [params.onEvent]
|
|
154
|
+
* @returns {Promise<{personaId: string, perFile: Array, findings: Array, visited: string[], skipped: string[], terminationReason: string}>}
|
|
155
|
+
*/
|
|
156
|
+
export async function runPersonaAcrossFiles({
|
|
157
|
+
personaId,
|
|
158
|
+
files,
|
|
159
|
+
rootPath,
|
|
160
|
+
budget,
|
|
161
|
+
onEvent = () => {},
|
|
162
|
+
} = {}) {
|
|
163
|
+
if (!Array.isArray(files)) throw new TypeError("runPersonaAcrossFiles requires files array");
|
|
164
|
+
const safeBudget = budget || createBudgetState();
|
|
165
|
+
const perFile = [];
|
|
166
|
+
const allFindings = [];
|
|
167
|
+
const visited = [];
|
|
168
|
+
const skipped = [];
|
|
169
|
+
let terminationReason = "ok";
|
|
170
|
+
|
|
171
|
+
for (const file of files) {
|
|
172
|
+
const budgetCheck = checkBudget(safeBudget);
|
|
173
|
+
if (!budgetCheck.ok) {
|
|
174
|
+
terminationReason = budgetCheck.reason;
|
|
175
|
+
skipped.push(file);
|
|
176
|
+
onEvent({ type: "persona_file_skipped", personaId, file, stopReason: budgetCheck.reason });
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
onEvent({ type: "persona_file_start", personaId, file });
|
|
181
|
+
const { findings, toolInvocations, stoppedEarly } = await runPersonaOnFile({
|
|
182
|
+
personaId,
|
|
183
|
+
file,
|
|
184
|
+
rootPath,
|
|
185
|
+
budget: safeBudget,
|
|
186
|
+
onEvent,
|
|
187
|
+
});
|
|
188
|
+
perFile.push({ file, findings, toolInvocations, stoppedEarly });
|
|
189
|
+
allFindings.push(...findings);
|
|
190
|
+
visited.push(file);
|
|
191
|
+
onEvent({
|
|
192
|
+
type: "persona_file_complete",
|
|
193
|
+
personaId,
|
|
194
|
+
file,
|
|
195
|
+
findingCount: findings.length,
|
|
196
|
+
toolCount: toolInvocations.length,
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
return {
|
|
201
|
+
personaId,
|
|
202
|
+
perFile,
|
|
203
|
+
findings: allFindings,
|
|
204
|
+
visited,
|
|
205
|
+
skipped,
|
|
206
|
+
terminationReason,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Run every persona in the supplied routing table in sequence. Each
|
|
212
|
+
* persona's runtime is bounded by the shared budget; when the budget
|
|
213
|
+
* trips, remaining personas (and remaining files within the current
|
|
214
|
+
* persona) are marked `skipped` so the partial-report generator can
|
|
215
|
+
* still emit what finished.
|
|
216
|
+
*
|
|
217
|
+
* @param {object} params
|
|
218
|
+
* @param {Record<string, string[]>} params.routing - { personaId: filesInScope[] }
|
|
219
|
+
* @param {string} params.rootPath
|
|
220
|
+
* @param {object} params.budget
|
|
221
|
+
* @param {Function} [params.onEvent]
|
|
222
|
+
* @returns {Promise<{byPersona: Record<string, object>, findings: Array, terminationReason: string}>}
|
|
223
|
+
*/
|
|
224
|
+
export async function runAllPersonas({
|
|
225
|
+
routing = {},
|
|
226
|
+
rootPath,
|
|
227
|
+
budget,
|
|
228
|
+
onEvent = () => {},
|
|
229
|
+
} = {}) {
|
|
230
|
+
if (!rootPath) throw new TypeError("runAllPersonas requires rootPath");
|
|
231
|
+
const safeBudget = budget || createBudgetState();
|
|
232
|
+
const byPersona = {};
|
|
233
|
+
const allFindings = [];
|
|
234
|
+
let terminationReason = "ok";
|
|
235
|
+
|
|
236
|
+
for (const [personaId, files] of Object.entries(routing)) {
|
|
237
|
+
const budgetCheck = checkBudget(safeBudget);
|
|
238
|
+
if (!budgetCheck.ok) {
|
|
239
|
+
terminationReason = budgetCheck.reason;
|
|
240
|
+
byPersona[personaId] = {
|
|
241
|
+
personaId,
|
|
242
|
+
perFile: [],
|
|
243
|
+
findings: [],
|
|
244
|
+
visited: [],
|
|
245
|
+
skipped: [...files],
|
|
246
|
+
terminationReason: budgetCheck.reason,
|
|
247
|
+
};
|
|
248
|
+
onEvent({ type: "persona_skipped", personaId, stopReason: budgetCheck.reason });
|
|
249
|
+
continue;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
onEvent({ type: "persona_start", personaId, fileCount: files.length });
|
|
253
|
+
const result = await runPersonaAcrossFiles({
|
|
254
|
+
personaId,
|
|
255
|
+
files,
|
|
256
|
+
rootPath,
|
|
257
|
+
budget: safeBudget,
|
|
258
|
+
onEvent,
|
|
259
|
+
});
|
|
260
|
+
byPersona[personaId] = result;
|
|
261
|
+
allFindings.push(...result.findings);
|
|
262
|
+
if (result.terminationReason !== "ok" && terminationReason === "ok") {
|
|
263
|
+
terminationReason = result.terminationReason;
|
|
264
|
+
}
|
|
265
|
+
onEvent({
|
|
266
|
+
type: "persona_complete",
|
|
267
|
+
personaId,
|
|
268
|
+
findingCount: result.findings.length,
|
|
269
|
+
visitedCount: result.visited.length,
|
|
270
|
+
skippedCount: result.skipped.length,
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return { byPersona, findings: allFindings, terminationReason };
|
|
275
|
+
}
|