sentinelayer-cli 0.8.11 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +10 -5
- package/src/agents/devtestbot/config/definition.js +100 -0
- package/src/agents/devtestbot/config/system-prompt.js +92 -0
- package/src/agents/devtestbot/index.js +9 -0
- package/src/agents/devtestbot/runner.js +769 -0
- package/src/agents/devtestbot/tool.js +707 -0
- package/src/agents/jules/stream.js +2 -12
- package/src/audit/orchestrator.js +471 -114
- package/src/audit/persona-loop.js +1342 -0
- package/src/audit/registry.js +58 -2
- package/src/commands/audit.js +42 -1
- package/src/commands/legacy-args.js +32 -1
- package/src/commands/omargate.js +4 -0
- package/src/commands/session.js +417 -89
- package/src/commands/swarm.js +11 -2
- package/src/cost/history.js +41 -21
- package/src/events/schema.js +27 -1
- package/src/guide/generator.js +14 -0
- package/src/legacy-cli.js +110 -18
- package/src/prompt/generator.js +4 -16
- package/src/review/ai-review.js +95 -6
- package/src/review/dd-report-email-client.js +148 -0
- package/src/review/investor-dd-devtestbot.js +599 -0
- package/src/review/investor-dd-orchestrator.js +135 -3
- package/src/review/omargate-cache.js +285 -0
- package/src/review/omargate-orchestrator.js +605 -4
- package/src/review/persona-prompts.js +34 -1
- package/src/review/report.js +189 -4
- package/src/session/coordination-guidance.js +48 -0
- package/src/session/daemon.js +3 -2
- package/src/session/listener.js +236 -0
- package/src/session/senti-naming.js +36 -0
- package/src/session/setup-guides.js +3 -15
- package/src/session/store.js +54 -5
- package/src/session/sync.js +23 -0
- package/src/spec/generator.js +8 -10
- package/src/swarm/registry.js +20 -0
- package/src/swarm/runtime.js +139 -1
|
@@ -37,6 +37,8 @@ import {
|
|
|
37
37
|
import { notifyRunCompleted } from "./investor-dd-notification.js";
|
|
38
38
|
import { attachReproducibilityChain } from "./reproducibility-chain.js";
|
|
39
39
|
import { renderInvestorDdHtml } from "./investor-dd-html-report.js";
|
|
40
|
+
import { runDevTestBotPhase } from "./investor-dd-devtestbot.js";
|
|
41
|
+
import { redactDdEmailError } from "./dd-report-email-client.js";
|
|
40
42
|
|
|
41
43
|
const INVESTOR_DD_PERSONAS = Object.freeze([
|
|
42
44
|
"security",
|
|
@@ -164,10 +166,95 @@ function buildSummaryMarkdown({ runId, summary, routing, byPersona }) {
|
|
|
164
166
|
lines.push(`- **${sev}**: ${count}`);
|
|
165
167
|
}
|
|
166
168
|
lines.push("");
|
|
169
|
+
if (summary.devTestBot) {
|
|
170
|
+
lines.push("## devTestBot");
|
|
171
|
+
lines.push("");
|
|
172
|
+
lines.push(`- Skipped: ${summary.devTestBot.skipped ? "yes" : "no"}`);
|
|
173
|
+
lines.push(`- Subagents: ${summary.devTestBot.swarmCount || 0}`);
|
|
174
|
+
lines.push(`- Identities: ${summary.devTestBot.identityCount || 0}`);
|
|
175
|
+
lines.push(`- Findings: ${summary.devTestBot.findingCount || 0}`);
|
|
176
|
+
lines.push(`- Artifacts: ${summary.devTestBot.artifactRoot || "n/a"}`);
|
|
177
|
+
lines.push("");
|
|
178
|
+
}
|
|
167
179
|
lines.push(`Total: ${allFindings.length}`);
|
|
168
180
|
return lines.join("\n");
|
|
169
181
|
}
|
|
170
182
|
|
|
183
|
+
async function triggerReportEmail({ reportEmail, runResult, dryRun, emit }) {
|
|
184
|
+
const to = String(reportEmail?.to || "").trim();
|
|
185
|
+
if (!to) return null;
|
|
186
|
+
|
|
187
|
+
if (dryRun && reportEmail.skipWhenDryRun !== false) {
|
|
188
|
+
const result = { queued: false, skipped: true, runId: runResult.runId, to, code: "DD_EMAIL_DRY_RUN" };
|
|
189
|
+
emit({
|
|
190
|
+
type: "dd_email_skipped",
|
|
191
|
+
event: "dd_email_skipped",
|
|
192
|
+
runId: runResult.runId,
|
|
193
|
+
to,
|
|
194
|
+
reason: "dry_run",
|
|
195
|
+
});
|
|
196
|
+
return result;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const client = reportEmail.client;
|
|
200
|
+
if (!client || typeof client.send !== "function") {
|
|
201
|
+
const result = { queued: false, runId: runResult.runId, to, code: "DD_EMAIL_CLIENT_MISSING" };
|
|
202
|
+
emit({
|
|
203
|
+
type: "dd_email_error",
|
|
204
|
+
event: "dd_email_error",
|
|
205
|
+
runId: runResult.runId,
|
|
206
|
+
to,
|
|
207
|
+
code: result.code,
|
|
208
|
+
error: "DD report email client is not configured.",
|
|
209
|
+
});
|
|
210
|
+
return result;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
try {
|
|
214
|
+
const result = await client.send({ runId: runResult.runId, to, run: runResult });
|
|
215
|
+
if (result?.queued) {
|
|
216
|
+
emit({
|
|
217
|
+
type: "dd_email_queued",
|
|
218
|
+
event: "dd_email_queued",
|
|
219
|
+
runId: String(result.runId || runResult.runId),
|
|
220
|
+
to: String(result.to || to),
|
|
221
|
+
messageId: result.messageId || "",
|
|
222
|
+
replay: Boolean(result.replay),
|
|
223
|
+
sent: result.sent !== false,
|
|
224
|
+
});
|
|
225
|
+
return result;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
emit({
|
|
229
|
+
type: "dd_email_error",
|
|
230
|
+
event: "dd_email_error",
|
|
231
|
+
runId: runResult.runId,
|
|
232
|
+
to,
|
|
233
|
+
code: String(result?.code || "DD_EMAIL_FAILED"),
|
|
234
|
+
status: Number(result?.status || 0),
|
|
235
|
+
error: redactDdEmailError(result?.error || "DD report email request failed."),
|
|
236
|
+
});
|
|
237
|
+
return result || { queued: false, runId: runResult.runId, to };
|
|
238
|
+
} catch (err) {
|
|
239
|
+
const result = {
|
|
240
|
+
queued: false,
|
|
241
|
+
runId: runResult.runId,
|
|
242
|
+
to,
|
|
243
|
+
code: "DD_EMAIL_EXCEPTION",
|
|
244
|
+
error: redactDdEmailError(err instanceof Error ? err.message : String(err)),
|
|
245
|
+
};
|
|
246
|
+
emit({
|
|
247
|
+
type: "dd_email_error",
|
|
248
|
+
event: "dd_email_error",
|
|
249
|
+
runId: runResult.runId,
|
|
250
|
+
to,
|
|
251
|
+
code: result.code,
|
|
252
|
+
error: result.error,
|
|
253
|
+
});
|
|
254
|
+
return result;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
171
258
|
/**
|
|
172
259
|
* Run the investor-DD orchestration end to end.
|
|
173
260
|
*
|
|
@@ -183,6 +270,10 @@ function buildSummaryMarkdown({ runId, summary, routing, byPersona }) {
|
|
|
183
270
|
* @param {object} [params.liveValidator.devTestBot] - DevTestBot client.
|
|
184
271
|
* @param {object} [params.liveValidator.aidenid] - AIdenID client.
|
|
185
272
|
* @param {number} [params.liveValidator.maxInteractions]
|
|
273
|
+
* @param {object|false} [params.devTestBot] - Automated devTestBot phase config.
|
|
274
|
+
* @param {object|null} [params.reportEmail] - Optional API-side report email trigger.
|
|
275
|
+
* @param {string} [params.reportEmail.to]
|
|
276
|
+
* @param {object} [params.reportEmail.client] - { send({ runId, to, run }) }.
|
|
186
277
|
* @param {object} [params.notification] - Optional notification config.
|
|
187
278
|
* @param {string} [params.notification.notifyEmail]
|
|
188
279
|
* @param {object} [params.notification.emailClient]
|
|
@@ -198,6 +289,8 @@ export async function runInvestorDd({
|
|
|
198
289
|
dryRun = false,
|
|
199
290
|
compliancePacks = COMPLIANCE_PACK_CATALOG,
|
|
200
291
|
liveValidator = null,
|
|
292
|
+
devTestBot = {},
|
|
293
|
+
reportEmail = null,
|
|
201
294
|
notification = null,
|
|
202
295
|
} = {}) {
|
|
203
296
|
if (!rootPath) throw new TypeError("runInvestorDd requires rootPath");
|
|
@@ -207,6 +300,10 @@ export async function runInvestorDd({
|
|
|
207
300
|
const artifactBase = outputDir
|
|
208
301
|
? path.resolve(outputDir, runId, INVESTOR_DD_ARTIFACT_SUBDIR)
|
|
209
302
|
: path.resolve(rootPath, ".sentinelayer", "runs", runId, INVESTOR_DD_ARTIFACT_SUBDIR);
|
|
303
|
+
const runRoot = path.dirname(artifactBase);
|
|
304
|
+
const outputRoot = outputDir
|
|
305
|
+
? path.resolve(outputDir)
|
|
306
|
+
: path.resolve(rootPath, ".sentinelayer");
|
|
210
307
|
await fsp.mkdir(artifactBase, { recursive: true });
|
|
211
308
|
|
|
212
309
|
const streamPath = path.join(artifactBase, "stream.ndjson");
|
|
@@ -244,9 +341,11 @@ export async function runInvestorDd({
|
|
|
244
341
|
let terminationReason = "ok";
|
|
245
342
|
let reconciliationAvailable = false;
|
|
246
343
|
let compliance = null;
|
|
344
|
+
let devTestBotPhase = null;
|
|
345
|
+
let budgetState = null;
|
|
247
346
|
|
|
248
347
|
if (!dryRun) {
|
|
249
|
-
|
|
348
|
+
budgetState = createBudgetState({
|
|
250
349
|
maxUsd: resolvedBudget.maxCostUsd,
|
|
251
350
|
maxRuntimeMs: resolvedBudget.maxRuntimeMinutes * 60_000,
|
|
252
351
|
});
|
|
@@ -274,6 +373,20 @@ export async function runInvestorDd({
|
|
|
274
373
|
totalGaps: compliance.totalGaps,
|
|
275
374
|
});
|
|
276
375
|
|
|
376
|
+
devTestBotPhase = await runDevTestBotPhase({
|
|
377
|
+
runId,
|
|
378
|
+
rootPath,
|
|
379
|
+
outputRoot,
|
|
380
|
+
runRoot,
|
|
381
|
+
artifactDir: artifactBase,
|
|
382
|
+
files,
|
|
383
|
+
findings,
|
|
384
|
+
budget: budgetState,
|
|
385
|
+
options: devTestBot === false ? { enabled: false } : devTestBot || {},
|
|
386
|
+
onEvent: emit,
|
|
387
|
+
});
|
|
388
|
+
findings.push(...(devTestBotPhase.findings || []));
|
|
389
|
+
|
|
277
390
|
// Live-web validation (Jules): optional; only runs when both
|
|
278
391
|
// devTestBot + aidenid clients are supplied (pluggable contracts).
|
|
279
392
|
if (
|
|
@@ -346,6 +459,16 @@ export async function runInvestorDd({
|
|
|
346
459
|
? { totalCovered: compliance.totalCovered, totalGaps: compliance.totalGaps }
|
|
347
460
|
: null,
|
|
348
461
|
reconciliation: reconciliationAvailable,
|
|
462
|
+
devTestBot: devTestBotPhase
|
|
463
|
+
? {
|
|
464
|
+
skipped: Boolean(devTestBotPhase.skipped),
|
|
465
|
+
reason: devTestBotPhase.reason || "",
|
|
466
|
+
identityCount: devTestBotPhase.plan?.identityCount || devTestBotPhase.identities?.length || 0,
|
|
467
|
+
swarmCount: devTestBotPhase.plan?.swarmCount || devTestBotPhase.subagents?.length || 0,
|
|
468
|
+
findingCount: devTestBotPhase.findingCount || 0,
|
|
469
|
+
artifactRoot: devTestBotPhase.artifactRoot || "",
|
|
470
|
+
}
|
|
471
|
+
: null,
|
|
349
472
|
};
|
|
350
473
|
await writeJson(path.join(artifactBase, "summary.json"), summary);
|
|
351
474
|
|
|
@@ -369,6 +492,17 @@ export async function runInvestorDd({
|
|
|
369
492
|
durationSeconds,
|
|
370
493
|
terminationReason,
|
|
371
494
|
});
|
|
495
|
+
|
|
496
|
+
const runResult = { runId, artifactDir: artifactBase, summary, findings, devTestBot: devTestBotPhase };
|
|
497
|
+
if (reportEmail) {
|
|
498
|
+
runResult.reportEmail = await triggerReportEmail({
|
|
499
|
+
reportEmail,
|
|
500
|
+
runResult,
|
|
501
|
+
dryRun,
|
|
502
|
+
emit,
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
|
|
372
506
|
await streamHandle.close();
|
|
373
507
|
|
|
374
508
|
const artifactFiles = await fsp.readdir(artifactBase);
|
|
@@ -385,8 +519,6 @@ export async function runInvestorDd({
|
|
|
385
519
|
}
|
|
386
520
|
await writeJson(path.join(artifactBase, "manifest.json"), manifest);
|
|
387
521
|
|
|
388
|
-
const runResult = { runId, artifactDir: artifactBase, summary, findings };
|
|
389
|
-
|
|
390
522
|
// Fire-and-forget notification dispatch (email + dashboard). Failures
|
|
391
523
|
// are non-fatal — the report is already persisted to disk + manifest.
|
|
392
524
|
if (notification && (notification.emailClient || notification.dashboardClient)) {
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
import fsp from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
import { resolveOutputRoot } from "../config/service.js";
|
|
5
|
+
|
|
6
|
+
const CACHE_SCHEMA_VERSION = "1.0.0";
|
|
7
|
+
const CACHE_KIND = "omargate-deterministic-cache";
|
|
8
|
+
const LATEST_INDEX_NAME = "latest-omargate.json";
|
|
9
|
+
|
|
10
|
+
function normalizeString(value) {
|
|
11
|
+
return String(value || "").trim();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function normalizeTargetPath(value) {
|
|
15
|
+
return path.resolve(String(value || "."));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function normalizeSummary(value = {}) {
|
|
19
|
+
const summary = value && typeof value === "object" ? value : {};
|
|
20
|
+
const P0 = Math.max(0, Math.floor(Number(summary.P0 || 0)));
|
|
21
|
+
const P1 = Math.max(0, Math.floor(Number(summary.P1 || 0)));
|
|
22
|
+
const P2 = Math.max(0, Math.floor(Number(summary.P2 || 0)));
|
|
23
|
+
const P3 = Math.max(0, Math.floor(Number(summary.P3 || 0)));
|
|
24
|
+
return {
|
|
25
|
+
P0,
|
|
26
|
+
P1,
|
|
27
|
+
P2,
|
|
28
|
+
P3,
|
|
29
|
+
blocking: summary.blocking === undefined ? P0 > 0 || P1 > 0 : Boolean(summary.blocking),
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function sanitizeRunId(value) {
|
|
34
|
+
const normalized = normalizeString(value);
|
|
35
|
+
if (!normalized) {
|
|
36
|
+
return "";
|
|
37
|
+
}
|
|
38
|
+
return normalized.replace(/[^A-Za-z0-9._-]/g, "-").replace(/^-+|-+$/g, "");
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function isSafeRequestedRunId(requested, sanitized) {
|
|
42
|
+
return Boolean(requested) && requested === sanitized && sanitized !== "." && sanitized !== "..";
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function deterministicCachePath(outputRoot, runId) {
|
|
46
|
+
return path.join(outputRoot, "runs", runId, "deterministic.json");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async function readJsonFile(filePath) {
|
|
50
|
+
const content = await fsp.readFile(filePath, "utf-8");
|
|
51
|
+
return JSON.parse(content);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function isCacheForTarget(cache, normalizedTargetPath) {
|
|
55
|
+
const cacheTarget = normalizeString(cache?.targetPath);
|
|
56
|
+
if (!cacheTarget) {
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
return path.resolve(cacheTarget) === normalizedTargetPath;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function buildMissingResult({ outputRoot, requested = "latest", reason = "not_found" } = {}) {
|
|
63
|
+
return {
|
|
64
|
+
found: false,
|
|
65
|
+
requested,
|
|
66
|
+
reason,
|
|
67
|
+
outputRoot,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function loadCacheFile({ filePath, outputRoot, requested, normalizedTargetPath }) {
|
|
72
|
+
let cache;
|
|
73
|
+
try {
|
|
74
|
+
cache = await readJsonFile(filePath);
|
|
75
|
+
} catch {
|
|
76
|
+
return buildMissingResult({
|
|
77
|
+
outputRoot,
|
|
78
|
+
requested,
|
|
79
|
+
reason: "malformed_or_missing_cache",
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (cache?.kind !== CACHE_KIND) {
|
|
84
|
+
return buildMissingResult({
|
|
85
|
+
outputRoot,
|
|
86
|
+
requested,
|
|
87
|
+
reason: "invalid_cache_kind",
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
if (!isCacheForTarget(cache, normalizedTargetPath)) {
|
|
91
|
+
return buildMissingResult({
|
|
92
|
+
outputRoot,
|
|
93
|
+
requested,
|
|
94
|
+
reason: "target_mismatch",
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
found: true,
|
|
100
|
+
requested,
|
|
101
|
+
runId: normalizeString(cache.runId),
|
|
102
|
+
artifactPath: filePath,
|
|
103
|
+
outputRoot,
|
|
104
|
+
cache,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function loadLatestFromIndex({ outputRoot, normalizedTargetPath }) {
|
|
109
|
+
const latestPath = path.join(outputRoot, "runs", LATEST_INDEX_NAME);
|
|
110
|
+
let index;
|
|
111
|
+
try {
|
|
112
|
+
index = await readJsonFile(latestPath);
|
|
113
|
+
} catch {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const runId = sanitizeRunId(index?.runId);
|
|
118
|
+
if (!runId || !isCacheForTarget(index, normalizedTargetPath)) {
|
|
119
|
+
return null;
|
|
120
|
+
}
|
|
121
|
+
const artifactPath = normalizeString(index.artifactPath) || deterministicCachePath(outputRoot, runId);
|
|
122
|
+
const loaded = await loadCacheFile({
|
|
123
|
+
filePath: artifactPath,
|
|
124
|
+
outputRoot,
|
|
125
|
+
requested: "latest",
|
|
126
|
+
normalizedTargetPath,
|
|
127
|
+
});
|
|
128
|
+
return loaded.found ? loaded : null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async function loadLatestByScanning({ outputRoot, normalizedTargetPath }) {
|
|
132
|
+
const runsDir = path.join(outputRoot, "runs");
|
|
133
|
+
let entries = [];
|
|
134
|
+
try {
|
|
135
|
+
entries = await fsp.readdir(runsDir, { withFileTypes: true });
|
|
136
|
+
} catch {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const candidates = [];
|
|
141
|
+
for (const entry of entries) {
|
|
142
|
+
if (!entry.isDirectory()) {
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
const runId = sanitizeRunId(entry.name);
|
|
146
|
+
if (!isSafeRequestedRunId(entry.name, runId)) {
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
const artifactPath = deterministicCachePath(outputRoot, runId);
|
|
150
|
+
try {
|
|
151
|
+
const stat = await fsp.stat(artifactPath);
|
|
152
|
+
candidates.push({ runId, artifactPath, mtimeMs: Number(stat.mtimeMs || 0) });
|
|
153
|
+
} catch {
|
|
154
|
+
// Ignore incomplete run directories.
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
candidates.sort((left, right) => right.mtimeMs - left.mtimeMs);
|
|
159
|
+
for (const candidate of candidates) {
|
|
160
|
+
const loaded = await loadCacheFile({
|
|
161
|
+
filePath: candidate.artifactPath,
|
|
162
|
+
outputRoot,
|
|
163
|
+
requested: "latest",
|
|
164
|
+
normalizedTargetPath,
|
|
165
|
+
});
|
|
166
|
+
if (loaded.found) {
|
|
167
|
+
return loaded;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export async function writeOmarGateDeterministicCache({
|
|
174
|
+
targetPath,
|
|
175
|
+
outputDir = "",
|
|
176
|
+
runId,
|
|
177
|
+
deterministic = {},
|
|
178
|
+
reportPath = "",
|
|
179
|
+
} = {}) {
|
|
180
|
+
const normalizedTargetPath = normalizeTargetPath(targetPath);
|
|
181
|
+
const outputRoot = await resolveOutputRoot({
|
|
182
|
+
cwd: normalizedTargetPath,
|
|
183
|
+
outputDirOverride: outputDir,
|
|
184
|
+
env: process.env,
|
|
185
|
+
});
|
|
186
|
+
const normalizedRunId = sanitizeRunId(runId || deterministic?.runId);
|
|
187
|
+
if (!normalizedRunId) {
|
|
188
|
+
throw new Error("OmarGate deterministic cache requires a runId.");
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const runDirectory = path.join(outputRoot, "runs", normalizedRunId);
|
|
192
|
+
const artifactPath = path.join(runDirectory, "deterministic.json");
|
|
193
|
+
const latestPath = path.join(outputRoot, "runs", LATEST_INDEX_NAME);
|
|
194
|
+
await fsp.mkdir(runDirectory, { recursive: true });
|
|
195
|
+
|
|
196
|
+
const cache = {
|
|
197
|
+
schemaVersion: CACHE_SCHEMA_VERSION,
|
|
198
|
+
kind: CACHE_KIND,
|
|
199
|
+
runId: normalizedRunId,
|
|
200
|
+
targetPath: normalizedTargetPath,
|
|
201
|
+
generatedAt: new Date().toISOString(),
|
|
202
|
+
deterministicRunId: normalizeString(deterministic?.runId),
|
|
203
|
+
mode: normalizeString(deterministic?.mode) || "full",
|
|
204
|
+
summary: normalizeSummary(deterministic?.summary),
|
|
205
|
+
findings: Array.isArray(deterministic?.findings) ? deterministic.findings : [],
|
|
206
|
+
scope: deterministic?.scope && typeof deterministic.scope === "object" ? deterministic.scope : {},
|
|
207
|
+
layers: deterministic?.layers && typeof deterministic.layers === "object" ? deterministic.layers : {},
|
|
208
|
+
metadata: deterministic?.metadata && typeof deterministic.metadata === "object" ? deterministic.metadata : {},
|
|
209
|
+
artifacts: deterministic?.artifacts && typeof deterministic.artifacts === "object" ? deterministic.artifacts : {},
|
|
210
|
+
source: {
|
|
211
|
+
command: "/omargate deep",
|
|
212
|
+
reportPath: normalizeString(reportPath),
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
await fsp.writeFile(artifactPath, `${JSON.stringify(cache, null, 2)}\n`, "utf-8");
|
|
216
|
+
await fsp.writeFile(
|
|
217
|
+
latestPath,
|
|
218
|
+
`${JSON.stringify(
|
|
219
|
+
{
|
|
220
|
+
schemaVersion: CACHE_SCHEMA_VERSION,
|
|
221
|
+
kind: "omargate-latest-index",
|
|
222
|
+
runId: normalizedRunId,
|
|
223
|
+
targetPath: normalizedTargetPath,
|
|
224
|
+
artifactPath,
|
|
225
|
+
updatedAt: cache.generatedAt,
|
|
226
|
+
},
|
|
227
|
+
null,
|
|
228
|
+
2
|
|
229
|
+
)}\n`,
|
|
230
|
+
"utf-8"
|
|
231
|
+
);
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
runId: normalizedRunId,
|
|
235
|
+
outputRoot,
|
|
236
|
+
runDirectory,
|
|
237
|
+
artifactPath,
|
|
238
|
+
latestPath,
|
|
239
|
+
cache,
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
export async function loadOmarGateDeterministicCache({
|
|
244
|
+
targetPath,
|
|
245
|
+
outputDir = "",
|
|
246
|
+
runIdOrLatest = "latest",
|
|
247
|
+
} = {}) {
|
|
248
|
+
const normalizedTargetPath = normalizeTargetPath(targetPath);
|
|
249
|
+
const outputRoot = await resolveOutputRoot({
|
|
250
|
+
cwd: normalizedTargetPath,
|
|
251
|
+
outputDirOverride: outputDir,
|
|
252
|
+
env: process.env,
|
|
253
|
+
});
|
|
254
|
+
const requested = normalizeString(runIdOrLatest) || "latest";
|
|
255
|
+
|
|
256
|
+
if (requested.toLowerCase() === "latest") {
|
|
257
|
+
const latestFromIndex = await loadLatestFromIndex({
|
|
258
|
+
outputRoot,
|
|
259
|
+
normalizedTargetPath,
|
|
260
|
+
});
|
|
261
|
+
if (latestFromIndex) {
|
|
262
|
+
return latestFromIndex;
|
|
263
|
+
}
|
|
264
|
+
const latestFromScan = await loadLatestByScanning({
|
|
265
|
+
outputRoot,
|
|
266
|
+
normalizedTargetPath,
|
|
267
|
+
});
|
|
268
|
+
return latestFromScan || buildMissingResult({ outputRoot, requested, reason: "not_found" });
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const runId = sanitizeRunId(requested);
|
|
272
|
+
if (!isSafeRequestedRunId(requested, runId)) {
|
|
273
|
+
return buildMissingResult({
|
|
274
|
+
outputRoot,
|
|
275
|
+
requested,
|
|
276
|
+
reason: "invalid_run_id",
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
return loadCacheFile({
|
|
280
|
+
filePath: deterministicCachePath(outputRoot, runId),
|
|
281
|
+
outputRoot,
|
|
282
|
+
requested,
|
|
283
|
+
normalizedTargetPath,
|
|
284
|
+
});
|
|
285
|
+
}
|