projscan 4.14.0 → 4.16.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 (199) hide show
  1. package/README.md +103 -21
  2. package/dist/cli/commands/assess.js +36 -2
  3. package/dist/cli/commands/assess.js.map +1 -1
  4. package/dist/cli/commands/evidencePack.js +2 -0
  5. package/dist/cli/commands/evidencePack.js.map +1 -1
  6. package/dist/cli/commands/prove.js +172 -23
  7. package/dist/cli/commands/prove.js.map +1 -1
  8. package/dist/cli/commands/startConsole.d.ts +2 -2
  9. package/dist/cli/commands/startConsole.js +2 -260
  10. package/dist/cli/commands/startConsole.js.map +1 -1
  11. package/dist/cli/commands/startConsoleExecution.d.ts +5 -0
  12. package/dist/cli/commands/startConsoleExecution.js +108 -0
  13. package/dist/cli/commands/startConsoleExecution.js.map +1 -0
  14. package/dist/cli/commands/startConsoleMission.d.ts +6 -0
  15. package/dist/cli/commands/startConsoleMission.js +157 -0
  16. package/dist/cli/commands/startConsoleMission.js.map +1 -0
  17. package/dist/cli/commands/startMissionBundle.js +24 -27
  18. package/dist/cli/commands/startMissionBundle.js.map +1 -1
  19. package/dist/core/adoption.d.ts +8 -81
  20. package/dist/core/adoption.js +4 -549
  21. package/dist/core/adoption.js.map +1 -1
  22. package/dist/core/adoptionFirstRunDiagnostics.d.ts +20 -0
  23. package/dist/core/adoptionFirstRunDiagnostics.js +240 -0
  24. package/dist/core/adoptionFirstRunDiagnostics.js.map +1 -0
  25. package/dist/core/adoptionMcpConfig.d.ts +27 -0
  26. package/dist/core/adoptionMcpConfig.js +123 -0
  27. package/dist/core/adoptionMcpConfig.js.map +1 -0
  28. package/dist/core/adoptionMcpDoctor.d.ts +23 -0
  29. package/dist/core/adoptionMcpDoctor.js +87 -0
  30. package/dist/core/adoptionMcpDoctor.js.map +1 -0
  31. package/dist/core/adoptionWorkflowRecipes.d.ts +14 -0
  32. package/dist/core/adoptionWorkflowRecipes.js +110 -0
  33. package/dist/core/adoptionWorkflowRecipes.js.map +1 -0
  34. package/dist/core/baseframeAssessment.d.ts +9 -0
  35. package/dist/core/baseframeAssessment.js +471 -0
  36. package/dist/core/baseframeAssessment.js.map +1 -0
  37. package/dist/core/bugHunt.js +26 -255
  38. package/dist/core/bugHunt.js.map +1 -1
  39. package/dist/core/bugHuntPreflightFindings.d.ts +2 -1
  40. package/dist/core/bugHuntPreflightFindings.js +20 -0
  41. package/dist/core/bugHuntPreflightFindings.js.map +1 -1
  42. package/dist/core/bugHuntReportAssembly.d.ts +20 -0
  43. package/dist/core/bugHuntReportAssembly.js +179 -0
  44. package/dist/core/bugHuntReportAssembly.js.map +1 -0
  45. package/dist/core/bugHuntSourceFindings.d.ts +3 -0
  46. package/dist/core/bugHuntSourceFindings.js +61 -0
  47. package/dist/core/bugHuntSourceFindings.js.map +1 -0
  48. package/dist/core/dogfood.js +4 -393
  49. package/dist/core/dogfood.js.map +1 -1
  50. package/dist/core/dogfoodMarketValidation.d.ts +5 -0
  51. package/dist/core/dogfoodMarketValidation.js +265 -0
  52. package/dist/core/dogfoodMarketValidation.js.map +1 -0
  53. package/dist/core/dogfoodRepoEvaluation.d.ts +4 -0
  54. package/dist/core/dogfoodRepoEvaluation.js +137 -0
  55. package/dist/core/dogfoodRepoEvaluation.js.map +1 -0
  56. package/dist/core/evidenceComment.js +50 -13
  57. package/dist/core/evidenceComment.js.map +1 -1
  58. package/dist/core/feedback.js +2 -252
  59. package/dist/core/feedback.js.map +1 -1
  60. package/dist/core/feedbackIntakeClassifier.d.ts +2 -0
  61. package/dist/core/feedbackIntakeClassifier.js +255 -0
  62. package/dist/core/feedbackIntakeClassifier.js.map +1 -0
  63. package/dist/core/intentRouterKeywordToolGuards.js +1 -55
  64. package/dist/core/intentRouterKeywordToolGuards.js.map +1 -1
  65. package/dist/core/intentRouterKeywordWeights.js +13 -28
  66. package/dist/core/intentRouterKeywordWeights.js.map +1 -1
  67. package/dist/core/intentRouterProductGuardSignals.d.ts +3 -0
  68. package/dist/core/intentRouterProductGuardSignals.js +59 -0
  69. package/dist/core/intentRouterProductGuardSignals.js.map +1 -0
  70. package/dist/core/markdownSafety.d.ts +3 -0
  71. package/dist/core/markdownSafety.js +14 -0
  72. package/dist/core/markdownSafety.js.map +1 -0
  73. package/dist/core/preflight.d.ts +2 -0
  74. package/dist/core/preflight.js.map +1 -1
  75. package/dist/core/preflightChangedFiles.d.ts +2 -0
  76. package/dist/core/preflightChangedFiles.js +1 -1
  77. package/dist/core/preflightChangedFiles.js.map +1 -1
  78. package/dist/core/preflightInputs.d.ts +2 -0
  79. package/dist/core/preflightInputs.js +5 -2
  80. package/dist/core/preflightInputs.js.map +1 -1
  81. package/dist/core/proofLedger.d.ts +5 -1
  82. package/dist/core/proofLedger.js +161 -12
  83. package/dist/core/proofLedger.js.map +1 -1
  84. package/dist/core/proofReplay.d.ts +9 -0
  85. package/dist/core/proofReplay.js +164 -0
  86. package/dist/core/proofReplay.js.map +1 -0
  87. package/dist/core/proofSufficiency.d.ts +19 -0
  88. package/dist/core/proofSufficiency.js +425 -0
  89. package/dist/core/proofSufficiency.js.map +1 -0
  90. package/dist/core/prove.d.ts +6 -0
  91. package/dist/core/prove.js +277 -87
  92. package/dist/core/prove.js.map +1 -1
  93. package/dist/core/qualityScorecard.js +8 -238
  94. package/dist/core/qualityScorecard.js.map +1 -1
  95. package/dist/core/qualityScorecardDimensions.d.ts +14 -0
  96. package/dist/core/qualityScorecardDimensions.js +99 -0
  97. package/dist/core/qualityScorecardDimensions.js.map +1 -0
  98. package/dist/core/qualityScorecardRisks.d.ts +8 -0
  99. package/dist/core/qualityScorecardRisks.js +107 -0
  100. package/dist/core/qualityScorecardRisks.js.map +1 -0
  101. package/dist/core/qualityScorecardSignals.d.ts +20 -0
  102. package/dist/core/qualityScorecardSignals.js +59 -0
  103. package/dist/core/qualityScorecardSignals.js.map +1 -0
  104. package/dist/core/releaseEvidence.d.ts +1 -0
  105. package/dist/core/releaseEvidence.js +15 -40
  106. package/dist/core/releaseEvidence.js.map +1 -1
  107. package/dist/core/releaseEvidenceBaseline.js +4 -1
  108. package/dist/core/releaseEvidenceBaseline.js.map +1 -1
  109. package/dist/core/releaseEvidenceProofReceipt.d.ts +6 -0
  110. package/dist/core/releaseEvidenceProofReceipt.js +140 -0
  111. package/dist/core/releaseEvidenceProofReceipt.js.map +1 -0
  112. package/dist/core/releaseEvidenceVerdict.d.ts +5 -2
  113. package/dist/core/releaseEvidenceVerdict.js +39 -1
  114. package/dist/core/releaseEvidenceVerdict.js.map +1 -1
  115. package/dist/core/repositoryScanner.d.ts +1 -0
  116. package/dist/core/repositoryScanner.js +5 -4
  117. package/dist/core/repositoryScanner.js.map +1 -1
  118. package/dist/core/sessionResources.d.ts +14 -2
  119. package/dist/core/sessionResources.js +3 -3
  120. package/dist/core/sessionResources.js.map +1 -1
  121. package/dist/core/startInputs.d.ts +1 -1
  122. package/dist/core/startIntentTargets.d.ts +1 -1
  123. package/dist/core/startIntentTargets.js +1 -16
  124. package/dist/core/startIntentTargets.js.map +1 -1
  125. package/dist/core/startMissionInputStatusPolicy.d.ts +7 -0
  126. package/dist/core/startMissionInputStatusPolicy.js +74 -0
  127. package/dist/core/startMissionInputStatusPolicy.js.map +1 -0
  128. package/dist/core/startMissionPolicy.d.ts +6 -15
  129. package/dist/core/startMissionPolicy.js +4 -305
  130. package/dist/core/startMissionPolicy.js.map +1 -1
  131. package/dist/core/startMissionProofPolicy.d.ts +6 -0
  132. package/dist/core/startMissionProofPolicy.js +84 -0
  133. package/dist/core/startMissionProofPolicy.js.map +1 -0
  134. package/dist/core/startMissionRiskPolicy.d.ts +4 -0
  135. package/dist/core/startMissionRiskPolicy.js +85 -0
  136. package/dist/core/startMissionRiskPolicy.js.map +1 -0
  137. package/dist/core/startMissionRoutingPolicy.d.ts +6 -0
  138. package/dist/core/startMissionRoutingPolicy.js +67 -0
  139. package/dist/core/startMissionRoutingPolicy.js.map +1 -0
  140. package/dist/core/startMode.d.ts +1 -2
  141. package/dist/core/startMode.js +4 -151
  142. package/dist/core/startMode.js.map +1 -1
  143. package/dist/core/startModeIntentPolicy.d.ts +12 -0
  144. package/dist/core/startModeIntentPolicy.js +41 -0
  145. package/dist/core/startModeIntentPolicy.js.map +1 -0
  146. package/dist/core/startModeRoutingPolicy.d.ts +4 -0
  147. package/dist/core/startModeRoutingPolicy.js +117 -0
  148. package/dist/core/startModeRoutingPolicy.js.map +1 -0
  149. package/dist/core/startSearchQueryTargets.d.ts +1 -0
  150. package/dist/core/startSearchQueryTargets.js +17 -0
  151. package/dist/core/startSearchQueryTargets.js.map +1 -0
  152. package/dist/core/workplan.d.ts +3 -2
  153. package/dist/core/workplan.js +11 -585
  154. package/dist/core/workplan.js.map +1 -1
  155. package/dist/core/workplanCoordinationTasks.d.ts +3 -0
  156. package/dist/core/workplanCoordinationTasks.js +82 -0
  157. package/dist/core/workplanCoordinationTasks.js.map +1 -0
  158. package/dist/core/workplanModeTasks.d.ts +2 -0
  159. package/dist/core/workplanModeTasks.js +192 -0
  160. package/dist/core/workplanModeTasks.js.map +1 -0
  161. package/dist/core/workplanPreflightTasks.d.ts +2 -0
  162. package/dist/core/workplanPreflightTasks.js +126 -0
  163. package/dist/core/workplanPreflightTasks.js.map +1 -0
  164. package/dist/core/workplanQualitySignals.d.ts +7 -0
  165. package/dist/core/workplanQualitySignals.js +63 -0
  166. package/dist/core/workplanQualitySignals.js.map +1 -0
  167. package/dist/core/workplanReport.d.ts +4 -0
  168. package/dist/core/workplanReport.js +79 -0
  169. package/dist/core/workplanReport.js.map +1 -0
  170. package/dist/core/workplanRiskOwnership.d.ts +5 -0
  171. package/dist/core/workplanRiskOwnership.js +97 -0
  172. package/dist/core/workplanRiskOwnership.js.map +1 -0
  173. package/dist/core/workplanSuggestedActions.d.ts +2 -0
  174. package/dist/core/workplanSuggestedActions.js +43 -0
  175. package/dist/core/workplanSuggestedActions.js.map +1 -0
  176. package/dist/mcp/tools/prove.js +23 -17
  177. package/dist/mcp/tools/prove.js.map +1 -1
  178. package/dist/projscan-sbom.cdx.json +6 -6
  179. package/dist/publicCore.d.ts +1 -0
  180. package/dist/publicCore.js +1 -0
  181. package/dist/publicCore.js.map +1 -1
  182. package/dist/tool-manifest.json +2 -2
  183. package/dist/types/baseframe.d.ts +75 -0
  184. package/dist/types/baseframe.js +2 -0
  185. package/dist/types/baseframe.js.map +1 -0
  186. package/dist/types/config.d.ts +15 -0
  187. package/dist/types/evidencePack.d.ts +21 -0
  188. package/dist/types/prove.d.ts +79 -0
  189. package/dist/types.d.ts +1 -0
  190. package/dist/utils/changedFiles.js +57 -16
  191. package/dist/utils/changedFiles.js.map +1 -1
  192. package/dist/utils/config.js +2 -0
  193. package/dist/utils/config.js.map +1 -1
  194. package/dist/utils/configProofRecipes.d.ts +2 -0
  195. package/dist/utils/configProofRecipes.js +91 -0
  196. package/dist/utils/configProofRecipes.js.map +1 -0
  197. package/docs/GUIDE.md +120 -19
  198. package/docs/integrations/baseframe-suite-v1.md +163 -0
  199. package/package.json +2 -1
@@ -3,7 +3,9 @@ import { spawn } from 'node:child_process';
3
3
  import fs from 'node:fs/promises';
4
4
  import path from 'node:path';
5
5
  import { readFeedbackFile } from './feedback.js';
6
- import { appendProofLedgerRecord, changedFileFingerprint, latestProofRecordFor, readProofLedger, redactProofOutput, } from './proofLedger.js';
6
+ import { appendProofLedgerRecord, changedFileFingerprint, normalizeProofCommand, prepareProofArtifactReadPath, prepareProofArtifactWritePath, readLatestProofLedgerRecords, redactProofOutput, } from './proofLedger.js';
7
+ import { buildProofRequirements, isConfigPath, isDocumentationPath, isGeneratedPath, isProductionPath, isSecuritySensitivePath, isTestPath, proofRelevantChangedFiles, proofSufficiencyFor, } from './proofSufficiency.js';
8
+ import { buildProofReplay } from './proofReplay.js';
7
9
  import { quoteShellArg } from './startShellArgs.js';
8
10
  import { computeSimulation } from './simulate.js';
9
11
  import { getChangedFiles } from '../utils/changedFiles.js';
@@ -30,6 +32,7 @@ const HIGH_RISK_FORBIDDEN_FILES = [
30
32
  'package-lock.json',
31
33
  'package.json',
32
34
  ];
35
+ const PATH_MATCH_REGEX_CACHE = new Map();
33
36
  const CHANGED_FILE_RULES = [
34
37
  {
35
38
  kind: 'generated',
@@ -83,14 +86,6 @@ const CHANGED_FILE_RULES = [
83
86
  },
84
87
  ];
85
88
  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
89
  export async function computeProve(rootPath, options = {}) {
95
90
  const modeCount = [
96
91
  Boolean(options.intent?.trim()),
@@ -144,7 +139,7 @@ async function computeRecordProof(rootPath, options) {
144
139
  changedFiles: proofRelevantChangedFiles(changedFiles.files),
145
140
  outputSummary: proof.summary,
146
141
  logPath: proof.logPath,
147
- source: 'prove-record',
142
+ source: options.recordSource ?? 'prove-record',
148
143
  });
149
144
  const verdict = record.status === 'passed' ? 'ready' : 'blocked';
150
145
  const verifiedWorkflow = verifiedWorkflowForRecord(verdict, record.status);
@@ -306,7 +301,7 @@ async function writeProofRunLog(rootPath, input) {
306
301
  if (!relativeToRoot || relativeToRoot.startsWith('..') || path.isAbsolute(relativeToRoot)) {
307
302
  throw new Error('Proof log path must stay inside the project root.');
308
303
  }
309
- await fs.mkdir(path.dirname(fullPath), { recursive: true });
304
+ await prepareProofArtifactWritePath(rootPath, fullPath);
310
305
  await fs.writeFile(fullPath, redactedProofRunLog(input), 'utf-8');
311
306
  return relativePath;
312
307
  }
@@ -352,7 +347,12 @@ async function computeIntentProof(rootPath, options) {
352
347
  }),
353
348
  readTrustMemory(options.feedbackPath),
354
349
  ]);
355
- const contract = buildContract({ intent, simulation, trustMemory });
350
+ const contract = buildContract({
351
+ intent,
352
+ simulation,
353
+ trustMemory,
354
+ proofRecipes: options.proofRecipes,
355
+ });
356
356
  let savedContractPath;
357
357
  if (options.saveContractPath) {
358
358
  savedContractPath = await writeContract(rootPath, options.saveContractPath, contract);
@@ -370,12 +370,17 @@ async function computeIntentProof(rootPath, options) {
370
370
  };
371
371
  }
372
372
  async function computeChangedProof(rootPath, options) {
373
- const [contract, changedFiles, ledger] = await Promise.all([
373
+ const [contract, changedFiles] = await Promise.all([
374
374
  resolveContract(rootPath, options),
375
- getChangedFiles(rootPath, options.baseRef),
376
- readProofLedger(rootPath, options.ledgerPath),
375
+ options.changedFiles ? Promise.resolve(options.changedFiles) : getChangedFiles(rootPath, options.baseRef),
377
376
  ]);
378
377
  const quickPreflight = quickProofPreflight(changedFiles);
378
+ const relevantChangedFiles = proofRelevantChangedFiles(changedFiles.files);
379
+ const proofCommands = proofCommandsForReceipt(contract.contract);
380
+ const [currentChangedFileFingerprint, ledger] = await Promise.all([
381
+ changedFileFingerprint(rootPath, relevantChangedFiles),
382
+ readLatestProofLedgerRecords(rootPath, options.ledgerPath, proofCommands),
383
+ ]);
379
384
  const receipt = buildReceipt({
380
385
  contract: contract.contract,
381
386
  contractPath: contract.path,
@@ -386,6 +391,8 @@ async function computeChangedProof(rootPath, options) {
386
391
  newRisks: quickPreflight.risks,
387
392
  preflightVerdict: quickPreflight.verdict,
388
393
  ledger,
394
+ proofCommands,
395
+ currentChangedFileFingerprint,
389
396
  });
390
397
  return {
391
398
  schemaVersion: 1,
@@ -421,8 +428,27 @@ function buildContract(input) {
421
428
  const simulationFiles = input.simulation.filesLikelyTouched.map((file) => file.path);
422
429
  const allowedFiles = unique(simulationFiles);
423
430
  const likelyTests = unique(input.simulation.testsLikelyAffected);
424
- const forbiddenFiles = forbiddenFilesFor(input.intent, [...allowedFiles, ...likelyTests]);
425
- const proofCommands = contractProofCommands(input.simulation.proofCommands);
431
+ const matchedRecipes = matchTeamProofRecipes(input.proofRecipes ?? [], [
432
+ ...allowedFiles,
433
+ ...likelyTests,
434
+ ]);
435
+ const forbiddenFiles = unique([
436
+ ...forbiddenFilesFor(input.intent, [...allowedFiles, ...likelyTests]),
437
+ ...matchedRecipes.flatMap((recipe) => recipe.forbiddenFiles ?? []),
438
+ ]);
439
+ const proofCommands = unique([
440
+ ...contractProofCommands(input.simulation.proofCommands),
441
+ ...matchedRecipes.flatMap((recipe) => recipe.requiredCommands),
442
+ ]);
443
+ const proofRequirements = [
444
+ ...buildProofRequirements({
445
+ allowedFiles,
446
+ likelyTests,
447
+ riskyContracts: riskyContractsFor(input.simulation.contractsLikelyAffected, allowedFiles),
448
+ proofCommands,
449
+ }),
450
+ ...recipeProofRequirements(matchedRecipes),
451
+ ];
426
452
  const evidenceGaps = unique([
427
453
  ...(input.simulation.warnings.length > 0 ? input.simulation.warnings : []),
428
454
  ...(likelyTests.length === 0 ? ['No likely regression test was inferred from the plan.'] : []),
@@ -440,6 +466,8 @@ function buildContract(input) {
440
466
  likelyTests,
441
467
  missingRegressionTests: likelyTests.length > 0 ? [] : ['Add one regression test around the behavior named by the intent.'],
442
468
  proofCommands,
469
+ proofRequirements,
470
+ ...(matchedRecipes.length > 0 ? { teamProofRecipes: matchedRecipes } : {}),
443
471
  safeChangeShape: safeChangeShape(input.simulation.recommendedAlternative.summary),
444
472
  rollbackPlan: rollbackPlan([...allowedFiles, ...likelyTests]),
445
473
  confidence,
@@ -472,39 +500,125 @@ function contractProofCommands(simulationCommands) {
472
500
  'projscan preflight --mode before_commit --format json',
473
501
  ].filter((command) => typeof command === 'string'));
474
502
  }
503
+ function matchTeamProofRecipes(recipes, files) {
504
+ return recipes
505
+ .map((recipe) => {
506
+ const matchedFiles = unique(files.filter((file) => recipe.matches.some((pattern) => pathMatches(file, pattern))));
507
+ if (matchedFiles.length === 0)
508
+ return null;
509
+ return {
510
+ ...recipe,
511
+ matchedFiles,
512
+ };
513
+ })
514
+ .filter((recipe) => Boolean(recipe));
515
+ }
516
+ function recipeProofRequirements(recipes) {
517
+ return recipes.map((recipe) => ({
518
+ id: `recipe:${recipe.id}`,
519
+ surface: 'custom',
520
+ files: recipe.matchedFiles,
521
+ requiredCommands: recipe.requiredCommands,
522
+ requiredReview: recipe.requiredReviewers?.length
523
+ ? `require review from ${recipe.requiredReviewers.join(', ')}`
524
+ : `review Team Proof Recipe ${recipe.id}`,
525
+ reason: recipe.reason ?? `Team Proof Recipe ${recipe.id} matched ${recipe.matchedFiles.join(', ')}.`,
526
+ source: 'recipe',
527
+ recipeId: recipe.id,
528
+ ...(recipe.requiredReviewers ? { requiredReviewers: recipe.requiredReviewers } : {}),
529
+ }));
530
+ }
531
+ function teamProofRecipesForReceipt(recipes, changedFiles, proofStatus) {
532
+ return recipes
533
+ .map((recipe) => {
534
+ const matchedFiles = unique(changedFiles.filter((file) => recipe.matches.some((pattern) => pathMatches(file, pattern))));
535
+ const forbiddenTouched = unique(changedFiles.filter((file) => (recipe.forbiddenFiles ?? []).some((pattern) => pathMatches(file, pattern))));
536
+ if (matchedFiles.length === 0 && forbiddenTouched.length === 0)
537
+ return null;
538
+ const missingCommands = recipe.requiredCommands.filter((command) => proofStatus.missingCommands.includes(command));
539
+ const failedCommands = recipe.requiredCommands.filter((command) => proofStatus.failedCommands.includes(command));
540
+ const staleCommands = recipe.requiredCommands.filter((command) => proofStatus.staleCommands.includes(command));
541
+ return {
542
+ ...recipe,
543
+ matchedFiles,
544
+ ...(forbiddenTouched.length > 0 ? { forbiddenTouched } : {}),
545
+ ...(missingCommands.length > 0 ? { missingCommands } : {}),
546
+ ...(failedCommands.length > 0 ? { failedCommands } : {}),
547
+ ...(staleCommands.length > 0 ? { staleCommands } : {}),
548
+ };
549
+ })
550
+ .filter((recipe) => Boolean(recipe));
551
+ }
552
+ function recipeGapsFor(recipes) {
553
+ const gaps = [];
554
+ for (const recipe of recipes) {
555
+ for (const command of recipe.missingCommands ?? []) {
556
+ gaps.push(`${recipe.id} requires proof command: ${command}`);
557
+ }
558
+ for (const command of recipe.failedCommands ?? []) {
559
+ gaps.push(`${recipe.id} has failed proof command: ${command}`);
560
+ }
561
+ for (const command of recipe.staleCommands ?? []) {
562
+ gaps.push(`${recipe.id} has stale proof command: ${command}`);
563
+ }
564
+ }
565
+ return gaps;
566
+ }
475
567
  function buildReceipt(input) {
476
568
  const scope = scopeFor(input.contract, input.contractPath, input.changedFiles);
477
569
  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);
570
+ const proofStatus = proofStatusFor(input.proofCommands, input.ledger, input.changedFiles, input.currentChangedFileFingerprint);
571
+ const teamProofRecipes = teamProofRecipesForReceipt(input.contract?.teamProofRecipes ?? [], input.changedFiles, proofStatus);
572
+ const requiredReviewers = unique(teamProofRecipes.flatMap((recipe) => recipe.requiredReviewers ?? []));
573
+ const recipeForbiddenTouched = unique(teamProofRecipes.flatMap((recipe) => recipe.forbiddenTouched ?? []));
574
+ const recipeGaps = recipeGapsFor(teamProofRecipes);
575
+ const proofSufficiency = proofSufficiencyFor({
576
+ contract: input.contract,
577
+ scope,
578
+ proofStatus,
579
+ });
483
580
  const commitReadiness = readinessFor({
484
581
  scopeStatus: scope.status,
485
582
  forbiddenTouched: scope.forbiddenTouched,
486
583
  preflightVerdict: input.preflightVerdict,
487
584
  evidenceGaps,
488
585
  proofStatus: proofStatus.status,
586
+ proofSufficiencyStatus: proofSufficiency.status,
489
587
  });
490
588
  const riskDeltaDirection = riskDeltaDirectionFor(input.riskDelta);
491
589
  const reviewerDecision = reviewerDecisionFor({
492
590
  commitReadiness,
493
591
  proofStatus: proofStatus.status,
592
+ proofSufficiencyStatus: proofSufficiency.status,
494
593
  scope,
495
594
  preflightVerdict: input.preflightVerdict,
496
595
  });
596
+ const proofReplay = buildProofReplay({
597
+ scope,
598
+ proofStatus,
599
+ proofSufficiency,
600
+ riskDeltaDirection,
601
+ reviewerDecision,
602
+ replayCommand: replayCommandForReceipt(scope.contractPath),
603
+ });
497
604
  const receipt = {
498
605
  summary: summaryForReceipt(commitReadiness, scope),
499
606
  commitReadiness,
500
607
  scope,
501
608
  proofStatus,
609
+ proofSufficiency,
610
+ proofReplay,
611
+ ...(teamProofRecipes.length > 0 ? { teamProofRecipes } : {}),
612
+ ...(requiredReviewers.length > 0 ? { requiredReviewers } : {}),
613
+ ...(recipeForbiddenTouched.length > 0 ? { recipeForbiddenTouched } : {}),
614
+ ...(recipeForbiddenTouched.length > 0 ? { recipeDrift: recipeForbiddenTouched } : {}),
615
+ ...(recipeGaps.length > 0 ? { recipeGaps } : {}),
502
616
  riskDelta: input.riskDelta,
503
617
  riskDeltaDirection,
504
618
  reviewerDecision,
505
619
  newRisks: input.newRisks,
506
620
  evidenceGaps,
507
- reviewerGuidance: reviewerGuidanceFor(commitReadiness, scope, reviewerDecision, proofStatus.status),
621
+ reviewerGuidance: reviewerGuidanceFor(commitReadiness, scope, reviewerDecision, proofStatus.status, proofSufficiency.status),
508
622
  };
509
623
  return {
510
624
  ...receipt,
@@ -538,9 +652,9 @@ function verifiedWorkflowForRecord(verdict, recordStatus) {
538
652
  }
539
653
  function verifiedWorkflowForReceipt(receipt) {
540
654
  const proofStatus = receipt.proofStatus.status;
655
+ const proofSufficiencyStatus = receipt.proofSufficiency?.status ?? 'missing';
541
656
  const staleProof = proofStatus === 'stale' || receipt.proofStatus.staleCommands.length > 0;
542
- const missingProof = proofStatus === 'missing' ||
543
- proofStatus === 'partial' ||
657
+ const missingProof = isMissingProofStatus(proofStatus) ||
544
658
  receipt.proofStatus.missingCommands.length > 0;
545
659
  const failedProof = proofStatus === 'failed' || receipt.proofStatus.failedCommands.length > 0;
546
660
  return {
@@ -561,6 +675,7 @@ function verifiedWorkflowForReceipt(receipt) {
561
675
  reviewerDecision: receipt.reviewerDecision,
562
676
  scopeStatus: receipt.scope.status,
563
677
  proofStatus,
678
+ proofSufficiencyStatus,
564
679
  riskDeltaDirection: receipt.riskDeltaDirection,
565
680
  staleProof,
566
681
  missingProof,
@@ -572,6 +687,9 @@ function nextActionForReceipt(input) {
572
687
  return 'fix failed proof commands before review';
573
688
  if (input.staleProof)
574
689
  return 'rerun stale proof commands before review';
690
+ if (input.receipt.proofStatus.status === 'not-run') {
691
+ return 'add required proof commands to the Proof Contract before review';
692
+ }
575
693
  if (input.missingProof)
576
694
  return 'record missing proof commands before review';
577
695
  if (input.receipt.scope.status === 'drifted') {
@@ -589,6 +707,9 @@ function nextCommandForReceipt(input) {
589
707
  if (input.staleProof) {
590
708
  return `projscan prove --record-command ${quoteShellArg(input.receipt.proofStatus.staleCommands[0] ?? '<command>')} --exit-code 0 --duration-ms <ms>`;
591
709
  }
710
+ if (input.receipt.proofStatus.status === 'not-run') {
711
+ return 'projscan prove --intent "<change intent>" --save-contract .projscan/proof-contract.json';
712
+ }
592
713
  if (input.missingProof) {
593
714
  return 'projscan prove --record-command "<command>" --exit-code 0 --duration-ms <ms>';
594
715
  }
@@ -596,11 +717,11 @@ function nextCommandForReceipt(input) {
596
717
  return 'projscan prove --changed --format markdown';
597
718
  return 'projscan evidence-pack --pr-comment';
598
719
  }
599
- function proofStatusFor(proofCommands, ledger, changedFiles) {
720
+ function proofStatusFor(proofCommands, ledger, changedFiles, currentFingerprint) {
600
721
  const relevantChangedFiles = proofRelevantChangedFiles(changedFiles);
601
- const currentFingerprint = changedFileFingerprint(relevantChangedFiles);
722
+ const latestLedgerByCommand = latestProofRecordsByCommand(ledger);
602
723
  const commandEvidence = proofCommands.map((command) => {
603
- const record = latestProofRecordFor(ledger, command);
724
+ const record = latestLedgerByCommand.get(normalizeProofCommand(command));
604
725
  if (!record) {
605
726
  return {
606
727
  command,
@@ -617,18 +738,24 @@ function proofStatusFor(proofCommands, ledger, changedFiles) {
617
738
  exitCode: record.exitCode,
618
739
  durationMs: record.durationMs,
619
740
  completedAt: record.completedAt,
741
+ source: record.source,
742
+ recordedChangedFiles: record.changedFiles,
743
+ recordedChangedFileFingerprint: record.changedFileFingerprint,
620
744
  outputSummary: record.outputSummary,
621
745
  ...(record.logPath ? { logPath: record.logPath } : {}),
622
- staleReason: 'Recorded changed files differ from current changed files.',
746
+ staleReason: staleProofReason(record.changedFiles, relevantChangedFiles),
623
747
  };
624
748
  }
625
749
  return {
626
750
  command,
627
751
  status: record.exitCode === 0 ? 'passed' : 'failed',
628
752
  fresh: true,
753
+ source: record.source,
629
754
  exitCode: record.exitCode,
630
755
  durationMs: record.durationMs,
631
756
  completedAt: record.completedAt,
757
+ recordedChangedFiles: record.changedFiles,
758
+ recordedChangedFileFingerprint: record.changedFileFingerprint,
632
759
  outputSummary: record.outputSummary,
633
760
  ...(record.logPath ? { logPath: record.logPath } : {}),
634
761
  };
@@ -661,6 +788,32 @@ function proofStatusFor(proofCommands, ledger, changedFiles) {
661
788
  commandEvidence,
662
789
  };
663
790
  }
791
+ function latestProofRecordsByCommand(records) {
792
+ const latest = new Map();
793
+ for (const record of records) {
794
+ const existing = latest.get(record.normalizedCommand);
795
+ if (!existing || record.completedAt.localeCompare(existing.completedAt) >= 0) {
796
+ latest.set(record.normalizedCommand, record);
797
+ }
798
+ }
799
+ return latest;
800
+ }
801
+ function staleProofReason(recordedFiles, currentFiles) {
802
+ return sameStringSet(recordedFiles, currentFiles)
803
+ ? 'Recorded changed-file content fingerprint differs from current changed-file content.'
804
+ : 'Recorded changed files differ from current changed files.';
805
+ }
806
+ function sameStringSet(left, right) {
807
+ if (left.length !== right.length)
808
+ return false;
809
+ const rightSet = new Set(right);
810
+ return left.every((value) => rightSet.has(value));
811
+ }
812
+ function replayCommandForReceipt(contractPath) {
813
+ return contractPath
814
+ ? `projscan prove --changed --contract ${quoteShellArg(contractPath)} --format markdown`
815
+ : 'projscan prove --changed --format markdown';
816
+ }
664
817
  function proofStatusSummary(input) {
665
818
  if (input.requiredCount === 0)
666
819
  return 'not-run';
@@ -674,8 +827,11 @@ function proofStatusSummary(input) {
674
827
  return 'partial';
675
828
  return 'passed';
676
829
  }
677
- function proofRelevantChangedFiles(files) {
678
- return files.filter((file) => !isGeneratedPath(file));
830
+ function proofCommandsForReceipt(contract) {
831
+ return (contract?.proofCommands ?? [
832
+ 'projscan assess --mode fix-first --format json',
833
+ 'projscan preflight --mode before_commit --format json',
834
+ ]);
679
835
  }
680
836
  function scopeFor(contract, contractPath, changedFiles) {
681
837
  if (!contract) {
@@ -697,13 +853,16 @@ function scopeFor(contract, contractPath, changedFiles) {
697
853
  ...(contractPath ? [contractPath] : []),
698
854
  ]);
699
855
  const forbiddenTouched = changedFiles.filter((file) => contract.forbiddenFiles.some((pattern) => pathMatches(file, pattern)));
856
+ const forbiddenTouchedSet = new Set(forbiddenTouched);
857
+ const allowedProductionSet = new Set(contract.allowedFiles);
858
+ const likelyTestSet = new Set(contract.likelyTests);
700
859
  const allowedTouched = changedFiles.filter((file) => allowed.has(file));
701
860
  const outsideAllowed = changedFiles.filter((file) => !allowed.has(file) && !isLocalProofArtifactPath(file));
702
861
  const classifications = changedFiles.map((file) => classifyChangedFile({
703
862
  file,
704
- forbidden: forbiddenTouched.includes(file),
705
- allowedProduction: contract.allowedFiles.includes(file),
706
- expectedTest: contract.likelyTests.includes(file),
863
+ forbidden: forbiddenTouchedSet.has(file),
864
+ allowedProduction: allowedProductionSet.has(file),
865
+ expectedTest: likelyTestSet.has(file),
707
866
  contractPath: contractPath === file,
708
867
  }));
709
868
  const status = forbiddenTouched.length > 0 || outsideAllowed.length > 0 ? 'drifted' : 'within-contract';
@@ -731,8 +890,9 @@ async function resolveContract(rootPath, options) {
731
890
  return contract ? { contract, path: DEFAULT_CONTRACT_PATH } : {};
732
891
  }
733
892
  async function readContract(rootPath, filePath, required) {
734
- const fullPath = path.resolve(rootPath, filePath);
893
+ const fullPath = resolveProofContractPath(rootPath, filePath);
735
894
  try {
895
+ await prepareProofArtifactReadPath(rootPath, fullPath);
736
896
  const parsed = JSON.parse(await fs.readFile(fullPath, 'utf-8'));
737
897
  if (parsed.schemaVersion !== 1 || !Array.isArray(parsed.allowedFiles) || !parsed.id) {
738
898
  throw new Error('invalid Proof Contract shape');
@@ -746,11 +906,28 @@ async function readContract(rootPath, filePath, required) {
746
906
  }
747
907
  }
748
908
  async function writeContract(rootPath, filePath, contract) {
749
- const fullPath = path.resolve(rootPath, filePath);
750
- await fs.mkdir(path.dirname(fullPath), { recursive: true });
909
+ const fullPath = resolveProofContractPath(rootPath, filePath);
910
+ await prepareProofArtifactWritePath(rootPath, fullPath);
751
911
  await fs.writeFile(fullPath, `${JSON.stringify(contract, null, 2)}\n`, 'utf-8');
752
912
  return filePath;
753
913
  }
914
+ function resolveProofContractPath(rootPath, filePath) {
915
+ const root = path.resolve(rootPath);
916
+ const requested = filePath.trim();
917
+ if (!requested)
918
+ throw new Error('Proof Contract path is required.');
919
+ const fullPath = path.resolve(root, requested);
920
+ const relative = path.relative(root, fullPath);
921
+ if (!relative || relative.startsWith('..') || path.isAbsolute(relative)) {
922
+ throw new Error('Proof Contract path must stay inside the project root.');
923
+ }
924
+ const normalizedRelative = relative.split(path.sep).join('/');
925
+ if (normalizedRelative !== DEFAULT_CONTRACT_PATH &&
926
+ !/^\.projscan\/proof-contracts\/[^/]+\.json$/.test(normalizedRelative)) {
927
+ throw new Error('Proof Contract path must be .projscan/proof-contract.json or .projscan/proof-contracts/<name>.json.');
928
+ }
929
+ return fullPath;
930
+ }
754
931
  function forbiddenFilesFor(intent, allowed) {
755
932
  const intentLower = intent.toLowerCase();
756
933
  const allowedSet = new Set(allowed);
@@ -794,16 +971,26 @@ function readinessFor(input) {
794
971
  return hasReviewReceiptSignal(input) ? 'needs-review' : 'ready';
795
972
  }
796
973
  function hasBlockingReceiptSignal(input) {
797
- return input.forbiddenTouched.length > 0 || input.preflightVerdict === 'block' || input.proofStatus === 'failed';
974
+ return (input.forbiddenTouched.length > 0 ||
975
+ input.preflightVerdict === 'block' ||
976
+ input.proofStatus === 'failed' ||
977
+ input.proofSufficiencyStatus === 'failed');
798
978
  }
799
979
  function hasReviewReceiptSignal(input) {
800
980
  return (input.scopeStatus !== 'within-contract' ||
801
981
  isIncompleteProofStatus(input.proofStatus) ||
982
+ isReviewProofSufficiencyStatus(input.proofSufficiencyStatus) ||
802
983
  input.preflightVerdict === 'caution' ||
803
984
  input.evidenceGaps.length > 0);
804
985
  }
805
986
  function isIncompleteProofStatus(status) {
806
- return status === 'missing' || status === 'partial' || status === 'stale';
987
+ return status === 'not-run' || status === 'missing' || status === 'partial' || status === 'stale';
988
+ }
989
+ function isMissingProofStatus(status) {
990
+ return status === 'not-run' || status === 'missing' || status === 'partial';
991
+ }
992
+ function isReviewProofSufficiencyStatus(status) {
993
+ return status === 'missing' || status === 'stale' || status === 'weak';
807
994
  }
808
995
  function riskDeltaDirectionFor(riskDelta) {
809
996
  if (riskDelta.delta > 0)
@@ -813,21 +1000,28 @@ function riskDeltaDirectionFor(riskDelta) {
813
1000
  return 'flat';
814
1001
  }
815
1002
  function reviewerDecisionFor(input) {
816
- if (input.commitReadiness === 'blocked' || input.proofStatus === 'failed')
1003
+ if (input.commitReadiness === 'blocked' ||
1004
+ input.proofStatus === 'failed' ||
1005
+ input.proofSufficiencyStatus === 'failed')
817
1006
  return 'stop';
818
1007
  if (input.commitReadiness === 'ready' &&
819
1008
  input.proofStatus === 'passed' &&
1009
+ (input.proofSufficiencyStatus === 'strong' || input.proofSufficiencyStatus === 'adequate') &&
820
1010
  input.scope.status === 'within-contract' &&
821
1011
  input.preflightVerdict === 'proceed') {
822
1012
  return 'safe-to-review';
823
1013
  }
824
1014
  return 'needs-focused-review';
825
1015
  }
826
- function reviewerGuidanceFor(verdict, scope, reviewerDecision, proofStatus) {
1016
+ function reviewerGuidanceFor(verdict, scope, reviewerDecision, proofStatus, proofSufficiencyStatus) {
827
1017
  return firstMatchingGuidance([
828
1018
  [reviewerDecision === 'stop', 'Stop this proof slice until failed proof commands, forbidden files, or preflight blockers are cleared.'],
1019
+ [proofSufficiencyStatus === 'failed', 'Fix failed proof for the affected risk surface before review.'],
829
1020
  [proofStatus === 'stale', 'Rerun the required proof commands; the ledger evidence is stale after newer file changes.'],
1021
+ [proofSufficiencyStatus === 'stale', 'Rerun stale proof for the affected risk surface before review.'],
830
1022
  [isIncompleteProofStatus(proofStatus), 'Record fresh proof-command evidence before approval. Missing or partial proof should not be treated as reviewer-ready.'],
1023
+ [proofSufficiencyStatus === 'missing', 'Record proof for each changed risk surface before approval.'],
1024
+ [proofSufficiencyStatus === 'weak', 'Review weak proof mapping before approval; a command passed but did not prove the changed surface strongly.'],
831
1025
  [verdict === 'blocked', 'Do not approve until forbidden files or preflight blockers are removed from this proof slice.'],
832
1026
  [scope.unexpectedProduction.length > 0, 'Review the unexpected production files first. Either update the Proof Contract intentionally or split those edits out.'],
833
1027
  [hasSensitiveScopeDrift(scope), 'Require explicit reviewer sign-off for config or security-sensitive drift before approving.'],
@@ -1055,56 +1249,52 @@ function confidenceReasonForSimulation(confidence, simulationConfidence, trustMe
1055
1249
  function normalizeIntent(value) {
1056
1250
  return value?.trim().replace(/\s+/g, ' ') ?? '';
1057
1251
  }
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
1252
  function isLocalProofArtifactPath(file) {
1073
1253
  return file.startsWith('.projscan/');
1074
1254
  }
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
1255
  function pathMatches(file, pattern) {
1105
- if (pattern.endsWith('/**'))
1106
- return file.startsWith(pattern.slice(0, -3));
1107
- return file === pattern;
1256
+ const normalizedFile = normalizeRepoPath(file);
1257
+ const normalizedPattern = normalizeRepoPath(pattern);
1258
+ if (normalizedFile === normalizedPattern)
1259
+ return true;
1260
+ if (!normalizedPattern.includes('*'))
1261
+ return false;
1262
+ let regex = PATH_MATCH_REGEX_CACHE.get(normalizedPattern);
1263
+ if (!regex) {
1264
+ regex = globToRegExp(normalizedPattern);
1265
+ PATH_MATCH_REGEX_CACHE.set(normalizedPattern, regex);
1266
+ }
1267
+ return regex.test(normalizedFile);
1268
+ }
1269
+ function globToRegExp(pattern) {
1270
+ let source = '^';
1271
+ for (let index = 0; index < pattern.length; index += 1) {
1272
+ const char = pattern[index];
1273
+ if (char === '*') {
1274
+ if (pattern[index + 1] === '*') {
1275
+ if (pattern[index + 2] === '/') {
1276
+ source += '(?:.*/)?';
1277
+ index += 2;
1278
+ }
1279
+ else {
1280
+ source += '.*';
1281
+ index += 1;
1282
+ }
1283
+ }
1284
+ else {
1285
+ source += '[^/]*';
1286
+ }
1287
+ continue;
1288
+ }
1289
+ source += escapeRegExp(char);
1290
+ }
1291
+ return new RegExp(`${source}$`);
1292
+ }
1293
+ function escapeRegExp(value) {
1294
+ return value.replace(/[|\\{}()[\]^$+?.]/g, '\\$&');
1295
+ }
1296
+ function normalizeRepoPath(value) {
1297
+ return value.split(path.sep).join('/').replace(/^\.\//, '');
1108
1298
  }
1109
1299
  function unique(values) {
1110
1300
  return [...new Set(values)];