projscan 4.14.0 → 4.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +71 -21
- package/dist/cli/commands/evidencePack.js +2 -0
- package/dist/cli/commands/evidencePack.js.map +1 -1
- package/dist/cli/commands/prove.js +172 -23
- package/dist/cli/commands/prove.js.map +1 -1
- package/dist/cli/commands/startConsole.d.ts +2 -2
- package/dist/cli/commands/startConsole.js +2 -260
- package/dist/cli/commands/startConsole.js.map +1 -1
- package/dist/cli/commands/startConsoleExecution.d.ts +5 -0
- package/dist/cli/commands/startConsoleExecution.js +108 -0
- package/dist/cli/commands/startConsoleExecution.js.map +1 -0
- package/dist/cli/commands/startConsoleMission.d.ts +6 -0
- package/dist/cli/commands/startConsoleMission.js +157 -0
- package/dist/cli/commands/startConsoleMission.js.map +1 -0
- package/dist/cli/commands/startMissionBundle.js +24 -27
- package/dist/cli/commands/startMissionBundle.js.map +1 -1
- package/dist/core/adoption.d.ts +8 -81
- package/dist/core/adoption.js +4 -549
- package/dist/core/adoption.js.map +1 -1
- package/dist/core/adoptionFirstRunDiagnostics.d.ts +20 -0
- package/dist/core/adoptionFirstRunDiagnostics.js +240 -0
- package/dist/core/adoptionFirstRunDiagnostics.js.map +1 -0
- package/dist/core/adoptionMcpConfig.d.ts +27 -0
- package/dist/core/adoptionMcpConfig.js +123 -0
- package/dist/core/adoptionMcpConfig.js.map +1 -0
- package/dist/core/adoptionMcpDoctor.d.ts +23 -0
- package/dist/core/adoptionMcpDoctor.js +87 -0
- package/dist/core/adoptionMcpDoctor.js.map +1 -0
- package/dist/core/adoptionWorkflowRecipes.d.ts +14 -0
- package/dist/core/adoptionWorkflowRecipes.js +110 -0
- package/dist/core/adoptionWorkflowRecipes.js.map +1 -0
- package/dist/core/bugHunt.js +26 -255
- package/dist/core/bugHunt.js.map +1 -1
- package/dist/core/bugHuntPreflightFindings.d.ts +2 -1
- package/dist/core/bugHuntPreflightFindings.js +20 -0
- package/dist/core/bugHuntPreflightFindings.js.map +1 -1
- package/dist/core/bugHuntReportAssembly.d.ts +20 -0
- package/dist/core/bugHuntReportAssembly.js +179 -0
- package/dist/core/bugHuntReportAssembly.js.map +1 -0
- package/dist/core/bugHuntSourceFindings.d.ts +3 -0
- package/dist/core/bugHuntSourceFindings.js +61 -0
- package/dist/core/bugHuntSourceFindings.js.map +1 -0
- package/dist/core/dogfood.js +4 -393
- package/dist/core/dogfood.js.map +1 -1
- package/dist/core/dogfoodMarketValidation.d.ts +5 -0
- package/dist/core/dogfoodMarketValidation.js +265 -0
- package/dist/core/dogfoodMarketValidation.js.map +1 -0
- package/dist/core/dogfoodRepoEvaluation.d.ts +4 -0
- package/dist/core/dogfoodRepoEvaluation.js +137 -0
- package/dist/core/dogfoodRepoEvaluation.js.map +1 -0
- package/dist/core/evidenceComment.js +50 -13
- package/dist/core/evidenceComment.js.map +1 -1
- package/dist/core/feedback.js +2 -252
- package/dist/core/feedback.js.map +1 -1
- package/dist/core/feedbackIntakeClassifier.d.ts +2 -0
- package/dist/core/feedbackIntakeClassifier.js +255 -0
- package/dist/core/feedbackIntakeClassifier.js.map +1 -0
- package/dist/core/intentRouterKeywordToolGuards.js +1 -55
- package/dist/core/intentRouterKeywordToolGuards.js.map +1 -1
- package/dist/core/intentRouterKeywordWeights.js +13 -28
- package/dist/core/intentRouterKeywordWeights.js.map +1 -1
- package/dist/core/intentRouterProductGuardSignals.d.ts +3 -0
- package/dist/core/intentRouterProductGuardSignals.js +59 -0
- package/dist/core/intentRouterProductGuardSignals.js.map +1 -0
- package/dist/core/markdownSafety.d.ts +3 -0
- package/dist/core/markdownSafety.js +14 -0
- package/dist/core/markdownSafety.js.map +1 -0
- package/dist/core/preflight.d.ts +2 -0
- package/dist/core/preflight.js.map +1 -1
- package/dist/core/preflightChangedFiles.d.ts +2 -0
- package/dist/core/preflightChangedFiles.js +1 -1
- package/dist/core/preflightChangedFiles.js.map +1 -1
- package/dist/core/preflightInputs.d.ts +2 -0
- package/dist/core/preflightInputs.js +5 -2
- package/dist/core/preflightInputs.js.map +1 -1
- package/dist/core/proofLedger.d.ts +5 -1
- package/dist/core/proofLedger.js +161 -12
- package/dist/core/proofLedger.js.map +1 -1
- package/dist/core/proofReplay.d.ts +9 -0
- package/dist/core/proofReplay.js +164 -0
- package/dist/core/proofReplay.js.map +1 -0
- package/dist/core/proofSufficiency.d.ts +19 -0
- package/dist/core/proofSufficiency.js +425 -0
- package/dist/core/proofSufficiency.js.map +1 -0
- package/dist/core/prove.d.ts +6 -0
- package/dist/core/prove.js +277 -87
- package/dist/core/prove.js.map +1 -1
- package/dist/core/qualityScorecard.js +8 -238
- package/dist/core/qualityScorecard.js.map +1 -1
- package/dist/core/qualityScorecardDimensions.d.ts +14 -0
- package/dist/core/qualityScorecardDimensions.js +99 -0
- package/dist/core/qualityScorecardDimensions.js.map +1 -0
- package/dist/core/qualityScorecardRisks.d.ts +8 -0
- package/dist/core/qualityScorecardRisks.js +107 -0
- package/dist/core/qualityScorecardRisks.js.map +1 -0
- package/dist/core/qualityScorecardSignals.d.ts +20 -0
- package/dist/core/qualityScorecardSignals.js +59 -0
- package/dist/core/qualityScorecardSignals.js.map +1 -0
- package/dist/core/releaseEvidence.d.ts +1 -0
- package/dist/core/releaseEvidence.js +15 -40
- package/dist/core/releaseEvidence.js.map +1 -1
- package/dist/core/releaseEvidenceBaseline.js +4 -1
- package/dist/core/releaseEvidenceBaseline.js.map +1 -1
- package/dist/core/releaseEvidenceProofReceipt.d.ts +6 -0
- package/dist/core/releaseEvidenceProofReceipt.js +140 -0
- package/dist/core/releaseEvidenceProofReceipt.js.map +1 -0
- package/dist/core/releaseEvidenceVerdict.d.ts +5 -2
- package/dist/core/releaseEvidenceVerdict.js +39 -1
- package/dist/core/releaseEvidenceVerdict.js.map +1 -1
- package/dist/core/repositoryScanner.d.ts +1 -0
- package/dist/core/repositoryScanner.js +5 -4
- package/dist/core/repositoryScanner.js.map +1 -1
- package/dist/core/sessionResources.d.ts +14 -2
- package/dist/core/sessionResources.js +3 -3
- package/dist/core/sessionResources.js.map +1 -1
- package/dist/core/startInputs.d.ts +1 -1
- package/dist/core/startIntentTargets.d.ts +1 -1
- package/dist/core/startIntentTargets.js +1 -16
- package/dist/core/startIntentTargets.js.map +1 -1
- package/dist/core/startMissionInputStatusPolicy.d.ts +7 -0
- package/dist/core/startMissionInputStatusPolicy.js +74 -0
- package/dist/core/startMissionInputStatusPolicy.js.map +1 -0
- package/dist/core/startMissionPolicy.d.ts +6 -15
- package/dist/core/startMissionPolicy.js +4 -305
- package/dist/core/startMissionPolicy.js.map +1 -1
- package/dist/core/startMissionProofPolicy.d.ts +6 -0
- package/dist/core/startMissionProofPolicy.js +84 -0
- package/dist/core/startMissionProofPolicy.js.map +1 -0
- package/dist/core/startMissionRiskPolicy.d.ts +4 -0
- package/dist/core/startMissionRiskPolicy.js +85 -0
- package/dist/core/startMissionRiskPolicy.js.map +1 -0
- package/dist/core/startMissionRoutingPolicy.d.ts +6 -0
- package/dist/core/startMissionRoutingPolicy.js +67 -0
- package/dist/core/startMissionRoutingPolicy.js.map +1 -0
- package/dist/core/startMode.d.ts +1 -2
- package/dist/core/startMode.js +4 -151
- package/dist/core/startMode.js.map +1 -1
- package/dist/core/startModeIntentPolicy.d.ts +12 -0
- package/dist/core/startModeIntentPolicy.js +41 -0
- package/dist/core/startModeIntentPolicy.js.map +1 -0
- package/dist/core/startModeRoutingPolicy.d.ts +4 -0
- package/dist/core/startModeRoutingPolicy.js +117 -0
- package/dist/core/startModeRoutingPolicy.js.map +1 -0
- package/dist/core/startSearchQueryTargets.d.ts +1 -0
- package/dist/core/startSearchQueryTargets.js +17 -0
- package/dist/core/startSearchQueryTargets.js.map +1 -0
- package/dist/core/workplan.d.ts +3 -2
- package/dist/core/workplan.js +11 -585
- package/dist/core/workplan.js.map +1 -1
- package/dist/core/workplanCoordinationTasks.d.ts +3 -0
- package/dist/core/workplanCoordinationTasks.js +82 -0
- package/dist/core/workplanCoordinationTasks.js.map +1 -0
- package/dist/core/workplanModeTasks.d.ts +2 -0
- package/dist/core/workplanModeTasks.js +192 -0
- package/dist/core/workplanModeTasks.js.map +1 -0
- package/dist/core/workplanPreflightTasks.d.ts +2 -0
- package/dist/core/workplanPreflightTasks.js +126 -0
- package/dist/core/workplanPreflightTasks.js.map +1 -0
- package/dist/core/workplanQualitySignals.d.ts +7 -0
- package/dist/core/workplanQualitySignals.js +63 -0
- package/dist/core/workplanQualitySignals.js.map +1 -0
- package/dist/core/workplanReport.d.ts +4 -0
- package/dist/core/workplanReport.js +79 -0
- package/dist/core/workplanReport.js.map +1 -0
- package/dist/core/workplanRiskOwnership.d.ts +5 -0
- package/dist/core/workplanRiskOwnership.js +97 -0
- package/dist/core/workplanRiskOwnership.js.map +1 -0
- package/dist/core/workplanSuggestedActions.d.ts +2 -0
- package/dist/core/workplanSuggestedActions.js +43 -0
- package/dist/core/workplanSuggestedActions.js.map +1 -0
- package/dist/mcp/tools/prove.js +23 -17
- package/dist/mcp/tools/prove.js.map +1 -1
- package/dist/projscan-sbom.cdx.json +6 -6
- package/dist/tool-manifest.json +2 -2
- package/dist/types/config.d.ts +15 -0
- package/dist/types/evidencePack.d.ts +21 -0
- package/dist/types/prove.d.ts +79 -0
- package/dist/utils/changedFiles.js +57 -16
- package/dist/utils/changedFiles.js.map +1 -1
- package/dist/utils/config.js +2 -0
- package/dist/utils/config.js.map +1 -1
- package/dist/utils/configProofRecipes.d.ts +2 -0
- package/dist/utils/configProofRecipes.js +91 -0
- package/dist/utils/configProofRecipes.js.map +1 -0
- package/docs/GUIDE.md +120 -19
- package/package.json +1 -1
package/dist/core/prove.js
CHANGED
|
@@ -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,
|
|
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
|
|
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({
|
|
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
|
|
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
|
|
425
|
-
|
|
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
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
];
|
|
482
|
-
const
|
|
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
|
|
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
|
|
722
|
+
const latestLedgerByCommand = latestProofRecordsByCommand(ledger);
|
|
602
723
|
const commandEvidence = proofCommands.map((command) => {
|
|
603
|
-
const record =
|
|
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:
|
|
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
|
|
678
|
-
return
|
|
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:
|
|
705
|
-
allowedProduction:
|
|
706
|
-
expectedTest:
|
|
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 =
|
|
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 =
|
|
750
|
-
await
|
|
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 ||
|
|
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' ||
|
|
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
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
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)];
|