projscan 4.12.0 → 4.13.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 (51) hide show
  1. package/README.md +69 -11
  2. package/dist/cli/commands/prove.d.ts +3 -0
  3. package/dist/cli/commands/prove.js +298 -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/proofLedger.d.ts +8 -0
  14. package/dist/core/proofLedger.js +134 -0
  15. package/dist/core/proofLedger.js.map +1 -0
  16. package/dist/core/prove.d.ts +18 -0
  17. package/dist/core/prove.js +821 -0
  18. package/dist/core/prove.js.map +1 -0
  19. package/dist/core/releaseEvidence.js +48 -0
  20. package/dist/core/releaseEvidence.js.map +1 -1
  21. package/dist/core/simulate.js +109 -2
  22. package/dist/core/simulate.js.map +1 -1
  23. package/dist/mcp/toolCatalog.js +2 -0
  24. package/dist/mcp/toolCatalog.js.map +1 -1
  25. package/dist/mcp/tools/prove.d.ts +2 -0
  26. package/dist/mcp/tools/prove.js +93 -0
  27. package/dist/mcp/tools/prove.js.map +1 -0
  28. package/dist/projscan-sbom.cdx.json +6 -6
  29. package/dist/publicCore.d.ts +1 -0
  30. package/dist/publicCore.js +1 -0
  31. package/dist/publicCore.js.map +1 -1
  32. package/dist/tool-manifest.json +68 -3
  33. package/dist/types/dogfood.d.ts +4 -0
  34. package/dist/types/evidencePack.d.ts +13 -0
  35. package/dist/types/proofLedger.d.ts +30 -0
  36. package/dist/types/proofLedger.js +2 -0
  37. package/dist/types/proofLedger.js.map +1 -0
  38. package/dist/types/prove.d.ts +107 -0
  39. package/dist/types/prove.js +2 -0
  40. package/dist/types/prove.js.map +1 -0
  41. package/dist/types.d.ts +2 -0
  42. package/dist/utils/formatSupport.d.ts +1 -0
  43. package/dist/utils/formatSupport.js +1 -0
  44. package/dist/utils/formatSupport.js.map +1 -1
  45. package/docs/GUIDE.md +35 -1
  46. package/docs/demos/projscan-4-1-demo.html +24 -24
  47. package/docs/projscan-mission-control.gif +0 -0
  48. package/docs/projscan-mission-control.png +0 -0
  49. package/docs/projscan-mission-proof.gif +0 -0
  50. package/docs/projscan-proof-router.png +0 -0
  51. package/package.json +1 -1
@@ -0,0 +1,821 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { readFeedbackFile } from './feedback.js';
4
+ import { appendProofLedgerRecord, changedFileFingerprint, latestProofRecordFor, readProofLedger, } from './proofLedger.js';
5
+ import { quoteShellArg } from './startShellArgs.js';
6
+ import { computeSimulation } from './simulate.js';
7
+ import { getChangedFiles } from '../utils/changedFiles.js';
8
+ const DEFAULT_CONTRACT_PATH = '.projscan/proof-contract.json';
9
+ const GENERATED_FORBIDDEN_PATTERNS = [
10
+ '.agentflight/**',
11
+ '.agentloop/**',
12
+ '.git/**',
13
+ '.projscan-memory/**',
14
+ 'coverage/**',
15
+ 'dist/**',
16
+ 'node_modules/**',
17
+ ];
18
+ const HIGH_RISK_FORBIDDEN_FILES = [
19
+ '.env',
20
+ '.env.local',
21
+ '.github/mcp-registry/server.json',
22
+ 'CHANGELOG.md',
23
+ 'package-lock.json',
24
+ 'package.json',
25
+ ];
26
+ const CHANGED_FILE_RULES = [
27
+ {
28
+ kind: 'generated',
29
+ reason: 'Proof Contract artifact used for validation.',
30
+ matches: (input) => Boolean(input.contractPath),
31
+ },
32
+ {
33
+ kind: 'forbidden',
34
+ reason: 'Matched forbidden Proof Contract scope.',
35
+ matches: (input) => input.forbidden,
36
+ },
37
+ {
38
+ kind: 'expected-test',
39
+ reason: 'Expected regression test from the Proof Contract.',
40
+ matches: (input) => Boolean(input.expectedTest),
41
+ },
42
+ {
43
+ kind: 'allowed-production',
44
+ reason: 'Allowed by the Proof Contract.',
45
+ matches: (input) => Boolean(input.allowedProduction),
46
+ },
47
+ {
48
+ kind: 'documentation',
49
+ reason: 'Documentation change outside contract scope.',
50
+ matches: (input) => isDocumentationPath(input.file),
51
+ },
52
+ {
53
+ kind: 'generated',
54
+ reason: 'Generated or local tool artifact changed outside contract scope.',
55
+ matches: (input) => isGeneratedPath(input.file),
56
+ },
57
+ {
58
+ kind: 'security-sensitive',
59
+ reason: 'Security-sensitive file changed outside the Proof Contract.',
60
+ matches: (input) => isSecuritySensitivePath(input.file),
61
+ },
62
+ {
63
+ kind: 'config',
64
+ reason: 'Configuration or release file changed outside the Proof Contract.',
65
+ matches: (input) => isConfigPath(input.file),
66
+ },
67
+ {
68
+ kind: 'unexpected-test',
69
+ reason: 'Test file changed outside the Proof Contract.',
70
+ matches: (input) => isTestPath(input.file),
71
+ },
72
+ {
73
+ kind: 'unexpected-production',
74
+ reason: 'Production source changed outside the Proof Contract.',
75
+ matches: (input) => isProductionPath(input.file),
76
+ },
77
+ ];
78
+ const NEGATIVE_PROOF_OUTCOMES = new Set(['rejected', 'reverted', 'suppressed', 'noisy']);
79
+ const CONFIG_BASENAMES = new Set([
80
+ 'package.json',
81
+ 'package-lock.json',
82
+ 'pnpm-lock.yaml',
83
+ 'yarn.lock',
84
+ 'tsconfig.json',
85
+ ]);
86
+ const CONFIG_SUFFIXES = ['.config.js', '.config.cjs', '.config.mjs', '.config.ts'];
87
+ export async function computeProve(rootPath, options = {}) {
88
+ const modeCount = [Boolean(options.intent?.trim()), Boolean(options.changed), Boolean(options.recordCommand?.trim())].filter(Boolean).length;
89
+ if (modeCount > 1) {
90
+ throw new Error('prove accepts only one of --intent, --changed, or --record-command');
91
+ }
92
+ if (options.recordCommand?.trim())
93
+ return computeRecordProof(rootPath, options);
94
+ if (options.changed)
95
+ return computeChangedProof(rootPath, options);
96
+ return computeIntentProof(rootPath, options);
97
+ }
98
+ async function computeRecordProof(rootPath, options) {
99
+ const proof = recordProofInput(options);
100
+ const changedFiles = await getChangedFiles(rootPath, options.baseRef);
101
+ const record = await appendProofLedgerRecord(rootPath, options.ledgerPath, {
102
+ command: proof.command,
103
+ exitCode: proof.exitCode,
104
+ durationMs: proof.durationMs,
105
+ changedFiles: proofRelevantChangedFiles(changedFiles.files),
106
+ outputSummary: proof.summary,
107
+ logPath: proof.logPath,
108
+ source: 'prove-record',
109
+ });
110
+ const verdict = record.status === 'passed' ? 'ready' : 'blocked';
111
+ return {
112
+ schemaVersion: 1,
113
+ mode: 'record',
114
+ verdict,
115
+ summary: `${verdict}: recorded ${record.status} proof for ${record.command}`,
116
+ commands: [record.command],
117
+ warnings: changedFiles.available ? [] : [changedFiles.reason ?? 'Changed-file evidence is unavailable.'],
118
+ ledgerRecord: record,
119
+ };
120
+ }
121
+ function recordProofInput(options) {
122
+ const command = options.recordCommand?.trim();
123
+ if (!command)
124
+ throw new Error('prove --record-command requires a non-empty command');
125
+ if (typeof options.exitCode !== 'number' || !Number.isInteger(options.exitCode)) {
126
+ throw new Error('prove --record-command requires a numeric exit code');
127
+ }
128
+ if (!isNonNegativeFiniteNumber(options.durationMs)) {
129
+ throw new Error('prove --record-command requires a non-negative duration-ms value');
130
+ }
131
+ return {
132
+ command,
133
+ exitCode: options.exitCode,
134
+ durationMs: options.durationMs,
135
+ summary: options.summary,
136
+ logPath: options.logPath,
137
+ };
138
+ }
139
+ function isNonNegativeFiniteNumber(value) {
140
+ return typeof value === 'number' && Number.isFinite(value) && value >= 0;
141
+ }
142
+ async function computeIntentProof(rootPath, options) {
143
+ const intent = normalizeIntent(options.intent);
144
+ if (!intent)
145
+ throw new Error('prove --intent requires a non-empty change intent');
146
+ const [simulation, trustMemory] = await Promise.all([
147
+ computeSimulation(rootPath, {
148
+ plan: intent,
149
+ maxFiles: options.maxFiles,
150
+ }),
151
+ readTrustMemory(options.feedbackPath),
152
+ ]);
153
+ const contract = buildContract({ intent, simulation, trustMemory });
154
+ let savedContractPath;
155
+ if (options.saveContractPath) {
156
+ savedContractPath = await writeContract(rootPath, options.saveContractPath, contract);
157
+ }
158
+ return {
159
+ schemaVersion: 1,
160
+ mode: 'intent',
161
+ verdict: intentVerdict(contract),
162
+ summary: `ready: Proof Contract ${contract.id} constrains ${contract.allowedFiles.length} file(s) and requires ${contract.proofCommands.length} proof command(s).`,
163
+ contract,
164
+ commands: contract.proofCommands,
165
+ warnings: simulation.warnings,
166
+ ...(savedContractPath ? { savedContractPath } : {}),
167
+ };
168
+ }
169
+ async function computeChangedProof(rootPath, options) {
170
+ const [contract, changedFiles, ledger] = await Promise.all([
171
+ resolveContract(rootPath, options),
172
+ getChangedFiles(rootPath, options.baseRef),
173
+ readProofLedger(rootPath, options.ledgerPath),
174
+ ]);
175
+ const quickPreflight = quickProofPreflight(changedFiles);
176
+ const receipt = buildReceipt({
177
+ contract: contract.contract,
178
+ contractPath: contract.path,
179
+ changedFiles: changedFiles.files,
180
+ changedFilesAvailable: changedFiles.available,
181
+ changedFilesReason: changedFiles.reason,
182
+ riskDelta: contract.contract?.riskDelta ?? fallbackRiskDelta(),
183
+ newRisks: quickPreflight.risks,
184
+ preflightVerdict: quickPreflight.verdict,
185
+ ledger,
186
+ });
187
+ return {
188
+ schemaVersion: 1,
189
+ mode: 'changed',
190
+ verdict: receipt.commitReadiness,
191
+ summary: receipt.summary,
192
+ ...(contract.contract ? { contract: contract.contract } : {}),
193
+ receipt,
194
+ commands: receipt.proofStatus.commandsRequired,
195
+ warnings: receipt.evidenceGaps,
196
+ };
197
+ }
198
+ function quickProofPreflight(changedFiles) {
199
+ if (!changedFiles.available) {
200
+ return {
201
+ verdict: 'caution',
202
+ risks: [changedFiles.reason ?? 'Changed-file evidence is unavailable.'],
203
+ };
204
+ }
205
+ if (changedFiles.files.length > 50) {
206
+ return {
207
+ verdict: 'caution',
208
+ risks: [`${changedFiles.files.length} changed files exceeds the proof replay focus threshold of 50.`],
209
+ };
210
+ }
211
+ return {
212
+ verdict: 'proceed',
213
+ risks: [],
214
+ };
215
+ }
216
+ function buildContract(input) {
217
+ const simulationFiles = input.simulation.filesLikelyTouched.map((file) => file.path);
218
+ const allowedFiles = unique(simulationFiles);
219
+ const likelyTests = unique(input.simulation.testsLikelyAffected);
220
+ const forbiddenFiles = forbiddenFilesFor(input.intent, [...allowedFiles, ...likelyTests]);
221
+ const proofCommands = contractProofCommands(input.simulation.proofCommands);
222
+ const evidenceGaps = unique([
223
+ ...(input.simulation.warnings.length > 0 ? input.simulation.warnings : []),
224
+ ...(likelyTests.length === 0 ? ['No likely regression test was inferred from the plan.'] : []),
225
+ ...(input.trustMemory?.gaps ?? []),
226
+ ]);
227
+ const confidence = confidenceForTrustMemory(input.simulation.confidence, input.trustMemory);
228
+ return {
229
+ schemaVersion: 1,
230
+ id: `proof-contract-${slug(input.intent)}`,
231
+ intent: input.intent,
232
+ createdAt: new Date().toISOString(),
233
+ allowedFiles,
234
+ forbiddenFiles,
235
+ riskyContracts: riskyContractsFor(input.simulation.contractsLikelyAffected, allowedFiles),
236
+ likelyTests,
237
+ missingRegressionTests: likelyTests.length > 0 ? [] : ['Add one regression test around the behavior named by the intent.'],
238
+ proofCommands,
239
+ safeChangeShape: safeChangeShape(input.simulation.recommendedAlternative.summary),
240
+ rollbackPlan: rollbackPlan([...allowedFiles, ...likelyTests]),
241
+ confidence,
242
+ confidenceReason: confidenceReasonForSimulation(confidence, input.simulation.confidence, input.trustMemory),
243
+ evidenceStrength: {
244
+ level: input.simulation.evidence.length > 1 ? 'moderate' : 'thin',
245
+ score: Math.min(80, input.simulation.evidence.length * 15),
246
+ sources: unique(input.simulation.evidence.map((entry) => entry.source)),
247
+ gaps: evidenceGaps,
248
+ },
249
+ trustMemory: {
250
+ status: input.trustMemory?.status ?? 'none',
251
+ summary: input.trustMemory?.summary ?? 'No local trust-memory artifact was applied.',
252
+ signals: input.trustMemory?.signals ?? [],
253
+ },
254
+ reviewerGuidance: 'Review scope first, then require the listed proof commands before approving commit or handoff.',
255
+ receiptCommand: `projscan prove --changed --contract ${quoteShellArg(DEFAULT_CONTRACT_PATH)} --format markdown`,
256
+ riskDelta: input.simulation.riskDelta,
257
+ };
258
+ }
259
+ function contractProofCommands(simulationCommands) {
260
+ return unique([
261
+ simulationCommands.find((command) => command.startsWith('projscan simulate ')),
262
+ simulationCommands.find((command) => /^npm (?:run )?test\b/.test(command)),
263
+ 'projscan assess --mode fix-first --format json',
264
+ 'projscan preflight --mode before_commit --format json',
265
+ ].filter((command) => typeof command === 'string'));
266
+ }
267
+ function buildReceipt(input) {
268
+ const scope = scopeFor(input.contract, input.contractPath, input.changedFiles);
269
+ const evidenceGaps = evidenceGapsFor(input);
270
+ const proofCommands = input.contract?.proofCommands ?? [
271
+ 'projscan assess --mode fix-first --format json',
272
+ 'projscan preflight --mode before_commit --format json',
273
+ ];
274
+ const proofStatus = proofStatusFor(proofCommands, input.ledger, input.changedFiles);
275
+ const commitReadiness = readinessFor({
276
+ scopeStatus: scope.status,
277
+ forbiddenTouched: scope.forbiddenTouched,
278
+ preflightVerdict: input.preflightVerdict,
279
+ evidenceGaps,
280
+ proofStatus: proofStatus.status,
281
+ });
282
+ const riskDeltaDirection = riskDeltaDirectionFor(input.riskDelta);
283
+ const reviewerDecision = reviewerDecisionFor({
284
+ commitReadiness,
285
+ proofStatus: proofStatus.status,
286
+ scope,
287
+ preflightVerdict: input.preflightVerdict,
288
+ });
289
+ return {
290
+ summary: summaryForReceipt(commitReadiness, scope),
291
+ commitReadiness,
292
+ scope,
293
+ proofStatus,
294
+ riskDelta: input.riskDelta,
295
+ riskDeltaDirection,
296
+ reviewerDecision,
297
+ newRisks: input.newRisks,
298
+ evidenceGaps,
299
+ reviewerGuidance: reviewerGuidanceFor(commitReadiness, scope, reviewerDecision, proofStatus.status),
300
+ };
301
+ }
302
+ function proofStatusFor(proofCommands, ledger, changedFiles) {
303
+ const relevantChangedFiles = proofRelevantChangedFiles(changedFiles);
304
+ const currentFingerprint = changedFileFingerprint(relevantChangedFiles);
305
+ const commandEvidence = proofCommands.map((command) => {
306
+ const record = latestProofRecordFor(ledger, command);
307
+ if (!record) {
308
+ return {
309
+ command,
310
+ status: 'missing',
311
+ fresh: false,
312
+ };
313
+ }
314
+ const fresh = record.changedFileFingerprint === currentFingerprint;
315
+ if (!fresh) {
316
+ return {
317
+ command,
318
+ status: 'stale',
319
+ fresh: false,
320
+ exitCode: record.exitCode,
321
+ durationMs: record.durationMs,
322
+ completedAt: record.completedAt,
323
+ outputSummary: record.outputSummary,
324
+ ...(record.logPath ? { logPath: record.logPath } : {}),
325
+ staleReason: 'Recorded changed files differ from current changed files.',
326
+ };
327
+ }
328
+ return {
329
+ command,
330
+ status: record.exitCode === 0 ? 'passed' : 'failed',
331
+ fresh: true,
332
+ exitCode: record.exitCode,
333
+ durationMs: record.durationMs,
334
+ completedAt: record.completedAt,
335
+ outputSummary: record.outputSummary,
336
+ ...(record.logPath ? { logPath: record.logPath } : {}),
337
+ };
338
+ });
339
+ const missingCommands = commandEvidence
340
+ .filter((entry) => entry.status === 'missing')
341
+ .map((entry) => entry.command);
342
+ const failedCommands = commandEvidence
343
+ .filter((entry) => entry.status === 'failed')
344
+ .map((entry) => entry.command);
345
+ const staleCommands = commandEvidence
346
+ .filter((entry) => entry.status === 'stale')
347
+ .map((entry) => entry.command);
348
+ const commandsRun = commandEvidence
349
+ .filter((entry) => typeof entry.exitCode === 'number')
350
+ .map((entry) => entry.command);
351
+ const status = proofStatusSummary({
352
+ requiredCount: proofCommands.length,
353
+ missingCount: missingCommands.length,
354
+ failedCount: failedCommands.length,
355
+ staleCount: staleCommands.length,
356
+ });
357
+ return {
358
+ status,
359
+ commandsRequired: proofCommands,
360
+ commandsRun,
361
+ missingCommands,
362
+ failedCommands,
363
+ staleCommands,
364
+ commandEvidence,
365
+ };
366
+ }
367
+ function proofStatusSummary(input) {
368
+ if (input.requiredCount === 0)
369
+ return 'not-run';
370
+ if (input.failedCount > 0)
371
+ return 'failed';
372
+ if (input.staleCount === input.requiredCount)
373
+ return 'stale';
374
+ if (input.missingCount === input.requiredCount)
375
+ return 'missing';
376
+ if (input.staleCount > 0 || input.missingCount > 0)
377
+ return 'partial';
378
+ return 'passed';
379
+ }
380
+ function proofRelevantChangedFiles(files) {
381
+ return files.filter((file) => !isGeneratedPath(file));
382
+ }
383
+ function scopeFor(contract, contractPath, changedFiles) {
384
+ if (!contract) {
385
+ const classifications = changedFiles.map((file) => classifyChangedFile({ file, forbidden: false }));
386
+ return {
387
+ status: 'missing-contract',
388
+ changedFiles,
389
+ allowedTouched: [],
390
+ forbiddenTouched: [],
391
+ outsideAllowed: changedFiles,
392
+ classifications,
393
+ ...classificationBuckets(classifications),
394
+ ...(contractPath ? { contractPath } : {}),
395
+ };
396
+ }
397
+ const allowed = new Set([
398
+ ...contract.allowedFiles,
399
+ ...contract.likelyTests,
400
+ ...(contractPath ? [contractPath] : []),
401
+ ]);
402
+ const forbiddenTouched = changedFiles.filter((file) => contract.forbiddenFiles.some((pattern) => pathMatches(file, pattern)));
403
+ const allowedTouched = changedFiles.filter((file) => allowed.has(file));
404
+ const outsideAllowed = changedFiles.filter((file) => !allowed.has(file));
405
+ const classifications = changedFiles.map((file) => classifyChangedFile({
406
+ file,
407
+ forbidden: forbiddenTouched.includes(file),
408
+ allowedProduction: contract.allowedFiles.includes(file),
409
+ expectedTest: contract.likelyTests.includes(file),
410
+ contractPath: contractPath === file,
411
+ }));
412
+ const status = forbiddenTouched.length > 0 || outsideAllowed.length > 0 ? 'drifted' : 'within-contract';
413
+ return {
414
+ status,
415
+ changedFiles,
416
+ allowedTouched,
417
+ forbiddenTouched,
418
+ outsideAllowed,
419
+ classifications,
420
+ ...classificationBuckets(classifications),
421
+ ...(contractPath ? { contractPath } : {}),
422
+ };
423
+ }
424
+ async function resolveContract(rootPath, options) {
425
+ if (options.contract)
426
+ return { contract: options.contract };
427
+ if (options.contractPath) {
428
+ return {
429
+ contract: await readContract(rootPath, options.contractPath, true),
430
+ path: options.contractPath,
431
+ };
432
+ }
433
+ const contract = await readContract(rootPath, DEFAULT_CONTRACT_PATH, false);
434
+ return contract ? { contract, path: DEFAULT_CONTRACT_PATH } : {};
435
+ }
436
+ async function readContract(rootPath, filePath, required) {
437
+ const fullPath = path.resolve(rootPath, filePath);
438
+ try {
439
+ const parsed = JSON.parse(await fs.readFile(fullPath, 'utf-8'));
440
+ if (parsed.schemaVersion !== 1 || !Array.isArray(parsed.allowedFiles) || !parsed.id) {
441
+ throw new Error('invalid Proof Contract shape');
442
+ }
443
+ return parsed;
444
+ }
445
+ catch (error) {
446
+ if (!required && isNodeErrorCode(error, 'ENOENT'))
447
+ return undefined;
448
+ throw new Error(`Could not read Proof Contract ${filePath}: ${error instanceof Error ? error.message : 'invalid JSON'}`, { cause: error });
449
+ }
450
+ }
451
+ async function writeContract(rootPath, filePath, contract) {
452
+ const fullPath = path.resolve(rootPath, filePath);
453
+ await fs.mkdir(path.dirname(fullPath), { recursive: true });
454
+ await fs.writeFile(fullPath, `${JSON.stringify(contract, null, 2)}\n`, 'utf-8');
455
+ return filePath;
456
+ }
457
+ function forbiddenFilesFor(intent, allowed) {
458
+ const intentLower = intent.toLowerCase();
459
+ const allowedSet = new Set(allowed);
460
+ return unique([...GENERATED_FORBIDDEN_PATTERNS, ...HIGH_RISK_FORBIDDEN_FILES]).filter((file) => {
461
+ if (allowedSet.has(file))
462
+ return false;
463
+ if (!file.includes('*') && intentLower.includes(file.toLowerCase()))
464
+ return false;
465
+ return true;
466
+ });
467
+ }
468
+ function riskyContractsFor(inferred, allowedFiles) {
469
+ const contracts = new Set(inferred);
470
+ if (allowedFiles.some((file) => file.startsWith('src/cli/')))
471
+ contracts.add('CLI command surface');
472
+ if (allowedFiles.some((file) => file.startsWith('src/mcp/')))
473
+ contracts.add('MCP tool surface');
474
+ if (allowedFiles.some((file) => file.includes('/types') || file === 'src/types.ts'))
475
+ contracts.add('public API/types');
476
+ return [...contracts].sort();
477
+ }
478
+ function evidenceGapsFor(input) {
479
+ const gaps = [];
480
+ if (!input.contract) {
481
+ gaps.push(`No Proof Contract was supplied or found at ${DEFAULT_CONTRACT_PATH}.`);
482
+ }
483
+ else {
484
+ gaps.push(...input.contract.evidenceStrength.gaps);
485
+ }
486
+ if (!input.changedFilesAvailable) {
487
+ gaps.push(input.changedFilesReason ?? 'Changed-file evidence is unavailable.');
488
+ }
489
+ if (input.preflightVerdict === 'block') {
490
+ gaps.push('Preflight returned block for the current working tree.');
491
+ }
492
+ return unique(gaps);
493
+ }
494
+ function readinessFor(input) {
495
+ if (hasBlockingReceiptSignal(input))
496
+ return 'blocked';
497
+ return hasReviewReceiptSignal(input) ? 'needs-review' : 'ready';
498
+ }
499
+ function hasBlockingReceiptSignal(input) {
500
+ return input.forbiddenTouched.length > 0 || input.preflightVerdict === 'block' || input.proofStatus === 'failed';
501
+ }
502
+ function hasReviewReceiptSignal(input) {
503
+ return (input.scopeStatus !== 'within-contract' ||
504
+ isIncompleteProofStatus(input.proofStatus) ||
505
+ input.preflightVerdict === 'caution' ||
506
+ input.evidenceGaps.length > 0);
507
+ }
508
+ function isIncompleteProofStatus(status) {
509
+ return status === 'missing' || status === 'partial' || status === 'stale';
510
+ }
511
+ function riskDeltaDirectionFor(riskDelta) {
512
+ if (riskDelta.delta > 0)
513
+ return 'improved';
514
+ if (riskDelta.delta < 0)
515
+ return 'worse';
516
+ return 'flat';
517
+ }
518
+ function reviewerDecisionFor(input) {
519
+ if (input.commitReadiness === 'blocked' || input.proofStatus === 'failed')
520
+ return 'stop';
521
+ if (input.commitReadiness === 'ready' &&
522
+ input.proofStatus === 'passed' &&
523
+ input.scope.status === 'within-contract' &&
524
+ input.preflightVerdict === 'proceed') {
525
+ return 'safe-to-review';
526
+ }
527
+ return 'needs-focused-review';
528
+ }
529
+ function reviewerGuidanceFor(verdict, scope, reviewerDecision, proofStatus) {
530
+ return firstMatchingGuidance([
531
+ [reviewerDecision === 'stop', 'Stop this proof slice until failed proof commands, forbidden files, or preflight blockers are cleared.'],
532
+ [proofStatus === 'stale', 'Rerun the required proof commands; the ledger evidence is stale after newer file changes.'],
533
+ [isIncompleteProofStatus(proofStatus), 'Record fresh proof-command evidence before approval. Missing or partial proof should not be treated as reviewer-ready.'],
534
+ [verdict === 'blocked', 'Do not approve until forbidden files or preflight blockers are removed from this proof slice.'],
535
+ [scope.unexpectedProduction.length > 0, 'Review the unexpected production files first. Either update the Proof Contract intentionally or split those edits out.'],
536
+ [hasSensitiveScopeDrift(scope), 'Require explicit reviewer sign-off for config or security-sensitive drift before approving.'],
537
+ [hasDocsOnlyScopeDrift(scope), 'Documentation drift is the only scope issue. Confirm it explains the same proof slice, then require proof-command evidence.'],
538
+ [verdict === 'ready', 'Scope is inside the Proof Contract. Require proof-command evidence before final approval.'],
539
+ ]);
540
+ }
541
+ function firstMatchingGuidance(checks) {
542
+ return (checks.find(([matches]) => matches)?.[1] ??
543
+ 'Do not approve until scope drift, missing contract evidence, or preflight blockers are resolved.');
544
+ }
545
+ function hasSensitiveScopeDrift(scope) {
546
+ return scope.configTouched.length > 0 || scope.securitySensitiveTouched.length > 0;
547
+ }
548
+ function hasDocsOnlyScopeDrift(scope) {
549
+ return scope.documentationTouched.length > 0 && scope.outsideAllowed.length === scope.documentationTouched.length;
550
+ }
551
+ function summaryForReceipt(verdict, scope) {
552
+ if (scope.forbiddenTouched.length > 0) {
553
+ return `${verdict}: forbidden file(s) touched: ${scope.forbiddenTouched.join(', ')}`;
554
+ }
555
+ if (scope.status === 'missing-contract') {
556
+ return `${verdict}: no Proof Contract was applied to ${scope.changedFiles.length} changed file(s)`;
557
+ }
558
+ if (scope.unexpectedProduction.length > 0) {
559
+ return `${verdict}: production file(s) outside the Proof Contract: ${scope.unexpectedProduction.join(', ')}`;
560
+ }
561
+ if (scope.securitySensitiveTouched.length > 0) {
562
+ return `${verdict}: security-sensitive file(s) need explicit review`;
563
+ }
564
+ if (scope.configTouched.length > 0) {
565
+ return `${verdict}: config file(s) need explicit review`;
566
+ }
567
+ if (scope.status === 'drifted') {
568
+ return `${verdict}: ${scope.outsideAllowed.length} changed file(s) outside the Proof Contract`;
569
+ }
570
+ return `${verdict}: ${scope.changedFiles.length} changed file(s) stay inside the Proof Contract`;
571
+ }
572
+ function classifyChangedFile(input) {
573
+ const rule = CHANGED_FILE_RULES.find((candidate) => candidate.matches(input));
574
+ return {
575
+ file: input.file,
576
+ kind: rule?.kind ?? 'unknown',
577
+ reason: rule?.reason ?? 'Changed file is outside the Proof Contract and could not be classified.',
578
+ };
579
+ }
580
+ function classificationBuckets(classifications) {
581
+ return {
582
+ allowedProduction: filesByKind(classifications, 'allowed-production'),
583
+ expectedTests: filesByKind(classifications, 'expected-test'),
584
+ unexpectedProduction: filesByKind(classifications, 'unexpected-production'),
585
+ unexpectedTests: filesByKind(classifications, 'unexpected-test'),
586
+ documentationTouched: classifications
587
+ .filter((entry) => entry.kind === 'documentation' || isDocumentationPath(entry.file))
588
+ .map((entry) => entry.file),
589
+ configTouched: classifications
590
+ .filter((entry) => entry.kind === 'config' || isConfigPath(entry.file))
591
+ .map((entry) => entry.file),
592
+ securitySensitiveTouched: classifications
593
+ .filter((entry) => entry.kind === 'security-sensitive' || isSecuritySensitivePath(entry.file))
594
+ .map((entry) => entry.file),
595
+ generatedTouched: classifications
596
+ .filter((entry) => entry.kind === 'generated' || isGeneratedPath(entry.file))
597
+ .map((entry) => entry.file),
598
+ };
599
+ }
600
+ function filesByKind(classifications, kind) {
601
+ return classifications.filter((entry) => entry.kind === kind).map((entry) => entry.file);
602
+ }
603
+ async function readTrustMemory(feedbackPath) {
604
+ if (!feedbackPath)
605
+ return undefined;
606
+ try {
607
+ const feedback = await readFeedbackFile(feedbackPath);
608
+ return evaluateTrustMemoryResponses(feedback.responses.slice(0, 5));
609
+ }
610
+ catch (error) {
611
+ if (isNodeErrorCode(error, 'ENOENT'))
612
+ return undefined;
613
+ throw error;
614
+ }
615
+ }
616
+ function evaluateTrustMemoryResponses(responses) {
617
+ if (responses.length === 0)
618
+ return emptyTrustMemoryEvaluation();
619
+ const signals = trustMemorySignals(responses);
620
+ const counts = trustMemorySignalCounts(responses, signals);
621
+ const confidenceImpact = trustMemoryConfidenceImpact(counts);
622
+ return {
623
+ status: trustMemoryStatus(confidenceImpact),
624
+ summary: `${responses.length} local feedback response(s) applied to contract confidence.`,
625
+ signals: trustMemorySignalLabels(responses, signals),
626
+ confidenceImpact,
627
+ gaps: trustMemoryGaps(confidenceImpact, counts.negativeProofOutcomeCount),
628
+ };
629
+ }
630
+ function emptyTrustMemoryEvaluation() {
631
+ return {
632
+ status: 'none',
633
+ summary: 'Feedback artifact has no recorded responses.',
634
+ signals: [],
635
+ confidenceImpact: 'neutral',
636
+ gaps: [],
637
+ };
638
+ }
639
+ function trustMemorySignals(responses) {
640
+ const proofOutcomes = unique(responses
641
+ .map((response) => response.proofOutcome)
642
+ .filter((outcome) => Boolean(outcome)));
643
+ return {
644
+ missingSignals: unique(responses.flatMap((response) => response.missingSignals ?? [])),
645
+ noisyFindings: unique(responses.flatMap((response) => response.noisyFindings ?? [])),
646
+ falsePositiveRules: unique(responses.flatMap((response) => response.falsePositiveRules ?? [])),
647
+ proofOutcomes,
648
+ negativeProofOutcomeCount: proofOutcomes.filter(isNegativeProofOutcome).length,
649
+ };
650
+ }
651
+ function trustMemorySignalCounts(responses, signals) {
652
+ return {
653
+ positive: positiveTrustSignalCount(responses),
654
+ negative: responses.filter((response) => response.useful === false).length +
655
+ signals.missingSignals.length +
656
+ signals.noisyFindings.length +
657
+ signals.falsePositiveRules.length +
658
+ signals.negativeProofOutcomeCount,
659
+ negativeProofOutcomeCount: signals.negativeProofOutcomeCount,
660
+ };
661
+ }
662
+ function positiveTrustSignalCount(responses) {
663
+ return (responses.filter((response) => response.useful === true).length +
664
+ responses.filter((response) => response.proofOutcome === 'accepted').length +
665
+ responses.filter((response) => response.preventedBadEdit === true).length +
666
+ responses.filter((response) => response.ownerRoutingClear === true).length +
667
+ responses.filter((response) => response.nextCommandClear === true).length);
668
+ }
669
+ function trustMemoryConfidenceImpact(counts) {
670
+ if (counts.negative > counts.positive)
671
+ return 'negative';
672
+ return counts.positive > 0 ? 'positive' : 'neutral';
673
+ }
674
+ function trustMemoryStatus(confidenceImpact) {
675
+ if (confidenceImpact === 'negative')
676
+ return 'needs-tuning';
677
+ return confidenceImpact === 'positive' ? 'trusted' : 'mixed';
678
+ }
679
+ function trustMemoryGaps(confidenceImpact, negativeProofOutcomeCount) {
680
+ if (confidenceImpact !== 'negative')
681
+ return [];
682
+ return [
683
+ negativeProofOutcomeCount > 0
684
+ ? 'Trust Memory reports rejected, reverted, suppressed, or noisy proof outcomes for similar workflows.'
685
+ : 'Trust Memory reports missing signals or noisy findings for similar proof workflows.',
686
+ ];
687
+ }
688
+ function trustMemorySignalLabels(responses, signals) {
689
+ return unique([
690
+ ...responses.map(trustMemoryResponseLabel),
691
+ ...signals.missingSignals.map((signal) => `missing signal: ${signal}`),
692
+ ...signals.noisyFindings.map((finding) => `noisy finding: ${finding}`),
693
+ ...signals.falsePositiveRules.map((rule) => `false positive: ${rule}`),
694
+ ...signals.proofOutcomes.map((outcome) => `proof outcome: ${outcome}`),
695
+ ]);
696
+ }
697
+ function trustMemoryResponseLabel(response) {
698
+ const repo = response.repo ?? 'unknown repo';
699
+ const pr = response.pr ? ` PR ${response.pr}` : '';
700
+ const useful = response.useful === false ? 'not useful' : response.useful === true ? 'useful' : 'feedback';
701
+ return `${useful}: ${repo}${pr}`;
702
+ }
703
+ function isNegativeProofOutcome(outcome) {
704
+ return NEGATIVE_PROOF_OUTCOMES.has(outcome);
705
+ }
706
+ function fallbackRiskDelta() {
707
+ return {
708
+ baselineScore: 0,
709
+ projectedScore: 0,
710
+ delta: 0,
711
+ basis: ['No saved Proof Contract risk delta was available.'],
712
+ };
713
+ }
714
+ function safeChangeShape(summary) {
715
+ if (summary.toLowerCase().includes('bounded')) {
716
+ return `${summary} Keep the edit bounded to allowed files and likely tests.`;
717
+ }
718
+ return `Use a bounded change: ${summary}`;
719
+ }
720
+ function rollbackPlan(files) {
721
+ if (files.length === 0)
722
+ return 'Run git restore . to roll back this proof slice.';
723
+ return `Run git restore ${files.map(quoteShellArg).join(' ')} to roll back this proof slice.`;
724
+ }
725
+ function intentVerdict(contract) {
726
+ return contract.confidence === 'low' ? 'needs-review' : 'ready';
727
+ }
728
+ function confidenceForTrustMemory(confidence, trustMemory) {
729
+ if (trustMemory?.confidenceImpact === 'negative')
730
+ return lowerConfidence(confidence);
731
+ if (trustMemory?.confidenceImpact === 'positive')
732
+ return raiseConfidence(confidence);
733
+ return confidence;
734
+ }
735
+ function lowerConfidence(confidence) {
736
+ if (confidence === 'high')
737
+ return 'medium';
738
+ if (confidence === 'medium')
739
+ return 'low';
740
+ return 'low';
741
+ }
742
+ function raiseConfidence(confidence) {
743
+ if (confidence === 'low')
744
+ return 'medium';
745
+ if (confidence === 'medium')
746
+ return 'high';
747
+ return 'high';
748
+ }
749
+ function confidenceReasonForSimulation(confidence, simulationConfidence, trustMemory) {
750
+ if (trustMemory?.confidenceImpact === 'negative') {
751
+ return `Trust Memory lowered confidence from ${simulationConfidence} to ${confidence} because local feedback reported missing signals or noisy findings.`;
752
+ }
753
+ if (trustMemory?.confidenceImpact === 'positive') {
754
+ return `Trust Memory raised confidence from ${simulationConfidence} to ${confidence} because local feedback marked similar proof workflows useful.`;
755
+ }
756
+ return `${confidence} confidence from local simulation evidence.`;
757
+ }
758
+ function normalizeIntent(value) {
759
+ return value?.trim().replace(/\s+/g, ' ') ?? '';
760
+ }
761
+ function isDocumentationPath(file) {
762
+ return (file === 'README.md' ||
763
+ file.startsWith('docs/') ||
764
+ file.endsWith('.md') ||
765
+ file.endsWith('.mdx'));
766
+ }
767
+ function isGeneratedPath(file) {
768
+ return (file.startsWith('.projscan/') ||
769
+ file.startsWith('.projscan-memory/') ||
770
+ file.startsWith('.agentloop/') ||
771
+ file.startsWith('.agentflight/') ||
772
+ file.startsWith('coverage/') ||
773
+ file.startsWith('dist/'));
774
+ }
775
+ function isSecuritySensitivePath(file) {
776
+ return (file === '.env' ||
777
+ file.startsWith('.env.') ||
778
+ file.includes('/auth') ||
779
+ file.includes('/security') ||
780
+ file.includes('/secrets') ||
781
+ file.endsWith('.pem') ||
782
+ file.endsWith('.key'));
783
+ }
784
+ function isConfigPath(file) {
785
+ const basename = path.posix.basename(file);
786
+ return (CONFIG_BASENAMES.has(basename) ||
787
+ CONFIG_SUFFIXES.some((suffix) => basename.endsWith(suffix)) ||
788
+ file.startsWith('.github/'));
789
+ }
790
+ function isTestPath(file) {
791
+ return (file.startsWith('test/') ||
792
+ file.startsWith('tests/') ||
793
+ file.includes('/__tests__/') ||
794
+ /\.test\.[cm]?[jt]sx?$/.test(file) ||
795
+ /\.spec\.[cm]?[jt]sx?$/.test(file));
796
+ }
797
+ function isProductionPath(file) {
798
+ return (file.startsWith('src/') ||
799
+ file.startsWith('app/') ||
800
+ file.startsWith('lib/') ||
801
+ file.startsWith('packages/') ||
802
+ file.startsWith('apps/'));
803
+ }
804
+ function pathMatches(file, pattern) {
805
+ if (pattern.endsWith('/**'))
806
+ return file.startsWith(pattern.slice(0, -3));
807
+ return file === pattern;
808
+ }
809
+ function unique(values) {
810
+ return [...new Set(values)];
811
+ }
812
+ function slug(value) {
813
+ return value.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '').slice(0, 80);
814
+ }
815
+ function isNodeErrorCode(error, code) {
816
+ return (typeof error === 'object' &&
817
+ error !== null &&
818
+ 'code' in error &&
819
+ error.code === code);
820
+ }
821
+ //# sourceMappingURL=prove.js.map