@saiteja1123/mcp-server 1.1.4 → 1.1.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/package.json +59 -55
  2. package/src/api-scan.mjs +362 -93
  3. package/src/cli.js +771 -322
  4. package/src/deep-scan/contracts.js +201 -0
  5. package/src/deep-scan/deterministic-scan.js +337 -0
  6. package/src/deep-scan/index.js +109 -0
  7. package/src/deep-scan/project-map.js +507 -0
  8. package/src/deep-scan/ralph-accept.js +510 -0
  9. package/src/deep-scan/ralph-compare.js +498 -0
  10. package/src/deep-scan/ralph-tasks.js +598 -0
  11. package/src/deep-scan/ralph-track.js +548 -0
  12. package/src/deep-scan/registry.js +159 -0
  13. package/src/deep-scan/runtime.js +275 -0
  14. package/src/deep-scan/sample-steppers.js +128 -0
  15. package/src/deep-scan/sourceSafe.js +73 -0
  16. package/src/deep-scan/status.js +70 -0
  17. package/src/deep-scan/store.js +57 -0
  18. package/src/deep-scan/test-plan.js +760 -0
  19. package/src/index.js +6 -5
  20. package/src/lock.mjs +55 -14
  21. package/src/mcp-config.mjs +161 -0
  22. package/src/middleware/governance.js +135 -0
  23. package/src/orchestrator/runScan.js +211 -0
  24. package/src/project-bindings.mjs +215 -0
  25. package/src/rule-engine/index.js +2 -1
  26. package/src/rule-engine/localScan.js +39 -12
  27. package/src/rule-engine/metadata.js +20 -0
  28. package/src/rule-engine/prompt.js +6 -5
  29. package/src/rule-engine/rules.js +71 -43
  30. package/src/rule-engine/score.js +5 -4
  31. package/src/security/pathGuard.js +170 -0
  32. package/src/selftest.js +2473 -0
  33. package/src/server.js +109 -150
  34. package/src/tools/deepScan.js +286 -0
  35. package/src/tools/localScan.js +85 -0
  36. package/src/tools/projects.js +124 -0
  37. package/src/tools/scanFile.js +131 -0
@@ -0,0 +1,498 @@
1
+ import crypto from 'crypto';
2
+ import fs from 'fs/promises';
3
+ import path from 'path';
4
+ import { createArtifactRef, validateRunIdSegment } from './contracts.js';
5
+ import { assertSourceSafePayload, cloneSourceSafe } from './sourceSafe.js';
6
+ import { indexAcceptedRiskForComparison, readAcceptedRiskRegister } from './ralph-accept.js';
7
+
8
+ export const RALPH_COMPARISON_SCHEMA_VERSION = 'ralph_comparison.v1';
9
+ export const RALPH_COMPARISON_STEPPER_ID = 'ralph.compare';
10
+ export const RALPH_COMPARISON_STEPPER_VERSION = '1.0.0';
11
+ export const RALPH_COMPARISON_STATUSES = Object.freeze([
12
+ 'resolved',
13
+ 'unchanged',
14
+ 'new',
15
+ 'worsened',
16
+ 'improved',
17
+ 'acceptedRisk',
18
+ 'manualReview',
19
+ ]);
20
+
21
+ const COMPARISON_FILE = 'ralph-comparison.json';
22
+ const SHA256 = /^[a-f0-9]{64}$/i;
23
+ const UUIDISH = /^[a-zA-Z0-9_.:-]+$/;
24
+ const SEVERITY_ORDER = Object.freeze({ critical: 4, high: 3, medium: 2, low: 1 });
25
+ const RISKY_SEVERITY = new Set(['critical', 'high']);
26
+ const nowIso = () => new Date().toISOString();
27
+
28
+ function stableSort(value) {
29
+ if (Array.isArray(value)) return value.map(stableSort);
30
+ if (!value || typeof value !== 'object') return value;
31
+ return Object.keys(value)
32
+ .sort()
33
+ .reduce((acc, key) => {
34
+ acc[key] = stableSort(value[key]);
35
+ return acc;
36
+ }, {});
37
+ }
38
+
39
+ function stableStringify(value) {
40
+ return JSON.stringify(stableSort(value));
41
+ }
42
+
43
+ function sha256(value) {
44
+ return crypto.createHash('sha256').update(value).digest('hex');
45
+ }
46
+
47
+ function requireString(value, name, { max = 500, pattern = null } = {}) {
48
+ if (typeof value !== 'string' || !value.trim()) throw new Error(`${name} must be a non-empty string`);
49
+ if (value.length > max) throw new Error(`${name} must be ${max} characters or fewer`);
50
+ if (pattern && !pattern.test(value)) throw new Error(`${name} has an invalid format`);
51
+ return value;
52
+ }
53
+
54
+ function optionalString(value, name, options) {
55
+ if (value === undefined || value === null) return null;
56
+ return requireString(value, name, options);
57
+ }
58
+
59
+ function requireIsoDate(value, name) {
60
+ requireString(value, name, { max: 40 });
61
+ if (Number.isNaN(Date.parse(value))) throw new Error(`${name} must be an ISO timestamp`);
62
+ return value;
63
+ }
64
+
65
+ function asArray(value, name) {
66
+ if (value === undefined) return [];
67
+ if (!Array.isArray(value)) throw new Error(`${name} must be an array`);
68
+ return value;
69
+ }
70
+
71
+ function normalizeSeverity(value) {
72
+ const severity = String(value || 'medium').toLowerCase();
73
+ return Object.prototype.hasOwnProperty.call(SEVERITY_ORDER, severity) ? severity : 'medium';
74
+ }
75
+
76
+ function severityScore(value) {
77
+ return SEVERITY_ORDER[normalizeSeverity(value)];
78
+ }
79
+
80
+ function isSha256(value) {
81
+ return typeof value === 'string' && SHA256.test(value);
82
+ }
83
+
84
+ function buildFindingIdentity(finding = {}) {
85
+ if (isSha256(finding.fingerprint)) return finding.fingerprint;
86
+ if (finding.id) return sha256(`id:${String(finding.id)}`);
87
+ const source = finding.source || {};
88
+ const seed = stableStringify({
89
+ ruleId: finding.ruleId || 'unknown_rule',
90
+ filePath: source.filePath || 'unknown_file',
91
+ lineNumber: Number.isInteger(source.lineNumber) ? source.lineNumber : null,
92
+ endLineNumber: Number.isInteger(source.endLineNumber) ? source.endLineNumber : null,
93
+ snippetPreview: finding.snippetPreview || null,
94
+ });
95
+ return sha256(seed);
96
+ }
97
+
98
+ function mapFindings(findings = []) {
99
+ const mapped = new Map();
100
+ for (const finding of findings) {
101
+ const hasFingerprint = isSha256(finding.fingerprint);
102
+ const hasId = typeof finding.id === 'string' && finding.id.trim().length > 0;
103
+ const key = buildFindingIdentity(finding);
104
+ const confidence = hasFingerprint && hasId ? 'high' : (hasFingerprint || hasId ? 'medium' : 'low');
105
+ const keySource = hasFingerprint ? 'fingerprint' : (hasId ? 'id' : 'synthetic');
106
+ if (!mapped.has(key)) mapped.set(key, { finding, confidence, keySource });
107
+ }
108
+ return mapped;
109
+ }
110
+
111
+ function acceptedRiskMap(agentFixTasks = {}) {
112
+ const map = new Map();
113
+ const tasks = asArray(agentFixTasks.tasks, 'agentFixTasks.tasks');
114
+ for (const task of tasks) {
115
+ if (!task || !task.acceptedRiskRef) continue;
116
+ for (const findingId of asArray(task.findingIds, 'agentFixTask.findingIds')) {
117
+ if (!map.has(findingId)) map.set(findingId, task.acceptedRiskRef);
118
+ }
119
+ }
120
+ return map;
121
+ }
122
+
123
+ function findingSnapshot(finding = null) {
124
+ if (!finding) return null;
125
+ return {
126
+ id: optionalString(finding.id, 'finding.id', { max: 120, pattern: UUIDISH }),
127
+ ruleId: optionalString(finding.ruleId, 'finding.ruleId', { max: 80, pattern: UUIDISH }),
128
+ severity: normalizeSeverity(finding.severity),
129
+ category: optionalString(finding.category, 'finding.category', { max: 80, pattern: UUIDISH }),
130
+ filePath: optionalString(finding.source?.filePath, 'finding.source.filePath', { max: 300 }),
131
+ lineNumber: Number.isInteger(finding.source?.lineNumber) ? finding.source.lineNumber : null,
132
+ fingerprint: buildFindingIdentity(finding),
133
+ };
134
+ }
135
+
136
+ function summarizeComparison(rows) {
137
+ const byStatus = RALPH_COMPARISON_STATUSES.reduce((acc, status) => ({ ...acc, [status]: 0 }), {});
138
+ let unresolvedHighOrCritical = 0;
139
+ let riskyNewCount = 0;
140
+ for (const row of rows) {
141
+ byStatus[row.status] += 1;
142
+ const severity = row.current?.severity || row.previous?.severity || 'low';
143
+ const unresolved = ['new', 'worsened', 'unchanged', 'acceptedRisk', 'manualReview'].includes(row.status);
144
+ if (unresolved && RISKY_SEVERITY.has(severity)) unresolvedHighOrCritical += 1;
145
+ if (row.status === 'new' && RISKY_SEVERITY.has(severity)) riskyNewCount += 1;
146
+ }
147
+ return {
148
+ total: rows.length,
149
+ byStatus,
150
+ unresolvedHighOrCritical,
151
+ riskyNewCount,
152
+ acceptedRiskCount: byStatus.acceptedRisk,
153
+ manualReviewCount: byStatus.manualReview,
154
+ };
155
+ }
156
+
157
+ // Resolve the accepted-risk reference for one finding. A canonical accepted-risk
158
+ // register (VIB-85) is authoritative when it knows the finding: an active record
159
+ // marks it accepted, an expired record reverts it to unresolved and suppresses the
160
+ // legacy AgentFixTask field. Findings the register does not know fall back to the
161
+ // legacy task acceptedRiskRef for backward compatibility.
162
+ function resolveAcceptedRiskRef({ key, currentFinding, registerIndex, taskMap }) {
163
+ const findingId = currentFinding?.id || null;
164
+ const known = registerIndex.knownByKey.get(key)
165
+ || (findingId ? registerIndex.knownById.get(findingId) : null)
166
+ || null;
167
+ if (known) {
168
+ const active = registerIndex.activeByKey.get(key)
169
+ || (findingId ? registerIndex.activeById.get(findingId) : null)
170
+ || null;
171
+ return active ? active.riskId : null;
172
+ }
173
+ return findingId ? (taskMap.get(findingId) || null) : null;
174
+ }
175
+
176
+ export function buildRalphComparisonArtifact({
177
+ previousDeterministicScan = {},
178
+ currentDeterministicScan = {},
179
+ agentFixTasks = {},
180
+ acceptedRiskRegister = null,
181
+ generatedAt = nowIso(),
182
+ sourceRefs = {},
183
+ } = {}) {
184
+ const previous = cloneSourceSafe(previousDeterministicScan, 'ralphCompare.previousDeterministicScan');
185
+ const current = cloneSourceSafe(currentDeterministicScan, 'ralphCompare.currentDeterministicScan');
186
+ const tasks = cloneSourceSafe(agentFixTasks, 'ralphCompare.agentFixTasks');
187
+ const acceptedRiskByFindingId = acceptedRiskMap(tasks);
188
+ const registerIndex = indexAcceptedRiskForComparison({
189
+ register: acceptedRiskRegister || { records: [] },
190
+ generatedAt,
191
+ });
192
+ const previousFindings = mapFindings(previous.findings || []);
193
+ const currentFindings = mapFindings(current.findings || []);
194
+ const allKeys = [...new Set([...previousFindings.keys(), ...currentFindings.keys()])].sort();
195
+
196
+ const comparisons = allKeys.map((key) => {
197
+ const previousRecord = previousFindings.get(key) || null;
198
+ const currentRecord = currentFindings.get(key) || null;
199
+ const previousFinding = previousRecord?.finding || null;
200
+ const currentFinding = currentRecord?.finding || null;
201
+ const previousSnapshot = findingSnapshot(previousFinding);
202
+ const currentSnapshot = findingSnapshot(currentFinding);
203
+ const acceptedRiskRef = resolveAcceptedRiskRef({
204
+ key,
205
+ currentFinding,
206
+ registerIndex,
207
+ taskMap: acceptedRiskByFindingId,
208
+ });
209
+ const lowConfidence = previousRecord?.confidence === 'low' || currentRecord?.confidence === 'low';
210
+ const manualReviewReason = lowConfidence
211
+ ? 'Low-confidence finding match (synthetic identity fallback) requires manual review before resolve/new claims.'
212
+ : null;
213
+
214
+ let status = 'unchanged';
215
+ if (acceptedRiskRef) {
216
+ status = 'acceptedRisk';
217
+ } else if (lowConfidence) {
218
+ status = 'manualReview';
219
+ } else if (previousFinding && !currentFinding) {
220
+ status = 'resolved';
221
+ } else if (!previousFinding && currentFinding) {
222
+ status = 'new';
223
+ } else {
224
+ const previousSeverity = severityScore(previousFinding?.severity);
225
+ const currentSeverity = severityScore(currentFinding?.severity);
226
+ if (currentSeverity > previousSeverity) status = 'worsened';
227
+ else if (currentSeverity < previousSeverity) status = 'improved';
228
+ else status = 'unchanged';
229
+ }
230
+
231
+ return {
232
+ findingKey: key,
233
+ status,
234
+ acceptedRiskRef,
235
+ manualReviewReason,
236
+ matchConfidence: lowConfidence ? 'low' : 'high',
237
+ previous: previousSnapshot,
238
+ current: currentSnapshot,
239
+ };
240
+ });
241
+
242
+ const summary = summarizeComparison(comparisons);
243
+ const artifact = {
244
+ schemaVersion: RALPH_COMPARISON_SCHEMA_VERSION,
245
+ generatedAt: requireIsoDate(generatedAt, 'ralphComparison.generatedAt'),
246
+ root: {
247
+ name: current.root?.name || previous.root?.name || tasks.root?.name || 'project',
248
+ pathHash: current.root?.pathHash || previous.root?.pathHash || tasks.root?.pathHash || null,
249
+ },
250
+ sources: {
251
+ previousDeterministicScan: sourceRefs.previousDeterministicScan || 'previous_deterministic_scan',
252
+ currentDeterministicScan: sourceRefs.currentDeterministicScan || 'current_deterministic_scan',
253
+ agentFixTasks: sourceRefs.agentFixTasks || null,
254
+ acceptedRiskRegister: sourceRefs.acceptedRiskRegister || null,
255
+ },
256
+ summary,
257
+ reportHandoff: {
258
+ unresolvedHighOrCritical: summary.unresolvedHighOrCritical,
259
+ newHighOrCritical: summary.riskyNewCount,
260
+ acceptedRiskCount: summary.acceptedRiskCount,
261
+ manualReviewCount: summary.manualReviewCount,
262
+ canMarkFixed: summary.byStatus.new === 0
263
+ && summary.byStatus.worsened === 0
264
+ && summary.unresolvedHighOrCritical === 0
265
+ && summary.byStatus.manualReview === 0,
266
+ note: 'Comparison shows resolved, unchanged, new, worsened, improved, accepted-risk, and low-confidence manual-review records without raw source.',
267
+ },
268
+ comparisons,
269
+ privacy: {
270
+ rawSourceStored: false,
271
+ secretValuesCaptured: false,
272
+ comparisonUsesHashesAndMetadata: true,
273
+ artifactStorage: 'local_metadata_and_hash_refs',
274
+ },
275
+ };
276
+
277
+ return validateRalphComparisonArtifact(artifact);
278
+ }
279
+
280
+ function validateComparisonRow(input = {}) {
281
+ assertSourceSafePayload(input, 'ralphComparison.row');
282
+ const status = requireString(input.status, 'ralphComparison.row.status', { max: 40 });
283
+ if (!RALPH_COMPARISON_STATUSES.includes(status)) {
284
+ throw new Error(`ralphComparison.row.status must be one of ${RALPH_COMPARISON_STATUSES.join(', ')}`);
285
+ }
286
+ const row = {
287
+ findingKey: requireString(input.findingKey, 'ralphComparison.row.findingKey', { max: 64, pattern: SHA256 }),
288
+ status,
289
+ acceptedRiskRef: optionalString(input.acceptedRiskRef, 'ralphComparison.row.acceptedRiskRef', { max: 160, pattern: UUIDISH }),
290
+ manualReviewReason: optionalString(input.manualReviewReason, 'ralphComparison.row.manualReviewReason', { max: 500 }),
291
+ matchConfidence: optionalString(input.matchConfidence, 'ralphComparison.row.matchConfidence', { max: 40 }),
292
+ previous: input.previous ? cloneSourceSafe(input.previous, 'ralphComparison.row.previous') : null,
293
+ current: input.current ? cloneSourceSafe(input.current, 'ralphComparison.row.current') : null,
294
+ };
295
+ if (!row.previous && !row.current) {
296
+ throw new Error('ralphComparison rows must include previous or current finding snapshot');
297
+ }
298
+ if (row.status === 'acceptedRisk' && !row.acceptedRiskRef) {
299
+ throw new Error('acceptedRisk rows require acceptedRiskRef');
300
+ }
301
+ if (row.status === 'manualReview' && !row.manualReviewReason) {
302
+ throw new Error('manualReview rows require manualReviewReason');
303
+ }
304
+ return row;
305
+ }
306
+
307
+ export function validateRalphComparisonArtifact(input = {}) {
308
+ assertSourceSafePayload(input, 'ralphComparisonArtifact');
309
+ const artifact = {
310
+ schemaVersion: requireString(input.schemaVersion, 'ralphComparison.schemaVersion', { max: 80, pattern: UUIDISH }),
311
+ generatedAt: requireIsoDate(input.generatedAt, 'ralphComparison.generatedAt'),
312
+ root: cloneSourceSafe(input.root || {}, 'ralphComparison.root'),
313
+ sources: cloneSourceSafe(input.sources || {}, 'ralphComparison.sources'),
314
+ summary: cloneSourceSafe(input.summary || {}, 'ralphComparison.summary'),
315
+ reportHandoff: cloneSourceSafe(input.reportHandoff || {}, 'ralphComparison.reportHandoff'),
316
+ comparisons: asArray(input.comparisons, 'ralphComparison.comparisons').map(validateComparisonRow),
317
+ privacy: cloneSourceSafe(input.privacy || {}, 'ralphComparison.privacy'),
318
+ };
319
+ if (artifact.schemaVersion !== RALPH_COMPARISON_SCHEMA_VERSION) {
320
+ throw new Error(`ralphComparison.schemaVersion must be ${RALPH_COMPARISON_SCHEMA_VERSION}`);
321
+ }
322
+ return artifact;
323
+ }
324
+
325
+ export function hashRalphComparisonArtifact(artifact) {
326
+ assertSourceSafePayload(artifact, 'ralphComparisonArtifact');
327
+ return sha256(stableStringify(validateRalphComparisonArtifact(artifact)));
328
+ }
329
+
330
+ async function readLocalArtifact({ rootPath, ref, type }) {
331
+ if (!ref || ref.type !== type || ref.storage !== 'local' || !ref.uri) {
332
+ throw new Error(`Ralph comparison requires a local ${type} artifact reference`);
333
+ }
334
+ const resolvedRoot = path.resolve(rootPath);
335
+ const target = path.resolve(resolvedRoot, ref.uri);
336
+ const relative = path.relative(resolvedRoot, target);
337
+ if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) {
338
+ throw new Error(`Ralph comparison artifact ref for ${type} escapes the bound root`);
339
+ }
340
+ const raw = await fs.readFile(target, 'utf8');
341
+ if (!ref.hash) {
342
+ throw new Error(`Ralph comparison requires ${type} artifact refs to include a hash`);
343
+ }
344
+ const actualHash = sha256(raw);
345
+ if (actualHash.toLowerCase() !== String(ref.hash).toLowerCase()) {
346
+ throw new Error(`Ralph comparison ${type} artifact hash mismatch: expected ${ref.hash}, got ${actualHash}`);
347
+ }
348
+ const parsed = JSON.parse(raw);
349
+ assertSourceSafePayload(parsed, `ralphComparison.${type}`);
350
+ return parsed;
351
+ }
352
+
353
+ function findArtifactRef(state, type, preferredKeys = []) {
354
+ const artifacts = state?.artifacts || {};
355
+ for (const key of preferredKeys) {
356
+ if (artifacts[key]?.type === type) return artifacts[key];
357
+ }
358
+ return Object.values(artifacts).find((ref) => ref?.type === type) || null;
359
+ }
360
+
361
+ function normalizeRef(input = null) {
362
+ if (!input || typeof input !== 'object') return null;
363
+ if (!input.uri || !input.hash) return null;
364
+ return {
365
+ id: input.id || 'artifact-baseline-deterministic-scan',
366
+ type: 'deterministic_scan',
367
+ storage: input.storage || 'local',
368
+ uri: input.uri,
369
+ hash: input.hash,
370
+ };
371
+ }
372
+
373
+ export async function writeRalphComparisonArtifact({ rootPath, runId, artifact }) {
374
+ const safeArtifact = validateRalphComparisonArtifact(artifact);
375
+ const safeRunId = validateRunIdSegment(runId, 'runId');
376
+ const relativeUri = `.vibesecur/deep-scans/${safeRunId}/${COMPARISON_FILE}`;
377
+ const target = path.join(path.resolve(rootPath), relativeUri);
378
+ await fs.mkdir(path.dirname(target), { recursive: true });
379
+ const serialized = `${JSON.stringify(stableSort(safeArtifact), null, 2)}\n`;
380
+ const tmp = `${target}.${process.pid}.${Date.now()}.tmp`;
381
+ await fs.writeFile(tmp, serialized, 'utf8');
382
+ await fs.rename(tmp, target);
383
+ return {
384
+ uri: relativeUri,
385
+ hash: sha256(serialized),
386
+ };
387
+ }
388
+
389
+ export function ralphComparisonStepper() {
390
+ return {
391
+ id: RALPH_COMPARISON_STEPPER_ID,
392
+ version: RALPH_COMPARISON_STEPPER_VERSION,
393
+ title: 'Ralph Comparison Stepper',
394
+ category: 'ralph_loop',
395
+ requiredInputs: ['deterministic_scan'],
396
+ producedArtifacts: ['ralph_comparison'],
397
+ defaultTimeoutMs: 30000,
398
+ async run({ runId, config = {}, state = {}, tools = {} }) {
399
+ const rootPath = tools.rootPath || config.rootPath || '.';
400
+ const startedAt = nowIso();
401
+ const previousRef = normalizeRef(config.previousDeterministicScanRef);
402
+ if (!previousRef) {
403
+ return {
404
+ stepperId: RALPH_COMPARISON_STEPPER_ID,
405
+ version: RALPH_COMPARISON_STEPPER_VERSION,
406
+ status: 'skipped',
407
+ startedAt,
408
+ finishedAt: nowIso(),
409
+ evidence: [],
410
+ findings: [],
411
+ artifacts: [],
412
+ receipts: [],
413
+ summary: 'Ralph comparison skipped because no previous deterministic scan artifact reference was supplied.',
414
+ skippedReason: 'Missing previousDeterministicScanRef in step configuration.',
415
+ nextActions: ['Pass previousDeterministicScanRef { uri, hash } to compare rerun findings.'],
416
+ };
417
+ }
418
+ const currentRef = findArtifactRef(state, 'deterministic_scan', ['rules.scan']);
419
+ const tasksRef = findArtifactRef(state, 'agent_fix_tasks', ['ralph.tasks']);
420
+ const previousDeterministicScan = await readLocalArtifact({
421
+ rootPath,
422
+ ref: previousRef,
423
+ type: 'deterministic_scan',
424
+ });
425
+ const currentDeterministicScan = await readLocalArtifact({
426
+ rootPath,
427
+ ref: currentRef,
428
+ type: 'deterministic_scan',
429
+ });
430
+ const agentFixTasks = tasksRef
431
+ ? await readLocalArtifact({
432
+ rootPath,
433
+ ref: tasksRef,
434
+ type: 'agent_fix_tasks',
435
+ })
436
+ : { tasks: [] };
437
+ const { register: acceptedRiskRegister, exists: hasAcceptedRiskRegister } = await readAcceptedRiskRegister({ rootPath });
438
+ const artifact = buildRalphComparisonArtifact({
439
+ previousDeterministicScan,
440
+ currentDeterministicScan,
441
+ agentFixTasks,
442
+ acceptedRiskRegister,
443
+ generatedAt: config.generatedAt || startedAt,
444
+ sourceRefs: {
445
+ previousDeterministicScan: previousRef.id || 'artifact-previous-deterministic-scan',
446
+ currentDeterministicScan: currentRef.id,
447
+ agentFixTasks: tasksRef?.id || null,
448
+ acceptedRiskRegister: hasAcceptedRiskRegister ? 'accepted_risk_register' : null,
449
+ },
450
+ });
451
+ const contentHash = hashRalphComparisonArtifact(artifact);
452
+ const written = await writeRalphComparisonArtifact({ rootPath, runId, artifact });
453
+ const safeRunId = validateRunIdSegment(runId, 'runId');
454
+ const ref = createArtifactRef({
455
+ id: `artifact-${safeRunId}-ralph-comparison`,
456
+ type: 'ralph_comparison',
457
+ storage: 'local',
458
+ uri: written.uri,
459
+ hash: written.hash,
460
+ preview: 'Ralph comparison with resolved, unchanged, new, worsened, improved, accepted-risk, and manual-review statuses.',
461
+ metadata: {
462
+ schemaVersion: RALPH_COMPARISON_SCHEMA_VERSION,
463
+ contentHash,
464
+ generatedBy: RALPH_COMPARISON_STEPPER_ID,
465
+ total: artifact.summary.total,
466
+ unresolvedHighOrCritical: artifact.summary.unresolvedHighOrCritical,
467
+ acceptedRiskCount: artifact.summary.acceptedRiskCount,
468
+ },
469
+ });
470
+
471
+ return {
472
+ stepperId: RALPH_COMPARISON_STEPPER_ID,
473
+ version: RALPH_COMPARISON_STEPPER_VERSION,
474
+ status: 'passed',
475
+ startedAt,
476
+ finishedAt: nowIso(),
477
+ evidence: [{
478
+ type: 'hash',
479
+ label: 'Ralph comparison artifact hash',
480
+ hash: ref.hash,
481
+ preview: 'Rerun comparison artifact written locally.',
482
+ summary: `${artifact.summary.byStatus.resolved} resolved, ${artifact.summary.byStatus.new} new, ${artifact.summary.byStatus.worsened} worsened, ${artifact.summary.byStatus.manualReview} manual-review.`,
483
+ metadata: {
484
+ artifactId: ref.id,
485
+ contentHash,
486
+ previousDeterministicScanArtifactId: previousRef.id || null,
487
+ currentDeterministicScanArtifactId: currentRef.id,
488
+ },
489
+ }],
490
+ findings: [],
491
+ artifacts: [ref],
492
+ receipts: [],
493
+ summary: `Ralph comparison generated: ${artifact.summary.total} tracked findings, ${artifact.summary.byStatus.resolved} resolved, ${artifact.summary.byStatus.new} new, ${artifact.summary.byStatus.acceptedRisk} accepted-risk, ${artifact.summary.byStatus.manualReview} manual-review.`,
494
+ nextActions: ['Use ralph-comparison artifact for report/passport handoff and repeat the fix-rerun loop for unresolved risks.'],
495
+ };
496
+ },
497
+ };
498
+ }