projscan 4.12.1 → 4.14.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.
Files changed (59) hide show
  1. package/README.md +76 -12
  2. package/dist/cli/commands/prove.d.ts +3 -0
  3. package/dist/cli/commands/prove.js +379 -0
  4. package/dist/cli/commands/prove.js.map +1 -0
  5. package/dist/cli/commands/startMissionBundle.js +38 -0
  6. package/dist/cli/commands/startMissionBundle.js.map +1 -1
  7. package/dist/cli/registerCommands.js +2 -0
  8. package/dist/cli/registerCommands.js.map +1 -1
  9. package/dist/core/evidenceComment.js +31 -0
  10. package/dist/core/evidenceComment.js.map +1 -1
  11. package/dist/core/feedback.js +18 -0
  12. package/dist/core/feedback.js.map +1 -1
  13. package/dist/core/intentRouterCatalog.js +34 -0
  14. package/dist/core/intentRouterCatalog.js.map +1 -1
  15. package/dist/core/intentRouterKeywordToolGuards.js +10 -0
  16. package/dist/core/intentRouterKeywordToolGuards.js.map +1 -1
  17. package/dist/core/intentRouterWorkflowKeywordWeights.js +29 -0
  18. package/dist/core/intentRouterWorkflowKeywordWeights.js.map +1 -1
  19. package/dist/core/proofLedger.d.ts +9 -0
  20. package/dist/core/proofLedger.js +144 -0
  21. package/dist/core/proofLedger.js.map +1 -0
  22. package/dist/core/prove.d.ts +20 -0
  23. package/dist/core/prove.js +1121 -0
  24. package/dist/core/prove.js.map +1 -0
  25. package/dist/core/releaseEvidence.js +48 -0
  26. package/dist/core/releaseEvidence.js.map +1 -1
  27. package/dist/core/startFixedRouteCriteria.js +4 -0
  28. package/dist/core/startFixedRouteCriteria.js.map +1 -1
  29. package/dist/core/startRouteActions.js +5 -0
  30. package/dist/core/startRouteActions.js.map +1 -1
  31. package/dist/mcp/toolCatalog.js +2 -0
  32. package/dist/mcp/toolCatalog.js.map +1 -1
  33. package/dist/mcp/tools/prove.d.ts +2 -0
  34. package/dist/mcp/tools/prove.js +93 -0
  35. package/dist/mcp/tools/prove.js.map +1 -0
  36. package/dist/projscan-sbom.cdx.json +6 -6
  37. package/dist/publicCore.d.ts +1 -0
  38. package/dist/publicCore.js +1 -0
  39. package/dist/publicCore.js.map +1 -1
  40. package/dist/tool-manifest.json +68 -3
  41. package/dist/types/dogfood.d.ts +4 -0
  42. package/dist/types/evidencePack.d.ts +13 -0
  43. package/dist/types/proofLedger.d.ts +30 -0
  44. package/dist/types/proofLedger.js +2 -0
  45. package/dist/types/proofLedger.js.map +1 -0
  46. package/dist/types/prove.d.ts +123 -0
  47. package/dist/types/prove.js +2 -0
  48. package/dist/types/prove.js.map +1 -0
  49. package/dist/types.d.ts +2 -0
  50. package/dist/utils/formatSupport.d.ts +1 -0
  51. package/dist/utils/formatSupport.js +1 -0
  52. package/dist/utils/formatSupport.js.map +1 -1
  53. package/docs/GUIDE.md +54 -1
  54. package/docs/demos/projscan-4-1-demo.html +24 -24
  55. package/docs/projscan-mission-control.gif +0 -0
  56. package/docs/projscan-mission-control.png +0 -0
  57. package/docs/projscan-mission-proof.gif +0 -0
  58. package/docs/projscan-proof-router.png +0 -0
  59. package/package.json +1 -1
@@ -0,0 +1,1121 @@
1
+ import crypto from 'node:crypto';
2
+ import { spawn } from 'node:child_process';
3
+ import fs from 'node:fs/promises';
4
+ import path from 'node:path';
5
+ import { readFeedbackFile } from './feedback.js';
6
+ import { appendProofLedgerRecord, changedFileFingerprint, latestProofRecordFor, readProofLedger, redactProofOutput, } from './proofLedger.js';
7
+ import { quoteShellArg } from './startShellArgs.js';
8
+ import { computeSimulation } from './simulate.js';
9
+ import { getChangedFiles } from '../utils/changedFiles.js';
10
+ const DEFAULT_CONTRACT_PATH = '.projscan/proof-contract.json';
11
+ const DEFAULT_RUN_TIMEOUT_MS = 10 * 60 * 1000;
12
+ const PROOF_RUN_TIMEOUT_EXIT_CODE = 124;
13
+ const COMMAND_NOT_FOUND_EXIT_CODE = 127;
14
+ const MAX_PROOF_RUN_OUTPUT_CHARS = 256 * 1024;
15
+ const MAX_PROOF_RUN_LOG_CHARS = 512 * 1024;
16
+ const GENERATED_FORBIDDEN_PATTERNS = [
17
+ '.agentflight/**',
18
+ '.agentloop/**',
19
+ '.git/**',
20
+ '.projscan-memory/**',
21
+ 'coverage/**',
22
+ 'dist/**',
23
+ 'node_modules/**',
24
+ ];
25
+ const HIGH_RISK_FORBIDDEN_FILES = [
26
+ '.env',
27
+ '.env.local',
28
+ '.github/mcp-registry/server.json',
29
+ 'CHANGELOG.md',
30
+ 'package-lock.json',
31
+ 'package.json',
32
+ ];
33
+ const CHANGED_FILE_RULES = [
34
+ {
35
+ kind: 'generated',
36
+ reason: 'Proof Contract artifact used for validation.',
37
+ matches: (input) => Boolean(input.contractPath),
38
+ },
39
+ {
40
+ kind: 'forbidden',
41
+ reason: 'Matched forbidden Proof Contract scope.',
42
+ matches: (input) => input.forbidden,
43
+ },
44
+ {
45
+ kind: 'expected-test',
46
+ reason: 'Expected regression test from the Proof Contract.',
47
+ matches: (input) => Boolean(input.expectedTest),
48
+ },
49
+ {
50
+ kind: 'allowed-production',
51
+ reason: 'Allowed by the Proof Contract.',
52
+ matches: (input) => Boolean(input.allowedProduction),
53
+ },
54
+ {
55
+ kind: 'documentation',
56
+ reason: 'Documentation change outside contract scope.',
57
+ matches: (input) => isDocumentationPath(input.file),
58
+ },
59
+ {
60
+ kind: 'generated',
61
+ reason: 'Generated or local tool artifact changed outside contract scope.',
62
+ matches: (input) => isGeneratedPath(input.file),
63
+ },
64
+ {
65
+ kind: 'security-sensitive',
66
+ reason: 'Security-sensitive file changed outside the Proof Contract.',
67
+ matches: (input) => isSecuritySensitivePath(input.file),
68
+ },
69
+ {
70
+ kind: 'config',
71
+ reason: 'Configuration or release file changed outside the Proof Contract.',
72
+ matches: (input) => isConfigPath(input.file),
73
+ },
74
+ {
75
+ kind: 'unexpected-test',
76
+ reason: 'Test file changed outside the Proof Contract.',
77
+ matches: (input) => isTestPath(input.file),
78
+ },
79
+ {
80
+ kind: 'unexpected-production',
81
+ reason: 'Production source changed outside the Proof Contract.',
82
+ matches: (input) => isProductionPath(input.file),
83
+ },
84
+ ];
85
+ const NEGATIVE_PROOF_OUTCOMES = new Set(['rejected', 'reverted', 'suppressed', 'noisy']);
86
+ const CONFIG_BASENAMES = new Set([
87
+ 'package.json',
88
+ 'package-lock.json',
89
+ 'pnpm-lock.yaml',
90
+ 'yarn.lock',
91
+ 'tsconfig.json',
92
+ ]);
93
+ const CONFIG_SUFFIXES = ['.config.js', '.config.cjs', '.config.mjs', '.config.ts'];
94
+ export async function computeProve(rootPath, options = {}) {
95
+ const modeCount = [
96
+ Boolean(options.intent?.trim()),
97
+ Boolean(options.changed),
98
+ Boolean(options.recordCommand?.trim()),
99
+ options.runCommand !== undefined,
100
+ ].filter(Boolean).length;
101
+ if (modeCount > 1) {
102
+ throw new Error('prove accepts only one of --intent, --changed, --record-command, or --run');
103
+ }
104
+ if (options.runCommand !== undefined)
105
+ return computeRunProof(rootPath, options);
106
+ if (options.recordCommand?.trim())
107
+ return computeRecordProof(rootPath, options);
108
+ if (options.changed)
109
+ return computeChangedProof(rootPath, options);
110
+ return computeIntentProof(rootPath, options);
111
+ }
112
+ async function computeRunProof(rootPath, options) {
113
+ const run = await executeProofCommand(rootPath, options.runCommand ?? [], options.runTimeoutMs);
114
+ const changedFiles = await getChangedFiles(rootPath, options.baseRef);
115
+ const record = await appendProofLedgerRecord(rootPath, options.ledgerPath, {
116
+ command: run.command,
117
+ exitCode: run.exitCode,
118
+ durationMs: run.durationMs,
119
+ changedFiles: proofRelevantChangedFiles(changedFiles.files),
120
+ outputSummary: run.outputSummary,
121
+ logPath: run.logPath,
122
+ source: 'prove-run',
123
+ });
124
+ const verdict = record.status === 'passed' ? 'ready' : 'blocked';
125
+ const verifiedWorkflow = verifiedWorkflowForRecord(verdict, record.status);
126
+ return {
127
+ schemaVersion: 1,
128
+ mode: 'run',
129
+ verdict,
130
+ summary: `${verdict}: executed ${record.status} proof for ${record.command}`,
131
+ commands: [record.command],
132
+ warnings: changedFiles.available ? [] : [changedFiles.reason ?? 'Changed-file evidence is unavailable.'],
133
+ verifiedWorkflow,
134
+ ledgerRecord: record,
135
+ };
136
+ }
137
+ async function computeRecordProof(rootPath, options) {
138
+ const proof = recordProofInput(options);
139
+ const changedFiles = await getChangedFiles(rootPath, options.baseRef);
140
+ const record = await appendProofLedgerRecord(rootPath, options.ledgerPath, {
141
+ command: proof.command,
142
+ exitCode: proof.exitCode,
143
+ durationMs: proof.durationMs,
144
+ changedFiles: proofRelevantChangedFiles(changedFiles.files),
145
+ outputSummary: proof.summary,
146
+ logPath: proof.logPath,
147
+ source: 'prove-record',
148
+ });
149
+ const verdict = record.status === 'passed' ? 'ready' : 'blocked';
150
+ const verifiedWorkflow = verifiedWorkflowForRecord(verdict, record.status);
151
+ return {
152
+ schemaVersion: 1,
153
+ mode: 'record',
154
+ verdict,
155
+ summary: `${verdict}: recorded ${record.status} proof for ${record.command}`,
156
+ commands: [record.command],
157
+ warnings: changedFiles.available ? [] : [changedFiles.reason ?? 'Changed-file evidence is unavailable.'],
158
+ verifiedWorkflow,
159
+ ledgerRecord: record,
160
+ };
161
+ }
162
+ function recordProofInput(options) {
163
+ const command = options.recordCommand?.trim();
164
+ if (!command)
165
+ throw new Error('prove --record-command requires a non-empty command');
166
+ if (typeof options.exitCode !== 'number' || !Number.isInteger(options.exitCode)) {
167
+ throw new Error('prove --record-command requires a numeric exit code');
168
+ }
169
+ if (!isNonNegativeFiniteNumber(options.durationMs)) {
170
+ throw new Error('prove --record-command requires a non-negative duration-ms value');
171
+ }
172
+ return {
173
+ command,
174
+ exitCode: options.exitCode,
175
+ durationMs: options.durationMs,
176
+ summary: options.summary,
177
+ logPath: options.logPath,
178
+ };
179
+ }
180
+ async function executeProofCommand(rootPath, command, timeoutMs) {
181
+ const commandVector = normalizeRunCommand(command);
182
+ const displayCommand = redactProofOutput(commandVector.map(quoteShellArg).join(' '));
183
+ const startedAtMs = Date.now();
184
+ const effectiveTimeoutMs = resolveRunTimeoutMs(timeoutMs);
185
+ const result = await spawnProofCommand(rootPath, commandVector, effectiveTimeoutMs);
186
+ const durationMs = Date.now() - startedAtMs;
187
+ const outputSummary = proofRunOutputSummary(result, effectiveTimeoutMs);
188
+ const logPath = await writeProofRunLog(rootPath, {
189
+ command: displayCommand,
190
+ exitCode: result.exitCode,
191
+ durationMs,
192
+ stdout: result.stdout,
193
+ stderr: result.stderr,
194
+ errorMessage: result.errorMessage,
195
+ timedOut: result.timedOut,
196
+ truncated: result.truncated,
197
+ });
198
+ return {
199
+ command: displayCommand,
200
+ exitCode: result.exitCode,
201
+ durationMs,
202
+ outputSummary,
203
+ logPath,
204
+ };
205
+ }
206
+ function normalizeRunCommand(command) {
207
+ const normalized = command.map((part) => String(part));
208
+ if (normalized.length === 0 || normalized[0]?.trim().length === 0) {
209
+ throw new Error('prove --run requires a command after --, for example: projscan prove --run -- npm test');
210
+ }
211
+ return normalized;
212
+ }
213
+ function resolveRunTimeoutMs(value) {
214
+ if (value === undefined)
215
+ return DEFAULT_RUN_TIMEOUT_MS;
216
+ if (!Number.isFinite(value) || value <= 0) {
217
+ throw new Error('prove --run-timeout-ms requires a positive number');
218
+ }
219
+ return Math.round(value);
220
+ }
221
+ function spawnProofCommand(rootPath, command, timeoutMs) {
222
+ return new Promise((resolve) => {
223
+ const [executable, ...args] = command;
224
+ let stdout = '';
225
+ let stderr = '';
226
+ let truncated = false;
227
+ let timedOut = false;
228
+ let finished = false;
229
+ let killTimer;
230
+ const timeout = setTimeout(() => {
231
+ timedOut = true;
232
+ child.kill('SIGTERM');
233
+ killTimer = setTimeout(() => child.kill('SIGKILL'), 1_000);
234
+ killTimer.unref();
235
+ }, timeoutMs);
236
+ timeout.unref();
237
+ const child = spawn(executable, args, {
238
+ cwd: rootPath,
239
+ env: process.env,
240
+ shell: false,
241
+ stdio: ['ignore', 'pipe', 'pipe'],
242
+ });
243
+ const finish = (exitCode, errorMessage) => {
244
+ if (finished)
245
+ return;
246
+ finished = true;
247
+ clearTimeout(timeout);
248
+ if (killTimer)
249
+ clearTimeout(killTimer);
250
+ resolve({
251
+ exitCode: timedOut ? PROOF_RUN_TIMEOUT_EXIT_CODE : exitCode,
252
+ stdout,
253
+ stderr,
254
+ ...(errorMessage ? { errorMessage } : {}),
255
+ timedOut,
256
+ truncated,
257
+ });
258
+ };
259
+ child.stdout?.on('data', (chunk) => {
260
+ const next = appendBoundedOutput(stdout, chunk);
261
+ stdout = next.value;
262
+ truncated ||= next.truncated;
263
+ });
264
+ child.stderr?.on('data', (chunk) => {
265
+ const next = appendBoundedOutput(stderr, chunk);
266
+ stderr = next.value;
267
+ truncated ||= next.truncated;
268
+ });
269
+ child.on('error', (error) => {
270
+ finish(COMMAND_NOT_FOUND_EXIT_CODE, error instanceof Error ? error.message : String(error));
271
+ });
272
+ child.on('close', (code, signal) => {
273
+ if (code === null) {
274
+ finish(signal ? 1 : 0);
275
+ return;
276
+ }
277
+ finish(code);
278
+ });
279
+ });
280
+ }
281
+ function appendBoundedOutput(current, chunk) {
282
+ const text = chunk.toString('utf-8');
283
+ const remaining = MAX_PROOF_RUN_OUTPUT_CHARS - current.length;
284
+ if (remaining <= 0)
285
+ return { value: current, truncated: text.length > 0 };
286
+ if (text.length > remaining) {
287
+ return { value: current + text.slice(0, remaining), truncated: true };
288
+ }
289
+ return { value: current + text, truncated: false };
290
+ }
291
+ function proofRunOutputSummary(result, timeoutMs) {
292
+ const parts = [
293
+ result.timedOut ? `timed out after ${timeoutMs}ms` : undefined,
294
+ result.errorMessage ? `start error: ${result.errorMessage}` : undefined,
295
+ result.stdout.trim() ? `stdout: ${result.stdout.trim()}` : undefined,
296
+ result.stderr.trim() ? `stderr: ${result.stderr.trim()}` : undefined,
297
+ result.truncated ? 'output truncated' : undefined,
298
+ ].filter((part) => Boolean(part));
299
+ return parts.join(' | ');
300
+ }
301
+ async function writeProofRunLog(rootPath, input) {
302
+ const relativePath = proofRunLogPath(input.command);
303
+ const fullPath = path.resolve(rootPath, relativePath);
304
+ const root = path.resolve(rootPath);
305
+ const relativeToRoot = path.relative(root, fullPath);
306
+ if (!relativeToRoot || relativeToRoot.startsWith('..') || path.isAbsolute(relativeToRoot)) {
307
+ throw new Error('Proof log path must stay inside the project root.');
308
+ }
309
+ await fs.mkdir(path.dirname(fullPath), { recursive: true });
310
+ await fs.writeFile(fullPath, redactedProofRunLog(input), 'utf-8');
311
+ return relativePath;
312
+ }
313
+ function proofRunLogPath(command) {
314
+ const stamp = new Date().toISOString().replace(/[:.]/g, '-');
315
+ const digest = crypto.createHash('sha256').update(command).digest('hex').slice(0, 10);
316
+ return `.projscan/proof-logs/prove-run-${stamp}-${digest}.log`;
317
+ }
318
+ function redactedProofRunLog(input) {
319
+ const raw = [
320
+ `command: ${input.command}`,
321
+ `exitCode: ${input.exitCode}`,
322
+ `durationMs: ${input.durationMs}`,
323
+ `timedOut: ${input.timedOut ? 'yes' : 'no'}`,
324
+ `truncated: ${input.truncated ? 'yes' : 'no'}`,
325
+ input.errorMessage ? `error: ${input.errorMessage}` : undefined,
326
+ '--- stdout ---',
327
+ input.stdout || '(empty)',
328
+ '--- stderr ---',
329
+ input.stderr || '(empty)',
330
+ ]
331
+ .filter((line) => typeof line === 'string')
332
+ .join('\n');
333
+ const redacted = redactProofOutput(raw);
334
+ return `${truncateText(redacted, MAX_PROOF_RUN_LOG_CHARS)}\n`;
335
+ }
336
+ function truncateText(value, maxLength) {
337
+ if (value.length <= maxLength)
338
+ return value;
339
+ return `${value.slice(0, maxLength - 24)}\n[projscan log truncated]\n`;
340
+ }
341
+ function isNonNegativeFiniteNumber(value) {
342
+ return typeof value === 'number' && Number.isFinite(value) && value >= 0;
343
+ }
344
+ async function computeIntentProof(rootPath, options) {
345
+ const intent = normalizeIntent(options.intent);
346
+ if (!intent)
347
+ throw new Error('prove --intent requires a non-empty change intent');
348
+ const [simulation, trustMemory] = await Promise.all([
349
+ computeSimulation(rootPath, {
350
+ plan: intent,
351
+ maxFiles: options.maxFiles,
352
+ }),
353
+ readTrustMemory(options.feedbackPath),
354
+ ]);
355
+ const contract = buildContract({ intent, simulation, trustMemory });
356
+ let savedContractPath;
357
+ if (options.saveContractPath) {
358
+ savedContractPath = await writeContract(rootPath, options.saveContractPath, contract);
359
+ }
360
+ return {
361
+ schemaVersion: 1,
362
+ mode: 'intent',
363
+ verdict: intentVerdict(contract),
364
+ summary: `ready: Proof Contract ${contract.id} constrains ${contract.allowedFiles.length} file(s) and requires ${contract.proofCommands.length} proof command(s).`,
365
+ contract,
366
+ commands: contract.proofCommands,
367
+ warnings: simulation.warnings,
368
+ verifiedWorkflow: contract.verifiedWorkflow,
369
+ ...(savedContractPath ? { savedContractPath } : {}),
370
+ };
371
+ }
372
+ async function computeChangedProof(rootPath, options) {
373
+ const [contract, changedFiles, ledger] = await Promise.all([
374
+ resolveContract(rootPath, options),
375
+ getChangedFiles(rootPath, options.baseRef),
376
+ readProofLedger(rootPath, options.ledgerPath),
377
+ ]);
378
+ const quickPreflight = quickProofPreflight(changedFiles);
379
+ const receipt = buildReceipt({
380
+ contract: contract.contract,
381
+ contractPath: contract.path,
382
+ changedFiles: changedFiles.files,
383
+ changedFilesAvailable: changedFiles.available,
384
+ changedFilesReason: changedFiles.reason,
385
+ riskDelta: contract.contract?.riskDelta ?? fallbackRiskDelta(),
386
+ newRisks: quickPreflight.risks,
387
+ preflightVerdict: quickPreflight.verdict,
388
+ ledger,
389
+ });
390
+ return {
391
+ schemaVersion: 1,
392
+ mode: 'changed',
393
+ verdict: receipt.commitReadiness,
394
+ summary: receipt.summary,
395
+ ...(contract.contract ? { contract: contract.contract } : {}),
396
+ receipt,
397
+ commands: receipt.proofStatus.commandsRequired,
398
+ warnings: receipt.evidenceGaps,
399
+ verifiedWorkflow: receipt.verifiedWorkflow,
400
+ };
401
+ }
402
+ function quickProofPreflight(changedFiles) {
403
+ if (!changedFiles.available) {
404
+ return {
405
+ verdict: 'caution',
406
+ risks: [changedFiles.reason ?? 'Changed-file evidence is unavailable.'],
407
+ };
408
+ }
409
+ if (changedFiles.files.length > 50) {
410
+ return {
411
+ verdict: 'caution',
412
+ risks: [`${changedFiles.files.length} changed files exceeds the proof replay focus threshold of 50.`],
413
+ };
414
+ }
415
+ return {
416
+ verdict: 'proceed',
417
+ risks: [],
418
+ };
419
+ }
420
+ function buildContract(input) {
421
+ const simulationFiles = input.simulation.filesLikelyTouched.map((file) => file.path);
422
+ const allowedFiles = unique(simulationFiles);
423
+ const likelyTests = unique(input.simulation.testsLikelyAffected);
424
+ const forbiddenFiles = forbiddenFilesFor(input.intent, [...allowedFiles, ...likelyTests]);
425
+ const proofCommands = contractProofCommands(input.simulation.proofCommands);
426
+ const evidenceGaps = unique([
427
+ ...(input.simulation.warnings.length > 0 ? input.simulation.warnings : []),
428
+ ...(likelyTests.length === 0 ? ['No likely regression test was inferred from the plan.'] : []),
429
+ ...(input.trustMemory?.gaps ?? []),
430
+ ]);
431
+ const confidence = confidenceForTrustMemory(input.simulation.confidence, input.trustMemory);
432
+ const contract = {
433
+ schemaVersion: 1,
434
+ id: `proof-contract-${slug(input.intent)}`,
435
+ intent: input.intent,
436
+ createdAt: new Date().toISOString(),
437
+ allowedFiles,
438
+ forbiddenFiles,
439
+ riskyContracts: riskyContractsFor(input.simulation.contractsLikelyAffected, allowedFiles),
440
+ likelyTests,
441
+ missingRegressionTests: likelyTests.length > 0 ? [] : ['Add one regression test around the behavior named by the intent.'],
442
+ proofCommands,
443
+ safeChangeShape: safeChangeShape(input.simulation.recommendedAlternative.summary),
444
+ rollbackPlan: rollbackPlan([...allowedFiles, ...likelyTests]),
445
+ confidence,
446
+ confidenceReason: confidenceReasonForSimulation(confidence, input.simulation.confidence, input.trustMemory),
447
+ evidenceStrength: {
448
+ level: input.simulation.evidence.length > 1 ? 'moderate' : 'thin',
449
+ score: Math.min(80, input.simulation.evidence.length * 15),
450
+ sources: unique(input.simulation.evidence.map((entry) => entry.source)),
451
+ gaps: evidenceGaps,
452
+ },
453
+ trustMemory: {
454
+ status: input.trustMemory?.status ?? 'none',
455
+ summary: input.trustMemory?.summary ?? 'No local trust-memory artifact was applied.',
456
+ signals: input.trustMemory?.signals ?? [],
457
+ },
458
+ reviewerGuidance: 'Review scope first, then require the listed proof commands before approving commit or handoff.',
459
+ receiptCommand: `projscan prove --changed --contract ${quoteShellArg(DEFAULT_CONTRACT_PATH)} --format markdown`,
460
+ riskDelta: input.simulation.riskDelta,
461
+ };
462
+ return {
463
+ ...contract,
464
+ verifiedWorkflow: verifiedWorkflowForContract(contract),
465
+ };
466
+ }
467
+ function contractProofCommands(simulationCommands) {
468
+ return unique([
469
+ simulationCommands.find((command) => command.startsWith('projscan simulate ')),
470
+ simulationCommands.find((command) => /^npm (?:run )?test\b/.test(command)),
471
+ 'projscan assess --mode fix-first --format json',
472
+ 'projscan preflight --mode before_commit --format json',
473
+ ].filter((command) => typeof command === 'string'));
474
+ }
475
+ function buildReceipt(input) {
476
+ const scope = scopeFor(input.contract, input.contractPath, input.changedFiles);
477
+ const evidenceGaps = evidenceGapsFor(input);
478
+ const proofCommands = input.contract?.proofCommands ?? [
479
+ 'projscan assess --mode fix-first --format json',
480
+ 'projscan preflight --mode before_commit --format json',
481
+ ];
482
+ const proofStatus = proofStatusFor(proofCommands, input.ledger, input.changedFiles);
483
+ const commitReadiness = readinessFor({
484
+ scopeStatus: scope.status,
485
+ forbiddenTouched: scope.forbiddenTouched,
486
+ preflightVerdict: input.preflightVerdict,
487
+ evidenceGaps,
488
+ proofStatus: proofStatus.status,
489
+ });
490
+ const riskDeltaDirection = riskDeltaDirectionFor(input.riskDelta);
491
+ const reviewerDecision = reviewerDecisionFor({
492
+ commitReadiness,
493
+ proofStatus: proofStatus.status,
494
+ scope,
495
+ preflightVerdict: input.preflightVerdict,
496
+ });
497
+ const receipt = {
498
+ summary: summaryForReceipt(commitReadiness, scope),
499
+ commitReadiness,
500
+ scope,
501
+ proofStatus,
502
+ riskDelta: input.riskDelta,
503
+ riskDeltaDirection,
504
+ reviewerDecision,
505
+ newRisks: input.newRisks,
506
+ evidenceGaps,
507
+ reviewerGuidance: reviewerGuidanceFor(commitReadiness, scope, reviewerDecision, proofStatus.status),
508
+ };
509
+ return {
510
+ ...receipt,
511
+ verifiedWorkflow: verifiedWorkflowForReceipt(receipt),
512
+ };
513
+ }
514
+ function verifiedWorkflowForContract(contract) {
515
+ return {
516
+ phase: 'contract',
517
+ status: intentVerdict(contract),
518
+ nextAction: 'save the Proof Contract, make the bounded edit, then record proof commands',
519
+ nextCommand: contract.receiptCommand,
520
+ staleProof: false,
521
+ missingProof: contract.proofCommands.length > 0,
522
+ failedProof: false,
523
+ };
524
+ }
525
+ function verifiedWorkflowForRecord(verdict, recordStatus) {
526
+ const failedProof = recordStatus === 'failed';
527
+ return {
528
+ phase: 'record',
529
+ status: verdict,
530
+ nextAction: failedProof
531
+ ? 'fix the failed proof command, record it again, then replay changed proof'
532
+ : 'run projscan prove --changed to replay the ledger against the current diff',
533
+ nextCommand: 'projscan prove --changed --format markdown',
534
+ staleProof: false,
535
+ missingProof: false,
536
+ failedProof,
537
+ };
538
+ }
539
+ function verifiedWorkflowForReceipt(receipt) {
540
+ const proofStatus = receipt.proofStatus.status;
541
+ const staleProof = proofStatus === 'stale' || receipt.proofStatus.staleCommands.length > 0;
542
+ const missingProof = proofStatus === 'missing' ||
543
+ proofStatus === 'partial' ||
544
+ receipt.proofStatus.missingCommands.length > 0;
545
+ const failedProof = proofStatus === 'failed' || receipt.proofStatus.failedCommands.length > 0;
546
+ return {
547
+ phase: 'receipt',
548
+ status: receipt.commitReadiness,
549
+ nextAction: nextActionForReceipt({
550
+ receipt,
551
+ staleProof,
552
+ missingProof,
553
+ failedProof,
554
+ }),
555
+ nextCommand: nextCommandForReceipt({
556
+ receipt,
557
+ staleProof,
558
+ missingProof,
559
+ failedProof,
560
+ }),
561
+ reviewerDecision: receipt.reviewerDecision,
562
+ scopeStatus: receipt.scope.status,
563
+ proofStatus,
564
+ riskDeltaDirection: receipt.riskDeltaDirection,
565
+ staleProof,
566
+ missingProof,
567
+ failedProof,
568
+ };
569
+ }
570
+ function nextActionForReceipt(input) {
571
+ if (input.failedProof)
572
+ return 'fix failed proof commands before review';
573
+ if (input.staleProof)
574
+ return 'rerun stale proof commands before review';
575
+ if (input.missingProof)
576
+ return 'record missing proof commands before review';
577
+ if (input.receipt.scope.status === 'drifted') {
578
+ return 'resolve scope drift or update the Proof Contract before review';
579
+ }
580
+ if (input.receipt.reviewerDecision === 'safe-to-review') {
581
+ return 'share the Proof Receipt with the reviewer';
582
+ }
583
+ return 'review focused scope and proof gaps before approval';
584
+ }
585
+ function nextCommandForReceipt(input) {
586
+ if (input.failedProof) {
587
+ return `projscan prove --record-command ${quoteShellArg(input.receipt.proofStatus.failedCommands[0] ?? '<command>')} --exit-code 0 --duration-ms <ms>`;
588
+ }
589
+ if (input.staleProof) {
590
+ return `projscan prove --record-command ${quoteShellArg(input.receipt.proofStatus.staleCommands[0] ?? '<command>')} --exit-code 0 --duration-ms <ms>`;
591
+ }
592
+ if (input.missingProof) {
593
+ return 'projscan prove --record-command "<command>" --exit-code 0 --duration-ms <ms>';
594
+ }
595
+ if (input.receipt.scope.status === 'drifted')
596
+ return 'projscan prove --changed --format markdown';
597
+ return 'projscan evidence-pack --pr-comment';
598
+ }
599
+ function proofStatusFor(proofCommands, ledger, changedFiles) {
600
+ const relevantChangedFiles = proofRelevantChangedFiles(changedFiles);
601
+ const currentFingerprint = changedFileFingerprint(relevantChangedFiles);
602
+ const commandEvidence = proofCommands.map((command) => {
603
+ const record = latestProofRecordFor(ledger, command);
604
+ if (!record) {
605
+ return {
606
+ command,
607
+ status: 'missing',
608
+ fresh: false,
609
+ };
610
+ }
611
+ const fresh = record.changedFileFingerprint === currentFingerprint;
612
+ if (!fresh) {
613
+ return {
614
+ command,
615
+ status: 'stale',
616
+ fresh: false,
617
+ exitCode: record.exitCode,
618
+ durationMs: record.durationMs,
619
+ completedAt: record.completedAt,
620
+ outputSummary: record.outputSummary,
621
+ ...(record.logPath ? { logPath: record.logPath } : {}),
622
+ staleReason: 'Recorded changed files differ from current changed files.',
623
+ };
624
+ }
625
+ return {
626
+ command,
627
+ status: record.exitCode === 0 ? 'passed' : 'failed',
628
+ fresh: true,
629
+ exitCode: record.exitCode,
630
+ durationMs: record.durationMs,
631
+ completedAt: record.completedAt,
632
+ outputSummary: record.outputSummary,
633
+ ...(record.logPath ? { logPath: record.logPath } : {}),
634
+ };
635
+ });
636
+ const missingCommands = commandEvidence
637
+ .filter((entry) => entry.status === 'missing')
638
+ .map((entry) => entry.command);
639
+ const failedCommands = commandEvidence
640
+ .filter((entry) => entry.status === 'failed')
641
+ .map((entry) => entry.command);
642
+ const staleCommands = commandEvidence
643
+ .filter((entry) => entry.status === 'stale')
644
+ .map((entry) => entry.command);
645
+ const commandsRun = commandEvidence
646
+ .filter((entry) => typeof entry.exitCode === 'number')
647
+ .map((entry) => entry.command);
648
+ const status = proofStatusSummary({
649
+ requiredCount: proofCommands.length,
650
+ missingCount: missingCommands.length,
651
+ failedCount: failedCommands.length,
652
+ staleCount: staleCommands.length,
653
+ });
654
+ return {
655
+ status,
656
+ commandsRequired: proofCommands,
657
+ commandsRun,
658
+ missingCommands,
659
+ failedCommands,
660
+ staleCommands,
661
+ commandEvidence,
662
+ };
663
+ }
664
+ function proofStatusSummary(input) {
665
+ if (input.requiredCount === 0)
666
+ return 'not-run';
667
+ if (input.failedCount > 0)
668
+ return 'failed';
669
+ if (input.staleCount === input.requiredCount)
670
+ return 'stale';
671
+ if (input.missingCount === input.requiredCount)
672
+ return 'missing';
673
+ if (input.staleCount > 0 || input.missingCount > 0)
674
+ return 'partial';
675
+ return 'passed';
676
+ }
677
+ function proofRelevantChangedFiles(files) {
678
+ return files.filter((file) => !isGeneratedPath(file));
679
+ }
680
+ function scopeFor(contract, contractPath, changedFiles) {
681
+ if (!contract) {
682
+ const classifications = changedFiles.map((file) => classifyChangedFile({ file, forbidden: false }));
683
+ return {
684
+ status: 'missing-contract',
685
+ changedFiles,
686
+ allowedTouched: [],
687
+ forbiddenTouched: [],
688
+ outsideAllowed: changedFiles,
689
+ classifications,
690
+ ...classificationBuckets(classifications),
691
+ ...(contractPath ? { contractPath } : {}),
692
+ };
693
+ }
694
+ const allowed = new Set([
695
+ ...contract.allowedFiles,
696
+ ...contract.likelyTests,
697
+ ...(contractPath ? [contractPath] : []),
698
+ ]);
699
+ const forbiddenTouched = changedFiles.filter((file) => contract.forbiddenFiles.some((pattern) => pathMatches(file, pattern)));
700
+ const allowedTouched = changedFiles.filter((file) => allowed.has(file));
701
+ const outsideAllowed = changedFiles.filter((file) => !allowed.has(file) && !isLocalProofArtifactPath(file));
702
+ const classifications = changedFiles.map((file) => classifyChangedFile({
703
+ file,
704
+ forbidden: forbiddenTouched.includes(file),
705
+ allowedProduction: contract.allowedFiles.includes(file),
706
+ expectedTest: contract.likelyTests.includes(file),
707
+ contractPath: contractPath === file,
708
+ }));
709
+ const status = forbiddenTouched.length > 0 || outsideAllowed.length > 0 ? 'drifted' : 'within-contract';
710
+ return {
711
+ status,
712
+ changedFiles,
713
+ allowedTouched,
714
+ forbiddenTouched,
715
+ outsideAllowed,
716
+ classifications,
717
+ ...classificationBuckets(classifications),
718
+ ...(contractPath ? { contractPath } : {}),
719
+ };
720
+ }
721
+ async function resolveContract(rootPath, options) {
722
+ if (options.contract)
723
+ return { contract: options.contract };
724
+ if (options.contractPath) {
725
+ return {
726
+ contract: await readContract(rootPath, options.contractPath, true),
727
+ path: options.contractPath,
728
+ };
729
+ }
730
+ const contract = await readContract(rootPath, DEFAULT_CONTRACT_PATH, false);
731
+ return contract ? { contract, path: DEFAULT_CONTRACT_PATH } : {};
732
+ }
733
+ async function readContract(rootPath, filePath, required) {
734
+ const fullPath = path.resolve(rootPath, filePath);
735
+ try {
736
+ const parsed = JSON.parse(await fs.readFile(fullPath, 'utf-8'));
737
+ if (parsed.schemaVersion !== 1 || !Array.isArray(parsed.allowedFiles) || !parsed.id) {
738
+ throw new Error('invalid Proof Contract shape');
739
+ }
740
+ return parsed;
741
+ }
742
+ catch (error) {
743
+ if (!required && isNodeErrorCode(error, 'ENOENT'))
744
+ return undefined;
745
+ throw new Error(`Could not read Proof Contract ${filePath}: ${error instanceof Error ? error.message : 'invalid JSON'}`, { cause: error });
746
+ }
747
+ }
748
+ async function writeContract(rootPath, filePath, contract) {
749
+ const fullPath = path.resolve(rootPath, filePath);
750
+ await fs.mkdir(path.dirname(fullPath), { recursive: true });
751
+ await fs.writeFile(fullPath, `${JSON.stringify(contract, null, 2)}\n`, 'utf-8');
752
+ return filePath;
753
+ }
754
+ function forbiddenFilesFor(intent, allowed) {
755
+ const intentLower = intent.toLowerCase();
756
+ const allowedSet = new Set(allowed);
757
+ return unique([...GENERATED_FORBIDDEN_PATTERNS, ...HIGH_RISK_FORBIDDEN_FILES]).filter((file) => {
758
+ if (allowedSet.has(file))
759
+ return false;
760
+ if (!file.includes('*') && intentLower.includes(file.toLowerCase()))
761
+ return false;
762
+ return true;
763
+ });
764
+ }
765
+ function riskyContractsFor(inferred, allowedFiles) {
766
+ const contracts = new Set(inferred);
767
+ if (allowedFiles.some((file) => file.startsWith('src/cli/')))
768
+ contracts.add('CLI command surface');
769
+ if (allowedFiles.some((file) => file.startsWith('src/mcp/')))
770
+ contracts.add('MCP tool surface');
771
+ if (allowedFiles.some((file) => file.includes('/types') || file === 'src/types.ts'))
772
+ contracts.add('public API/types');
773
+ return [...contracts].sort();
774
+ }
775
+ function evidenceGapsFor(input) {
776
+ const gaps = [];
777
+ if (!input.contract) {
778
+ gaps.push(`No Proof Contract was supplied or found at ${DEFAULT_CONTRACT_PATH}.`);
779
+ }
780
+ else {
781
+ gaps.push(...input.contract.evidenceStrength.gaps);
782
+ }
783
+ if (!input.changedFilesAvailable) {
784
+ gaps.push(input.changedFilesReason ?? 'Changed-file evidence is unavailable.');
785
+ }
786
+ if (input.preflightVerdict === 'block') {
787
+ gaps.push('Preflight returned block for the current working tree.');
788
+ }
789
+ return unique(gaps);
790
+ }
791
+ function readinessFor(input) {
792
+ if (hasBlockingReceiptSignal(input))
793
+ return 'blocked';
794
+ return hasReviewReceiptSignal(input) ? 'needs-review' : 'ready';
795
+ }
796
+ function hasBlockingReceiptSignal(input) {
797
+ return input.forbiddenTouched.length > 0 || input.preflightVerdict === 'block' || input.proofStatus === 'failed';
798
+ }
799
+ function hasReviewReceiptSignal(input) {
800
+ return (input.scopeStatus !== 'within-contract' ||
801
+ isIncompleteProofStatus(input.proofStatus) ||
802
+ input.preflightVerdict === 'caution' ||
803
+ input.evidenceGaps.length > 0);
804
+ }
805
+ function isIncompleteProofStatus(status) {
806
+ return status === 'missing' || status === 'partial' || status === 'stale';
807
+ }
808
+ function riskDeltaDirectionFor(riskDelta) {
809
+ if (riskDelta.delta > 0)
810
+ return 'improved';
811
+ if (riskDelta.delta < 0)
812
+ return 'worse';
813
+ return 'flat';
814
+ }
815
+ function reviewerDecisionFor(input) {
816
+ if (input.commitReadiness === 'blocked' || input.proofStatus === 'failed')
817
+ return 'stop';
818
+ if (input.commitReadiness === 'ready' &&
819
+ input.proofStatus === 'passed' &&
820
+ input.scope.status === 'within-contract' &&
821
+ input.preflightVerdict === 'proceed') {
822
+ return 'safe-to-review';
823
+ }
824
+ return 'needs-focused-review';
825
+ }
826
+ function reviewerGuidanceFor(verdict, scope, reviewerDecision, proofStatus) {
827
+ return firstMatchingGuidance([
828
+ [reviewerDecision === 'stop', 'Stop this proof slice until failed proof commands, forbidden files, or preflight blockers are cleared.'],
829
+ [proofStatus === 'stale', 'Rerun the required proof commands; the ledger evidence is stale after newer file changes.'],
830
+ [isIncompleteProofStatus(proofStatus), 'Record fresh proof-command evidence before approval. Missing or partial proof should not be treated as reviewer-ready.'],
831
+ [verdict === 'blocked', 'Do not approve until forbidden files or preflight blockers are removed from this proof slice.'],
832
+ [scope.unexpectedProduction.length > 0, 'Review the unexpected production files first. Either update the Proof Contract intentionally or split those edits out.'],
833
+ [hasSensitiveScopeDrift(scope), 'Require explicit reviewer sign-off for config or security-sensitive drift before approving.'],
834
+ [hasDocsOnlyScopeDrift(scope), 'Documentation drift is the only scope issue. Confirm it explains the same proof slice, then require proof-command evidence.'],
835
+ [verdict === 'ready', 'Scope is inside the Proof Contract. Require proof-command evidence before final approval.'],
836
+ ]);
837
+ }
838
+ function firstMatchingGuidance(checks) {
839
+ return (checks.find(([matches]) => matches)?.[1] ??
840
+ 'Do not approve until scope drift, missing contract evidence, or preflight blockers are resolved.');
841
+ }
842
+ function hasSensitiveScopeDrift(scope) {
843
+ return scope.configTouched.length > 0 || scope.securitySensitiveTouched.length > 0;
844
+ }
845
+ function hasDocsOnlyScopeDrift(scope) {
846
+ return scope.documentationTouched.length > 0 && scope.outsideAllowed.length === scope.documentationTouched.length;
847
+ }
848
+ function summaryForReceipt(verdict, scope) {
849
+ if (scope.forbiddenTouched.length > 0) {
850
+ return `${verdict}: forbidden file(s) touched: ${scope.forbiddenTouched.join(', ')}`;
851
+ }
852
+ if (scope.status === 'missing-contract') {
853
+ return `${verdict}: no Proof Contract was applied to ${scope.changedFiles.length} changed file(s)`;
854
+ }
855
+ if (scope.unexpectedProduction.length > 0) {
856
+ return `${verdict}: production file(s) outside the Proof Contract: ${scope.unexpectedProduction.join(', ')}`;
857
+ }
858
+ if (scope.securitySensitiveTouched.length > 0) {
859
+ return `${verdict}: security-sensitive file(s) need explicit review`;
860
+ }
861
+ if (scope.configTouched.length > 0) {
862
+ return `${verdict}: config file(s) need explicit review`;
863
+ }
864
+ if (scope.status === 'drifted') {
865
+ return `${verdict}: ${scope.outsideAllowed.length} changed file(s) outside the Proof Contract`;
866
+ }
867
+ return `${verdict}: ${scope.changedFiles.length} changed file(s) stay inside the Proof Contract`;
868
+ }
869
+ function classifyChangedFile(input) {
870
+ const rule = CHANGED_FILE_RULES.find((candidate) => candidate.matches(input));
871
+ return {
872
+ file: input.file,
873
+ kind: rule?.kind ?? 'unknown',
874
+ reason: rule?.reason ?? 'Changed file is outside the Proof Contract and could not be classified.',
875
+ };
876
+ }
877
+ function classificationBuckets(classifications) {
878
+ return {
879
+ allowedProduction: filesByKind(classifications, 'allowed-production'),
880
+ expectedTests: filesByKind(classifications, 'expected-test'),
881
+ unexpectedProduction: filesByKind(classifications, 'unexpected-production'),
882
+ unexpectedTests: filesByKind(classifications, 'unexpected-test'),
883
+ documentationTouched: classifications
884
+ .filter((entry) => entry.kind === 'documentation' || isDocumentationPath(entry.file))
885
+ .map((entry) => entry.file),
886
+ configTouched: classifications
887
+ .filter((entry) => entry.kind === 'config' || isConfigPath(entry.file))
888
+ .map((entry) => entry.file),
889
+ securitySensitiveTouched: classifications
890
+ .filter((entry) => entry.kind === 'security-sensitive' || isSecuritySensitivePath(entry.file))
891
+ .map((entry) => entry.file),
892
+ generatedTouched: classifications
893
+ .filter((entry) => entry.kind === 'generated' || isGeneratedPath(entry.file))
894
+ .map((entry) => entry.file),
895
+ };
896
+ }
897
+ function filesByKind(classifications, kind) {
898
+ return classifications.filter((entry) => entry.kind === kind).map((entry) => entry.file);
899
+ }
900
+ async function readTrustMemory(feedbackPath) {
901
+ if (!feedbackPath)
902
+ return undefined;
903
+ try {
904
+ const feedback = await readFeedbackFile(feedbackPath);
905
+ return evaluateTrustMemoryResponses(feedback.responses.slice(0, 5));
906
+ }
907
+ catch (error) {
908
+ if (isNodeErrorCode(error, 'ENOENT'))
909
+ return undefined;
910
+ throw error;
911
+ }
912
+ }
913
+ function evaluateTrustMemoryResponses(responses) {
914
+ if (responses.length === 0)
915
+ return emptyTrustMemoryEvaluation();
916
+ const signals = trustMemorySignals(responses);
917
+ const counts = trustMemorySignalCounts(responses, signals);
918
+ const confidenceImpact = trustMemoryConfidenceImpact(counts);
919
+ return {
920
+ status: trustMemoryStatus(confidenceImpact),
921
+ summary: `${responses.length} local feedback response(s) applied to contract confidence.`,
922
+ signals: trustMemorySignalLabels(responses, signals),
923
+ confidenceImpact,
924
+ gaps: trustMemoryGaps(confidenceImpact, counts.negativeProofOutcomeCount),
925
+ };
926
+ }
927
+ function emptyTrustMemoryEvaluation() {
928
+ return {
929
+ status: 'none',
930
+ summary: 'Feedback artifact has no recorded responses.',
931
+ signals: [],
932
+ confidenceImpact: 'neutral',
933
+ gaps: [],
934
+ };
935
+ }
936
+ function trustMemorySignals(responses) {
937
+ const proofOutcomes = unique(responses
938
+ .map((response) => response.proofOutcome)
939
+ .filter((outcome) => Boolean(outcome)));
940
+ return {
941
+ missingSignals: unique(responses.flatMap((response) => response.missingSignals ?? [])),
942
+ noisyFindings: unique(responses.flatMap((response) => response.noisyFindings ?? [])),
943
+ falsePositiveRules: unique(responses.flatMap((response) => response.falsePositiveRules ?? [])),
944
+ proofOutcomes,
945
+ negativeProofOutcomeCount: proofOutcomes.filter(isNegativeProofOutcome).length,
946
+ };
947
+ }
948
+ function trustMemorySignalCounts(responses, signals) {
949
+ return {
950
+ positive: positiveTrustSignalCount(responses),
951
+ negative: responses.filter((response) => response.useful === false).length +
952
+ signals.missingSignals.length +
953
+ signals.noisyFindings.length +
954
+ signals.falsePositiveRules.length +
955
+ signals.negativeProofOutcomeCount,
956
+ negativeProofOutcomeCount: signals.negativeProofOutcomeCount,
957
+ };
958
+ }
959
+ function positiveTrustSignalCount(responses) {
960
+ return (responses.filter((response) => response.useful === true).length +
961
+ responses.filter((response) => response.proofOutcome === 'accepted').length +
962
+ responses.filter((response) => response.preventedBadEdit === true).length +
963
+ responses.filter((response) => response.ownerRoutingClear === true).length +
964
+ responses.filter((response) => response.nextCommandClear === true).length);
965
+ }
966
+ function trustMemoryConfidenceImpact(counts) {
967
+ if (counts.negative > counts.positive)
968
+ return 'negative';
969
+ return counts.positive > 0 ? 'positive' : 'neutral';
970
+ }
971
+ function trustMemoryStatus(confidenceImpact) {
972
+ if (confidenceImpact === 'negative')
973
+ return 'needs-tuning';
974
+ return confidenceImpact === 'positive' ? 'trusted' : 'mixed';
975
+ }
976
+ function trustMemoryGaps(confidenceImpact, negativeProofOutcomeCount) {
977
+ if (confidenceImpact !== 'negative')
978
+ return [];
979
+ return [
980
+ negativeProofOutcomeCount > 0
981
+ ? 'Trust Memory reports rejected, reverted, suppressed, or noisy proof outcomes for similar workflows.'
982
+ : 'Trust Memory reports missing signals or noisy findings for similar proof workflows.',
983
+ ];
984
+ }
985
+ function trustMemorySignalLabels(responses, signals) {
986
+ return unique([
987
+ ...responses.map(trustMemoryResponseLabel),
988
+ ...signals.missingSignals.map((signal) => `missing signal: ${signal}`),
989
+ ...signals.noisyFindings.map((finding) => `noisy finding: ${finding}`),
990
+ ...signals.falsePositiveRules.map((rule) => `false positive: ${rule}`),
991
+ ...signals.proofOutcomes.map((outcome) => `proof outcome: ${outcome}`),
992
+ ]);
993
+ }
994
+ function trustMemoryResponseLabel(response) {
995
+ const repo = response.repo ?? 'unknown repo';
996
+ const pr = response.pr ? ` PR ${response.pr}` : '';
997
+ const useful = response.useful === false ? 'not useful' : response.useful === true ? 'useful' : 'feedback';
998
+ return `${useful}: ${repo}${pr}`;
999
+ }
1000
+ function isNegativeProofOutcome(outcome) {
1001
+ return NEGATIVE_PROOF_OUTCOMES.has(outcome);
1002
+ }
1003
+ function fallbackRiskDelta() {
1004
+ return {
1005
+ baselineScore: 0,
1006
+ projectedScore: 0,
1007
+ delta: 0,
1008
+ basis: ['No saved Proof Contract risk delta was available.'],
1009
+ };
1010
+ }
1011
+ function safeChangeShape(summary) {
1012
+ if (summary.toLowerCase().includes('bounded')) {
1013
+ return `${summary} Keep the edit bounded to allowed files and likely tests.`;
1014
+ }
1015
+ return `Use a bounded change: ${summary}`;
1016
+ }
1017
+ function rollbackPlan(files) {
1018
+ if (files.length === 0)
1019
+ return 'Run git restore . to roll back this proof slice.';
1020
+ return `Run git restore ${files.map(quoteShellArg).join(' ')} to roll back this proof slice.`;
1021
+ }
1022
+ function intentVerdict(contract) {
1023
+ return contract.confidence === 'low' ? 'needs-review' : 'ready';
1024
+ }
1025
+ function confidenceForTrustMemory(confidence, trustMemory) {
1026
+ if (trustMemory?.confidenceImpact === 'negative')
1027
+ return lowerConfidence(confidence);
1028
+ if (trustMemory?.confidenceImpact === 'positive')
1029
+ return raiseConfidence(confidence);
1030
+ return confidence;
1031
+ }
1032
+ function lowerConfidence(confidence) {
1033
+ if (confidence === 'high')
1034
+ return 'medium';
1035
+ if (confidence === 'medium')
1036
+ return 'low';
1037
+ return 'low';
1038
+ }
1039
+ function raiseConfidence(confidence) {
1040
+ if (confidence === 'low')
1041
+ return 'medium';
1042
+ if (confidence === 'medium')
1043
+ return 'high';
1044
+ return 'high';
1045
+ }
1046
+ function confidenceReasonForSimulation(confidence, simulationConfidence, trustMemory) {
1047
+ if (trustMemory?.confidenceImpact === 'negative') {
1048
+ return `Trust Memory lowered confidence from ${simulationConfidence} to ${confidence} because local feedback reported missing signals or noisy findings.`;
1049
+ }
1050
+ if (trustMemory?.confidenceImpact === 'positive') {
1051
+ return `Trust Memory raised confidence from ${simulationConfidence} to ${confidence} because local feedback marked similar proof workflows useful.`;
1052
+ }
1053
+ return `${confidence} confidence from local simulation evidence.`;
1054
+ }
1055
+ function normalizeIntent(value) {
1056
+ return value?.trim().replace(/\s+/g, ' ') ?? '';
1057
+ }
1058
+ function isDocumentationPath(file) {
1059
+ return (file === 'README.md' ||
1060
+ file.startsWith('docs/') ||
1061
+ file.endsWith('.md') ||
1062
+ file.endsWith('.mdx'));
1063
+ }
1064
+ function isGeneratedPath(file) {
1065
+ return (file.startsWith('.projscan/') ||
1066
+ file.startsWith('.projscan-memory/') ||
1067
+ file.startsWith('.agentloop/') ||
1068
+ file.startsWith('.agentflight/') ||
1069
+ file.startsWith('coverage/') ||
1070
+ file.startsWith('dist/'));
1071
+ }
1072
+ function isLocalProofArtifactPath(file) {
1073
+ return file.startsWith('.projscan/');
1074
+ }
1075
+ function isSecuritySensitivePath(file) {
1076
+ return (file === '.env' ||
1077
+ file.startsWith('.env.') ||
1078
+ file.includes('/auth') ||
1079
+ file.includes('/security') ||
1080
+ file.includes('/secrets') ||
1081
+ file.endsWith('.pem') ||
1082
+ file.endsWith('.key'));
1083
+ }
1084
+ function isConfigPath(file) {
1085
+ const basename = path.posix.basename(file);
1086
+ return (CONFIG_BASENAMES.has(basename) ||
1087
+ CONFIG_SUFFIXES.some((suffix) => basename.endsWith(suffix)) ||
1088
+ file.startsWith('.github/'));
1089
+ }
1090
+ function isTestPath(file) {
1091
+ return (file.startsWith('test/') ||
1092
+ file.startsWith('tests/') ||
1093
+ file.includes('/__tests__/') ||
1094
+ /\.test\.[cm]?[jt]sx?$/.test(file) ||
1095
+ /\.spec\.[cm]?[jt]sx?$/.test(file));
1096
+ }
1097
+ function isProductionPath(file) {
1098
+ return (file.startsWith('src/') ||
1099
+ file.startsWith('app/') ||
1100
+ file.startsWith('lib/') ||
1101
+ file.startsWith('packages/') ||
1102
+ file.startsWith('apps/'));
1103
+ }
1104
+ function pathMatches(file, pattern) {
1105
+ if (pattern.endsWith('/**'))
1106
+ return file.startsWith(pattern.slice(0, -3));
1107
+ return file === pattern;
1108
+ }
1109
+ function unique(values) {
1110
+ return [...new Set(values)];
1111
+ }
1112
+ function slug(value) {
1113
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 80);
1114
+ }
1115
+ function isNodeErrorCode(error, code) {
1116
+ return (typeof error === 'object' &&
1117
+ error !== null &&
1118
+ 'code' in error &&
1119
+ error.code === code);
1120
+ }
1121
+ //# sourceMappingURL=prove.js.map