@webpieces/pr-gate 0.4.654 → 0.4.655
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/package.json +2 -2
- package/src/scripts/commands/review-upsert-pr-command.d.ts +12 -1
- package/src/scripts/commands/review-upsert-pr-command.js +18 -1
- package/src/scripts/commands/review-upsert-pr-command.js.map +1 -1
- package/src/scripts/workflow/active-hatches.d.ts +58 -0
- package/src/scripts/workflow/active-hatches.js +130 -0
- package/src/scripts/workflow/active-hatches.js.map +1 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webpieces/pr-gate",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.655",
|
|
4
4
|
"description": "Gated PR system: 3-point squash-merge, merge validation gate, and red/yellow/green PR dashboard. Standalone scripts, no Nx dependency required.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"directory": "packages/tooling/pr-gate"
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@webpieces/rules-config": "0.4.
|
|
18
|
+
"@webpieces/rules-config": "0.4.655",
|
|
19
19
|
"@inversifyjs/binding-decorators": "1.1.5",
|
|
20
20
|
"inversify": "7.10.4",
|
|
21
21
|
"reflect-metadata": "0.2.2"
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { RepoRootFinder, ReviewerInstructionsService, ReviewJsonService } from '@webpieces/rules-config';
|
|
2
|
+
import { ActiveHatchReport } from '../workflow/active-hatches';
|
|
2
3
|
import { AiBranchName } from '../workflow/git-readAiBranchName';
|
|
3
4
|
import { BuildAffected } from '../workflow/build-affected';
|
|
4
5
|
import { BuildArtifactGate } from '../workflow/build-artifact-gate';
|
|
@@ -60,9 +61,19 @@ export declare class ReviewUpsertPrCommand {
|
|
|
60
61
|
private readonly briefingBuilder;
|
|
61
62
|
private readonly reviewerInstructions;
|
|
62
63
|
private readonly receipts;
|
|
64
|
+
private readonly activeHatchReport;
|
|
63
65
|
private readonly reviewJsonService;
|
|
64
|
-
constructor(repoRootFinder: RepoRootFinder, aiBranchName: AiBranchName, gitExec: GitExec, buildAffected: BuildAffected, buildArtifactGate: BuildArtifactGate, mergeState: MergeState, mergeEnd: MergeEnd, checklistScanner: ChecklistScanner, reviewReport: ReviewReport, materializer: DiffMaterializer, prContextWriter: PrContextWriter, briefingBuilder: ReviewerBriefingBuilder, reviewerInstructions: ReviewerInstructionsService, receipts: ReviewStageReceiptService, reviewJsonService: ReviewJsonService);
|
|
66
|
+
constructor(repoRootFinder: RepoRootFinder, aiBranchName: AiBranchName, gitExec: GitExec, buildAffected: BuildAffected, buildArtifactGate: BuildArtifactGate, mergeState: MergeState, mergeEnd: MergeEnd, checklistScanner: ChecklistScanner, reviewReport: ReviewReport, materializer: DiffMaterializer, prContextWriter: PrContextWriter, briefingBuilder: ReviewerBriefingBuilder, reviewerInstructions: ReviewerInstructionsService, receipts: ReviewStageReceiptService, activeHatchReport: ActiveHatchReport, reviewJsonService: ReviewJsonService);
|
|
65
67
|
run(opts?: ReviewUpsertPrOptions): Promise<void>;
|
|
68
|
+
/**
|
|
69
|
+
* List every rule that is currently switched off in webpieces.config.json. NON-BLOCKING and printed
|
|
70
|
+
* before the instruction block, because it is context for the review rather than a step in it.
|
|
71
|
+
*
|
|
72
|
+
* A hatch is the one config change that makes the build QUIETER, so no gate, reviewer or red check
|
|
73
|
+
* will ever mention it — which is how a repo ends up with rules off for weeks that nobody chose to
|
|
74
|
+
* leave off. This is the only place in the flow that says so out loud.
|
|
75
|
+
*/
|
|
76
|
+
private reportActiveHatches;
|
|
66
77
|
/**
|
|
67
78
|
* Validate + commit + finalize a conflict resolution, if one is in flight. MOVED here from
|
|
68
79
|
* `wp-finish-upsert-pr` so that exactly one command owns merge finalization — two owners is the drift
|
|
@@ -5,6 +5,7 @@ const tslib_1 = require("tslib");
|
|
|
5
5
|
const fs = tslib_1.__importStar(require("fs"));
|
|
6
6
|
const rules_config_1 = require("@webpieces/rules-config");
|
|
7
7
|
const inversify_1 = require("inversify");
|
|
8
|
+
const active_hatches_1 = require("../workflow/active-hatches");
|
|
8
9
|
const git_readAiBranchName_1 = require("../workflow/git-readAiBranchName");
|
|
9
10
|
const build_affected_1 = require("../workflow/build-affected");
|
|
10
11
|
const build_gate_log_1 = require("../workflow/build-gate-log");
|
|
@@ -72,9 +73,10 @@ let ReviewUpsertPrCommand = class ReviewUpsertPrCommand {
|
|
|
72
73
|
briefingBuilder;
|
|
73
74
|
reviewerInstructions;
|
|
74
75
|
receipts;
|
|
76
|
+
activeHatchReport;
|
|
75
77
|
reviewJsonService;
|
|
76
78
|
// eslint-disable-next-line @typescript-eslint/max-params
|
|
77
|
-
constructor(repoRootFinder, aiBranchName, gitExec, buildAffected, buildArtifactGate, mergeState, mergeEnd, checklistScanner, reviewReport, materializer, prContextWriter, briefingBuilder, reviewerInstructions, receipts,
|
|
79
|
+
constructor(repoRootFinder, aiBranchName, gitExec, buildAffected, buildArtifactGate, mergeState, mergeEnd, checklistScanner, reviewReport, materializer, prContextWriter, briefingBuilder, reviewerInstructions, receipts, activeHatchReport,
|
|
78
80
|
// Injected only to RESOLVE + RENDER refusals (see refusals()). This stage never archives a verdict.
|
|
79
81
|
reviewJsonService) {
|
|
80
82
|
this.repoRootFinder = repoRootFinder;
|
|
@@ -91,6 +93,7 @@ let ReviewUpsertPrCommand = class ReviewUpsertPrCommand {
|
|
|
91
93
|
this.briefingBuilder = briefingBuilder;
|
|
92
94
|
this.reviewerInstructions = reviewerInstructions;
|
|
93
95
|
this.receipts = receipts;
|
|
96
|
+
this.activeHatchReport = activeHatchReport;
|
|
94
97
|
this.reviewJsonService = reviewJsonService;
|
|
95
98
|
}
|
|
96
99
|
async run(opts = new ReviewUpsertPrOptions()) {
|
|
@@ -105,8 +108,21 @@ let ReviewUpsertPrCommand = class ReviewUpsertPrCommand {
|
|
|
105
108
|
const scan = this.checklistScanner.scan(repoRoot, config.checklists, new checklist_scanner_1.ChecklistScanOptions(false, '')); // '' — THIS command writes the context itself, after materializing
|
|
106
109
|
const briefings = this.briefReviewers(repoRoot, featureName, scan, config);
|
|
107
110
|
this.receipts.write(repoRoot, featureName, new review_stage_receipt_1.ReviewStageReceipt(scan.basis.headSha, mergeValidated, this.buildAffected.resolveBuildCommand(repoRoot), buildPassedAt, briefings.map((b) => b.subagent)));
|
|
111
|
+
this.reportActiveHatches(repoRoot);
|
|
108
112
|
this.report(repoRoot, featureName, scan, briefings, opts);
|
|
109
113
|
}
|
|
114
|
+
/**
|
|
115
|
+
* List every rule that is currently switched off in webpieces.config.json. NON-BLOCKING and printed
|
|
116
|
+
* before the instruction block, because it is context for the review rather than a step in it.
|
|
117
|
+
*
|
|
118
|
+
* A hatch is the one config change that makes the build QUIETER, so no gate, reviewer or red check
|
|
119
|
+
* will ever mention it — which is how a repo ends up with rules off for weeks that nobody chose to
|
|
120
|
+
* leave off. This is the only place in the flow that says so out loud.
|
|
121
|
+
*/
|
|
122
|
+
reportActiveHatches(repoRoot) {
|
|
123
|
+
const hatches = this.activeHatchReport.scan(repoRoot);
|
|
124
|
+
process.stdout.write(this.activeHatchReport.render(hatches));
|
|
125
|
+
}
|
|
110
126
|
/**
|
|
111
127
|
* Validate + commit + finalize a conflict resolution, if one is in flight. MOVED here from
|
|
112
128
|
* `wp-finish-upsert-pr` so that exactly one command owns merge finalization — two owners is the drift
|
|
@@ -225,6 +241,7 @@ exports.ReviewUpsertPrCommand = ReviewUpsertPrCommand = tslib_1.__decorate([
|
|
|
225
241
|
reviewer_briefing_builder_1.ReviewerBriefingBuilder,
|
|
226
242
|
rules_config_1.ReviewerInstructionsService,
|
|
227
243
|
review_stage_receipt_1.ReviewStageReceiptService,
|
|
244
|
+
active_hatches_1.ActiveHatchReport,
|
|
228
245
|
rules_config_1.ReviewJsonService])
|
|
229
246
|
], ReviewUpsertPrCommand);
|
|
230
247
|
//# sourceMappingURL=review-upsert-pr-command.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"review-upsert-pr-command.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/commands/review-upsert-pr-command.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport {\n loadAndValidate, writeTemplate, PrGateConfig, RepoRootFinder, RequiredChecklist,\n ReviewerBriefing, ReviewerInstructionsService, ReviewJsonService,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { AiBranchName } from '../workflow/git-readAiBranchName';\nimport { BuildAffected, BuildGateOptions } from '../workflow/build-affected';\nimport { REVIEW_STAGE } from '../workflow/build-gate-log';\nimport { BuildArtifactGate } from '../workflow/build-artifact-gate';\nimport { ChecklistScan, ChecklistScanOptions, ChecklistScanner } from '../workflow/checklist-scanner';\nimport { DiffMaterializer } from '../workflow/diff-materializer';\nimport { GitExec } from '../workflow/git-exec';\nimport { MergeContext } from '../workflow/merge-start';\nimport { MergeEnd, MergeEndOptions } from '../workflow/merge-end';\nimport { MergeState } from '../workflow/merge-state';\nimport { PrContextWriter } from '../workflow/pr-context-writer';\nimport { ReviewerBriefingBuilder } from '../workflow/reviewer-briefing-builder';\nimport { RefusedReviewer, ReviewReport, ReviewReportInput } from '../workflow/review-report';\nimport { ReviewStageReceipt, ReviewStageReceiptService } from '../workflow/review-stage-receipt';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n/** The `--no-optional` decision, as data (per CLAUDE.md) rather than a bare boolean parameter. */\nexport class ReviewUpsertPrOptions {\n /**\n * true ⇒ the human ALREADY said to submit without the optional reviews, so stage ② must not print the\n * block telling the AI to offer them. It changes nothing about what BLOCKS: required checklists are\n * unaffected, and an optional checklist that already has a red verdict on this branch still refuses the\n * PR at finish. This only suppresses the offer.\n */\n skipOptional: boolean;\n\n constructor(skipOptional = false) {\n this.skipOptional = skipOptional;\n }\n}\n\n/**\n * `wp-review-upsert-pr` — STAGE ② of the three-stage PR flow, between `wp-start-upsert-pr` (sync from main)\n * and `wp-finish-upsert-pr` (post the PR).\n *\n * It exists because reviewing came before verifying. Under the old two-command flow the 3-point merge was\n * finalized and the build gate ran inside `wp-finish-upsert-pr` — i.e. AFTER the reviewers had already run.\n * A reviewer could therefore spend a full review on an unresolved merge, or on code that does not compile,\n * and only afterwards would the branch be checked. This command puts the verification first:\n *\n * 1. finalize + VALIDATE any in-progress 3-point merge (moved here from finish)\n * 2. assert a clean tree (meaningful now — step 1 committed the resolution)\n * 3. run the build gate (on the merged, committed tree)\n * 4. scan the checklists, EXTRACT the diff, BRIEF the reviewers\n * 5. write the stage receipt, then print what to spawn + the review.json schema\n *\n * Unlike the report-only command it replaces — which always exited 0, because reporting is not gating — this\n * command CAN FAIL, at steps 1, 2 and 3. That is the point: it fails BEFORE any reviewer is spawned, so a\n * branch with an unresolved merge or a broken build costs zero reviewer tokens.\n *\n * The ordering of 1 → 2 → 3 is load-bearing. MergeEnd stages and commits the conflict resolution, so the\n * tree is clean by the time the build runs; building first would either build a dirty tree (proving nothing\n * about what ships) or refuse a tree that is legitimately mid-resolution.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class ReviewUpsertPrCommand {\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(\n private readonly repoRootFinder: RepoRootFinder,\n private readonly aiBranchName: AiBranchName,\n private readonly gitExec: GitExec,\n private readonly buildAffected: BuildAffected,\n private readonly buildArtifactGate: BuildArtifactGate,\n private readonly mergeState: MergeState,\n private readonly mergeEnd: MergeEnd,\n private readonly checklistScanner: ChecklistScanner,\n private readonly reviewReport: ReviewReport,\n private readonly materializer: DiffMaterializer,\n private readonly prContextWriter: PrContextWriter,\n private readonly briefingBuilder: ReviewerBriefingBuilder,\n private readonly reviewerInstructions: ReviewerInstructionsService,\n private readonly receipts: ReviewStageReceiptService,\n // Injected only to RESOLVE + RENDER refusals (see refusals()). This stage never archives a verdict.\n private readonly reviewJsonService: ReviewJsonService,\n ) {}\n\n async run(opts: ReviewUpsertPrOptions = new ReviewUpsertPrOptions()): Promise<void> {\n const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());\n writeTemplate(repoRoot, 'webpieces.git-workflow.md');\n writeTemplate(repoRoot, 'webpieces.review-checklists.md');\n const featureName = this.aiBranchName.getFeatureName();\n const config = loadAndValidate(repoRoot).prGate;\n\n const mergeValidated = await this.finalizeAnyInProgressMerge(repoRoot);\n this.gitExec.assertCleanTree(repoRoot);\n const buildPassedAt = this.runBuildGate(repoRoot);\n\n const scan = this.checklistScanner.scan(repoRoot, config.checklists, new ChecklistScanOptions(false, '')); // '' — THIS command writes the context itself, after materializing\n const briefings = this.briefReviewers(repoRoot, featureName, scan, config);\n this.receipts.write(repoRoot, featureName, new ReviewStageReceipt(\n scan.basis.headSha, mergeValidated, this.buildAffected.resolveBuildCommand(repoRoot), buildPassedAt,\n briefings.map((b: ReviewerBriefing): string => b.subagent),\n ));\n this.report(repoRoot, featureName, scan, briefings, opts);\n }\n\n /**\n * Validate + commit + finalize a conflict resolution, if one is in flight. MOVED here from\n * `wp-finish-upsert-pr` so that exactly one command owns merge finalization — two owners is the drift\n * failure `PrContextWriter`'s own docstring records having already happened once in this codebase.\n *\n * MergeEnd's three checks are what \"did the AI do the 3-point merge right?\" means concretely: no\n * conflict markers left in the scoped files, `git diff --diff-filter=U` empty, and a non-empty\n * merge-explanation.md beside every conflicted file.\n *\n * Returns true when a merge was finalized OR there was none to finalize; it throws rather than returning\n * false when validation fails, so the receipt can never record an unvalidated merge as validated.\n */\n private async finalizeAnyInProgressMerge(repoRoot: string): Promise<boolean> {\n const home = this.mergeState.mergeDirFor(repoRoot, this.aiBranchName.getFeatureName());\n const activeDir = this.mergeState.findActiveMergeRunDir(home);\n const marker = activeDir ? this.mergeState.readMergeMarker(activeDir) : null;\n if (!activeDir || !marker || marker.validated) return true;\n process.stdout.write('\\n' + SEP + '① Validating your 3-point merge\\n' + SEP + '\\n');\n // pushRemote:false — finish still owns the single push.\n await this.mergeEnd.mergeEnd(\n repoRoot, 'wp-review-upsert-pr', activeDir,\n new MergeContext(marker.currentBranch, marker.squashBranch, marker.backupBranch, marker.prNumber),\n new MergeEndOptions(marker.conflictedFiles, false),\n );\n return true;\n }\n\n /**\n * The build gate, run BEFORE any reviewer is briefed. Returns the ISO time it passed, for the receipt —\n * which is what lets `wp-finish-upsert-pr` skip its own gate when HEAD has not moved, so moving the gate\n * earlier did not turn one build into two.\n */\n private runBuildGate(repoRoot: string): string {\n this.buildAffected.runBuildGate(repoRoot, new BuildGateOptions(\n '🛠️ Build gate',\n 'pnpm wp-review-upsert-pr',\n 'Build failed — NO reviewer was briefed and no diff was extracted. Fix it, then re-run.',\n REVIEW_STAGE,\n ));\n // Repo-wide: the build must not have left anything uncommitted AND unstaged. Runs HERE, not in\n // finish, because this is the stage that ran buildCommand and is therefore holding the dirty\n // tree at the exact moment the question is answerable. Replaces the per-project\n // validate-di-graph-unchanged nx target — see BuildArtifactGate for why that one had to go.\n this.buildArtifactGate.assertBuildLeftNothingUncommitted(repoRoot);\n return new Date().toISOString();\n }\n\n /**\n * Extract the diff once, then write one instructions file per reviewer.\n *\n * The changed-file set and the basis both come off the SCAN rather than being recomputed, so the diff a\n * reviewer reads covers exactly the files its checklist matched against.\n */\n private briefReviewers(repoRoot: string, featureName: string, scan: ChecklistScan, config: PrGateConfig): ReviewerBriefing[] {\n if (scan.basis.unresolved) return [];\n const manifest = this.materializer.materialize(\n repoRoot, featureName, scan.basis, scan.changedFiles, config.reviewDiffExclude);\n const diffDir = this.materializer.diffDirFor(repoRoot, featureName);\n // Write pr-context.json ONCE, here — after materializing, so `diffDir` is populated on the first\n // write. The scan deliberately did not write it (ChecklistScanOptions.contextStage === ''); it used\n // to, which meant this command wrote the same file twice, the first time with an empty diffDir.\n // `scan.changedFiles` is passed through so the context is not recomputed from a second git call.\n scan.context = this.prContextWriter.ensure(\n repoRoot, featureName, scan.basis, 'stage2-review', scan.changedFiles, diffDir);\n const briefings = this.briefingBuilder.build(repoRoot, scan, manifest, diffDir, config);\n const dir = this.reviewerInstructions.instructionsDirFor(repoRoot, featureName);\n fs.rmSync(dir, { recursive: true, force: true }); // stale instructions read as current are worse than none\n fs.mkdirSync(dir, { recursive: true });\n for (const b of briefings) {\n fs.writeFileSync(this.reviewerInstructions.pathFor(repoRoot, featureName, b.subagent),\n this.reviewerInstructions.render(b));\n }\n return briefings;\n }\n\n /**\n * What the AI must do next — rendered by `ReviewReport`, which owns the ordering.\n *\n * The rendering moved OUT of this command so the ordering could be asserted on as a string. It had a\n * real bug: the zero-checklist notice signed off with \"Carry on and run: pnpm wp-finish-upsert-pr\"\n * while the block right beneath it said to write review.json first. An agent that reads top to bottom\n * obeyed the first line and posted a PR with no review at all.\n */\n // eslint-disable-next-line @typescript-eslint/max-params\n private report(\n repoRoot: string, featureName: string, scan: ChecklistScan, briefings: readonly ReviewerBriefing[],\n opts: ReviewUpsertPrOptions,\n ): void {\n const input = new ReviewReportInput(repoRoot, featureName, scan.reviewPath);\n input.definedCount = scan.defined.length;\n input.applicableCount = scan.applicable.length;\n input.reviewed = scan.reviewed.slice();\n input.formatErrors = scan.formatErrors.slice();\n input.briefings = briefings.slice();\n input.refused = this.refusals(scan);\n input.skipOptional = opts.skipOptional;\n process.stdout.write(this.reviewReport.render(input));\n }\n\n /**\n * The applicable checklists that ALREADY ANSWERED and refused, each with its refusal rendered by the ONE\n * renderer `wp-finish-upsert-pr` also uses.\n *\n * Stage ② had the identical defect finish did: a refused checklist has no passing verdict, so it is not\n * in `scan.reviewed`, so it was printed as an ordinary owed reviewer and re-instructed word for word —\n * and an agent that obeys spawns it against unchanged code, gets the same refusal, and loops one stage\n * earlier than the loop that was reported.\n *\n * NO archive path is passed, deliberately, and this stage moves nothing. Retiring a verdict is finish's\n * act on the refusal it is actually enforcing; doing it here would delete the live verdict of a branch\n * that has not even been asked to finish yet, and `refusalError`'s un-archived wording — \"set an override\n * in review-<id>.json\" — is only correct while that file is still there, which here it is.\n */\n private refusals(scan: ChecklistScan): RefusedReviewer[] {\n const refused = this.reviewJsonService.refusedChecklists(scan.applicable, scan.results);\n return refused.map((req: RequiredChecklist): RefusedReviewer => new RefusedReviewer(\n req.id, this.reviewJsonService.refusalError(req, this.reviewJsonService.resolveVerdict(req, scan.results))));\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"review-upsert-pr-command.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/commands/review-upsert-pr-command.ts"],"names":[],"mappings":";;;;AAAA,+CAAyB;AACzB,0DAGiC;AACjC,yCAA2D;AAC3D,+DAA4E;AAC5E,2EAAgE;AAChE,+DAA6E;AAC7E,+DAA0D;AAC1D,yEAAoE;AACpE,qEAAsG;AACtG,qEAAiE;AACjE,mDAA+C;AAC/C,yDAAuD;AACvD,qDAAkE;AAClE,yDAAqD;AACrD,qEAAgE;AAChE,qFAAgF;AAChF,6DAA6F;AAC7F,2EAAiG;AAEjG,MAAM,GAAG,GAAG,0DAA0D,CAAC;AAEvE,kGAAkG;AAClG,MAAa,qBAAqB;IAC9B;;;;;OAKG;IACH,YAAY,CAAU;IAEtB,YAAY,YAAY,GAAG,KAAK;QAC5B,IAAI,CAAC,YAAY,GAAG,YAAY,CAAC;IACrC,CAAC;CACJ;AAZD,sDAYC;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEI,IAAM,qBAAqB,GAA3B,MAAM,qBAAqB;IAGT;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IAEA;IAlBrB,yDAAyD;IACzD,YACqB,cAA8B,EAC9B,YAA0B,EAC1B,OAAgB,EAChB,aAA4B,EAC5B,iBAAoC,EACpC,UAAsB,EACtB,QAAkB,EAClB,gBAAkC,EAClC,YAA0B,EAC1B,YAA8B,EAC9B,eAAgC,EAChC,eAAwC,EACxC,oBAAiD,EACjD,QAAmC,EACnC,iBAAoC;IACrD,oGAAoG;IACnF,iBAAoC;QAhBpC,mBAAc,GAAd,cAAc,CAAgB;QAC9B,iBAAY,GAAZ,YAAY,CAAc;QAC1B,YAAO,GAAP,OAAO,CAAS;QAChB,kBAAa,GAAb,aAAa,CAAe;QAC5B,sBAAiB,GAAjB,iBAAiB,CAAmB;QACpC,eAAU,GAAV,UAAU,CAAY;QACtB,aAAQ,GAAR,QAAQ,CAAU;QAClB,qBAAgB,GAAhB,gBAAgB,CAAkB;QAClC,iBAAY,GAAZ,YAAY,CAAc;QAC1B,iBAAY,GAAZ,YAAY,CAAkB;QAC9B,oBAAe,GAAf,eAAe,CAAiB;QAChC,oBAAe,GAAf,eAAe,CAAyB;QACxC,yBAAoB,GAApB,oBAAoB,CAA6B;QACjD,aAAQ,GAAR,QAAQ,CAA2B;QACnC,sBAAiB,GAAjB,iBAAiB,CAAmB;QAEpC,sBAAiB,GAAjB,iBAAiB,CAAmB;IACtD,CAAC;IAEJ,KAAK,CAAC,GAAG,CAAC,OAA8B,IAAI,qBAAqB,EAAE;QAC/D,MAAM,QAAQ,GAAG,IAAI,CAAC,cAAc,CAAC,eAAe,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;QACpE,IAAA,4BAAa,EAAC,QAAQ,EAAE,2BAA2B,CAAC,CAAC;QACrD,IAAA,4BAAa,EAAC,QAAQ,EAAE,gCAAgC,CAAC,CAAC;QAC1D,MAAM,WAAW,GAAG,IAAI,CAAC,YAAY,CAAC,cAAc,EAAE,CAAC;QACvD,MAAM,MAAM,GAAG,IAAA,8BAAe,EAAC,QAAQ,CAAC,CAAC,MAAM,CAAC;QAEhD,MAAM,cAAc,GAAG,MAAM,IAAI,CAAC,0BAA0B,CAAC,QAAQ,CAAC,CAAC;QACvE,IAAI,CAAC,OAAO,CAAC,eAAe,CAAC,QAAQ,CAAC,CAAC;QACvC,MAAM,aAAa,GAAG,IAAI,CAAC,YAAY,CAAC,QAAQ,CAAC,CAAC;QAElD,MAAM,IAAI,GAAG,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC,QAAQ,EAAE,MAAM,CAAC,UAAU,EAAE,IAAI,wCAAoB,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,CAAC,CAAE,mEAAmE;QAC/K,MAAM,SAAS,GAAG,IAAI,CAAC,cAAc,CAAC,QAAQ,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,CAAC,CAAC;QAC3E,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,QAAQ,EAAE,WAAW,EAAE,IAAI,yCAAkB,CAC7D,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,cAAc,EAAE,IAAI,CAAC,aAAa,CAAC,mBAAmB,CAAC,QAAQ,CAAC,EAAE,aAAa,EACnG,SAAS,CAAC,GAAG,CAAC,CAAC,CAAmB,EAAU,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,CAC7D,CAAC,CAAC;QACH,IAAI,CAAC,mBAAmB,CAAC,QAAQ,CAAC,CAAC;QACnC,IAAI,CAAC,MAAM,CAAC,QAAQ,EAAE,WAAW,EAAE,IAAI,EAAE,SAAS,EAAE,IAAI,CAAC,CAAC;IAC9D,CAAC;IAED;;;;;;;OAOG;IACK,mBAAmB,CAAC,QAAgB;QACxC,MAAM,OAAO,GAAkB,IAAI,CAAC,iBAAiB,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACrE,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,iBAAiB,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CAAC;IACjE,CAAC;IAED;;;;;;;;;;;OAWG;IACK,KAAK,CAAC,0BAA0B,CAAC,QAAgB;QACrD,MAAM,IAAI,GAAG,IAAI,CAAC,UAAU,CAAC,WAAW,CAAC,QAAQ,EAAE,IAAI,CAAC,YAAY,CAAC,cAAc,EAAE,CAAC,CAAC;QACvF,MAAM,SAAS,GAAG,IAAI,CAAC,UAAU,CAAC,qBAAqB,CAAC,IAAI,CAAC,CAAC;QAC9D,MAAM,MAAM,GAAG,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC,eAAe,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QAC7E,IAAI,CAAC,SAAS,IAAI,CAAC,MAAM,IAAI,MAAM,CAAC,SAAS;YAAE,OAAO,IAAI,CAAC;QAC3D,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,GAAG,GAAG,GAAG,mCAAmC,GAAG,GAAG,GAAG,IAAI,CAAC,CAAC;QACpF,wDAAwD;QACxD,MAAM,IAAI,CAAC,QAAQ,CAAC,QAAQ,CACxB,QAAQ,EAAE,qBAAqB,EAAE,SAAS,EAC1C,IAAI,0BAAY,CAAC,MAAM,CAAC,aAAa,EAAE,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,YAAY,EAAE,MAAM,CAAC,QAAQ,CAAC,EACjG,IAAI,2BAAe,CAAC,MAAM,CAAC,eAAe,EAAE,KAAK,CAAC,CACrD,CAAC;QACF,OAAO,IAAI,CAAC;IAChB,CAAC;IAED;;;;OAIG;IACK,YAAY,CAAC,QAAgB;QACjC,IAAI,CAAC,aAAa,CAAC,YAAY,CAAC,QAAQ,EAAE,IAAI,iCAAgB,CAC1D,iBAAiB,EACjB,0BAA0B,EAC1B,wFAAwF,EACxF,6BAAY,CACf,CAAC,CAAC;QACH,+FAA+F;QAC/F,6FAA6F;QAC7F,gFAAgF;QAChF,4FAA4F;QAC5F,IAAI,CAAC,iBAAiB,CAAC,iCAAiC,CAAC,QAAQ,CAAC,CAAC;QACnE,OAAO,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;IACpC,CAAC;IAED;;;;;OAKG;IACK,cAAc,CAAC,QAAgB,EAAE,WAAmB,EAAE,IAAmB,EAAE,MAAoB;QACnG,IAAI,IAAI,CAAC,KAAK,CAAC,UAAU;YAAE,OAAO,EAAE,CAAC;QACrC,MAAM,QAAQ,GAAG,IAAI,CAAC,YAAY,CAAC,WAAW,CAC1C,QAAQ,EAAE,WAAW,EAAE,IAAI,CAAC,KAAK,EAAE,IAAI,CAAC,YAAY,EAAE,MAAM,CAAC,iBAAiB,CAAC,CAAC;QACpF,MAAM,OAAO,GAAG,IAAI,CAAC,YAAY,CAAC,UAAU,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAC;QACpE,iGAAiG;QACjG,oGAAoG;QACpG,gGAAgG;QAChG,iGAAiG;QACjG,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,eAAe,CAAC,MAAM,CACtC,QAAQ,EAAE,WAAW,EAAE,IAAI,CAAC,KAAK,EAAE,eAAe,EAAE,IAAI,CAAC,YAAY,EAAE,OAAO,CAAC,CAAC;QACpF,MAAM,SAAS,GAAG,IAAI,CAAC,eAAe,CAAC,KAAK,CAAC,QAAQ,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC;QACxF,MAAM,GAAG,GAAG,IAAI,CAAC,oBAAoB,CAAC,kBAAkB,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAC;QAChF,EAAE,CAAC,MAAM,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,yDAAyD;QAC3G,EAAE,CAAC,SAAS,CAAC,GAAG,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;QACvC,KAAK,MAAM,CAAC,IAAI,SAAS,EAAE,CAAC;YACxB,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,oBAAoB,CAAC,OAAO,CAAC,QAAQ,EAAE,WAAW,EAAE,CAAC,CAAC,QAAQ,CAAC,EACjF,IAAI,CAAC,oBAAoB,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC;QAC7C,CAAC;QACD,OAAO,SAAS,CAAC;IACrB,CAAC;IAED;;;;;;;OAOG;IACH,yDAAyD;IACjD,MAAM,CACV,QAAgB,EAAE,WAAmB,EAAE,IAAmB,EAAE,SAAsC,EAClG,IAA2B;QAE3B,MAAM,KAAK,GAAG,IAAI,iCAAiB,CAAC,QAAQ,EAAE,WAAW,EAAE,IAAI,CAAC,UAAU,CAAC,CAAC;QAC5E,KAAK,CAAC,YAAY,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC;QACzC,KAAK,CAAC,eAAe,GAAG,IAAI,CAAC,UAAU,CAAC,MAAM,CAAC;QAC/C,KAAK,CAAC,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,CAAC;QACvC,KAAK,CAAC,YAAY,GAAG,IAAI,CAAC,YAAY,CAAC,KAAK,EAAE,CAAC;QAC/C,KAAK,CAAC,SAAS,GAAG,SAAS,CAAC,KAAK,EAAE,CAAC;QACpC,KAAK,CAAC,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC;QACpC,KAAK,CAAC,YAAY,GAAG,IAAI,CAAC,YAAY,CAAC;QACvC,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,YAAY,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;IAC1D,CAAC;IAED;;;;;;;;;;;;;OAaG;IACK,QAAQ,CAAC,IAAmB;QAChC,MAAM,OAAO,GAAG,IAAI,CAAC,iBAAiB,CAAC,iBAAiB,CAAC,IAAI,CAAC,UAAU,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;QACxF,OAAO,OAAO,CAAC,GAAG,CAAC,CAAC,GAAsB,EAAmB,EAAE,CAAC,IAAI,+BAAe,CAC/E,GAAG,CAAC,EAAE,EAAE,IAAI,CAAC,iBAAiB,CAAC,YAAY,CAAC,GAAG,EAAE,IAAI,CAAC,iBAAiB,CAAC,cAAc,CAAC,GAAG,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC;IACrH,CAAC;CACJ,CAAA;AA9KY,sDAAqB;gCAArB,qBAAqB;IADjC,IAAA,sBAAU,EAAC,8BAAkB,CAAC,SAAS,CAAC;6CAIA,6BAAc;QAChB,mCAAY;QACjB,kBAAO;QACD,8BAAa;QACT,uCAAiB;QACxB,wBAAU;QACZ,oBAAQ;QACA,oCAAgB;QACpB,4BAAY;QACZ,oCAAgB;QACb,mCAAe;QACf,mDAAuB;QAClB,0CAA2B;QACvC,gDAAyB;QAChB,kCAAiB;QAEjB,gCAAiB;GAnBhD,qBAAqB,CA8KjC","sourcesContent":["import * as fs from 'fs';\nimport {\n loadAndValidate, writeTemplate, PrGateConfig, RepoRootFinder, RequiredChecklist,\n ReviewerBriefing, ReviewerInstructionsService, ReviewJsonService,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { ActiveHatch, ActiveHatchReport } from '../workflow/active-hatches';\nimport { AiBranchName } from '../workflow/git-readAiBranchName';\nimport { BuildAffected, BuildGateOptions } from '../workflow/build-affected';\nimport { REVIEW_STAGE } from '../workflow/build-gate-log';\nimport { BuildArtifactGate } from '../workflow/build-artifact-gate';\nimport { ChecklistScan, ChecklistScanOptions, ChecklistScanner } from '../workflow/checklist-scanner';\nimport { DiffMaterializer } from '../workflow/diff-materializer';\nimport { GitExec } from '../workflow/git-exec';\nimport { MergeContext } from '../workflow/merge-start';\nimport { MergeEnd, MergeEndOptions } from '../workflow/merge-end';\nimport { MergeState } from '../workflow/merge-state';\nimport { PrContextWriter } from '../workflow/pr-context-writer';\nimport { ReviewerBriefingBuilder } from '../workflow/reviewer-briefing-builder';\nimport { RefusedReviewer, ReviewReport, ReviewReportInput } from '../workflow/review-report';\nimport { ReviewStageReceipt, ReviewStageReceiptService } from '../workflow/review-stage-receipt';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n/** The `--no-optional` decision, as data (per CLAUDE.md) rather than a bare boolean parameter. */\nexport class ReviewUpsertPrOptions {\n /**\n * true ⇒ the human ALREADY said to submit without the optional reviews, so stage ② must not print the\n * block telling the AI to offer them. It changes nothing about what BLOCKS: required checklists are\n * unaffected, and an optional checklist that already has a red verdict on this branch still refuses the\n * PR at finish. This only suppresses the offer.\n */\n skipOptional: boolean;\n\n constructor(skipOptional = false) {\n this.skipOptional = skipOptional;\n }\n}\n\n/**\n * `wp-review-upsert-pr` — STAGE ② of the three-stage PR flow, between `wp-start-upsert-pr` (sync from main)\n * and `wp-finish-upsert-pr` (post the PR).\n *\n * It exists because reviewing came before verifying. Under the old two-command flow the 3-point merge was\n * finalized and the build gate ran inside `wp-finish-upsert-pr` — i.e. AFTER the reviewers had already run.\n * A reviewer could therefore spend a full review on an unresolved merge, or on code that does not compile,\n * and only afterwards would the branch be checked. This command puts the verification first:\n *\n * 1. finalize + VALIDATE any in-progress 3-point merge (moved here from finish)\n * 2. assert a clean tree (meaningful now — step 1 committed the resolution)\n * 3. run the build gate (on the merged, committed tree)\n * 4. scan the checklists, EXTRACT the diff, BRIEF the reviewers\n * 5. write the stage receipt, then print what to spawn + the review.json schema\n *\n * Unlike the report-only command it replaces — which always exited 0, because reporting is not gating — this\n * command CAN FAIL, at steps 1, 2 and 3. That is the point: it fails BEFORE any reviewer is spawned, so a\n * branch with an unresolved merge or a broken build costs zero reviewer tokens.\n *\n * The ordering of 1 → 2 → 3 is load-bearing. MergeEnd stages and commits the conflict resolution, so the\n * tree is clean by the time the build runs; building first would either build a dirty tree (proving nothing\n * about what ships) or refuse a tree that is legitimately mid-resolution.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class ReviewUpsertPrCommand {\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(\n private readonly repoRootFinder: RepoRootFinder,\n private readonly aiBranchName: AiBranchName,\n private readonly gitExec: GitExec,\n private readonly buildAffected: BuildAffected,\n private readonly buildArtifactGate: BuildArtifactGate,\n private readonly mergeState: MergeState,\n private readonly mergeEnd: MergeEnd,\n private readonly checklistScanner: ChecklistScanner,\n private readonly reviewReport: ReviewReport,\n private readonly materializer: DiffMaterializer,\n private readonly prContextWriter: PrContextWriter,\n private readonly briefingBuilder: ReviewerBriefingBuilder,\n private readonly reviewerInstructions: ReviewerInstructionsService,\n private readonly receipts: ReviewStageReceiptService,\n private readonly activeHatchReport: ActiveHatchReport,\n // Injected only to RESOLVE + RENDER refusals (see refusals()). This stage never archives a verdict.\n private readonly reviewJsonService: ReviewJsonService,\n ) {}\n\n async run(opts: ReviewUpsertPrOptions = new ReviewUpsertPrOptions()): Promise<void> {\n const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());\n writeTemplate(repoRoot, 'webpieces.git-workflow.md');\n writeTemplate(repoRoot, 'webpieces.review-checklists.md');\n const featureName = this.aiBranchName.getFeatureName();\n const config = loadAndValidate(repoRoot).prGate;\n\n const mergeValidated = await this.finalizeAnyInProgressMerge(repoRoot);\n this.gitExec.assertCleanTree(repoRoot);\n const buildPassedAt = this.runBuildGate(repoRoot);\n\n const scan = this.checklistScanner.scan(repoRoot, config.checklists, new ChecklistScanOptions(false, '')); // '' — THIS command writes the context itself, after materializing\n const briefings = this.briefReviewers(repoRoot, featureName, scan, config);\n this.receipts.write(repoRoot, featureName, new ReviewStageReceipt(\n scan.basis.headSha, mergeValidated, this.buildAffected.resolveBuildCommand(repoRoot), buildPassedAt,\n briefings.map((b: ReviewerBriefing): string => b.subagent),\n ));\n this.reportActiveHatches(repoRoot);\n this.report(repoRoot, featureName, scan, briefings, opts);\n }\n\n /**\n * List every rule that is currently switched off in webpieces.config.json. NON-BLOCKING and printed\n * before the instruction block, because it is context for the review rather than a step in it.\n *\n * A hatch is the one config change that makes the build QUIETER, so no gate, reviewer or red check\n * will ever mention it — which is how a repo ends up with rules off for weeks that nobody chose to\n * leave off. This is the only place in the flow that says so out loud.\n */\n private reportActiveHatches(repoRoot: string): void {\n const hatches: ActiveHatch[] = this.activeHatchReport.scan(repoRoot);\n process.stdout.write(this.activeHatchReport.render(hatches));\n }\n\n /**\n * Validate + commit + finalize a conflict resolution, if one is in flight. MOVED here from\n * `wp-finish-upsert-pr` so that exactly one command owns merge finalization — two owners is the drift\n * failure `PrContextWriter`'s own docstring records having already happened once in this codebase.\n *\n * MergeEnd's three checks are what \"did the AI do the 3-point merge right?\" means concretely: no\n * conflict markers left in the scoped files, `git diff --diff-filter=U` empty, and a non-empty\n * merge-explanation.md beside every conflicted file.\n *\n * Returns true when a merge was finalized OR there was none to finalize; it throws rather than returning\n * false when validation fails, so the receipt can never record an unvalidated merge as validated.\n */\n private async finalizeAnyInProgressMerge(repoRoot: string): Promise<boolean> {\n const home = this.mergeState.mergeDirFor(repoRoot, this.aiBranchName.getFeatureName());\n const activeDir = this.mergeState.findActiveMergeRunDir(home);\n const marker = activeDir ? this.mergeState.readMergeMarker(activeDir) : null;\n if (!activeDir || !marker || marker.validated) return true;\n process.stdout.write('\\n' + SEP + '① Validating your 3-point merge\\n' + SEP + '\\n');\n // pushRemote:false — finish still owns the single push.\n await this.mergeEnd.mergeEnd(\n repoRoot, 'wp-review-upsert-pr', activeDir,\n new MergeContext(marker.currentBranch, marker.squashBranch, marker.backupBranch, marker.prNumber),\n new MergeEndOptions(marker.conflictedFiles, false),\n );\n return true;\n }\n\n /**\n * The build gate, run BEFORE any reviewer is briefed. Returns the ISO time it passed, for the receipt —\n * which is what lets `wp-finish-upsert-pr` skip its own gate when HEAD has not moved, so moving the gate\n * earlier did not turn one build into two.\n */\n private runBuildGate(repoRoot: string): string {\n this.buildAffected.runBuildGate(repoRoot, new BuildGateOptions(\n '🛠️ Build gate',\n 'pnpm wp-review-upsert-pr',\n 'Build failed — NO reviewer was briefed and no diff was extracted. Fix it, then re-run.',\n REVIEW_STAGE,\n ));\n // Repo-wide: the build must not have left anything uncommitted AND unstaged. Runs HERE, not in\n // finish, because this is the stage that ran buildCommand and is therefore holding the dirty\n // tree at the exact moment the question is answerable. Replaces the per-project\n // validate-di-graph-unchanged nx target — see BuildArtifactGate for why that one had to go.\n this.buildArtifactGate.assertBuildLeftNothingUncommitted(repoRoot);\n return new Date().toISOString();\n }\n\n /**\n * Extract the diff once, then write one instructions file per reviewer.\n *\n * The changed-file set and the basis both come off the SCAN rather than being recomputed, so the diff a\n * reviewer reads covers exactly the files its checklist matched against.\n */\n private briefReviewers(repoRoot: string, featureName: string, scan: ChecklistScan, config: PrGateConfig): ReviewerBriefing[] {\n if (scan.basis.unresolved) return [];\n const manifest = this.materializer.materialize(\n repoRoot, featureName, scan.basis, scan.changedFiles, config.reviewDiffExclude);\n const diffDir = this.materializer.diffDirFor(repoRoot, featureName);\n // Write pr-context.json ONCE, here — after materializing, so `diffDir` is populated on the first\n // write. The scan deliberately did not write it (ChecklistScanOptions.contextStage === ''); it used\n // to, which meant this command wrote the same file twice, the first time with an empty diffDir.\n // `scan.changedFiles` is passed through so the context is not recomputed from a second git call.\n scan.context = this.prContextWriter.ensure(\n repoRoot, featureName, scan.basis, 'stage2-review', scan.changedFiles, diffDir);\n const briefings = this.briefingBuilder.build(repoRoot, scan, manifest, diffDir, config);\n const dir = this.reviewerInstructions.instructionsDirFor(repoRoot, featureName);\n fs.rmSync(dir, { recursive: true, force: true }); // stale instructions read as current are worse than none\n fs.mkdirSync(dir, { recursive: true });\n for (const b of briefings) {\n fs.writeFileSync(this.reviewerInstructions.pathFor(repoRoot, featureName, b.subagent),\n this.reviewerInstructions.render(b));\n }\n return briefings;\n }\n\n /**\n * What the AI must do next — rendered by `ReviewReport`, which owns the ordering.\n *\n * The rendering moved OUT of this command so the ordering could be asserted on as a string. It had a\n * real bug: the zero-checklist notice signed off with \"Carry on and run: pnpm wp-finish-upsert-pr\"\n * while the block right beneath it said to write review.json first. An agent that reads top to bottom\n * obeyed the first line and posted a PR with no review at all.\n */\n // eslint-disable-next-line @typescript-eslint/max-params\n private report(\n repoRoot: string, featureName: string, scan: ChecklistScan, briefings: readonly ReviewerBriefing[],\n opts: ReviewUpsertPrOptions,\n ): void {\n const input = new ReviewReportInput(repoRoot, featureName, scan.reviewPath);\n input.definedCount = scan.defined.length;\n input.applicableCount = scan.applicable.length;\n input.reviewed = scan.reviewed.slice();\n input.formatErrors = scan.formatErrors.slice();\n input.briefings = briefings.slice();\n input.refused = this.refusals(scan);\n input.skipOptional = opts.skipOptional;\n process.stdout.write(this.reviewReport.render(input));\n }\n\n /**\n * The applicable checklists that ALREADY ANSWERED and refused, each with its refusal rendered by the ONE\n * renderer `wp-finish-upsert-pr` also uses.\n *\n * Stage ② had the identical defect finish did: a refused checklist has no passing verdict, so it is not\n * in `scan.reviewed`, so it was printed as an ordinary owed reviewer and re-instructed word for word —\n * and an agent that obeys spawns it against unchanged code, gets the same refusal, and loops one stage\n * earlier than the loop that was reported.\n *\n * NO archive path is passed, deliberately, and this stage moves nothing. Retiring a verdict is finish's\n * act on the refusal it is actually enforcing; doing it here would delete the live verdict of a branch\n * that has not even been asked to finish yet, and `refusalError`'s un-archived wording — \"set an override\n * in review-<id>.json\" — is only correct while that file is still there, which here it is.\n */\n private refusals(scan: ChecklistScan): RefusedReviewer[] {\n const refused = this.reviewJsonService.refusedChecklists(scan.applicable, scan.results);\n return refused.map((req: RequiredChecklist): RefusedReviewer => new RefusedReviewer(\n req.id, this.reviewJsonService.refusalError(req, this.reviewJsonService.resolveVerdict(req, scan.results))));\n }\n}\n"]}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { ConfigFile } from '@webpieces/rules-config';
|
|
2
|
+
/** One rule that is currently NOT being enforced, and which of the two hatches is doing it. Data-only. */
|
|
3
|
+
export declare class ActiveHatch {
|
|
4
|
+
/** The rule/guard/match-rule name as it appears in webpieces.config.json. */
|
|
5
|
+
ruleName: string;
|
|
6
|
+
/** `turnOffRuleWhileOnBranch` or `turnOffRuleUntilEpoch` — the field, spelled the way the config spells it. */
|
|
7
|
+
hatchField: string;
|
|
8
|
+
/** Already rendered for a human: a quoted branch name, or the ISO date the epoch expires. */
|
|
9
|
+
value: string;
|
|
10
|
+
constructor(ruleName: string, hatchField: string, value: string);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* One named block of webpieces.config.json — a `rules`/`hookGuards` entry, or a `match-rules` array element
|
|
14
|
+
* that carries its own `name`. Data-only, so the three config shapes are scanned as one list.
|
|
15
|
+
*/
|
|
16
|
+
export declare class ConfigEntry {
|
|
17
|
+
ruleName: string;
|
|
18
|
+
fields: Record<string, unknown>;
|
|
19
|
+
constructor(ruleName: string, fields: Record<string, unknown>);
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* The active-hatch section of the `wp-review-upsert-pr` dashboard: every rule that is currently switched
|
|
23
|
+
* off, listed once per PR.
|
|
24
|
+
*
|
|
25
|
+
* PURELY INFORMATIONAL — it never fails a build, never blocks a PR, and never asks for anything. It exists
|
|
26
|
+
* because a hatch is the one config change that makes the build QUIETER, so nothing else in the flow will
|
|
27
|
+
* ever mention it. The motivating case is an EPOCH hatch: it is repo-wide while it lasts, so a long window
|
|
28
|
+
* shelters every unrelated change landing in it, and a fleet repo was found with max-file-lines and
|
|
29
|
+
* max-method-lines switched off until 2026-10-01 — 43 days out — with nobody aware. (The ONE-WEEK cap in
|
|
30
|
+
* rules-config stops new ones from getting that far; this is how you SEE the ones you have.)
|
|
31
|
+
*
|
|
32
|
+
* A branch hatch is listed for completeness, not as a complaint. It fires on ONE exact branch name, so a
|
|
33
|
+
* stale one is close to harmless — and deliberately there is NO "does that branch still exist on the
|
|
34
|
+
* remote?" check here, because a pruned remote would make that a false alarm on every PR.
|
|
35
|
+
*/
|
|
36
|
+
export declare class ActiveHatchReport {
|
|
37
|
+
private readonly configFile;
|
|
38
|
+
constructor(configFile: ConfigFile);
|
|
39
|
+
/**
|
|
40
|
+
* Every active hatch in the repo's config: a non-null `turnOffRuleWhileOnBranch`, or a
|
|
41
|
+
* `turnOffRuleUntilEpoch` still in the future. An epoch in the PAST is inert — it skips nothing — and
|
|
42
|
+
* listing those would bury the live ones under every rule in the file.
|
|
43
|
+
*/
|
|
44
|
+
scan(repoRoot: string): ActiveHatch[];
|
|
45
|
+
/** The section, or an empty string when nothing is hatched — the common case prints nothing at all. */
|
|
46
|
+
render(hatches: readonly ActiveHatch[]): string;
|
|
47
|
+
/**
|
|
48
|
+
* Every named block in the config that can carry a hatch: the keyed `rules` and `hookGuards` maps, plus
|
|
49
|
+
* the `match-rules` array, whose entries name themselves.
|
|
50
|
+
*/
|
|
51
|
+
private namedEntries;
|
|
52
|
+
/** A `name → block` map (`rules`, `hookGuards`) as entries. */
|
|
53
|
+
private keyedEntries;
|
|
54
|
+
/** The `match-rules` array, whose elements carry their own `name`. Typed `unknown` in RawConfigFile. */
|
|
55
|
+
private matchRuleEntries;
|
|
56
|
+
private hatchesOf;
|
|
57
|
+
private isoDate;
|
|
58
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ActiveHatchReport = exports.ConfigEntry = exports.ActiveHatch = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const path = tslib_1.__importStar(require("path"));
|
|
6
|
+
const rules_config_1 = require("@webpieces/rules-config");
|
|
7
|
+
const inversify_1 = require("inversify");
|
|
8
|
+
/** One rule that is currently NOT being enforced, and which of the two hatches is doing it. Data-only. */
|
|
9
|
+
class ActiveHatch {
|
|
10
|
+
/** The rule/guard/match-rule name as it appears in webpieces.config.json. */
|
|
11
|
+
ruleName;
|
|
12
|
+
/** `turnOffRuleWhileOnBranch` or `turnOffRuleUntilEpoch` — the field, spelled the way the config spells it. */
|
|
13
|
+
hatchField;
|
|
14
|
+
/** Already rendered for a human: a quoted branch name, or the ISO date the epoch expires. */
|
|
15
|
+
value;
|
|
16
|
+
constructor(ruleName, hatchField, value) {
|
|
17
|
+
this.ruleName = ruleName;
|
|
18
|
+
this.hatchField = hatchField;
|
|
19
|
+
this.value = value;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
exports.ActiveHatch = ActiveHatch;
|
|
23
|
+
/**
|
|
24
|
+
* One named block of webpieces.config.json — a `rules`/`hookGuards` entry, or a `match-rules` array element
|
|
25
|
+
* that carries its own `name`. Data-only, so the three config shapes are scanned as one list.
|
|
26
|
+
*/
|
|
27
|
+
class ConfigEntry {
|
|
28
|
+
ruleName;
|
|
29
|
+
// webpieces-disable no-any-unknown -- an unvalidated config block: the values are opaque JSON here, and each hatch field is type-checked before it is read
|
|
30
|
+
fields;
|
|
31
|
+
// webpieces-disable no-any-unknown -- see the field above
|
|
32
|
+
constructor(ruleName, fields) {
|
|
33
|
+
this.ruleName = ruleName;
|
|
34
|
+
this.fields = fields;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
exports.ConfigEntry = ConfigEntry;
|
|
38
|
+
/**
|
|
39
|
+
* The active-hatch section of the `wp-review-upsert-pr` dashboard: every rule that is currently switched
|
|
40
|
+
* off, listed once per PR.
|
|
41
|
+
*
|
|
42
|
+
* PURELY INFORMATIONAL — it never fails a build, never blocks a PR, and never asks for anything. It exists
|
|
43
|
+
* because a hatch is the one config change that makes the build QUIETER, so nothing else in the flow will
|
|
44
|
+
* ever mention it. The motivating case is an EPOCH hatch: it is repo-wide while it lasts, so a long window
|
|
45
|
+
* shelters every unrelated change landing in it, and a fleet repo was found with max-file-lines and
|
|
46
|
+
* max-method-lines switched off until 2026-10-01 — 43 days out — with nobody aware. (The ONE-WEEK cap in
|
|
47
|
+
* rules-config stops new ones from getting that far; this is how you SEE the ones you have.)
|
|
48
|
+
*
|
|
49
|
+
* A branch hatch is listed for completeness, not as a complaint. It fires on ONE exact branch name, so a
|
|
50
|
+
* stale one is close to harmless — and deliberately there is NO "does that branch still exist on the
|
|
51
|
+
* remote?" check here, because a pruned remote would make that a false alarm on every PR.
|
|
52
|
+
*/
|
|
53
|
+
let ActiveHatchReport = class ActiveHatchReport {
|
|
54
|
+
configFile;
|
|
55
|
+
constructor(configFile) {
|
|
56
|
+
this.configFile = configFile;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Every active hatch in the repo's config: a non-null `turnOffRuleWhileOnBranch`, or a
|
|
60
|
+
* `turnOffRuleUntilEpoch` still in the future. An epoch in the PAST is inert — it skips nothing — and
|
|
61
|
+
* listing those would bury the live ones under every rule in the file.
|
|
62
|
+
*/
|
|
63
|
+
scan(repoRoot) {
|
|
64
|
+
const raw = this.configFile.readRawConfig(path.join(repoRoot, rules_config_1.CONFIG_FILENAME));
|
|
65
|
+
const found = [];
|
|
66
|
+
for (const entry of this.namedEntries(raw))
|
|
67
|
+
found.push(...this.hatchesOf(entry));
|
|
68
|
+
return found;
|
|
69
|
+
}
|
|
70
|
+
/** The section, or an empty string when nothing is hatched — the common case prints nothing at all. */
|
|
71
|
+
render(hatches) {
|
|
72
|
+
if (hatches.length === 0)
|
|
73
|
+
return '';
|
|
74
|
+
const width = Math.max(...hatches.map((h) => h.ruleName.length));
|
|
75
|
+
const lines = hatches.map((h) => ` ${h.ruleName.padEnd(width)} ${h.hatchField.padEnd(24)} = ${h.value}`);
|
|
76
|
+
return (`\n⚠️ ${hatches.length} rule hatch(es) are currently active in ${rules_config_1.CONFIG_FILENAME}:\n` +
|
|
77
|
+
lines.join('\n') + '\n' +
|
|
78
|
+
' Not blocking, and not necessarily wrong — just so a rule that is OFF is never off silently.\n');
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Every named block in the config that can carry a hatch: the keyed `rules` and `hookGuards` maps, plus
|
|
82
|
+
* the `match-rules` array, whose entries name themselves.
|
|
83
|
+
*/
|
|
84
|
+
namedEntries(raw) {
|
|
85
|
+
const entries = [];
|
|
86
|
+
entries.push(...this.keyedEntries(raw.rules));
|
|
87
|
+
entries.push(...this.keyedEntries(raw.hookGuards));
|
|
88
|
+
entries.push(...this.matchRuleEntries(raw['match-rules']));
|
|
89
|
+
return entries;
|
|
90
|
+
}
|
|
91
|
+
/** A `name → block` map (`rules`, `hookGuards`) as entries. */
|
|
92
|
+
// webpieces-disable no-any-unknown -- an unvalidated config section: opaque JSON in, type-checked fields out
|
|
93
|
+
keyedEntries(section) {
|
|
94
|
+
if (!section)
|
|
95
|
+
return [];
|
|
96
|
+
return Object.keys(section).map((name) => new ConfigEntry(name, section[name]));
|
|
97
|
+
}
|
|
98
|
+
/** The `match-rules` array, whose elements carry their own `name`. Typed `unknown` in RawConfigFile. */
|
|
99
|
+
// webpieces-disable no-any-unknown -- RawConfigFile types this section unknown on purpose; it is validated structurally elsewhere
|
|
100
|
+
matchRuleEntries(section) {
|
|
101
|
+
if (!Array.isArray(section))
|
|
102
|
+
return [];
|
|
103
|
+
// webpieces-disable no-any-unknown -- one opaque config block per element
|
|
104
|
+
return section.map((entry) => {
|
|
105
|
+
const name = entry['name'];
|
|
106
|
+
return new ConfigEntry(typeof name === 'string' ? name : '(unnamed match-rule)', entry);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
hatchesOf(entry) {
|
|
110
|
+
const hatches = [];
|
|
111
|
+
const branch = entry.fields['turnOffRuleWhileOnBranch'];
|
|
112
|
+
if (typeof branch === 'string' && branch !== '') {
|
|
113
|
+
hatches.push(new ActiveHatch(entry.ruleName, 'turnOffRuleWhileOnBranch', `"${branch}"`));
|
|
114
|
+
}
|
|
115
|
+
const epoch = entry.fields['turnOffRuleUntilEpoch'];
|
|
116
|
+
if (typeof epoch === 'number' && epoch > Date.now() / 1000) {
|
|
117
|
+
hatches.push(new ActiveHatch(entry.ruleName, 'turnOffRuleUntilEpoch', this.isoDate(epoch)));
|
|
118
|
+
}
|
|
119
|
+
return hatches;
|
|
120
|
+
}
|
|
121
|
+
isoDate(epochSeconds) {
|
|
122
|
+
return new Date(epochSeconds * 1000).toISOString().split('T')[0];
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
exports.ActiveHatchReport = ActiveHatchReport;
|
|
126
|
+
exports.ActiveHatchReport = ActiveHatchReport = tslib_1.__decorate([
|
|
127
|
+
(0, inversify_1.injectable)(inversify_1.bindingScopeValues.Singleton),
|
|
128
|
+
tslib_1.__metadata("design:paramtypes", [rules_config_1.ConfigFile])
|
|
129
|
+
], ActiveHatchReport);
|
|
130
|
+
//# sourceMappingURL=active-hatches.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"active-hatches.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/active-hatches.ts"],"names":[],"mappings":"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","sourcesContent":["import * as path from 'path';\n\nimport { CONFIG_FILENAME, ConfigFile, RawConfigFile } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\n\n/** One rule that is currently NOT being enforced, and which of the two hatches is doing it. Data-only. */\nexport class ActiveHatch {\n /** The rule/guard/match-rule name as it appears in webpieces.config.json. */\n ruleName: string;\n /** `turnOffRuleWhileOnBranch` or `turnOffRuleUntilEpoch` — the field, spelled the way the config spells it. */\n hatchField: string;\n /** Already rendered for a human: a quoted branch name, or the ISO date the epoch expires. */\n value: string;\n\n constructor(ruleName: string, hatchField: string, value: string) {\n this.ruleName = ruleName;\n this.hatchField = hatchField;\n this.value = value;\n }\n}\n\n/**\n * One named block of webpieces.config.json — a `rules`/`hookGuards` entry, or a `match-rules` array element\n * that carries its own `name`. Data-only, so the three config shapes are scanned as one list.\n */\nexport class ConfigEntry {\n ruleName: string;\n // webpieces-disable no-any-unknown -- an unvalidated config block: the values are opaque JSON here, and each hatch field is type-checked before it is read\n fields: Record<string, unknown>;\n\n // webpieces-disable no-any-unknown -- see the field above\n constructor(ruleName: string, fields: Record<string, unknown>) {\n this.ruleName = ruleName;\n this.fields = fields;\n }\n}\n\n/**\n * The active-hatch section of the `wp-review-upsert-pr` dashboard: every rule that is currently switched\n * off, listed once per PR.\n *\n * PURELY INFORMATIONAL — it never fails a build, never blocks a PR, and never asks for anything. It exists\n * because a hatch is the one config change that makes the build QUIETER, so nothing else in the flow will\n * ever mention it. The motivating case is an EPOCH hatch: it is repo-wide while it lasts, so a long window\n * shelters every unrelated change landing in it, and a fleet repo was found with max-file-lines and\n * max-method-lines switched off until 2026-10-01 — 43 days out — with nobody aware. (The ONE-WEEK cap in\n * rules-config stops new ones from getting that far; this is how you SEE the ones you have.)\n *\n * A branch hatch is listed for completeness, not as a complaint. It fires on ONE exact branch name, so a\n * stale one is close to harmless — and deliberately there is NO \"does that branch still exist on the\n * remote?\" check here, because a pruned remote would make that a false alarm on every PR.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class ActiveHatchReport {\n constructor(private readonly configFile: ConfigFile) {}\n\n /**\n * Every active hatch in the repo's config: a non-null `turnOffRuleWhileOnBranch`, or a\n * `turnOffRuleUntilEpoch` still in the future. An epoch in the PAST is inert — it skips nothing — and\n * listing those would bury the live ones under every rule in the file.\n */\n scan(repoRoot: string): ActiveHatch[] {\n const raw = this.configFile.readRawConfig(path.join(repoRoot, CONFIG_FILENAME));\n const found: ActiveHatch[] = [];\n for (const entry of this.namedEntries(raw)) found.push(...this.hatchesOf(entry));\n return found;\n }\n\n /** The section, or an empty string when nothing is hatched — the common case prints nothing at all. */\n render(hatches: readonly ActiveHatch[]): string {\n if (hatches.length === 0) return '';\n const width = Math.max(...hatches.map((h: ActiveHatch): number => h.ruleName.length));\n const lines = hatches.map((h: ActiveHatch): string =>\n ` ${h.ruleName.padEnd(width)} ${h.hatchField.padEnd(24)} = ${h.value}`);\n return (\n `\\n⚠️ ${hatches.length} rule hatch(es) are currently active in ${CONFIG_FILENAME}:\\n` +\n lines.join('\\n') + '\\n' +\n ' Not blocking, and not necessarily wrong — just so a rule that is OFF is never off silently.\\n'\n );\n }\n\n /**\n * Every named block in the config that can carry a hatch: the keyed `rules` and `hookGuards` maps, plus\n * the `match-rules` array, whose entries name themselves.\n */\n private namedEntries(raw: RawConfigFile): ConfigEntry[] {\n const entries: ConfigEntry[] = [];\n entries.push(...this.keyedEntries(raw.rules));\n entries.push(...this.keyedEntries(raw.hookGuards));\n entries.push(...this.matchRuleEntries(raw['match-rules']));\n return entries;\n }\n\n /** A `name → block` map (`rules`, `hookGuards`) as entries. */\n // webpieces-disable no-any-unknown -- an unvalidated config section: opaque JSON in, type-checked fields out\n private keyedEntries(section: Record<string, Record<string, unknown>> | undefined): ConfigEntry[] {\n if (!section) return [];\n return Object.keys(section).map((name: string): ConfigEntry => new ConfigEntry(name, section[name]));\n }\n\n /** The `match-rules` array, whose elements carry their own `name`. Typed `unknown` in RawConfigFile. */\n // webpieces-disable no-any-unknown -- RawConfigFile types this section unknown on purpose; it is validated structurally elsewhere\n private matchRuleEntries(section: unknown): ConfigEntry[] {\n if (!Array.isArray(section)) return [];\n // webpieces-disable no-any-unknown -- one opaque config block per element\n return (section as Record<string, unknown>[]).map((entry: Record<string, unknown>): ConfigEntry => {\n const name = entry['name'];\n return new ConfigEntry(typeof name === 'string' ? name : '(unnamed match-rule)', entry);\n });\n }\n\n private hatchesOf(entry: ConfigEntry): ActiveHatch[] {\n const hatches: ActiveHatch[] = [];\n const branch = entry.fields['turnOffRuleWhileOnBranch'];\n if (typeof branch === 'string' && branch !== '') {\n hatches.push(new ActiveHatch(entry.ruleName, 'turnOffRuleWhileOnBranch', `\"${branch}\"`));\n }\n const epoch = entry.fields['turnOffRuleUntilEpoch'];\n if (typeof epoch === 'number' && epoch > Date.now() / 1000) {\n hatches.push(new ActiveHatch(entry.ruleName, 'turnOffRuleUntilEpoch', this.isoDate(epoch)));\n }\n return hatches;\n }\n\n private isoDate(epochSeconds: number): string {\n return new Date(epochSeconds * 1000).toISOString().split('T')[0];\n }\n}\n"]}
|