@webpieces/pr-gate 0.4.537 → 0.4.539
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/finish-upsert-pr-command.d.ts +18 -2
- package/src/scripts/commands/finish-upsert-pr-command.js +55 -12
- package/src/scripts/commands/finish-upsert-pr-command.js.map +1 -1
- package/src/scripts/workflow/diff-materializer.d.ts +26 -3
- package/src/scripts/workflow/diff-materializer.js +57 -10
- package/src/scripts/workflow/diff-materializer.js.map +1 -1
- package/src/scripts/workflow/reviewer-briefing-builder.d.ts +18 -0
- package/src/scripts/workflow/reviewer-briefing-builder.js +43 -5
- package/src/scripts/workflow/reviewer-briefing-builder.js.map +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webpieces/pr-gate",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.539",
|
|
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",
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
"directory": "packages/tooling/pr-gate"
|
|
27
27
|
},
|
|
28
28
|
"dependencies": {
|
|
29
|
-
"@webpieces/rules-config": "0.4.
|
|
29
|
+
"@webpieces/rules-config": "0.4.539",
|
|
30
30
|
"@inversifyjs/binding-decorators": "1.1.5",
|
|
31
31
|
"inversify": "7.10.4",
|
|
32
32
|
"reflect-metadata": "0.2.2"
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { RepoRootFinder, ReviewJsonService, GateTokenService, SubagentProvenanceService } from '@webpieces/rules-config';
|
|
1
|
+
import { RepoRootFinder, ReviewJsonService, GateTokenService, SubagentProvenanceService, ReviewProvenanceService, ReviewerInstructionsService } from '@webpieces/rules-config';
|
|
2
2
|
import { AiBranchName } from '../workflow/git-readAiBranchName';
|
|
3
3
|
import { BranchNaming } from '../workflow/branch-naming';
|
|
4
4
|
import { ChecklistScanner } from '../workflow/checklist-scanner';
|
|
@@ -26,9 +26,11 @@ export declare class FinishUpsertPrCommand {
|
|
|
26
26
|
private readonly reviewJsonService;
|
|
27
27
|
private readonly gateTokenService;
|
|
28
28
|
private readonly provenance;
|
|
29
|
+
private readonly provenanceRecord;
|
|
30
|
+
private readonly reviewerInstructions;
|
|
29
31
|
private readonly receipts;
|
|
30
32
|
private readonly banner;
|
|
31
|
-
constructor(repoRootFinder: RepoRootFinder, aiBranchName: AiBranchName, branchNaming: BranchNaming, gitExec: GitExec, buildAffected: BuildAffected, mergeState: MergeState, prMerger: PrMerger, publisher: GatedPrPublisher, dashboard: Dashboard, checklistScanner: ChecklistScanner, verdictGate: ReviewerVerdictGate, reviewJsonService: ReviewJsonService, gateTokenService: GateTokenService, provenance: SubagentProvenanceService, receipts: ReviewStageReceiptService, banner: FinishBanner);
|
|
33
|
+
constructor(repoRootFinder: RepoRootFinder, aiBranchName: AiBranchName, branchNaming: BranchNaming, gitExec: GitExec, buildAffected: BuildAffected, mergeState: MergeState, prMerger: PrMerger, publisher: GatedPrPublisher, dashboard: Dashboard, checklistScanner: ChecklistScanner, verdictGate: ReviewerVerdictGate, reviewJsonService: ReviewJsonService, gateTokenService: GateTokenService, provenance: SubagentProvenanceService, provenanceRecord: ReviewProvenanceService, reviewerInstructions: ReviewerInstructionsService, receipts: ReviewStageReceiptService, banner: FinishBanner);
|
|
32
34
|
run(): Promise<void>;
|
|
33
35
|
/**
|
|
34
36
|
* The build gate — SKIPPED when stage ②'s receipt already covers this exact HEAD.
|
|
@@ -102,6 +104,20 @@ export declare class FinishUpsertPrCommand {
|
|
|
102
104
|
* whether any of it blocks, and by default none of it does.
|
|
103
105
|
*/
|
|
104
106
|
private gatherEvidence;
|
|
107
|
+
/**
|
|
108
|
+
* Write this round's audit record: `.webpieces/pr-review/<branch>/provenance.json`, linking each verdict
|
|
109
|
+
* to the transcript of the subagent that produced it.
|
|
110
|
+
*
|
|
111
|
+
* A SEPARATE file rather than a field inside review.json / review-<id>.json, for two reasons. The
|
|
112
|
+
* reviewer cannot supply this itself — a subagent's environment exposes the PARENT session id and no
|
|
113
|
+
* agent id, so a self-reported transcript link would be invented — and keeping the AI-authored files
|
|
114
|
+
* byte-untouched means nothing in the record can be mistaken for something a reviewer claimed about
|
|
115
|
+
* itself. Every path here is derived by the tooling from the harness's own artifacts.
|
|
116
|
+
*
|
|
117
|
+
* Never fatal: an unwritable record is a lost audit trail, not a reason to refuse a PR.
|
|
118
|
+
*/
|
|
119
|
+
private writeProvenanceRecord;
|
|
120
|
+
private reviewerPathsFor;
|
|
105
121
|
/**
|
|
106
122
|
* WARN (default) or REFUSE (opt-in) on a reviewer that wrote a verdict without opening the diff.
|
|
107
123
|
*
|
|
@@ -79,9 +79,11 @@ let FinishUpsertPrCommand = class FinishUpsertPrCommand {
|
|
|
79
79
|
reviewJsonService;
|
|
80
80
|
gateTokenService;
|
|
81
81
|
provenance;
|
|
82
|
+
provenanceRecord;
|
|
83
|
+
reviewerInstructions;
|
|
82
84
|
receipts;
|
|
83
85
|
banner;
|
|
84
|
-
constructor(repoRootFinder, aiBranchName, branchNaming, gitExec, buildAffected, mergeState, prMerger, publisher, dashboard, checklistScanner, verdictGate, reviewJsonService, gateTokenService, provenance,
|
|
86
|
+
constructor(repoRootFinder, aiBranchName, branchNaming, gitExec, buildAffected, mergeState, prMerger, publisher, dashboard, checklistScanner, verdictGate, reviewJsonService, gateTokenService, provenance, provenanceRecord, reviewerInstructions,
|
|
85
87
|
// NOTE: ChecklistInstructionsService is deliberately NOT injected here any more. Its "You MUST run
|
|
86
88
|
// these N reviewer subagent(s)" block is now rendered by ReviewerVerdictGate and ONLY for checklists
|
|
87
89
|
// that genuinely never ran, so no other code path in this command can print it at a refusal.
|
|
@@ -100,6 +102,8 @@ let FinishUpsertPrCommand = class FinishUpsertPrCommand {
|
|
|
100
102
|
this.reviewJsonService = reviewJsonService;
|
|
101
103
|
this.gateTokenService = gateTokenService;
|
|
102
104
|
this.provenance = provenance;
|
|
105
|
+
this.provenanceRecord = provenanceRecord;
|
|
106
|
+
this.reviewerInstructions = reviewerInstructions;
|
|
103
107
|
this.receipts = receipts;
|
|
104
108
|
this.banner = banner;
|
|
105
109
|
}
|
|
@@ -264,6 +268,9 @@ let FinishUpsertPrCommand = class FinishUpsertPrCommand {
|
|
|
264
268
|
const archived = this.reviewJsonService.archiveReviewJson((0, rules_config_1.reviewJsonPath)(repoRoot, featureName));
|
|
265
269
|
if (archived !== '')
|
|
266
270
|
process.stdout.write(` archived this run's review.json → ${archived} (audit only) ✓\n`);
|
|
271
|
+
// Beside it, so an archived review keeps the transcript links belonging to the round that
|
|
272
|
+
// produced it — a review whose provenance was overwritten by the NEXT round audits nothing.
|
|
273
|
+
this.provenanceRecord.archive((0, rules_config_1.prDirFor)(repoRoot, featureName));
|
|
267
274
|
}
|
|
268
275
|
catch (err) {
|
|
269
276
|
const error = (0, rules_config_1.toError)(err);
|
|
@@ -348,18 +355,21 @@ let FinishUpsertPrCommand = class FinishUpsertPrCommand {
|
|
|
348
355
|
const errors = [];
|
|
349
356
|
const report = new ProvenanceReport(true, []); // no reviewers to verify ⇒ vacuously verified
|
|
350
357
|
const subagents = required.map((r) => r.subagent.trim()).filter((s) => s !== '');
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
else if (result.status === rules_config_1.PROVENANCE_SKIPPED) {
|
|
358
|
-
process.stderr.write(`⚠️ ${result.detail}\n`);
|
|
359
|
-
}
|
|
360
|
-
report.evidence = this.gatherEvidence(repoRoot, required, result, branch);
|
|
361
|
-
errors.push(...this.evidenceErrors(report.evidence, config));
|
|
358
|
+
// verifyDistinct short-circuits to OK on an empty set, so this runs unconditionally: a repo with no
|
|
359
|
+
// checklists still gets a provenance record naming the session and the main agent's own transcript.
|
|
360
|
+
const result = this.provenance.verifyDistinct(subagents, branch);
|
|
361
|
+
report.verified = result.status === rules_config_1.PROVENANCE_OK;
|
|
362
|
+
if (result.status === rules_config_1.PROVENANCE_MISSING) {
|
|
363
|
+
errors.push(result.detail);
|
|
362
364
|
}
|
|
365
|
+
else if (result.status === rules_config_1.PROVENANCE_SKIPPED) {
|
|
366
|
+
process.stderr.write(`⚠️ ${result.detail}\n`);
|
|
367
|
+
}
|
|
368
|
+
report.evidence = this.gatherEvidence(repoRoot, required, result, branch);
|
|
369
|
+
errors.push(...this.evidenceErrors(report.evidence, config));
|
|
370
|
+
// BEFORE the throw below, deliberately. A refused round is the one most worth auditing, and a record
|
|
371
|
+
// that only ever appeared on success could not answer what the reviewers did the time it was refused.
|
|
372
|
+
this.writeProvenanceRecord(repoRoot, required, result, report.evidence);
|
|
363
373
|
if (errors.length > 0) {
|
|
364
374
|
throw new rules_config_1.InformAiError(`${errors.length} checklist(s) require an independent reviewer subagent that did not run — fix, then re-run pnpm wp-finish-upsert-pr:\n\n` +
|
|
365
375
|
errors.map((e) => ` • ${e}`).join('\n') +
|
|
@@ -381,6 +391,37 @@ let FinishUpsertPrCommand = class FinishUpsertPrCommand {
|
|
|
381
391
|
const diffDir = path.join((0, rules_config_1.prDirFor)(repoRoot, this.aiBranchName.getFeatureName()), 'diff');
|
|
382
392
|
return this.provenance.evidenceFor(new rules_config_1.EvidenceRequest(branch, result.agentIds, diffDir, docPaths));
|
|
383
393
|
}
|
|
394
|
+
/**
|
|
395
|
+
* Write this round's audit record: `.webpieces/pr-review/<branch>/provenance.json`, linking each verdict
|
|
396
|
+
* to the transcript of the subagent that produced it.
|
|
397
|
+
*
|
|
398
|
+
* A SEPARATE file rather than a field inside review.json / review-<id>.json, for two reasons. The
|
|
399
|
+
* reviewer cannot supply this itself — a subagent's environment exposes the PARENT session id and no
|
|
400
|
+
* agent id, so a self-reported transcript link would be invented — and keeping the AI-authored files
|
|
401
|
+
* byte-untouched means nothing in the record can be mistaken for something a reviewer claimed about
|
|
402
|
+
* itself. Every path here is derived by the tooling from the harness's own artifacts.
|
|
403
|
+
*
|
|
404
|
+
* Never fatal: an unwritable record is a lost audit trail, not a reason to refuse a PR.
|
|
405
|
+
*/
|
|
406
|
+
// eslint-disable-next-line @typescript-eslint/max-params
|
|
407
|
+
writeProvenanceRecord(repoRoot, required, result, evidence) {
|
|
408
|
+
const featureName = this.aiBranchName.getFeatureName();
|
|
409
|
+
const prDir = (0, rules_config_1.prDirFor)(repoRoot, featureName);
|
|
410
|
+
const request = new rules_config_1.ProvenanceWriteRequest(prDir, featureName, this.gitOut(['rev-parse', 'HEAD']), result.status);
|
|
411
|
+
request.offered = new rules_config_1.OfferedContext(path.join(prDir, 'diff'), this.reviewerInstructions.instructionsDirFor(repoRoot, featureName));
|
|
412
|
+
request.reviewers = evidence.map((e) => new rules_config_1.ReviewerTranscript(e, this.reviewerPathsFor(repoRoot, featureName, required, e.agentType)));
|
|
413
|
+
const written = this.provenanceRecord.write(request);
|
|
414
|
+
if (written !== '')
|
|
415
|
+
process.stdout.write(` transcript provenance → ${written}\n`);
|
|
416
|
+
}
|
|
417
|
+
// Where ONE reviewer's verdict, instructions and checklist doc live. Keyed by agentType, which IS the
|
|
418
|
+
// checklist id: ChecklistDefinition sets `id = subagent`, so the two never diverge.
|
|
419
|
+
// eslint-disable-next-line @typescript-eslint/max-params
|
|
420
|
+
reviewerPathsFor(repoRoot, featureName, required, agentType) {
|
|
421
|
+
const req = required.find((r) => r.subagent.trim() === agentType);
|
|
422
|
+
const doc = req !== undefined && req.doc.trim() !== '' ? path.resolve(repoRoot, req.doc) : '';
|
|
423
|
+
return new rules_config_1.ReviewerPaths(this.reviewJsonService.checklistResultPath((0, rules_config_1.reviewJsonPath)(repoRoot, featureName), req?.id ?? agentType), this.reviewerInstructions.pathFor(repoRoot, featureName, agentType), doc);
|
|
424
|
+
}
|
|
384
425
|
/**
|
|
385
426
|
* WARN (default) or REFUSE (opt-in) on a reviewer that wrote a verdict without opening the diff.
|
|
386
427
|
*
|
|
@@ -503,6 +544,8 @@ exports.FinishUpsertPrCommand = FinishUpsertPrCommand = tslib_1.__decorate([
|
|
|
503
544
|
rules_config_1.ReviewJsonService,
|
|
504
545
|
rules_config_1.GateTokenService,
|
|
505
546
|
rules_config_1.SubagentProvenanceService,
|
|
547
|
+
rules_config_1.ReviewProvenanceService,
|
|
548
|
+
rules_config_1.ReviewerInstructionsService,
|
|
506
549
|
review_stage_receipt_1.ReviewStageReceiptService,
|
|
507
550
|
finish_banner_1.FinishBanner])
|
|
508
551
|
], FinishUpsertPrCommand);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"finish-upsert-pr-command.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/commands/finish-upsert-pr-command.ts"],"names":[],"mappings":"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","sourcesContent":["import { execSync, spawnSync } from 'child_process';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport {\n loadAndValidate, prDirFor, reviewJsonPath, ReviewJson, RequiredChecklist, ChecklistVerdict,\n writeTemplate, RepoRootFinder, ReviewJsonService,\n GateTokenService, SubagentProvenanceService, PROVENANCE_OK, PROVENANCE_MISSING, PROVENANCE_SKIPPED,\n ProvenanceResult, ReviewerEvidence, EvidenceRequest, PrGateConfig,\n InformAiError, toError,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { AiBranchName } from '../workflow/git-readAiBranchName';\nimport { BranchNaming } from '../workflow/branch-naming';\nimport { ChecklistScan, ChecklistScanOptions, ChecklistScanner } from '../workflow/checklist-scanner';\nimport { ReviewerVerdictGate } from '../workflow/reviewer-verdict-gate';\nimport { GitExec } from '../workflow/git-exec';\nimport { BuildAffected, BuildGateOptions } from '../workflow/build-affected';\nimport { MergeState } from '../workflow/merge-state';\nimport { ReviewStageReceiptService } from '../workflow/review-stage-receipt';\nimport { PrMerger, MergeOutcome, MERGE_RESULT_FAILED } from '../workflow/pr-merger';\nimport { FinishBanner, FinishBannerInput } from '../workflow/finish-banner';\nimport { GatedPrPublisher } from '../workflow/gated-pr-publisher';\nimport { TriggeredChecklist } from '../workflow/checklist-detector';\nimport {\n Dashboard, DashboardInput, ChecklistRow, ChecklistCommentRow, CHECKLIST_COMMENT_MARKER,\n} from '../../dashboard/dashboard';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n/**\n * The provenance outcome: whether each reviewer was VERIFIED to have run (the integrity check, which\n * blocks) plus what each one actually read (the quality signal, which is published). Data-only.\n */\nclass ProvenanceReport {\n verified: boolean;\n evidence: ReviewerEvidence[];\n\n constructor(verified: boolean, evidence: ReviewerEvidence[]) {\n this.verified = verified;\n this.evidence = evidence;\n }\n}\n\n// A resolved PR's number + web URL. Both '' when the PR can't be resolved (e.g. create failed).\nclass PrRef {\n number: string;\n url: string;\n\n constructor(number: string, url: string) {\n this.number = number;\n this.url = url;\n }\n}\n\n// The outcome of the whole upsert: the PR number + web URL ('' each when unresolved) plus what actually\n// happened to the merge, so the final summary reports the REAL result rather than assuming success. The\n// URL is carried so the closing block can hand the AI a clickable link to relay to the user verbatim.\nclass UpsertResult {\n prNumber: string;\n prUrl: string;\n merge: MergeOutcome;\n\n constructor(prNumber: string, prUrl: string, merge: MergeOutcome) {\n this.prNumber = prNumber;\n this.prUrl = prUrl;\n this.merge = merge;\n }\n}\n\n// STAGE ③ — FINISH of the AI-first PR flow, and the ONLY command that posts PRs. Runs after\n// `wp-review-upsert-pr` verified the branch and the AI spawned the reviewers + wrote review.json.\n//\n// In order: (1) REFUSE on an unvalidated 3-point merge and REQUIRE stage ②'s receipt; (2) REQUIRE\n// review.json + every reviewer's verdict + provenance; (3) run the build gate UNLESS the receipt already\n// covers this exact HEAD; (4) render the dashboard; (5) create/update the PR via `gh`.\n//\n// It no longer FINALIZES a merge — stage ② does. Two commands owning conflict-resolution validation is two\n// implementations that drift, and finalizing here came too late to help anyway: the reviewers had already\n// reviewed the pre-merge tree by then.\n@injectable(bindingScopeValues.Singleton)\nexport class FinishUpsertPrCommand {\n constructor(\n private readonly repoRootFinder: RepoRootFinder,\n private readonly aiBranchName: AiBranchName,\n private readonly branchNaming: BranchNaming,\n private readonly gitExec: GitExec,\n private readonly buildAffected: BuildAffected,\n private readonly mergeState: MergeState,\n private readonly prMerger: PrMerger,\n private readonly publisher: GatedPrPublisher,\n private readonly dashboard: Dashboard,\n private readonly checklistScanner: ChecklistScanner,\n private readonly verdictGate: ReviewerVerdictGate,\n private readonly reviewJsonService: ReviewJsonService,\n private readonly gateTokenService: GateTokenService,\n private readonly provenance: SubagentProvenanceService,\n // NOTE: ChecklistInstructionsService is deliberately NOT injected here any more. Its \"You MUST run\n // these N reviewer subagent(s)\" block is now rendered by ReviewerVerdictGate and ONLY for checklists\n // that genuinely never ran, so no other code path in this command can print it at a refusal.\n private readonly receipts: ReviewStageReceiptService,\n private readonly banner: FinishBanner,\n ) {}\n\n async run(): Promise<void> {\n const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());\n // Refresh the AI-facing workflow doc so it's present + current for any failure message to cite.\n writeTemplate(repoRoot, 'webpieces.git-workflow.md');\n // 1. REQUIRE stage ② — see assertStageTwoRan. Returns true when its receipt covers THIS commit,\n // which is what lets the build gate below be skipped rather than re-run for a foregone answer.\n const buildAlreadyGreen = this.assertStageTwoRan(repoRoot);\n\n // 2. REQUIRE the AI-authored review.json (throws InformAiError with the schema if missing/invalid).\n // Compute the consumer checklists this diff triggered FIRST so an unacknowledged BLOCK throws\n // here — BEFORE any `gh pr create` — matching the guarantee buildCommand already provides.\n // The SAME scan wp-review-upsert-pr runs, with filterAlreadyReviewed:true so `outstanding` is exactly what\n // still owes a verdict (Z of N of X). Sharing the computation is the point: the command that REPORTS\n // and the command that GATES must not be able to disagree about what is owed. review-<id>.json files\n // persist locally, so a re-run re-validates the EXISTING verdicts against the (possibly changed)\n // applicable set for free — an unchanged checklist needs no re-review, a newly-applicable one refuses\n // until its file is written.\n const featureName = this.aiBranchName.getFeatureName();\n const scan = this.checklistScanner.scan(repoRoot, loadAndValidate(repoRoot).prGate.checklists, new ChecklistScanOptions(true, 'stage3-finish'));\n const required = scan.applicable;\n // FAIL FAST on an unclear checklist, BEFORE review.json is parsed and before the build gate runs. A\n // missing reviewer is not a review.json defect (folding it in made the AI fix the wrong thing), and\n // nobody should wait on a build to be told a reviewer never ran. ReviewerVerdictGate owns the\n // distinction between unreadable / REFUSED / never-ran, and retires the red verdicts it acts on.\n this.verdictGate.assertEveryReviewerRan(scan);\n const review = this.reviewJsonService.loadReviewJson(reviewJsonPath(repoRoot, featureName), required);\n\n // 2c. For any BLOCK checklist that names a reviewer `subagent`, VERIFY (from the harness's own\n // artifacts) that such a subagent actually ran on this branch — the coding agent may not\n // self-certify. Absent CLAUDE_CODE_SESSION_ID this skips with a warning (CI / plain terminal).\n const currentBranch = execSync('git branch --show-current', { encoding: 'utf8' }).trim();\n const provenance = this.enforceProvenance(required, currentBranch, repoRoot, loadAndValidate(repoRoot).prGate);\n\n // 2b. The build gate validates the WORKING TREE but we push HEAD — so they MUST be identical.\n this.gitExec.assertCleanTree(repoRoot);\n\n // 3. Build gate, then post the gated body, then push (that ORDER — see GatedPrPublisher).\n this.runOrSkipBuildGate(repoRoot, buildAlreadyGreen);\n const base = this.branchNaming.baseBranchName(execSync('git branch --show-current', { encoding: 'utf8' }).trim());\n\n process.stdout.write('\\n' + SEP + '📋 Dashboard + PR\\n' + SEP + '\\n');\n const title = this.prTitleFrom(review);\n const input = this.computeDashboardInput(repoRoot, true, review, title, required);\n // Append the hidden HMAC gate token bound to the LOCAL HEAD sha — computed BEFORE the push, because\n // GatedPrPublisher writes the body first so CI's `synchronize` read can never see a stale token. A\n // valid token in the PR body is proof this gated flow ran + passed on this exact commit, which CI\n // (`wp-check-pr`) recomputes. We reach here only after the build gate + every BLOCK checklist\n // passed, so minting is legitimate. Nothing about HMAC(salt, HEAD) needs the remote to have it.\n const gateSalt = loadAndValidate(repoRoot).prGate.gateSalt;\n const headSha = this.gitOut(['rev-parse', 'HEAD']);\n const body = this.dashboard.renderDashboard(input) + this.gateTokenBody(gateSalt, headSha);\n const result = this.upsertPr(repoRoot, base, body, title, input);\n // Publish each reviewer's full output as ONE combined PR comment (idempotent, opt-out-aware). Never\n // fatal — the PR is already up by now, so a comment failure only warns.\n this.postChecklistComment(repoRoot, result.prNumber, scan, review, provenance);\n this.archiveConsumedReview(repoRoot, featureName, result);\n\n // The closing recap + the clickable-link directive, BOTH derived from the real merge outcome.\n // Nothing here may hard-code success: a stranded PR under a green checkmark is how PRs got\n // abandoned (see FinishBanner).\n const bannerInput = new FinishBannerInput(result.prNumber, result.prUrl, title, base, result.merge);\n process.stdout.write(this.banner.render(bannerInput));\n process.stdout.write(this.banner.linkDirective(bannerInput));\n }\n\n // Validate + commit + finalize a 3-point merge the AI resolved, if one is in progress. Finalizing here\n // does NOT push (pushRemote=false): this command pushes exactly ONCE, from GatedPrPublisher, and only\n // after review.json + every BLOCK checklist + the build gate pass and the gated PR body is written.\n /**\n * The build gate — SKIPPED when stage ②'s receipt already covers this exact HEAD.\n *\n * The gate is authoritative per-COMMIT, not per-command: re-running it on a tree that has not moved\n * since stage ② verified it buys nothing and costs a full `nx affected`. That skip is what makes the\n * three-stage flow cost ONE build rather than two, and it is why moving the gate earlier was affordable.\n */\n private runOrSkipBuildGate(repoRoot: string, alreadyGreen: boolean): void {\n if (alreadyGreen) {\n process.stdout.write('\\n🛠️ Build gate: already green for this commit (stage ② receipt) — skipping the rebuild.\\n');\n return;\n }\n this.buildAffected.runBuildGate(repoRoot, new BuildGateOptions(\n '🛠️ Build gate (authoritative)', 'pnpm wp-finish-upsert-pr', 'Build failed — no PR created/updated.',\n ));\n }\n\n /**\n * The two stage-② preconditions, together: no unvalidated merge, and a receipt proving stage ② ran.\n * Returns true when that receipt covers the CURRENT HEAD, i.e. the build gate can be skipped.\n */\n private assertStageTwoRan(repoRoot: string): boolean {\n this.assertNoUnvalidatedMerge(repoRoot);\n return this.assertReviewStageRan(\n repoRoot, this.aiBranchName.getFeatureName(), this.gitOut(['rev-parse', 'HEAD']));\n }\n\n /**\n * REFUSE while a 3-point merge is still unvalidated — do not finalize it here.\n *\n * This command used to do the finalizing itself. Finalizing means validating a conflict resolution, and\n * two commands owning that means two implementations that can drift; `PrContextWriter`'s docstring\n * records this codebase already paying for exactly that once. It also came too late to matter: by the\n * time finish ran, the reviewers had already reviewed the pre-merge tree.\n *\n * So stage ② owns it, and finish only checks. Nothing is lost — the recovery is one command away, and\n * running it also builds the merged tree and re-briefs the reviewers against it, which finalizing here\n * never did.\n */\n private assertNoUnvalidatedMerge(repoRoot: string): void {\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;\n throw new InformAiError(\n '⛔ NO PR — a 3-point merge on this branch is still unvalidated, so nothing here has been\\n' +\n 'verified: the conflict resolution is unchecked, the merged tree was never built, and any\\n' +\n 'reviewer that ran judged the PRE-merge code.\\n\\n' +\n `Conflicted file(s): ${marker.conflictedFiles.join(', ')}\\n\\n` +\n 'Resolve them (see .webpieces/instruct-ai/webpieces.mergeprocess.md), then run:\\n' +\n ' pnpm wp-review-upsert-pr\\n' +\n 'It validates the resolution, commits it, builds it, and re-briefs the reviewers.',\n );\n }\n\n /**\n * REQUIRE stage ② (`wp-review-upsert-pr`) to have run, and report whether it ran on THIS commit.\n *\n * Returns true when the receipt matches the current HEAD — the caller then skips its own build, because\n * the receipt IS the gate for that sha and rebuilding an unchanged tree is a second full `nx affected`\n * run for a foregone answer.\n *\n * Without this check a repo with NO checklists has nothing forcing stage ②: `assertEveryReviewerRan`\n * is vacuous, and review.json — the only other interlock — is a file the AI writes itself. It could\n * write it and come straight here, skipping the merge validation and the build entirely.\n */\n private assertReviewStageRan(repoRoot: string, featureName: string, headSha: string): boolean {\n const receipt = this.receipts.read(repoRoot, featureName);\n if (receipt === null) {\n throw new InformAiError(\n '⛔ NO PR — stage ② never ran on this branch. That means the 3-point merge is unvalidated,\\n' +\n 'the build gate has not run, no diff was extracted, and no reviewer was briefed.\\n\\n' +\n 'Run: pnpm wp-review-upsert-pr\\n' +\n '(then spawn the reviewers it names, write review.json, and re-run this command)',\n );\n }\n if (receipt.headSha === headSha) return true;\n // Not fatal. Re-reviewing on every follow-up commit would be intolerable, and most drift is a typo\n // fix. But it is never silent: the build re-runs here, and the PR says the reviewers saw an older tree.\n process.stderr.write(\n `\\n⚠️ HEAD moved since stage ② ran (reviewed ${receipt.headSha.slice(0, 8)}, now ${headSha.slice(0, 8)}).\\n` +\n ' The build gate will re-run, and the PR will record that reviewers judged an earlier tree.\\n' +\n ' If the change was substantive, re-run pnpm wp-review-upsert-pr and re-spawn the reviewers.\\n\\n');\n return false;\n }\n\n /**\n * Retire the review this run just used — move review.json to old-review.json beside it, stamped as\n * audit-only (see ReviewJsonService.archiveReviewJson).\n *\n * WHY it moves rather than staying put: review.json left behind is a live-looking file describing a\n * review that has already shipped, and the next `wp-review-upsert-pr` on this branch finds it sitting\n * there. A reviewer subagent that judges the PR's stated INTENT — its title, summary or risk level —\n * then reads the previous round's review and can return GREEN against a title that no longer exists,\n * with nothing in the verdict distinguishing that from a real pass. Moving it makes the only route back\n * to this command a freshly-written review.\n *\n * ONLY on a PR that actually went up. A run that died before publishing must stay re-runnable without\n * making the AI rewrite a review that was never used, and prNumber === '' is exactly that case.\n * Non-fatal: the PR is live by here, so an unwritable archive warns rather than failing the command.\n */\n private archiveConsumedReview(repoRoot: string, featureName: string, result: UpsertResult): void {\n if (result.prNumber === '') return;\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: the PR is already up; a failed archive must not fail the command\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const archived = this.reviewJsonService.archiveReviewJson(reviewJsonPath(repoRoot, featureName));\n if (archived !== '') process.stdout.write(` archived this run's review.json → ${archived} (audit only) ✓\\n`);\n } catch (err: unknown) {\n const error = toError(err);\n process.stderr.write(`⚠️ Could not archive review.json (non-fatal — the PR is already up): ${error.message}\\n`);\n }\n }\n\n private gitOut(args: string[]): string {\n const result = spawnSync('git', args, { encoding: 'utf8' });\n return result.status === 0 ? (result.stdout ?? '').trim() : '';\n }\n\n // The user-facing PR title: the AI-authored review.title, or — if omitted — a readable fallback\n // derived from the stable feature name (NEVER the internal `Squash merge of <branch>` commit subject).\n private prTitleFrom(review: ReviewJson): string {\n if (review.title !== '') return review.title;\n return this.aiBranchName.getFeatureName().replace(/[-/]+/g, ' ').trim();\n }\n\n // eslint-disable-next-line @typescript-eslint/max-params\n private computeDashboardInput(repoRoot: string, buildPassed: boolean, review: ReviewJson, title: string, required: readonly RequiredChecklist[]): DashboardInput {\n const config = loadAndValidate(repoRoot).prGate;\n const forkPoint = this.gitOut(['merge-base', 'origin/main', 'HEAD']);\n const featureHead = this.gitOut(['rev-parse', 'HEAD']);\n const mainHead = this.gitOut(['rev-parse', 'origin/main']);\n const range = `${forkPoint}..${featureHead}`;\n const changedFiles = this.gitOut(['diff', range, '--name-only']).split('\\n').filter((f: string): boolean => f.trim() !== '');\n const patch = this.gitOut(['diff', range]);\n\n const gateResults = this.dashboard.computeGateResults(config.gates, changedFiles);\n const disables = this.dashboard.countAddedDisables(patch);\n const rows = this.checklistRows(required, review);\n return new DashboardInput(title, gateResults, disables, buildPassed, forkPoint, featureHead, mainHead, review, rows);\n }\n\n // Pair each matched checklist with its resolved verdict for the dashboard.\n //\n // A checklist reaching this point is always PASS, WARN or OVERRIDDEN, and the reason is ReviewerVerdictGate\n // — NOT loadReviewJson. The gate runs one line earlier and throws on every other state (FAIL, MISSING,\n // BAD_FORMAT), so loadReviewJson's own checklist validation can no longer be the thing that rejects them;\n // it re-validates the same set and finds it clean. This comment used to credit loadReviewJson, which made\n // the ordering look deliberate while the gate's generic \"no verdict yet\" message masked every refusal.\n // WARN belongs in that list: yellow SHIPS, so it is not outstanding and reaches the dashboard.\n private checklistRows(required: readonly RequiredChecklist[], review: ReviewJson): ChecklistRow[] {\n return required.map((req: RequiredChecklist): ChecklistRow => {\n const verdict = this.reviewJsonService.resolveVerdict(req, review.results);\n return new ChecklistRow(req.id, verdict.status, verdict.detail);\n });\n }\n\n /**\n * One comment row per DEFINED checklist — matched or not — pairing the roster's match evidence with the\n * resolved verdict. Built from `scan.roster` rather than from the applicable set: the skipped ones are\n * absent from `applicable` by construction, and their \"why not\" evidence (the configured globs and the\n * changed-file total) exists nowhere else without recomputing the diff a second way.\n */\n private commentRows(scan: ChecklistScan, review: ReviewJson, provenance: ProvenanceReport): ChecklistCommentRow[] {\n // agentType -> did it open the diff. Absent ⇒ not assessed, which prints nothing (see ChecklistCommentRow.diffRead).\n const readByAgent = new Map<string, boolean>();\n for (const e of provenance.evidence) readByAgent.set(e.agentType, e.readDiff);\n return scan.roster.entries.map((entry: TriggeredChecklist): ChecklistCommentRow => {\n const ran = entry.matchedFiles.length > 0;\n const req = new RequiredChecklist(\n entry.def.id, entry.def.subagent, entry.def.doc, entry.matchedFiles, entry.matchedPatterns);\n // A skipped checklist has no verdict to resolve — asking for one would report it as MISSING,\n // i.e. as an unreviewed obligation, when in fact it never had one.\n const verdict = ran\n ? this.reviewJsonService.resolveVerdict(req, review.results)\n : new ChecklistVerdict(entry.def.id, '', '');\n const row = new ChecklistCommentRow(\n entry.def.subagent, verdict.status, verdict.detail, ran,\n entry.def.patterns, entry.matchedPatterns, entry.matchedFiles, scan.roster.changedFileCount);\n const read = readByAgent.get(entry.def.subagent);\n row.diffRead = read === undefined ? '' : (read ? 'yes' : 'no');\n return row;\n });\n }\n\n // Hidden HMAC gate-token marker (with a leading blank line) to append to the PR body, or '' when the\n // repo sets no gateSalt (byte-identical body to before this feature). Bound to the pushed HEAD sha.\n private gateTokenBody(gateSalt: string, headSha: string): string {\n const marker = this.gateTokenService.gateTokenMarker(gateSalt, headSha);\n return marker === '' ? '' : `\\n\\n${marker}\\n`;\n }\n\n // Enforce that EACH matched checklist was reviewed by its OWN named subagent, as a DISTINCT run —\n // the coding agent may not self-certify, and one reviewer may not stand in for several. A verified set\n // passes silently; no session id warns but passes; any missing reviewer throws so the PR does not open.\n // eslint-disable-next-line @typescript-eslint/max-params\n private enforceProvenance(required: readonly RequiredChecklist[], branch: string, repoRoot: string, config: PrGateConfig): ProvenanceReport {\n const errors: string[] = [];\n const report = new ProvenanceReport(true, []); // no reviewers to verify ⇒ vacuously verified\n const subagents = required.map((r: RequiredChecklist): string => r.subagent.trim()).filter((s: string): boolean => s !== '');\n if (subagents.length > 0) {\n const result = this.provenance.verifyDistinct(subagents, branch);\n report.verified = result.status === PROVENANCE_OK;\n if (result.status === PROVENANCE_MISSING) {\n errors.push(result.detail);\n } else if (result.status === PROVENANCE_SKIPPED) {\n process.stderr.write(`⚠️ ${result.detail}\\n`);\n }\n report.evidence = this.gatherEvidence(repoRoot, required, result, branch);\n errors.push(...this.evidenceErrors(report.evidence, config));\n }\n if (errors.length > 0) {\n throw new InformAiError(\n `${errors.length} checklist(s) require an independent reviewer subagent that did not run — fix, then re-run pnpm wp-finish-upsert-pr:\\n\\n` +\n errors.map((e: string): string => ` • ${e}`).join('\\n') +\n `\\n\\nSpawn the named reviewer subagent to review the checklist on THIS branch, then re-run.`,\n );\n }\n return report;\n }\n\n /**\n * What each credited reviewer actually read. Purely observational here — {@link evidenceErrors} decides\n * whether any of it blocks, and by default none of it does.\n */\n // eslint-disable-next-line @typescript-eslint/max-params\n private gatherEvidence(repoRoot: string, required: readonly RequiredChecklist[], result: ProvenanceResult, branch: string): ReviewerEvidence[] {\n const docPaths: Record<string, string> = {};\n for (const req of required) {\n if (req.subagent.trim() !== '') docPaths[req.subagent] = req.doc.trim() === '' ? '' : path.resolve(repoRoot, req.doc);\n }\n const diffDir = path.join(prDirFor(repoRoot, this.aiBranchName.getFeatureName()), 'diff');\n return this.provenance.evidenceFor(new EvidenceRequest(branch, result.agentIds, diffDir, docPaths));\n }\n\n /**\n * WARN (default) or REFUSE (opt-in) on a reviewer that wrote a verdict without opening the diff.\n *\n * Default-warn because the signal is derived from undocumented Claude Code transcript internals: if the\n * format shifts, a blocking check wedges every PR in every consumer repo with no self-service recovery.\n * `requireDiffEvidence` lets a repo that has watched the warning promote it deliberately.\n */\n private evidenceErrors(evidence: readonly ReviewerEvidence[], config: PrGateConfig): string[] {\n const blind = evidence.filter((e: ReviewerEvidence): boolean => !e.readDiff);\n if (blind.length === 0) return [];\n const names = blind.map((e: ReviewerEvidence): string => e.agentType).join(', ');\n if (!config.requireDiffEvidence) {\n process.stderr.write(\n `\\n⚠️ ${blind.length} reviewer(s) wrote a verdict with no record of opening the extracted diff: ${names}\\n` +\n ' Published on the PR as a note. Not blocking — set pr-gate.requireDiffEvidence:true to make it one.\\n');\n return [];\n }\n return [`these reviewers wrote a verdict without opening the diff (pr-gate.requireDiffEvidence is on): ${names}`];\n }\n\n /**\n * Publish the roster + every reviewer's full `output` as ONE combined PR comment, idempotently (find the\n * marker comment → PATCH it, else POST). Never fatal: by here the PR is already created/updated, so a\n * `gh` failure only warns.\n *\n * Posted on EVERY run of a repo that defines checklists — including one where nothing matched, because\n * \"all five were evaluated and none applied\" is the good news the old comment could not deliver. The\n * guard is `scan.defined.length`, deliberately NOT the number of rows that ran: a repo with no\n * checklists configured must still see no comment at all (see ChecklistNotice / renderDashboard).\n */\n // eslint-disable-next-line @typescript-eslint/max-params\n // eslint-disable-next-line @typescript-eslint/max-params\n private postChecklistComment(repoRoot: string, prNumber: string, scan: ChecklistScan, review: ReviewJson, provenance: ProvenanceReport): void {\n if (prNumber === '' || scan.defined.length === 0) return;\n if (!loadAndValidate(repoRoot).prGate.checklistComments) return;\n const body = this.dashboard.renderChecklistComment(\n this.commentRows(scan, review, provenance), provenance.verified, scan.roster.baseResolved);\n const prDir = prDirFor(repoRoot, this.aiBranchName.getFeatureName());\n fs.mkdirSync(prDir, { recursive: true });\n const payload = path.join(prDir, 'checklist-comment.json');\n fs.writeFileSync(payload, JSON.stringify({ body }));\n const commentId = this.findChecklistCommentId(prNumber);\n const args = commentId !== ''\n ? ['api', '--method', 'PATCH', `repos/{owner}/{repo}/issues/comments/${commentId}`, '--input', payload]\n : ['api', '--method', 'POST', `repos/{owner}/{repo}/issues/${prNumber}/comments`, '--input', payload];\n const res = spawnSync('gh', args, { encoding: 'utf8' });\n if (res.status !== 0) {\n process.stderr.write('⚠️ Could not post the checklist review comment (non-fatal — the PR is already up).\\n');\n } else {\n process.stdout.write(` ${commentId !== '' ? 'updated' : 'posted'} the checklist review comment ✓\\n`);\n }\n }\n\n // The id of THIS tool's existing checklist comment on the PR (by the hidden marker), or '' if none.\n private findChecklistCommentId(prNumber: string): string {\n const res = spawnSync('gh', [\n 'api', '--paginate', `repos/{owner}/{repo}/issues/${prNumber}/comments`,\n '--jq', `.[] | select(.body | contains(\"${CHECKLIST_COMMENT_MARKER}\")) | .id`,\n ], { encoding: 'utf8' });\n if (res.status !== 0) return '';\n return (res.stdout ?? '').trim().split('\\n')[0] ?? '';\n }\n\n // The PR, the remote branch, and the local branch all share the one stable feature name. Look up /\n // create / merge against `baseBranch` (baseBranchName tolerates a leftover `…wpN` mid-transition).\n // GatedPrPublisher owns the edit/push/create half and its ORDERING — the gated body goes up before\n // the push, so CI's `synchronize` read cannot see the previous run's token.\n private upsertPr(repoRoot: string, baseBranch: string, body: string, title: string, input: DashboardInput): UpsertResult {\n const prDir = prDirFor(repoRoot, this.aiBranchName.getFeatureName());\n fs.mkdirSync(prDir, { recursive: true });\n const bodyFile = path.join(prDir, 'pr-body.md');\n fs.writeFileSync(bodyFile, body + '\\n');\n\n const published = this.publisher.publish(baseBranch, title, bodyFile);\n if (published.createFailed) {\n process.stderr.write('⚠️ gh pr create failed — create the PR manually with the body in:\\n ' + bodyFile + '\\n');\n return new UpsertResult('', '', new MergeOutcome(false, false,\n '⚠️ did NOT merge — there is no PR to merge (gh pr create failed above)', MERGE_RESULT_FAILED));\n }\n const num = published.number;\n\n // Set the squash-merge SUBJECT to the PR title (+ the `(#N)` GitHub normally appends, which an\n // explicit --subject would otherwise drop) and the BODY to the compact commit summary, so main's\n // history carries the PR title + risk/flags/link — NOT the internal `Squash merge of <branch>`\n // subject GitHub would inherit from the single squash commit on the branch.\n const ref = this.prRef(baseBranch);\n const subject = ref.number !== '' ? `${title} (#${ref.number})` : title;\n const mergeBodyFile = path.join(prDir, 'merge-commit-body.md');\n fs.writeFileSync(mergeBodyFile, this.dashboard.renderCommitBody(input, ref.url) + '\\n');\n // PrMerger owns the direct-merge / auto-merge-fallback decision AND checks every gh status, so a\n // merge that did not happen is reported as such instead of being swallowed (see pr-merger.ts).\n // REQUIRED config — no default here on purpose. A missing value (an older published\n // rules-config that has no such field) reaches PrMerger as '' and is treated as \"do not merge\".\n const mergeMode = loadAndValidate(repoRoot).prGate.mergeMode ?? '';\n const outcome = this.prMerger.merge(baseBranch, subject, mergeBodyFile, mergeMode);\n return new UpsertResult(ref.number !== '' ? ref.number : num, ref.url, outcome);\n }\n\n // The PR's number + web URL (for the merge subject `(#N)` and the commit-body back-link). Both ''\n // if it can't be resolved. Rendered via jq into one tab-separated line so no JSON parsing is needed.\n private prRef(baseBranch: string): PrRef {\n const result = spawnSync(\n 'gh', ['pr', 'view', baseBranch, '--json', 'number,url', '--jq', '\"\\\\(.number)\\\\t\\\\(.url)\"'],\n { encoding: 'utf8' },\n );\n if (result.status !== 0) {\n return new PrRef('', '');\n }\n const parts = (result.stdout ?? '').trim().split('\\t');\n return new PrRef(parts[0] ?? '', parts[1] ?? '');\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"finish-upsert-pr-command.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/commands/finish-upsert-pr-command.ts"],"names":[],"mappings":"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","sourcesContent":["import { execSync, spawnSync } from 'child_process';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport {\n loadAndValidate, prDirFor, reviewJsonPath, ReviewJson, RequiredChecklist, ChecklistVerdict,\n writeTemplate, RepoRootFinder, ReviewJsonService,\n GateTokenService, SubagentProvenanceService, PROVENANCE_OK, PROVENANCE_MISSING, PROVENANCE_SKIPPED,\n ProvenanceResult, ReviewerEvidence, EvidenceRequest, PrGateConfig,\n ReviewProvenanceService, ProvenanceWriteRequest, ReviewerTranscript, ReviewerPaths, OfferedContext,\n ReviewerInstructionsService,\n InformAiError, toError,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { AiBranchName } from '../workflow/git-readAiBranchName';\nimport { BranchNaming } from '../workflow/branch-naming';\nimport { ChecklistScan, ChecklistScanOptions, ChecklistScanner } from '../workflow/checklist-scanner';\nimport { ReviewerVerdictGate } from '../workflow/reviewer-verdict-gate';\nimport { GitExec } from '../workflow/git-exec';\nimport { BuildAffected, BuildGateOptions } from '../workflow/build-affected';\nimport { MergeState } from '../workflow/merge-state';\nimport { ReviewStageReceiptService } from '../workflow/review-stage-receipt';\nimport { PrMerger, MergeOutcome, MERGE_RESULT_FAILED } from '../workflow/pr-merger';\nimport { FinishBanner, FinishBannerInput } from '../workflow/finish-banner';\nimport { GatedPrPublisher } from '../workflow/gated-pr-publisher';\nimport { TriggeredChecklist } from '../workflow/checklist-detector';\nimport {\n Dashboard, DashboardInput, ChecklistRow, ChecklistCommentRow, CHECKLIST_COMMENT_MARKER,\n} from '../../dashboard/dashboard';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n/**\n * The provenance outcome: whether each reviewer was VERIFIED to have run (the integrity check, which\n * blocks) plus what each one actually read (the quality signal, which is published). Data-only.\n */\nclass ProvenanceReport {\n verified: boolean;\n evidence: ReviewerEvidence[];\n\n constructor(verified: boolean, evidence: ReviewerEvidence[]) {\n this.verified = verified;\n this.evidence = evidence;\n }\n}\n\n// A resolved PR's number + web URL. Both '' when the PR can't be resolved (e.g. create failed).\nclass PrRef {\n number: string;\n url: string;\n\n constructor(number: string, url: string) {\n this.number = number;\n this.url = url;\n }\n}\n\n// The outcome of the whole upsert: the PR number + web URL ('' each when unresolved) plus what actually\n// happened to the merge, so the final summary reports the REAL result rather than assuming success. The\n// URL is carried so the closing block can hand the AI a clickable link to relay to the user verbatim.\nclass UpsertResult {\n prNumber: string;\n prUrl: string;\n merge: MergeOutcome;\n\n constructor(prNumber: string, prUrl: string, merge: MergeOutcome) {\n this.prNumber = prNumber;\n this.prUrl = prUrl;\n this.merge = merge;\n }\n}\n\n// STAGE ③ — FINISH of the AI-first PR flow, and the ONLY command that posts PRs. Runs after\n// `wp-review-upsert-pr` verified the branch and the AI spawned the reviewers + wrote review.json.\n//\n// In order: (1) REFUSE on an unvalidated 3-point merge and REQUIRE stage ②'s receipt; (2) REQUIRE\n// review.json + every reviewer's verdict + provenance; (3) run the build gate UNLESS the receipt already\n// covers this exact HEAD; (4) render the dashboard; (5) create/update the PR via `gh`.\n//\n// It no longer FINALIZES a merge — stage ② does. Two commands owning conflict-resolution validation is two\n// implementations that drift, and finalizing here came too late to help anyway: the reviewers had already\n// reviewed the pre-merge tree by then.\n@injectable(bindingScopeValues.Singleton)\nexport class FinishUpsertPrCommand {\n constructor(\n private readonly repoRootFinder: RepoRootFinder,\n private readonly aiBranchName: AiBranchName,\n private readonly branchNaming: BranchNaming,\n private readonly gitExec: GitExec,\n private readonly buildAffected: BuildAffected,\n private readonly mergeState: MergeState,\n private readonly prMerger: PrMerger,\n private readonly publisher: GatedPrPublisher,\n private readonly dashboard: Dashboard,\n private readonly checklistScanner: ChecklistScanner,\n private readonly verdictGate: ReviewerVerdictGate,\n private readonly reviewJsonService: ReviewJsonService,\n private readonly gateTokenService: GateTokenService,\n private readonly provenance: SubagentProvenanceService,\n private readonly provenanceRecord: ReviewProvenanceService,\n private readonly reviewerInstructions: ReviewerInstructionsService,\n // NOTE: ChecklistInstructionsService is deliberately NOT injected here any more. Its \"You MUST run\n // these N reviewer subagent(s)\" block is now rendered by ReviewerVerdictGate and ONLY for checklists\n // that genuinely never ran, so no other code path in this command can print it at a refusal.\n private readonly receipts: ReviewStageReceiptService,\n private readonly banner: FinishBanner,\n ) {}\n\n async run(): Promise<void> {\n const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());\n // Refresh the AI-facing workflow doc so it's present + current for any failure message to cite.\n writeTemplate(repoRoot, 'webpieces.git-workflow.md');\n // 1. REQUIRE stage ② — see assertStageTwoRan. Returns true when its receipt covers THIS commit,\n // which is what lets the build gate below be skipped rather than re-run for a foregone answer.\n const buildAlreadyGreen = this.assertStageTwoRan(repoRoot);\n\n // 2. REQUIRE the AI-authored review.json (throws InformAiError with the schema if missing/invalid).\n // Compute the consumer checklists this diff triggered FIRST so an unacknowledged BLOCK throws\n // here — BEFORE any `gh pr create` — matching the guarantee buildCommand already provides.\n // The SAME scan wp-review-upsert-pr runs, with filterAlreadyReviewed:true so `outstanding` is exactly what\n // still owes a verdict (Z of N of X). Sharing the computation is the point: the command that REPORTS\n // and the command that GATES must not be able to disagree about what is owed. review-<id>.json files\n // persist locally, so a re-run re-validates the EXISTING verdicts against the (possibly changed)\n // applicable set for free — an unchanged checklist needs no re-review, a newly-applicable one refuses\n // until its file is written.\n const featureName = this.aiBranchName.getFeatureName();\n const scan = this.checklistScanner.scan(repoRoot, loadAndValidate(repoRoot).prGate.checklists, new ChecklistScanOptions(true, 'stage3-finish'));\n const required = scan.applicable;\n // FAIL FAST on an unclear checklist, BEFORE review.json is parsed and before the build gate runs. A\n // missing reviewer is not a review.json defect (folding it in made the AI fix the wrong thing), and\n // nobody should wait on a build to be told a reviewer never ran. ReviewerVerdictGate owns the\n // distinction between unreadable / REFUSED / never-ran, and retires the red verdicts it acts on.\n this.verdictGate.assertEveryReviewerRan(scan);\n const review = this.reviewJsonService.loadReviewJson(reviewJsonPath(repoRoot, featureName), required);\n\n // 2c. For any BLOCK checklist that names a reviewer `subagent`, VERIFY (from the harness's own\n // artifacts) that such a subagent actually ran on this branch — the coding agent may not\n // self-certify. Absent CLAUDE_CODE_SESSION_ID this skips with a warning (CI / plain terminal).\n const currentBranch = execSync('git branch --show-current', { encoding: 'utf8' }).trim();\n const provenance = this.enforceProvenance(required, currentBranch, repoRoot, loadAndValidate(repoRoot).prGate);\n\n // 2b. The build gate validates the WORKING TREE but we push HEAD — so they MUST be identical.\n this.gitExec.assertCleanTree(repoRoot);\n\n // 3. Build gate, then post the gated body, then push (that ORDER — see GatedPrPublisher).\n this.runOrSkipBuildGate(repoRoot, buildAlreadyGreen);\n const base = this.branchNaming.baseBranchName(execSync('git branch --show-current', { encoding: 'utf8' }).trim());\n\n process.stdout.write('\\n' + SEP + '📋 Dashboard + PR\\n' + SEP + '\\n');\n const title = this.prTitleFrom(review);\n const input = this.computeDashboardInput(repoRoot, true, review, title, required);\n // Append the hidden HMAC gate token bound to the LOCAL HEAD sha — computed BEFORE the push, because\n // GatedPrPublisher writes the body first so CI's `synchronize` read can never see a stale token. A\n // valid token in the PR body is proof this gated flow ran + passed on this exact commit, which CI\n // (`wp-check-pr`) recomputes. We reach here only after the build gate + every BLOCK checklist\n // passed, so minting is legitimate. Nothing about HMAC(salt, HEAD) needs the remote to have it.\n const gateSalt = loadAndValidate(repoRoot).prGate.gateSalt;\n const headSha = this.gitOut(['rev-parse', 'HEAD']);\n const body = this.dashboard.renderDashboard(input) + this.gateTokenBody(gateSalt, headSha);\n const result = this.upsertPr(repoRoot, base, body, title, input);\n // Publish each reviewer's full output as ONE combined PR comment (idempotent, opt-out-aware). Never\n // fatal — the PR is already up by now, so a comment failure only warns.\n this.postChecklistComment(repoRoot, result.prNumber, scan, review, provenance);\n this.archiveConsumedReview(repoRoot, featureName, result);\n\n // The closing recap + the clickable-link directive, BOTH derived from the real merge outcome.\n // Nothing here may hard-code success: a stranded PR under a green checkmark is how PRs got\n // abandoned (see FinishBanner).\n const bannerInput = new FinishBannerInput(result.prNumber, result.prUrl, title, base, result.merge);\n process.stdout.write(this.banner.render(bannerInput));\n process.stdout.write(this.banner.linkDirective(bannerInput));\n }\n\n // Validate + commit + finalize a 3-point merge the AI resolved, if one is in progress. Finalizing here\n // does NOT push (pushRemote=false): this command pushes exactly ONCE, from GatedPrPublisher, and only\n // after review.json + every BLOCK checklist + the build gate pass and the gated PR body is written.\n /**\n * The build gate — SKIPPED when stage ②'s receipt already covers this exact HEAD.\n *\n * The gate is authoritative per-COMMIT, not per-command: re-running it on a tree that has not moved\n * since stage ② verified it buys nothing and costs a full `nx affected`. That skip is what makes the\n * three-stage flow cost ONE build rather than two, and it is why moving the gate earlier was affordable.\n */\n private runOrSkipBuildGate(repoRoot: string, alreadyGreen: boolean): void {\n if (alreadyGreen) {\n process.stdout.write('\\n🛠️ Build gate: already green for this commit (stage ② receipt) — skipping the rebuild.\\n');\n return;\n }\n this.buildAffected.runBuildGate(repoRoot, new BuildGateOptions(\n '🛠️ Build gate (authoritative)', 'pnpm wp-finish-upsert-pr', 'Build failed — no PR created/updated.',\n ));\n }\n\n /**\n * The two stage-② preconditions, together: no unvalidated merge, and a receipt proving stage ② ran.\n * Returns true when that receipt covers the CURRENT HEAD, i.e. the build gate can be skipped.\n */\n private assertStageTwoRan(repoRoot: string): boolean {\n this.assertNoUnvalidatedMerge(repoRoot);\n return this.assertReviewStageRan(\n repoRoot, this.aiBranchName.getFeatureName(), this.gitOut(['rev-parse', 'HEAD']));\n }\n\n /**\n * REFUSE while a 3-point merge is still unvalidated — do not finalize it here.\n *\n * This command used to do the finalizing itself. Finalizing means validating a conflict resolution, and\n * two commands owning that means two implementations that can drift; `PrContextWriter`'s docstring\n * records this codebase already paying for exactly that once. It also came too late to matter: by the\n * time finish ran, the reviewers had already reviewed the pre-merge tree.\n *\n * So stage ② owns it, and finish only checks. Nothing is lost — the recovery is one command away, and\n * running it also builds the merged tree and re-briefs the reviewers against it, which finalizing here\n * never did.\n */\n private assertNoUnvalidatedMerge(repoRoot: string): void {\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;\n throw new InformAiError(\n '⛔ NO PR — a 3-point merge on this branch is still unvalidated, so nothing here has been\\n' +\n 'verified: the conflict resolution is unchecked, the merged tree was never built, and any\\n' +\n 'reviewer that ran judged the PRE-merge code.\\n\\n' +\n `Conflicted file(s): ${marker.conflictedFiles.join(', ')}\\n\\n` +\n 'Resolve them (see .webpieces/instruct-ai/webpieces.mergeprocess.md), then run:\\n' +\n ' pnpm wp-review-upsert-pr\\n' +\n 'It validates the resolution, commits it, builds it, and re-briefs the reviewers.',\n );\n }\n\n /**\n * REQUIRE stage ② (`wp-review-upsert-pr`) to have run, and report whether it ran on THIS commit.\n *\n * Returns true when the receipt matches the current HEAD — the caller then skips its own build, because\n * the receipt IS the gate for that sha and rebuilding an unchanged tree is a second full `nx affected`\n * run for a foregone answer.\n *\n * Without this check a repo with NO checklists has nothing forcing stage ②: `assertEveryReviewerRan`\n * is vacuous, and review.json — the only other interlock — is a file the AI writes itself. It could\n * write it and come straight here, skipping the merge validation and the build entirely.\n */\n private assertReviewStageRan(repoRoot: string, featureName: string, headSha: string): boolean {\n const receipt = this.receipts.read(repoRoot, featureName);\n if (receipt === null) {\n throw new InformAiError(\n '⛔ NO PR — stage ② never ran on this branch. That means the 3-point merge is unvalidated,\\n' +\n 'the build gate has not run, no diff was extracted, and no reviewer was briefed.\\n\\n' +\n 'Run: pnpm wp-review-upsert-pr\\n' +\n '(then spawn the reviewers it names, write review.json, and re-run this command)',\n );\n }\n if (receipt.headSha === headSha) return true;\n // Not fatal. Re-reviewing on every follow-up commit would be intolerable, and most drift is a typo\n // fix. But it is never silent: the build re-runs here, and the PR says the reviewers saw an older tree.\n process.stderr.write(\n `\\n⚠️ HEAD moved since stage ② ran (reviewed ${receipt.headSha.slice(0, 8)}, now ${headSha.slice(0, 8)}).\\n` +\n ' The build gate will re-run, and the PR will record that reviewers judged an earlier tree.\\n' +\n ' If the change was substantive, re-run pnpm wp-review-upsert-pr and re-spawn the reviewers.\\n\\n');\n return false;\n }\n\n /**\n * Retire the review this run just used — move review.json to old-review.json beside it, stamped as\n * audit-only (see ReviewJsonService.archiveReviewJson).\n *\n * WHY it moves rather than staying put: review.json left behind is a live-looking file describing a\n * review that has already shipped, and the next `wp-review-upsert-pr` on this branch finds it sitting\n * there. A reviewer subagent that judges the PR's stated INTENT — its title, summary or risk level —\n * then reads the previous round's review and can return GREEN against a title that no longer exists,\n * with nothing in the verdict distinguishing that from a real pass. Moving it makes the only route back\n * to this command a freshly-written review.\n *\n * ONLY on a PR that actually went up. A run that died before publishing must stay re-runnable without\n * making the AI rewrite a review that was never used, and prNumber === '' is exactly that case.\n * Non-fatal: the PR is live by here, so an unwritable archive warns rather than failing the command.\n */\n private archiveConsumedReview(repoRoot: string, featureName: string, result: UpsertResult): void {\n if (result.prNumber === '') return;\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: the PR is already up; a failed archive must not fail the command\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const archived = this.reviewJsonService.archiveReviewJson(reviewJsonPath(repoRoot, featureName));\n if (archived !== '') process.stdout.write(` archived this run's review.json → ${archived} (audit only) ✓\\n`);\n // Beside it, so an archived review keeps the transcript links belonging to the round that\n // produced it — a review whose provenance was overwritten by the NEXT round audits nothing.\n this.provenanceRecord.archive(prDirFor(repoRoot, featureName));\n } catch (err: unknown) {\n const error = toError(err);\n process.stderr.write(`⚠️ Could not archive review.json (non-fatal — the PR is already up): ${error.message}\\n`);\n }\n }\n\n private gitOut(args: string[]): string {\n const result = spawnSync('git', args, { encoding: 'utf8' });\n return result.status === 0 ? (result.stdout ?? '').trim() : '';\n }\n\n // The user-facing PR title: the AI-authored review.title, or — if omitted — a readable fallback\n // derived from the stable feature name (NEVER the internal `Squash merge of <branch>` commit subject).\n private prTitleFrom(review: ReviewJson): string {\n if (review.title !== '') return review.title;\n return this.aiBranchName.getFeatureName().replace(/[-/]+/g, ' ').trim();\n }\n\n // eslint-disable-next-line @typescript-eslint/max-params\n private computeDashboardInput(repoRoot: string, buildPassed: boolean, review: ReviewJson, title: string, required: readonly RequiredChecklist[]): DashboardInput {\n const config = loadAndValidate(repoRoot).prGate;\n const forkPoint = this.gitOut(['merge-base', 'origin/main', 'HEAD']);\n const featureHead = this.gitOut(['rev-parse', 'HEAD']);\n const mainHead = this.gitOut(['rev-parse', 'origin/main']);\n const range = `${forkPoint}..${featureHead}`;\n const changedFiles = this.gitOut(['diff', range, '--name-only']).split('\\n').filter((f: string): boolean => f.trim() !== '');\n const patch = this.gitOut(['diff', range]);\n\n const gateResults = this.dashboard.computeGateResults(config.gates, changedFiles);\n const disables = this.dashboard.countAddedDisables(patch);\n const rows = this.checklistRows(required, review);\n return new DashboardInput(title, gateResults, disables, buildPassed, forkPoint, featureHead, mainHead, review, rows);\n }\n\n // Pair each matched checklist with its resolved verdict for the dashboard.\n //\n // A checklist reaching this point is always PASS, WARN or OVERRIDDEN, and the reason is ReviewerVerdictGate\n // — NOT loadReviewJson. The gate runs one line earlier and throws on every other state (FAIL, MISSING,\n // BAD_FORMAT), so loadReviewJson's own checklist validation can no longer be the thing that rejects them;\n // it re-validates the same set and finds it clean. This comment used to credit loadReviewJson, which made\n // the ordering look deliberate while the gate's generic \"no verdict yet\" message masked every refusal.\n // WARN belongs in that list: yellow SHIPS, so it is not outstanding and reaches the dashboard.\n private checklistRows(required: readonly RequiredChecklist[], review: ReviewJson): ChecklistRow[] {\n return required.map((req: RequiredChecklist): ChecklistRow => {\n const verdict = this.reviewJsonService.resolveVerdict(req, review.results);\n return new ChecklistRow(req.id, verdict.status, verdict.detail);\n });\n }\n\n /**\n * One comment row per DEFINED checklist — matched or not — pairing the roster's match evidence with the\n * resolved verdict. Built from `scan.roster` rather than from the applicable set: the skipped ones are\n * absent from `applicable` by construction, and their \"why not\" evidence (the configured globs and the\n * changed-file total) exists nowhere else without recomputing the diff a second way.\n */\n private commentRows(scan: ChecklistScan, review: ReviewJson, provenance: ProvenanceReport): ChecklistCommentRow[] {\n // agentType -> did it open the diff. Absent ⇒ not assessed, which prints nothing (see ChecklistCommentRow.diffRead).\n const readByAgent = new Map<string, boolean>();\n for (const e of provenance.evidence) readByAgent.set(e.agentType, e.readDiff);\n return scan.roster.entries.map((entry: TriggeredChecklist): ChecklistCommentRow => {\n const ran = entry.matchedFiles.length > 0;\n const req = new RequiredChecklist(\n entry.def.id, entry.def.subagent, entry.def.doc, entry.matchedFiles, entry.matchedPatterns);\n // A skipped checklist has no verdict to resolve — asking for one would report it as MISSING,\n // i.e. as an unreviewed obligation, when in fact it never had one.\n const verdict = ran\n ? this.reviewJsonService.resolveVerdict(req, review.results)\n : new ChecklistVerdict(entry.def.id, '', '');\n const row = new ChecklistCommentRow(\n entry.def.subagent, verdict.status, verdict.detail, ran,\n entry.def.patterns, entry.matchedPatterns, entry.matchedFiles, scan.roster.changedFileCount);\n const read = readByAgent.get(entry.def.subagent);\n row.diffRead = read === undefined ? '' : (read ? 'yes' : 'no');\n return row;\n });\n }\n\n // Hidden HMAC gate-token marker (with a leading blank line) to append to the PR body, or '' when the\n // repo sets no gateSalt (byte-identical body to before this feature). Bound to the pushed HEAD sha.\n private gateTokenBody(gateSalt: string, headSha: string): string {\n const marker = this.gateTokenService.gateTokenMarker(gateSalt, headSha);\n return marker === '' ? '' : `\\n\\n${marker}\\n`;\n }\n\n // Enforce that EACH matched checklist was reviewed by its OWN named subagent, as a DISTINCT run —\n // the coding agent may not self-certify, and one reviewer may not stand in for several. A verified set\n // passes silently; no session id warns but passes; any missing reviewer throws so the PR does not open.\n // eslint-disable-next-line @typescript-eslint/max-params\n private enforceProvenance(required: readonly RequiredChecklist[], branch: string, repoRoot: string, config: PrGateConfig): ProvenanceReport {\n const errors: string[] = [];\n const report = new ProvenanceReport(true, []); // no reviewers to verify ⇒ vacuously verified\n const subagents = required.map((r: RequiredChecklist): string => r.subagent.trim()).filter((s: string): boolean => s !== '');\n // verifyDistinct short-circuits to OK on an empty set, so this runs unconditionally: a repo with no\n // checklists still gets a provenance record naming the session and the main agent's own transcript.\n const result = this.provenance.verifyDistinct(subagents, branch);\n report.verified = result.status === PROVENANCE_OK;\n if (result.status === PROVENANCE_MISSING) {\n errors.push(result.detail);\n } else if (result.status === PROVENANCE_SKIPPED) {\n process.stderr.write(`⚠️ ${result.detail}\\n`);\n }\n report.evidence = this.gatherEvidence(repoRoot, required, result, branch);\n errors.push(...this.evidenceErrors(report.evidence, config));\n // BEFORE the throw below, deliberately. A refused round is the one most worth auditing, and a record\n // that only ever appeared on success could not answer what the reviewers did the time it was refused.\n this.writeProvenanceRecord(repoRoot, required, result, report.evidence);\n if (errors.length > 0) {\n throw new InformAiError(\n `${errors.length} checklist(s) require an independent reviewer subagent that did not run — fix, then re-run pnpm wp-finish-upsert-pr:\\n\\n` +\n errors.map((e: string): string => ` • ${e}`).join('\\n') +\n `\\n\\nSpawn the named reviewer subagent to review the checklist on THIS branch, then re-run.`,\n );\n }\n return report;\n }\n\n /**\n * What each credited reviewer actually read. Purely observational here — {@link evidenceErrors} decides\n * whether any of it blocks, and by default none of it does.\n */\n // eslint-disable-next-line @typescript-eslint/max-params\n private gatherEvidence(repoRoot: string, required: readonly RequiredChecklist[], result: ProvenanceResult, branch: string): ReviewerEvidence[] {\n const docPaths: Record<string, string> = {};\n for (const req of required) {\n if (req.subagent.trim() !== '') docPaths[req.subagent] = req.doc.trim() === '' ? '' : path.resolve(repoRoot, req.doc);\n }\n const diffDir = path.join(prDirFor(repoRoot, this.aiBranchName.getFeatureName()), 'diff');\n return this.provenance.evidenceFor(new EvidenceRequest(branch, result.agentIds, diffDir, docPaths));\n }\n\n /**\n * Write this round's audit record: `.webpieces/pr-review/<branch>/provenance.json`, linking each verdict\n * to the transcript of the subagent that produced it.\n *\n * A SEPARATE file rather than a field inside review.json / review-<id>.json, for two reasons. The\n * reviewer cannot supply this itself — a subagent's environment exposes the PARENT session id and no\n * agent id, so a self-reported transcript link would be invented — and keeping the AI-authored files\n * byte-untouched means nothing in the record can be mistaken for something a reviewer claimed about\n * itself. Every path here is derived by the tooling from the harness's own artifacts.\n *\n * Never fatal: an unwritable record is a lost audit trail, not a reason to refuse a PR.\n */\n // eslint-disable-next-line @typescript-eslint/max-params\n private writeProvenanceRecord(repoRoot: string, required: readonly RequiredChecklist[], result: ProvenanceResult, evidence: readonly ReviewerEvidence[]): void {\n const featureName = this.aiBranchName.getFeatureName();\n const prDir = prDirFor(repoRoot, featureName);\n const request = new ProvenanceWriteRequest(prDir, featureName, this.gitOut(['rev-parse', 'HEAD']), result.status);\n request.offered = new OfferedContext(\n path.join(prDir, 'diff'), this.reviewerInstructions.instructionsDirFor(repoRoot, featureName));\n request.reviewers = evidence.map((e: ReviewerEvidence): ReviewerTranscript =>\n new ReviewerTranscript(e, this.reviewerPathsFor(repoRoot, featureName, required, e.agentType)));\n const written = this.provenanceRecord.write(request);\n if (written !== '') process.stdout.write(` transcript provenance → ${written}\\n`);\n }\n\n // Where ONE reviewer's verdict, instructions and checklist doc live. Keyed by agentType, which IS the\n // checklist id: ChecklistDefinition sets `id = subagent`, so the two never diverge.\n // eslint-disable-next-line @typescript-eslint/max-params\n private reviewerPathsFor(repoRoot: string, featureName: string, required: readonly RequiredChecklist[], agentType: string): ReviewerPaths {\n const req = required.find((r: RequiredChecklist): boolean => r.subagent.trim() === agentType);\n const doc = req !== undefined && req.doc.trim() !== '' ? path.resolve(repoRoot, req.doc) : '';\n return new ReviewerPaths(\n this.reviewJsonService.checklistResultPath(reviewJsonPath(repoRoot, featureName), req?.id ?? agentType),\n this.reviewerInstructions.pathFor(repoRoot, featureName, agentType),\n doc,\n );\n }\n\n /**\n * WARN (default) or REFUSE (opt-in) on a reviewer that wrote a verdict without opening the diff.\n *\n * Default-warn because the signal is derived from undocumented Claude Code transcript internals: if the\n * format shifts, a blocking check wedges every PR in every consumer repo with no self-service recovery.\n * `requireDiffEvidence` lets a repo that has watched the warning promote it deliberately.\n */\n private evidenceErrors(evidence: readonly ReviewerEvidence[], config: PrGateConfig): string[] {\n const blind = evidence.filter((e: ReviewerEvidence): boolean => !e.readDiff);\n if (blind.length === 0) return [];\n const names = blind.map((e: ReviewerEvidence): string => e.agentType).join(', ');\n if (!config.requireDiffEvidence) {\n process.stderr.write(\n `\\n⚠️ ${blind.length} reviewer(s) wrote a verdict with no record of opening the extracted diff: ${names}\\n` +\n ' Published on the PR as a note. Not blocking — set pr-gate.requireDiffEvidence:true to make it one.\\n');\n return [];\n }\n return [`these reviewers wrote a verdict without opening the diff (pr-gate.requireDiffEvidence is on): ${names}`];\n }\n\n /**\n * Publish the roster + every reviewer's full `output` as ONE combined PR comment, idempotently (find the\n * marker comment → PATCH it, else POST). Never fatal: by here the PR is already created/updated, so a\n * `gh` failure only warns.\n *\n * Posted on EVERY run of a repo that defines checklists — including one where nothing matched, because\n * \"all five were evaluated and none applied\" is the good news the old comment could not deliver. The\n * guard is `scan.defined.length`, deliberately NOT the number of rows that ran: a repo with no\n * checklists configured must still see no comment at all (see ChecklistNotice / renderDashboard).\n */\n // eslint-disable-next-line @typescript-eslint/max-params\n // eslint-disable-next-line @typescript-eslint/max-params\n private postChecklistComment(repoRoot: string, prNumber: string, scan: ChecklistScan, review: ReviewJson, provenance: ProvenanceReport): void {\n if (prNumber === '' || scan.defined.length === 0) return;\n if (!loadAndValidate(repoRoot).prGate.checklistComments) return;\n const body = this.dashboard.renderChecklistComment(\n this.commentRows(scan, review, provenance), provenance.verified, scan.roster.baseResolved);\n const prDir = prDirFor(repoRoot, this.aiBranchName.getFeatureName());\n fs.mkdirSync(prDir, { recursive: true });\n const payload = path.join(prDir, 'checklist-comment.json');\n fs.writeFileSync(payload, JSON.stringify({ body }));\n const commentId = this.findChecklistCommentId(prNumber);\n const args = commentId !== ''\n ? ['api', '--method', 'PATCH', `repos/{owner}/{repo}/issues/comments/${commentId}`, '--input', payload]\n : ['api', '--method', 'POST', `repos/{owner}/{repo}/issues/${prNumber}/comments`, '--input', payload];\n const res = spawnSync('gh', args, { encoding: 'utf8' });\n if (res.status !== 0) {\n process.stderr.write('⚠️ Could not post the checklist review comment (non-fatal — the PR is already up).\\n');\n } else {\n process.stdout.write(` ${commentId !== '' ? 'updated' : 'posted'} the checklist review comment ✓\\n`);\n }\n }\n\n // The id of THIS tool's existing checklist comment on the PR (by the hidden marker), or '' if none.\n private findChecklistCommentId(prNumber: string): string {\n const res = spawnSync('gh', [\n 'api', '--paginate', `repos/{owner}/{repo}/issues/${prNumber}/comments`,\n '--jq', `.[] | select(.body | contains(\"${CHECKLIST_COMMENT_MARKER}\")) | .id`,\n ], { encoding: 'utf8' });\n if (res.status !== 0) return '';\n return (res.stdout ?? '').trim().split('\\n')[0] ?? '';\n }\n\n // The PR, the remote branch, and the local branch all share the one stable feature name. Look up /\n // create / merge against `baseBranch` (baseBranchName tolerates a leftover `…wpN` mid-transition).\n // GatedPrPublisher owns the edit/push/create half and its ORDERING — the gated body goes up before\n // the push, so CI's `synchronize` read cannot see the previous run's token.\n private upsertPr(repoRoot: string, baseBranch: string, body: string, title: string, input: DashboardInput): UpsertResult {\n const prDir = prDirFor(repoRoot, this.aiBranchName.getFeatureName());\n fs.mkdirSync(prDir, { recursive: true });\n const bodyFile = path.join(prDir, 'pr-body.md');\n fs.writeFileSync(bodyFile, body + '\\n');\n\n const published = this.publisher.publish(baseBranch, title, bodyFile);\n if (published.createFailed) {\n process.stderr.write('⚠️ gh pr create failed — create the PR manually with the body in:\\n ' + bodyFile + '\\n');\n return new UpsertResult('', '', new MergeOutcome(false, false,\n '⚠️ did NOT merge — there is no PR to merge (gh pr create failed above)', MERGE_RESULT_FAILED));\n }\n const num = published.number;\n\n // Set the squash-merge SUBJECT to the PR title (+ the `(#N)` GitHub normally appends, which an\n // explicit --subject would otherwise drop) and the BODY to the compact commit summary, so main's\n // history carries the PR title + risk/flags/link — NOT the internal `Squash merge of <branch>`\n // subject GitHub would inherit from the single squash commit on the branch.\n const ref = this.prRef(baseBranch);\n const subject = ref.number !== '' ? `${title} (#${ref.number})` : title;\n const mergeBodyFile = path.join(prDir, 'merge-commit-body.md');\n fs.writeFileSync(mergeBodyFile, this.dashboard.renderCommitBody(input, ref.url) + '\\n');\n // PrMerger owns the direct-merge / auto-merge-fallback decision AND checks every gh status, so a\n // merge that did not happen is reported as such instead of being swallowed (see pr-merger.ts).\n // REQUIRED config — no default here on purpose. A missing value (an older published\n // rules-config that has no such field) reaches PrMerger as '' and is treated as \"do not merge\".\n const mergeMode = loadAndValidate(repoRoot).prGate.mergeMode ?? '';\n const outcome = this.prMerger.merge(baseBranch, subject, mergeBodyFile, mergeMode);\n return new UpsertResult(ref.number !== '' ? ref.number : num, ref.url, outcome);\n }\n\n // The PR's number + web URL (for the merge subject `(#N)` and the commit-body back-link). Both ''\n // if it can't be resolved. Rendered via jq into one tab-separated line so no JSON parsing is needed.\n private prRef(baseBranch: string): PrRef {\n const result = spawnSync(\n 'gh', ['pr', 'view', baseBranch, '--json', 'number,url', '--jq', '\"\\\\(.number)\\\\t\\\\(.url)\"'],\n { encoding: 'utf8' },\n );\n if (result.status !== 0) {\n return new PrRef('', '');\n }\n const parts = (result.stdout ?? '').trim().split('\\t');\n return new PrRef(parts[0] ?? '', parts[1] ?? '');\n }\n}\n"]}
|
|
@@ -2,14 +2,23 @@ import { ReviewJsonService } from '@webpieces/rules-config';
|
|
|
2
2
|
import { DiffBasis } from './diff-basis';
|
|
3
3
|
export declare const FILE_DIFF_MAX_BYTES: number;
|
|
4
4
|
export declare const ALL_DIFF_MAX_BYTES: number;
|
|
5
|
-
/**
|
|
5
|
+
/**
|
|
6
|
+
* One materialized file's diff. Data-only (per CLAUDE.md).
|
|
7
|
+
*
|
|
8
|
+
* Both paths are recorded TWICE, relative and absolute, deliberately. Relative is the stable identity that
|
|
9
|
+
* survives the repo being cloned somewhere else; absolute is the only form a reviewer subagent can actually
|
|
10
|
+
* open, because its working directory is not guaranteed to be the repo root. Recording only the relative
|
|
11
|
+
* form is what turned "read this file" back into a search — the exact cost this whole feature removes.
|
|
12
|
+
*/
|
|
6
13
|
export declare class DiffManifestEntry {
|
|
7
14
|
file: string;
|
|
15
|
+
fileAbs: string;
|
|
8
16
|
diffFile: string;
|
|
17
|
+
diffAbs: string;
|
|
9
18
|
status: string;
|
|
10
19
|
bytes: number;
|
|
11
20
|
truncated: boolean;
|
|
12
|
-
constructor(file: string, diffFile: string, status: string, bytes: number, truncated?: boolean);
|
|
21
|
+
constructor(repoRoot: string, file: string, diffFile: string, status: string, bytes: number, truncated?: boolean);
|
|
13
22
|
}
|
|
14
23
|
/** The authoritative map from a mangled `.diff` name back to its real path, plus what was dropped. */
|
|
15
24
|
export declare class DiffManifest {
|
|
@@ -23,6 +32,8 @@ export declare class DiffManifest {
|
|
|
23
32
|
entries: DiffManifestEntry[];
|
|
24
33
|
excluded: string[];
|
|
25
34
|
omittedFromAllDiff: string[];
|
|
35
|
+
allDiffLines: number;
|
|
36
|
+
allDiffBytes: number;
|
|
26
37
|
constructor(base: string, head: string, dirty: boolean, diffCommand: string, entries?: DiffManifestEntry[], excluded?: string[], omittedFromAllDiff?: string[], hashMainHead?: string);
|
|
27
38
|
}
|
|
28
39
|
/**
|
|
@@ -74,9 +85,21 @@ export declare class DiffMaterializer {
|
|
|
74
85
|
private capped;
|
|
75
86
|
/**
|
|
76
87
|
* `ALL.diff` — the single Read that answers "what changed on this branch?". Files are appended smallest
|
|
77
|
-
* first so a cap drops the fewest of them, and anything dropped is named in a
|
|
88
|
+
* first so a cap drops the fewest of them, and anything dropped is named in a HEADER as well as in the
|
|
89
|
+
* manifest.
|
|
90
|
+
*
|
|
91
|
+
* The header used to be a footer, and that was a live correctness bug. `ALL_DIFF_MAX_BYTES` is 2 MB —
|
|
92
|
+
* roughly 30–50k lines — while the Read tool truncates at ~2000. So on the exact PR where files were
|
|
93
|
+
* dropped, a reviewer Reads the first ~5% and the notice telling it files are missing sits in the 95% it
|
|
94
|
+
* never reaches: the in-band signal went invisible precisely when it mattered. A reviewer reads a diff
|
|
95
|
+
* from the start, so the top is the only position that survives a truncated read.
|
|
78
96
|
*/
|
|
79
97
|
private writeAllDiff;
|
|
98
|
+
/**
|
|
99
|
+
* The "this combined view is NOT the whole change" notice, at the TOP (see {@link writeAllDiff}). Emitted
|
|
100
|
+
* only when something really is missing — a header on every diff trains a reader to skip the header.
|
|
101
|
+
*/
|
|
102
|
+
private allDiffHeader;
|
|
80
103
|
/**
|
|
81
104
|
* `a/b/c.ts` → `a__b__c.ts.diff`, reusing the SAME `/`→`__` convention MergeState.perFileContextDir
|
|
82
105
|
* already uses for 3-point merge context. One mangling scheme in the repo, not two. A real `__` in a
|
|
@@ -11,17 +11,30 @@ const inversify_1 = require("inversify");
|
|
|
11
11
|
exports.FILE_DIFF_MAX_BYTES = 256 * 1024;
|
|
12
12
|
// Cap for the combined ALL.diff. Over it, whole files are omitted — again, named, never silently.
|
|
13
13
|
exports.ALL_DIFF_MAX_BYTES = 2 * 1024 * 1024;
|
|
14
|
-
/**
|
|
14
|
+
/**
|
|
15
|
+
* One materialized file's diff. Data-only (per CLAUDE.md).
|
|
16
|
+
*
|
|
17
|
+
* Both paths are recorded TWICE, relative and absolute, deliberately. Relative is the stable identity that
|
|
18
|
+
* survives the repo being cloned somewhere else; absolute is the only form a reviewer subagent can actually
|
|
19
|
+
* open, because its working directory is not guaranteed to be the repo root. Recording only the relative
|
|
20
|
+
* form is what turned "read this file" back into a search — the exact cost this whole feature removes.
|
|
21
|
+
*/
|
|
15
22
|
class DiffManifestEntry {
|
|
16
23
|
file; // repo-relative source path
|
|
24
|
+
fileAbs; // ABSOLUTE working-tree path; '' for a deleted file, which has no after-state
|
|
17
25
|
diffFile; // repo-relative path of the .diff under diff/files/
|
|
26
|
+
diffAbs; // ABSOLUTE path of the same .diff
|
|
18
27
|
status; // 'M' | 'A' | 'D' | 'U' (untracked) | 'X' (excluded by config)
|
|
19
28
|
bytes;
|
|
20
29
|
truncated;
|
|
21
30
|
// eslint-disable-next-line @typescript-eslint/max-params
|
|
22
|
-
constructor(file, diffFile, status, bytes, truncated = false) {
|
|
31
|
+
constructor(repoRoot, file, diffFile, status, bytes, truncated = false) {
|
|
23
32
|
this.file = file;
|
|
33
|
+
// Not `gitTrackedSourceFileAfterChanges`: `status` already says what happened, and a compound name
|
|
34
|
+
// would lie for 'U' (untracked) and for 'D' (no after-state). The path field answers only WHERE.
|
|
35
|
+
this.fileAbs = status === 'D' ? '' : path.join(repoRoot, file);
|
|
24
36
|
this.diffFile = diffFile;
|
|
37
|
+
this.diffAbs = path.resolve(repoRoot, diffFile);
|
|
25
38
|
this.status = status;
|
|
26
39
|
this.bytes = bytes;
|
|
27
40
|
this.truncated = truncated;
|
|
@@ -42,6 +55,11 @@ class DiffManifest {
|
|
|
42
55
|
entries;
|
|
43
56
|
excluded; // matched pr-gate.reviewDiffExclude — present, stubbed, NOT materialized
|
|
44
57
|
omittedFromAllDiff; // too big for ALL.diff; their per-file .diff still exists
|
|
58
|
+
// Size of the combined view, measured once HERE so the instructions renderer and any future consumer
|
|
59
|
+
// read one number instead of each running its own `wc -l`. A reviewer needs the LINE count specifically:
|
|
60
|
+
// the Read tool truncates at ~2000 lines, silently, which is how a fraction of a change gets reviewed.
|
|
61
|
+
allDiffLines;
|
|
62
|
+
allDiffBytes;
|
|
45
63
|
// eslint-disable-next-line @typescript-eslint/max-params
|
|
46
64
|
constructor(base, head, dirty, diffCommand, entries = [], excluded = [], omittedFromAllDiff = [], hashMainHead = '') {
|
|
47
65
|
this.base = base;
|
|
@@ -55,6 +73,8 @@ class DiffManifest {
|
|
|
55
73
|
this.entries = entries;
|
|
56
74
|
this.excluded = excluded;
|
|
57
75
|
this.omittedFromAllDiff = omittedFromAllDiff;
|
|
76
|
+
this.allDiffLines = 0;
|
|
77
|
+
this.allDiffBytes = 0;
|
|
58
78
|
}
|
|
59
79
|
}
|
|
60
80
|
exports.DiffManifest = DiffManifest;
|
|
@@ -169,14 +189,14 @@ let DiffMaterializer = class DiffMaterializer {
|
|
|
169
189
|
const stub = `[excluded from materialization by pr-gate.reviewDiffExclude]\nIf you need it: git diff ${manifest.base} -- ${file}\n`;
|
|
170
190
|
fs.writeFileSync(abs, stub);
|
|
171
191
|
manifest.excluded.push(file);
|
|
172
|
-
manifest.entries.push(new DiffManifestEntry(file, rel, 'X', stub.length));
|
|
192
|
+
manifest.entries.push(new DiffManifestEntry(repoRoot, file, rel, 'X', stub.length));
|
|
173
193
|
return;
|
|
174
194
|
}
|
|
175
195
|
const raw = byFile.get(file) ?? '';
|
|
176
196
|
const status = this.statusOf(file, raw, repoRoot);
|
|
177
197
|
const body = this.capped(raw, file, manifest.base);
|
|
178
198
|
fs.writeFileSync(abs, body);
|
|
179
|
-
manifest.entries.push(new DiffManifestEntry(file, rel, status, body.length, body !== raw));
|
|
199
|
+
manifest.entries.push(new DiffManifestEntry(repoRoot, file, rel, status, body.length, body !== raw));
|
|
180
200
|
}
|
|
181
201
|
statusOf(file, raw, repoRoot) {
|
|
182
202
|
if (raw.includes('\nnew file (untracked)'))
|
|
@@ -200,7 +220,14 @@ let DiffMaterializer = class DiffMaterializer {
|
|
|
200
220
|
}
|
|
201
221
|
/**
|
|
202
222
|
* `ALL.diff` — the single Read that answers "what changed on this branch?". Files are appended smallest
|
|
203
|
-
* first so a cap drops the fewest of them, and anything dropped is named in a
|
|
223
|
+
* first so a cap drops the fewest of them, and anything dropped is named in a HEADER as well as in the
|
|
224
|
+
* manifest.
|
|
225
|
+
*
|
|
226
|
+
* The header used to be a footer, and that was a live correctness bug. `ALL_DIFF_MAX_BYTES` is 2 MB —
|
|
227
|
+
* roughly 30–50k lines — while the Read tool truncates at ~2000. So on the exact PR where files were
|
|
228
|
+
* dropped, a reviewer Reads the first ~5% and the notice telling it files are missing sits in the 95% it
|
|
229
|
+
* never reaches: the in-band signal went invisible precisely when it mattered. A reviewer reads a diff
|
|
230
|
+
* from the start, so the top is the only position that survives a truncated read.
|
|
204
231
|
*/
|
|
205
232
|
writeAllDiff(dir, filesDir, manifest) {
|
|
206
233
|
const ordered = [...manifest.entries].sort((a, b) => a.bytes - b.bytes);
|
|
@@ -215,13 +242,33 @@ let DiffMaterializer = class DiffMaterializer {
|
|
|
215
242
|
parts.push(body);
|
|
216
243
|
total += body.length;
|
|
217
244
|
}
|
|
218
|
-
|
|
245
|
+
const out = this.allDiffHeader(manifest) + parts.join('\n');
|
|
246
|
+
manifest.allDiffBytes = out.length;
|
|
247
|
+
manifest.allDiffLines = out === '' ? 0 : out.split('\n').length;
|
|
248
|
+
fs.writeFileSync(path.join(dir, 'ALL.diff'), out);
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* The "this combined view is NOT the whole change" notice, at the TOP (see {@link writeAllDiff}). Emitted
|
|
252
|
+
* only when something really is missing — a header on every diff trains a reader to skip the header.
|
|
253
|
+
*/
|
|
254
|
+
allDiffHeader(manifest) {
|
|
255
|
+
const truncated = manifest.entries.filter((e) => e.truncated);
|
|
256
|
+
if (manifest.omittedFromAllDiff.length === 0 && truncated.length === 0)
|
|
257
|
+
return '';
|
|
258
|
+
const lines = ['=== ⚠️ THIS COMBINED VIEW IS INCOMPLETE — read this notice before the diff below ==='];
|
|
219
259
|
if (manifest.omittedFromAllDiff.length > 0) {
|
|
220
|
-
|
|
221
|
-
`${exports.ALL_DIFF_MAX_BYTES}-byte cap). Each still has its
|
|
222
|
-
|
|
260
|
+
lines.push(`${manifest.omittedFromAllDiff.length} file(s) are OMITTED here (this view hit the ` +
|
|
261
|
+
`${exports.ALL_DIFF_MAX_BYTES}-byte cap). Each still has its OWN complete diff`, 'under files/ — open those, and see manifest.json:');
|
|
262
|
+
for (const f of manifest.omittedFromAllDiff)
|
|
263
|
+
lines.push(` ${f}`);
|
|
223
264
|
}
|
|
224
|
-
|
|
265
|
+
if (truncated.length > 0) {
|
|
266
|
+
lines.push(`${truncated.length} file(s) below are TRUNCATED at ${exports.FILE_DIFF_MAX_BYTES} bytes; each says so inline where it cuts off:`);
|
|
267
|
+
for (const e of truncated)
|
|
268
|
+
lines.push(` ${e.file}`);
|
|
269
|
+
}
|
|
270
|
+
lines.push('=== end of notice ===', '');
|
|
271
|
+
return lines.join('\n') + '\n';
|
|
225
272
|
}
|
|
226
273
|
/**
|
|
227
274
|
* `a/b/c.ts` → `a__b__c.ts.diff`, reusing the SAME `/`→`__` convention MergeState.perFileContextDir
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"diff-materializer.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/diff-materializer.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawnSync } from 'child_process';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { isPathExcluded, ReviewJsonService, toError } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { DiffBasis } from './diff-basis';\n\n// Per-file cap. Over it, the file's diff is truncated WITH a footer naming the command that gets the rest.\nexport const FILE_DIFF_MAX_BYTES = 256 * 1024;\n// Cap for the combined ALL.diff. Over it, whole files are omitted — again, named, never silently.\nexport const ALL_DIFF_MAX_BYTES = 2 * 1024 * 1024;\n\n/** One materialized file's diff. Data-only (per CLAUDE.md). */\nexport class DiffManifestEntry {\n file: string; // repo-relative source path\n diffFile: string; // repo-relative path of the .diff under diff/files/\n status: string; // 'M' | 'A' | 'D' | 'U' (untracked) | 'X' (excluded by config)\n bytes: number;\n truncated: boolean;\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(file: string, diffFile: string, status: string, bytes: number, truncated = false) {\n this.file = file;\n this.diffFile = diffFile;\n this.status = status;\n this.bytes = bytes;\n this.truncated = truncated;\n }\n}\n\n/** The authoritative map from a mangled `.diff` name back to its real path, plus what was dropped. */\nexport class DiffManifest {\n // Points A and B. Also exposed as hashForkPoint/hashFeatureHead below, which is the vocabulary the\n // 3-point merge already uses — one grep should find the same sha on both halves of the system.\n base: string;\n head: string;\n hashForkPoint: string;\n hashFeatureHead: string;\n hashMainHead: string; // point C: main as this clone last saw it. '' when unresolvable.\n dirty: boolean;\n diffCommand: string;\n entries: DiffManifestEntry[];\n excluded: string[]; // matched pr-gate.reviewDiffExclude — present, stubbed, NOT materialized\n omittedFromAllDiff: string[]; // too big for ALL.diff; their per-file .diff still exists\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(\n base: string, head: string, dirty: boolean, diffCommand: string,\n entries: DiffManifestEntry[] = [], excluded: string[] = [], omittedFromAllDiff: string[] = [],\n hashMainHead = '',\n ) {\n this.base = base;\n this.head = head;\n // Derived, never passed separately, so the two names for one sha cannot drift into disagreement.\n this.hashForkPoint = base;\n this.hashFeatureHead = head;\n this.hashMainHead = hashMainHead;\n this.dirty = dirty;\n this.diffCommand = diffCommand;\n this.entries = entries;\n this.excluded = excluded;\n this.omittedFromAllDiff = omittedFromAllDiff;\n }\n}\n\n/**\n * Extracts this branch's diff ONCE, to disk, so a reviewer subagent reads it instead of reconstructing it.\n *\n * The motivating measurement: a reviewer on monorepo-nx2 spent 14 of its 26 tool calls on context\n * archaeology, and the very first `git diff` it was told to run returned nothing (see {@link DiffBasis}).\n * Handing it a file it can simply open removes both failure modes — the empty range and the shell-out.\n *\n * Cost discipline: **two git invocations total, regardless of file count.** One `git diff` for everything,\n * split on `diff --git` headers, plus one `ls-files --others` for untracked files. A per-file `git diff`\n * loop would be N spawns for the same bytes.\n *\n * `@injectable(bindingScopeValues.Singleton)` so it is injected by type and drawn in the DI design.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class DiffMaterializer {\n constructor(private readonly reviewJsonService: ReviewJsonService) {}\n\n /** The dir this writes into: `.webpieces/pr-review/<feature>/diff`. */\n diffDirFor(repoRoot: string, featureName: string): string {\n return path.join(this.reviewJsonService.prDirFor(repoRoot, featureName), 'diff');\n }\n\n /**\n * Write `diff/ALL.diff`, `diff/files/*.diff` and `diff/manifest.json`. Returns the manifest.\n *\n * `changedFiles` is the set the CHECKLIST MATCHING used — passed in rather than recomputed, so the diff\n * a reviewer reads covers exactly the files its checklist matched on. Recomputing here is how the two\n * would drift apart again.\n */\n materialize(repoRoot: string, featureName: string, basis: DiffBasis, changedFiles: readonly string[], excludeGlobs: readonly string[]): DiffManifest {\n const dir = this.diffDirFor(repoRoot, featureName);\n const filesDir = path.join(dir, 'files');\n fs.rmSync(dir, { recursive: true, force: true }); // a stale diff read as current is worse than none\n fs.mkdirSync(filesDir, { recursive: true });\n const manifest = new DiffManifest(\n basis.base, basis.headSha, basis.dirty, basis.diffCommand, [], [], [], basis.hashMainHead);\n if (basis.unresolved) return this.writeManifest(dir, manifest);\n const byFile = this.captureByFile(repoRoot, basis);\n const used = new Set<string>();\n for (const file of [...changedFiles].sort()) {\n this.writeOne(repoRoot, filesDir, file, byFile, used, excludeGlobs, manifest);\n }\n this.writeAllDiff(dir, filesDir, manifest);\n return this.writeManifest(dir, manifest);\n }\n\n /**\n * ONE `git diff` for the whole branch, split into per-file patches keyed by repo-relative path.\n * Untracked files never appear in `git diff`, so they are synthesized as all-added patches — the same\n * treatment DiffScope.getFileDiff already gives them, kept consistent so a reviewer sees one format.\n */\n private captureByFile(repoRoot: string, basis: DiffBasis): Map<string, string> {\n const args = basis.dirty ? ['diff', basis.base] : ['diff', basis.base, basis.headSha];\n const out = this.gitOut(repoRoot, args);\n const byFile = new Map<string, string>();\n // Split BEFORE each `diff --git` header while keeping the header on its chunk.\n for (const chunk of out.split(/\\n(?=diff --git )/)) {\n if (!chunk.startsWith('diff --git ')) continue;\n const file = this.pathFromHeader(chunk);\n if (file !== '') byFile.set(file, chunk.endsWith('\\n') ? chunk : `${chunk}\\n`);\n }\n for (const file of this.untrackedFiles(repoRoot, basis)) {\n if (!byFile.has(file)) byFile.set(file, this.syntheticAddedDiff(repoRoot, file));\n }\n return byFile;\n }\n\n /**\n * The b-side path out of a `diff --git a/x b/x` header. The b-side is the right one: for a rename it is\n * the name that exists on disk, which is the file a reviewer can actually open.\n */\n private pathFromHeader(chunk: string): string {\n const header = chunk.slice(0, chunk.indexOf('\\n') === -1 ? chunk.length : chunk.indexOf('\\n'));\n const match = header.match(/^diff --git a\\/(.+) b\\/(.+)$/);\n return match ? match[2] : '';\n }\n\n private untrackedFiles(repoRoot: string, basis: DiffBasis): string[] {\n if (!basis.dirty) return [];\n const out = this.gitOut(repoRoot, ['ls-files', '--others', '--exclude-standard']);\n return out === '' ? [] : out.split('\\n').filter((f: string): boolean => f.trim() !== '');\n }\n\n private syntheticAddedDiff(repoRoot: string, file: string): string {\n const abs = path.join(repoRoot, file);\n if (!fs.existsSync(abs)) return '';\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: an unreadable/binary untracked file yields a note, never a crash\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const body = fs.readFileSync(abs, 'utf8').split('\\n').map((l: string): string => `+${l}`).join('\\n');\n return `diff --git a/${file} b/${file}\\nnew file (untracked)\\n--- /dev/null\\n+++ b/${file}\\n${body}\\n`;\n } catch (err: unknown) {\n const error = toError(err);\n void error; // a binary/unreadable untracked file gets a NOTE, never a crash\n return `diff --git a/${file} b/${file}\\nnew file (untracked, unreadable as text)\\n`;\n }\n }\n\n /** Write ONE file's diff, recording it (or its exclusion) in the manifest. Never drops a file silently. */\n // eslint-disable-next-line @typescript-eslint/max-params\n private writeOne(\n repoRoot: string, filesDir: string, file: string, byFile: Map<string, string>,\n used: Set<string>, excludeGlobs: readonly string[], manifest: DiffManifest,\n ): void {\n const name = this.safeName(file, used);\n const abs = path.join(filesDir, name);\n const rel = path.relative(repoRoot, abs);\n // Excluded files stay in the manifest and keep a stub. Dropping them entirely would read as \"this\n // file did not change\"; they are noise to READ, not facts to hide — and they still match checklists.\n if (excludeGlobs.length > 0 && isPathExcluded(file, [...excludeGlobs])) {\n const stub = `[excluded from materialization by pr-gate.reviewDiffExclude]\\nIf you need it: git diff ${manifest.base} -- ${file}\\n`;\n fs.writeFileSync(abs, stub);\n manifest.excluded.push(file);\n manifest.entries.push(new DiffManifestEntry(file, rel, 'X', stub.length));\n return;\n }\n const raw = byFile.get(file) ?? '';\n const status = this.statusOf(file, raw, repoRoot);\n const body = this.capped(raw, file, manifest.base);\n fs.writeFileSync(abs, body);\n manifest.entries.push(new DiffManifestEntry(file, rel, status, body.length, body !== raw));\n }\n\n private statusOf(file: string, raw: string, repoRoot: string): string {\n if (raw.includes('\\nnew file (untracked)')) return 'U';\n if (raw.includes('\\nnew file mode ')) return 'A';\n if (raw.includes('\\ndeleted file mode ')) return 'D';\n return fs.existsSync(path.join(repoRoot, file)) ? 'M' : 'D';\n }\n\n /**\n * Truncate to the cap WITH a footer. Never silently: a truncated diff that looks whole is how a reviewer\n * reports \"reviewed\" on a tenth of a change — the same trap `formatFileList` states its dropped count for.\n */\n private capped(raw: string, file: string, base: string): string {\n if (raw.length <= FILE_DIFF_MAX_BYTES) return raw;\n return `${raw.slice(0, FILE_DIFF_MAX_BYTES)}\\n` +\n `\\n… TRUNCATED at ${FILE_DIFF_MAX_BYTES} bytes (full diff is ${raw.length} bytes).\\n` +\n ` Read the rest with: git diff ${base} -- ${file}\\n`;\n }\n\n /**\n * `ALL.diff` — the single Read that answers \"what changed on this branch?\". Files are appended smallest\n * first so a cap drops the fewest of them, and anything dropped is named in a footer AND in the manifest.\n */\n private writeAllDiff(dir: string, filesDir: string, manifest: DiffManifest): void {\n const ordered = [...manifest.entries].sort((a: DiffManifestEntry, b: DiffManifestEntry): number => a.bytes - b.bytes);\n const parts: string[] = [];\n let total = 0;\n for (const entry of ordered) {\n const body = fs.readFileSync(path.join(filesDir, path.basename(entry.diffFile)), 'utf8');\n if (total + body.length > ALL_DIFF_MAX_BYTES) {\n manifest.omittedFromAllDiff.push(entry.file);\n continue;\n }\n parts.push(body);\n total += body.length;\n }\n let out = parts.join('\\n');\n if (manifest.omittedFromAllDiff.length > 0) {\n out += `\\n\\n… ${manifest.omittedFromAllDiff.length} file(s) OMITTED from this combined view (it hit the ` +\n `${ALL_DIFF_MAX_BYTES}-byte cap). Each still has its own diff under files/ — see manifest.json:\\n` +\n manifest.omittedFromAllDiff.map((f: string): string => ` ${f}`).join('\\n') + '\\n';\n }\n fs.writeFileSync(path.join(dir, 'ALL.diff'), out);\n }\n\n /**\n * `a/b/c.ts` → `a__b__c.ts.diff`, reusing the SAME `/`→`__` convention MergeState.perFileContextDir\n * already uses for 3-point merge context. One mangling scheme in the repo, not two. A real `__` in a\n * path can collide, so collisions get a numeric suffix and manifest.json stays authoritative.\n */\n private safeName(file: string, used: Set<string>): string {\n const flat = `${file.replace(/\\//g, '__')}.diff`;\n if (!used.has(flat)) {\n used.add(flat);\n return flat;\n }\n for (let n = 2; ; n++) {\n const candidate = `${file.replace(/\\//g, '__')}-${n}.diff`;\n if (!used.has(candidate)) {\n used.add(candidate);\n return candidate;\n }\n }\n }\n\n private writeManifest(dir: string, manifest: DiffManifest): DiffManifest {\n fs.mkdirSync(dir, { recursive: true });\n fs.writeFileSync(path.join(dir, 'manifest.json'), JSON.stringify(manifest, null, 2) + '\\n');\n return manifest;\n }\n\n private gitOut(repoRoot: string, args: string[]): string {\n // maxBuffer: a monorepo diff routinely exceeds node's 1MB default, and the failure mode is a\n // truncated capture that looks like a complete one.\n const result = spawnSync('git', args, { cwd: repoRoot, encoding: 'utf8', maxBuffer: 256 * 1024 * 1024 });\n return result.status === 0 ? (result.stdout ?? '') : '';\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"diff-materializer.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/diff-materializer.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawnSync } from 'child_process';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { isPathExcluded, ReviewJsonService, toError } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { DiffBasis } from './diff-basis';\n\n// Per-file cap. Over it, the file's diff is truncated WITH a footer naming the command that gets the rest.\nexport const FILE_DIFF_MAX_BYTES = 256 * 1024;\n// Cap for the combined ALL.diff. Over it, whole files are omitted — again, named, never silently.\nexport const ALL_DIFF_MAX_BYTES = 2 * 1024 * 1024;\n\n/**\n * One materialized file's diff. Data-only (per CLAUDE.md).\n *\n * Both paths are recorded TWICE, relative and absolute, deliberately. Relative is the stable identity that\n * survives the repo being cloned somewhere else; absolute is the only form a reviewer subagent can actually\n * open, because its working directory is not guaranteed to be the repo root. Recording only the relative\n * form is what turned \"read this file\" back into a search — the exact cost this whole feature removes.\n */\nexport class DiffManifestEntry {\n file: string; // repo-relative source path\n fileAbs: string; // ABSOLUTE working-tree path; '' for a deleted file, which has no after-state\n diffFile: string; // repo-relative path of the .diff under diff/files/\n diffAbs: string; // ABSOLUTE path of the same .diff\n status: string; // 'M' | 'A' | 'D' | 'U' (untracked) | 'X' (excluded by config)\n bytes: number;\n truncated: boolean;\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(repoRoot: string, file: string, diffFile: string, status: string, bytes: number, truncated = false) {\n this.file = file;\n // Not `gitTrackedSourceFileAfterChanges`: `status` already says what happened, and a compound name\n // would lie for 'U' (untracked) and for 'D' (no after-state). The path field answers only WHERE.\n this.fileAbs = status === 'D' ? '' : path.join(repoRoot, file);\n this.diffFile = diffFile;\n this.diffAbs = path.resolve(repoRoot, diffFile);\n this.status = status;\n this.bytes = bytes;\n this.truncated = truncated;\n }\n}\n\n/** The authoritative map from a mangled `.diff` name back to its real path, plus what was dropped. */\nexport class DiffManifest {\n // Points A and B. Also exposed as hashForkPoint/hashFeatureHead below, which is the vocabulary the\n // 3-point merge already uses — one grep should find the same sha on both halves of the system.\n base: string;\n head: string;\n hashForkPoint: string;\n hashFeatureHead: string;\n hashMainHead: string; // point C: main as this clone last saw it. '' when unresolvable.\n dirty: boolean;\n diffCommand: string;\n entries: DiffManifestEntry[];\n excluded: string[]; // matched pr-gate.reviewDiffExclude — present, stubbed, NOT materialized\n omittedFromAllDiff: string[]; // too big for ALL.diff; their per-file .diff still exists\n // Size of the combined view, measured once HERE so the instructions renderer and any future consumer\n // read one number instead of each running its own `wc -l`. A reviewer needs the LINE count specifically:\n // the Read tool truncates at ~2000 lines, silently, which is how a fraction of a change gets reviewed.\n allDiffLines: number;\n allDiffBytes: number;\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(\n base: string, head: string, dirty: boolean, diffCommand: string,\n entries: DiffManifestEntry[] = [], excluded: string[] = [], omittedFromAllDiff: string[] = [],\n hashMainHead = '',\n ) {\n this.base = base;\n this.head = head;\n // Derived, never passed separately, so the two names for one sha cannot drift into disagreement.\n this.hashForkPoint = base;\n this.hashFeatureHead = head;\n this.hashMainHead = hashMainHead;\n this.dirty = dirty;\n this.diffCommand = diffCommand;\n this.entries = entries;\n this.excluded = excluded;\n this.omittedFromAllDiff = omittedFromAllDiff;\n this.allDiffLines = 0;\n this.allDiffBytes = 0;\n }\n}\n\n/**\n * Extracts this branch's diff ONCE, to disk, so a reviewer subagent reads it instead of reconstructing it.\n *\n * The motivating measurement: a reviewer on monorepo-nx2 spent 14 of its 26 tool calls on context\n * archaeology, and the very first `git diff` it was told to run returned nothing (see {@link DiffBasis}).\n * Handing it a file it can simply open removes both failure modes — the empty range and the shell-out.\n *\n * Cost discipline: **two git invocations total, regardless of file count.** One `git diff` for everything,\n * split on `diff --git` headers, plus one `ls-files --others` for untracked files. A per-file `git diff`\n * loop would be N spawns for the same bytes.\n *\n * `@injectable(bindingScopeValues.Singleton)` so it is injected by type and drawn in the DI design.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class DiffMaterializer {\n constructor(private readonly reviewJsonService: ReviewJsonService) {}\n\n /** The dir this writes into: `.webpieces/pr-review/<feature>/diff`. */\n diffDirFor(repoRoot: string, featureName: string): string {\n return path.join(this.reviewJsonService.prDirFor(repoRoot, featureName), 'diff');\n }\n\n /**\n * Write `diff/ALL.diff`, `diff/files/*.diff` and `diff/manifest.json`. Returns the manifest.\n *\n * `changedFiles` is the set the CHECKLIST MATCHING used — passed in rather than recomputed, so the diff\n * a reviewer reads covers exactly the files its checklist matched on. Recomputing here is how the two\n * would drift apart again.\n */\n materialize(repoRoot: string, featureName: string, basis: DiffBasis, changedFiles: readonly string[], excludeGlobs: readonly string[]): DiffManifest {\n const dir = this.diffDirFor(repoRoot, featureName);\n const filesDir = path.join(dir, 'files');\n fs.rmSync(dir, { recursive: true, force: true }); // a stale diff read as current is worse than none\n fs.mkdirSync(filesDir, { recursive: true });\n const manifest = new DiffManifest(\n basis.base, basis.headSha, basis.dirty, basis.diffCommand, [], [], [], basis.hashMainHead);\n if (basis.unresolved) return this.writeManifest(dir, manifest);\n const byFile = this.captureByFile(repoRoot, basis);\n const used = new Set<string>();\n for (const file of [...changedFiles].sort()) {\n this.writeOne(repoRoot, filesDir, file, byFile, used, excludeGlobs, manifest);\n }\n this.writeAllDiff(dir, filesDir, manifest);\n return this.writeManifest(dir, manifest);\n }\n\n /**\n * ONE `git diff` for the whole branch, split into per-file patches keyed by repo-relative path.\n * Untracked files never appear in `git diff`, so they are synthesized as all-added patches — the same\n * treatment DiffScope.getFileDiff already gives them, kept consistent so a reviewer sees one format.\n */\n private captureByFile(repoRoot: string, basis: DiffBasis): Map<string, string> {\n const args = basis.dirty ? ['diff', basis.base] : ['diff', basis.base, basis.headSha];\n const out = this.gitOut(repoRoot, args);\n const byFile = new Map<string, string>();\n // Split BEFORE each `diff --git` header while keeping the header on its chunk.\n for (const chunk of out.split(/\\n(?=diff --git )/)) {\n if (!chunk.startsWith('diff --git ')) continue;\n const file = this.pathFromHeader(chunk);\n if (file !== '') byFile.set(file, chunk.endsWith('\\n') ? chunk : `${chunk}\\n`);\n }\n for (const file of this.untrackedFiles(repoRoot, basis)) {\n if (!byFile.has(file)) byFile.set(file, this.syntheticAddedDiff(repoRoot, file));\n }\n return byFile;\n }\n\n /**\n * The b-side path out of a `diff --git a/x b/x` header. The b-side is the right one: for a rename it is\n * the name that exists on disk, which is the file a reviewer can actually open.\n */\n private pathFromHeader(chunk: string): string {\n const header = chunk.slice(0, chunk.indexOf('\\n') === -1 ? chunk.length : chunk.indexOf('\\n'));\n const match = header.match(/^diff --git a\\/(.+) b\\/(.+)$/);\n return match ? match[2] : '';\n }\n\n private untrackedFiles(repoRoot: string, basis: DiffBasis): string[] {\n if (!basis.dirty) return [];\n const out = this.gitOut(repoRoot, ['ls-files', '--others', '--exclude-standard']);\n return out === '' ? [] : out.split('\\n').filter((f: string): boolean => f.trim() !== '');\n }\n\n private syntheticAddedDiff(repoRoot: string, file: string): string {\n const abs = path.join(repoRoot, file);\n if (!fs.existsSync(abs)) return '';\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: an unreadable/binary untracked file yields a note, never a crash\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const body = fs.readFileSync(abs, 'utf8').split('\\n').map((l: string): string => `+${l}`).join('\\n');\n return `diff --git a/${file} b/${file}\\nnew file (untracked)\\n--- /dev/null\\n+++ b/${file}\\n${body}\\n`;\n } catch (err: unknown) {\n const error = toError(err);\n void error; // a binary/unreadable untracked file gets a NOTE, never a crash\n return `diff --git a/${file} b/${file}\\nnew file (untracked, unreadable as text)\\n`;\n }\n }\n\n /** Write ONE file's diff, recording it (or its exclusion) in the manifest. Never drops a file silently. */\n // eslint-disable-next-line @typescript-eslint/max-params\n private writeOne(\n repoRoot: string, filesDir: string, file: string, byFile: Map<string, string>,\n used: Set<string>, excludeGlobs: readonly string[], manifest: DiffManifest,\n ): void {\n const name = this.safeName(file, used);\n const abs = path.join(filesDir, name);\n const rel = path.relative(repoRoot, abs);\n // Excluded files stay in the manifest and keep a stub. Dropping them entirely would read as \"this\n // file did not change\"; they are noise to READ, not facts to hide — and they still match checklists.\n if (excludeGlobs.length > 0 && isPathExcluded(file, [...excludeGlobs])) {\n const stub = `[excluded from materialization by pr-gate.reviewDiffExclude]\\nIf you need it: git diff ${manifest.base} -- ${file}\\n`;\n fs.writeFileSync(abs, stub);\n manifest.excluded.push(file);\n manifest.entries.push(new DiffManifestEntry(repoRoot, file, rel, 'X', stub.length));\n return;\n }\n const raw = byFile.get(file) ?? '';\n const status = this.statusOf(file, raw, repoRoot);\n const body = this.capped(raw, file, manifest.base);\n fs.writeFileSync(abs, body);\n manifest.entries.push(new DiffManifestEntry(repoRoot, file, rel, status, body.length, body !== raw));\n }\n\n private statusOf(file: string, raw: string, repoRoot: string): string {\n if (raw.includes('\\nnew file (untracked)')) return 'U';\n if (raw.includes('\\nnew file mode ')) return 'A';\n if (raw.includes('\\ndeleted file mode ')) return 'D';\n return fs.existsSync(path.join(repoRoot, file)) ? 'M' : 'D';\n }\n\n /**\n * Truncate to the cap WITH a footer. Never silently: a truncated diff that looks whole is how a reviewer\n * reports \"reviewed\" on a tenth of a change — the same trap `formatFileList` states its dropped count for.\n */\n private capped(raw: string, file: string, base: string): string {\n if (raw.length <= FILE_DIFF_MAX_BYTES) return raw;\n return `${raw.slice(0, FILE_DIFF_MAX_BYTES)}\\n` +\n `\\n… TRUNCATED at ${FILE_DIFF_MAX_BYTES} bytes (full diff is ${raw.length} bytes).\\n` +\n ` Read the rest with: git diff ${base} -- ${file}\\n`;\n }\n\n /**\n * `ALL.diff` — the single Read that answers \"what changed on this branch?\". Files are appended smallest\n * first so a cap drops the fewest of them, and anything dropped is named in a HEADER as well as in the\n * manifest.\n *\n * The header used to be a footer, and that was a live correctness bug. `ALL_DIFF_MAX_BYTES` is 2 MB —\n * roughly 30–50k lines — while the Read tool truncates at ~2000. So on the exact PR where files were\n * dropped, a reviewer Reads the first ~5% and the notice telling it files are missing sits in the 95% it\n * never reaches: the in-band signal went invisible precisely when it mattered. A reviewer reads a diff\n * from the start, so the top is the only position that survives a truncated read.\n */\n private writeAllDiff(dir: string, filesDir: string, manifest: DiffManifest): void {\n const ordered = [...manifest.entries].sort((a: DiffManifestEntry, b: DiffManifestEntry): number => a.bytes - b.bytes);\n const parts: string[] = [];\n let total = 0;\n for (const entry of ordered) {\n const body = fs.readFileSync(path.join(filesDir, path.basename(entry.diffFile)), 'utf8');\n if (total + body.length > ALL_DIFF_MAX_BYTES) {\n manifest.omittedFromAllDiff.push(entry.file);\n continue;\n }\n parts.push(body);\n total += body.length;\n }\n const out = this.allDiffHeader(manifest) + parts.join('\\n');\n manifest.allDiffBytes = out.length;\n manifest.allDiffLines = out === '' ? 0 : out.split('\\n').length;\n fs.writeFileSync(path.join(dir, 'ALL.diff'), out);\n }\n\n /**\n * The \"this combined view is NOT the whole change\" notice, at the TOP (see {@link writeAllDiff}). Emitted\n * only when something really is missing — a header on every diff trains a reader to skip the header.\n */\n private allDiffHeader(manifest: DiffManifest): string {\n const truncated = manifest.entries.filter((e: DiffManifestEntry): boolean => e.truncated);\n if (manifest.omittedFromAllDiff.length === 0 && truncated.length === 0) return '';\n const lines = ['=== ⚠️ THIS COMBINED VIEW IS INCOMPLETE — read this notice before the diff below ==='];\n if (manifest.omittedFromAllDiff.length > 0) {\n lines.push(`${manifest.omittedFromAllDiff.length} file(s) are OMITTED here (this view hit the ` +\n `${ALL_DIFF_MAX_BYTES}-byte cap). Each still has its OWN complete diff`, 'under files/ — open those, and see manifest.json:');\n for (const f of manifest.omittedFromAllDiff) lines.push(` ${f}`);\n }\n if (truncated.length > 0) {\n lines.push(`${truncated.length} file(s) below are TRUNCATED at ${FILE_DIFF_MAX_BYTES} bytes; each says so inline where it cuts off:`);\n for (const e of truncated) lines.push(` ${e.file}`);\n }\n lines.push('=== end of notice ===', '');\n return lines.join('\\n') + '\\n';\n }\n\n /**\n * `a/b/c.ts` → `a__b__c.ts.diff`, reusing the SAME `/`→`__` convention MergeState.perFileContextDir\n * already uses for 3-point merge context. One mangling scheme in the repo, not two. A real `__` in a\n * path can collide, so collisions get a numeric suffix and manifest.json stays authoritative.\n */\n private safeName(file: string, used: Set<string>): string {\n const flat = `${file.replace(/\\//g, '__')}.diff`;\n if (!used.has(flat)) {\n used.add(flat);\n return flat;\n }\n for (let n = 2; ; n++) {\n const candidate = `${file.replace(/\\//g, '__')}-${n}.diff`;\n if (!used.has(candidate)) {\n used.add(candidate);\n return candidate;\n }\n }\n }\n\n private writeManifest(dir: string, manifest: DiffManifest): DiffManifest {\n fs.mkdirSync(dir, { recursive: true });\n fs.writeFileSync(path.join(dir, 'manifest.json'), JSON.stringify(manifest, null, 2) + '\\n');\n return manifest;\n }\n\n private gitOut(repoRoot: string, args: string[]): string {\n // maxBuffer: a monorepo diff routinely exceeds node's 1MB default, and the failure mode is a\n // truncated capture that looks like a complete one.\n const result = spawnSync('git', args, { cwd: repoRoot, encoding: 'utf8', maxBuffer: 256 * 1024 * 1024 });\n return result.status === 0 ? (result.stdout ?? '') : '';\n }\n}\n"]}
|
|
@@ -13,6 +13,24 @@ export declare class ReviewerBriefingBuilder {
|
|
|
13
13
|
constructor(reviewJsonService: ReviewJsonService);
|
|
14
14
|
build(repoRoot: string, scan: ChecklistScan, manifest: DiffManifest, diffDir: string, config: PrGateConfig): ReviewerBriefing[];
|
|
15
15
|
private one;
|
|
16
|
+
/**
|
|
17
|
+
* One row of the reviewer's table. The SOURCE path comes over with the diff path because the two need
|
|
18
|
+
* identical affordance: when the diff was an absolute path and the source was a parent directory, four of
|
|
19
|
+
* four reviewers read the diff and none opened a source file.
|
|
20
|
+
*/
|
|
21
|
+
private briefedFile;
|
|
22
|
+
/**
|
|
23
|
+
* Manifest headline facts, copied onto the briefing so the instructions can state them INLINE. A reviewer
|
|
24
|
+
* that never opens the manifest still has to learn whether anything was truncated or excluded — that is
|
|
25
|
+
* precisely the thing it could not otherwise know it was missing.
|
|
26
|
+
*/
|
|
27
|
+
private copyManifestFacts;
|
|
28
|
+
/**
|
|
29
|
+
* The reviewer's own `.claude/agents/<subagent>.md`, when THIS diff changes it. Detected here rather than
|
|
30
|
+
* left to the reviewer to notice: on the measured run two reviewers reviewed their own definition and
|
|
31
|
+
* neither mentioned it, and every PR that edits the review gate reproduces that.
|
|
32
|
+
*/
|
|
33
|
+
private ownAgentFileIn;
|
|
16
34
|
/**
|
|
17
35
|
* Deduped absolute parent dirs of the matched files. Dirs rather than files because the point is to say
|
|
18
36
|
* "the neighbourhood is here" — a reviewer already has the file list; what it lacks is where to look for
|
|
@@ -22,27 +22,65 @@ let ReviewerBriefingBuilder = class ReviewerBriefingBuilder {
|
|
|
22
22
|
// Resolved ONCE for the whole run, not per reviewer: `require.resolve` hits the filesystem, and the
|
|
23
23
|
// answer cannot differ between two reviewers in the same process.
|
|
24
24
|
const shared = this.contextEntries(repoRoot, config);
|
|
25
|
-
const
|
|
25
|
+
const entryByFile = new Map();
|
|
26
26
|
for (const e of manifest.entries)
|
|
27
|
-
|
|
28
|
-
return scan.applicable.map((req) => this.one(repoRoot, req, scan,
|
|
27
|
+
entryByFile.set(e.file, e);
|
|
28
|
+
return scan.applicable.map((req) => this.one(repoRoot, req, scan, entryByFile, diffDir, shared, manifest));
|
|
29
29
|
}
|
|
30
30
|
// eslint-disable-next-line @typescript-eslint/max-params
|
|
31
|
-
one(repoRoot, req, scan,
|
|
31
|
+
one(repoRoot, req, scan, entryByFile, diffDir, shared, manifest) {
|
|
32
32
|
const b = new rules_config_1.ReviewerBriefing(req.subagent, req.id, repoRoot);
|
|
33
33
|
b.docPath = req.doc.trim() === '' ? '' : path.resolve(repoRoot, req.doc);
|
|
34
34
|
b.diffDir = diffDir;
|
|
35
35
|
b.allDiffPath = diffDir === '' ? '' : path.join(diffDir, 'ALL.diff');
|
|
36
36
|
b.manifestPath = diffDir === '' ? '' : path.join(diffDir, 'manifest.json');
|
|
37
|
-
b.myFiles = req.matchedFiles.map((f) =>
|
|
37
|
+
b.myFiles = req.matchedFiles.map((f) => this.briefedFile(repoRoot, f, entryByFile));
|
|
38
38
|
b.matchedPatterns = req.matchedPatterns;
|
|
39
39
|
b.sourceDirs = this.sourceDirsFor(repoRoot, req.matchedFiles);
|
|
40
40
|
b.contextEntries = shared;
|
|
41
41
|
b.verdictPath = this.reviewJsonService.checklistResultPath(scan.reviewPath, req.id);
|
|
42
42
|
b.fileDiffCommand = scan.context.fileDiffCommand;
|
|
43
43
|
b.dirty = scan.basis.dirty;
|
|
44
|
+
b.ownAgentFileInDiff = this.ownAgentFileIn(manifest, req.subagent);
|
|
45
|
+
this.copyManifestFacts(b, manifest);
|
|
44
46
|
return b;
|
|
45
47
|
}
|
|
48
|
+
/**
|
|
49
|
+
* One row of the reviewer's table. The SOURCE path comes over with the diff path because the two need
|
|
50
|
+
* identical affordance: when the diff was an absolute path and the source was a parent directory, four of
|
|
51
|
+
* four reviewers read the diff and none opened a source file.
|
|
52
|
+
*/
|
|
53
|
+
briefedFile(repoRoot, file, entryByFile) {
|
|
54
|
+
const e = entryByFile.get(file);
|
|
55
|
+
if (e === undefined)
|
|
56
|
+
return new rules_config_1.BriefedFile(file, '', path.resolve(repoRoot, file));
|
|
57
|
+
return new rules_config_1.BriefedFile(file, e.diffAbs, e.fileAbs, e.status, e.bytes, e.truncated);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Manifest headline facts, copied onto the briefing so the instructions can state them INLINE. A reviewer
|
|
61
|
+
* that never opens the manifest still has to learn whether anything was truncated or excluded — that is
|
|
62
|
+
* precisely the thing it could not otherwise know it was missing.
|
|
63
|
+
*/
|
|
64
|
+
copyManifestFacts(b, manifest) {
|
|
65
|
+
b.changedFileCount = manifest.entries.length;
|
|
66
|
+
b.truncatedCount = manifest.entries.filter((e) => e.truncated).length;
|
|
67
|
+
b.excludedCount = manifest.excluded.length;
|
|
68
|
+
b.allDiffLines = manifest.allDiffLines;
|
|
69
|
+
b.allDiffBytes = manifest.allDiffBytes;
|
|
70
|
+
b.hashForkPoint = manifest.hashForkPoint;
|
|
71
|
+
b.hashFeatureHead = manifest.hashFeatureHead;
|
|
72
|
+
b.hashMainHead = manifest.hashMainHead;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* The reviewer's own `.claude/agents/<subagent>.md`, when THIS diff changes it. Detected here rather than
|
|
76
|
+
* left to the reviewer to notice: on the measured run two reviewers reviewed their own definition and
|
|
77
|
+
* neither mentioned it, and every PR that edits the review gate reproduces that.
|
|
78
|
+
*/
|
|
79
|
+
ownAgentFileIn(manifest, subagent) {
|
|
80
|
+
const suffix = path.join('.claude', 'agents', `${subagent}.md`);
|
|
81
|
+
const hit = manifest.entries.find((e) => e.file.split('/').join(path.sep).endsWith(suffix));
|
|
82
|
+
return hit === undefined ? '' : (hit.fileAbs === '' ? hit.file : hit.fileAbs);
|
|
83
|
+
}
|
|
46
84
|
/**
|
|
47
85
|
* Deduped absolute parent dirs of the matched files. Dirs rather than files because the point is to say
|
|
48
86
|
* "the neighbourhood is here" — a reviewer already has the file list; what it lacks is where to look for
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"reviewer-briefing-builder.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/reviewer-briefing-builder.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\nimport {\n BriefedFile, ContextEntry, PrGateConfig, RequiredChecklist, ReviewerBriefing, ReviewJsonService, toError,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { DiffManifest } from './diff-materializer';\nimport { ChecklistScan } from './checklist-scanner';\n\n/**\n * Turns a {@link ChecklistScan} plus the materialized {@link DiffManifest} into one {@link ReviewerBriefing}\n * per reviewer — resolving every path to an absolute one, because a reviewer that has to resolve a path\n * ends up searching, and searching is the cost this whole feature exists to remove.\n *\n * `@injectable(bindingScopeValues.Singleton)` so it is injected by type and drawn in the DI design.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class ReviewerBriefingBuilder {\n constructor(private readonly reviewJsonService: ReviewJsonService) {}\n\n build(repoRoot: string, scan: ChecklistScan, manifest: DiffManifest, diffDir: string, config: PrGateConfig): ReviewerBriefing[] {\n // Resolved ONCE for the whole run, not per reviewer: `require.resolve` hits the filesystem, and the\n // answer cannot differ between two reviewers in the same process.\n const shared = this.contextEntries(repoRoot, config);\n const diffByFile = new Map<string, string>();\n for (const e of manifest.entries) diffByFile.set(e.file, path.resolve(repoRoot, e.diffFile));\n return scan.applicable.map((req: RequiredChecklist): ReviewerBriefing =>\n this.one(repoRoot, req, scan, diffByFile, diffDir, shared));\n }\n\n // eslint-disable-next-line @typescript-eslint/max-params\n private one(\n repoRoot: string, req: RequiredChecklist, scan: ChecklistScan,\n diffByFile: Map<string, string>, diffDir: string, shared: ContextEntry[],\n ): ReviewerBriefing {\n const b = new ReviewerBriefing(req.subagent, req.id, repoRoot);\n b.docPath = req.doc.trim() === '' ? '' : path.resolve(repoRoot, req.doc);\n b.diffDir = diffDir;\n b.allDiffPath = diffDir === '' ? '' : path.join(diffDir, 'ALL.diff');\n b.manifestPath = diffDir === '' ? '' : path.join(diffDir, 'manifest.json');\n b.myFiles = req.matchedFiles.map((f: string): BriefedFile => new BriefedFile(f, diffByFile.get(f) ?? ''));\n b.matchedPatterns = req.matchedPatterns;\n b.sourceDirs = this.sourceDirsFor(repoRoot, req.matchedFiles);\n b.contextEntries = shared;\n b.verdictPath = this.reviewJsonService.checklistResultPath(scan.reviewPath, req.id);\n b.fileDiffCommand = scan.context.fileDiffCommand;\n b.dirty = scan.basis.dirty;\n return b;\n }\n\n /**\n * Deduped absolute parent dirs of the matched files. Dirs rather than files because the point is to say\n * \"the neighbourhood is here\" — a reviewer already has the file list; what it lacks is where to look for\n * the code AROUND the change.\n */\n private sourceDirsFor(repoRoot: string, files: readonly string[]): string[] {\n const dirs = new Set<string>();\n for (const f of files) dirs.add(path.resolve(repoRoot, path.dirname(f)));\n return [...dirs].sort();\n }\n\n /**\n * The pre-resolved context list, from two config keys with deliberately different jobs:\n *\n * `reviewContextPackages` — installed packages, resolved to their real on-disk dir. This is what deletes\n * the repeated `node_modules/@webpieces` greps: the tooling knows where a package resolved to, and the\n * reviewer does not.\n * `reviewContext` — the repo-specific escape hatch (`{label, path}`) for what tooling cannot possibly\n * guess, e.g. \"queue names live in terraform/services/\".\n *\n * Neither ever drops an entry: an unresolvable package or a missing path is LISTED, with the reason. A\n * silently shorter list is indistinguishable from a complete one.\n */\n private contextEntries(repoRoot: string, config: PrGateConfig): ContextEntry[] {\n const entries: ContextEntry[] = [];\n for (const pkg of config.reviewContextPackages) entries.push(this.resolvePackage(repoRoot, pkg));\n for (const item of config.reviewContext) {\n const abs = path.resolve(repoRoot, item.path);\n entries.push(fs.existsSync(abs)\n ? new ContextEntry(item.label, abs)\n : new ContextEntry(item.label, '', `(configured as \"${item.path}\" but missing)`));\n }\n return entries;\n }\n\n private resolvePackage(repoRoot: string, pkg: string): ContextEntry {\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: an unresolvable package is REPORTED as such, never fatal\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const manifestPath = require.resolve(`${pkg}/package.json`, { paths: [repoRoot] });\n const dir = path.dirname(manifestPath);\n // webpieces-disable no-any-unknown -- opaque package.json, only `description` is read\n const meta = JSON.parse(fs.readFileSync(manifestPath, 'utf8')) as Record<string, unknown>;\n const desc = typeof meta['description'] === 'string' ? (meta['description'] as string) : '';\n return new ContextEntry(desc === '' ? pkg : `${pkg} — ${desc}`, dir);\n } catch (err: unknown) {\n const error = toError(err);\n void error; // an unresolvable package is REPORTED below, never fatal\n return new ContextEntry(pkg, '', '(not installed — configured in pr-gate.reviewContextPackages)');\n }\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"reviewer-briefing-builder.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/reviewer-briefing-builder.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\nimport {\n BriefedFile, ContextEntry, PrGateConfig, RequiredChecklist, ReviewerBriefing, ReviewJsonService, toError,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { DiffManifest, DiffManifestEntry } from './diff-materializer';\nimport { ChecklistScan } from './checklist-scanner';\n\n/**\n * Turns a {@link ChecklistScan} plus the materialized {@link DiffManifest} into one {@link ReviewerBriefing}\n * per reviewer — resolving every path to an absolute one, because a reviewer that has to resolve a path\n * ends up searching, and searching is the cost this whole feature exists to remove.\n *\n * `@injectable(bindingScopeValues.Singleton)` so it is injected by type and drawn in the DI design.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class ReviewerBriefingBuilder {\n constructor(private readonly reviewJsonService: ReviewJsonService) {}\n\n build(repoRoot: string, scan: ChecklistScan, manifest: DiffManifest, diffDir: string, config: PrGateConfig): ReviewerBriefing[] {\n // Resolved ONCE for the whole run, not per reviewer: `require.resolve` hits the filesystem, and the\n // answer cannot differ between two reviewers in the same process.\n const shared = this.contextEntries(repoRoot, config);\n const entryByFile = new Map<string, DiffManifestEntry>();\n for (const e of manifest.entries) entryByFile.set(e.file, e);\n return scan.applicable.map((req: RequiredChecklist): ReviewerBriefing =>\n this.one(repoRoot, req, scan, entryByFile, diffDir, shared, manifest));\n }\n\n // eslint-disable-next-line @typescript-eslint/max-params\n private one(\n repoRoot: string, req: RequiredChecklist, scan: ChecklistScan,\n entryByFile: Map<string, DiffManifestEntry>, diffDir: string, shared: ContextEntry[], manifest: DiffManifest,\n ): ReviewerBriefing {\n const b = new ReviewerBriefing(req.subagent, req.id, repoRoot);\n b.docPath = req.doc.trim() === '' ? '' : path.resolve(repoRoot, req.doc);\n b.diffDir = diffDir;\n b.allDiffPath = diffDir === '' ? '' : path.join(diffDir, 'ALL.diff');\n b.manifestPath = diffDir === '' ? '' : path.join(diffDir, 'manifest.json');\n b.myFiles = req.matchedFiles.map((f: string): BriefedFile => this.briefedFile(repoRoot, f, entryByFile));\n b.matchedPatterns = req.matchedPatterns;\n b.sourceDirs = this.sourceDirsFor(repoRoot, req.matchedFiles);\n b.contextEntries = shared;\n b.verdictPath = this.reviewJsonService.checklistResultPath(scan.reviewPath, req.id);\n b.fileDiffCommand = scan.context.fileDiffCommand;\n b.dirty = scan.basis.dirty;\n b.ownAgentFileInDiff = this.ownAgentFileIn(manifest, req.subagent);\n this.copyManifestFacts(b, manifest);\n return b;\n }\n\n /**\n * One row of the reviewer's table. The SOURCE path comes over with the diff path because the two need\n * identical affordance: when the diff was an absolute path and the source was a parent directory, four of\n * four reviewers read the diff and none opened a source file.\n */\n private briefedFile(repoRoot: string, file: string, entryByFile: Map<string, DiffManifestEntry>): BriefedFile {\n const e = entryByFile.get(file);\n if (e === undefined) return new BriefedFile(file, '', path.resolve(repoRoot, file));\n return new BriefedFile(file, e.diffAbs, e.fileAbs, e.status, e.bytes, e.truncated);\n }\n\n /**\n * Manifest headline facts, copied onto the briefing so the instructions can state them INLINE. A reviewer\n * that never opens the manifest still has to learn whether anything was truncated or excluded — that is\n * precisely the thing it could not otherwise know it was missing.\n */\n private copyManifestFacts(b: ReviewerBriefing, manifest: DiffManifest): void {\n b.changedFileCount = manifest.entries.length;\n b.truncatedCount = manifest.entries.filter((e: DiffManifestEntry): boolean => e.truncated).length;\n b.excludedCount = manifest.excluded.length;\n b.allDiffLines = manifest.allDiffLines;\n b.allDiffBytes = manifest.allDiffBytes;\n b.hashForkPoint = manifest.hashForkPoint;\n b.hashFeatureHead = manifest.hashFeatureHead;\n b.hashMainHead = manifest.hashMainHead;\n }\n\n /**\n * The reviewer's own `.claude/agents/<subagent>.md`, when THIS diff changes it. Detected here rather than\n * left to the reviewer to notice: on the measured run two reviewers reviewed their own definition and\n * neither mentioned it, and every PR that edits the review gate reproduces that.\n */\n private ownAgentFileIn(manifest: DiffManifest, subagent: string): string {\n const suffix = path.join('.claude', 'agents', `${subagent}.md`);\n const hit = manifest.entries.find((e: DiffManifestEntry): boolean => e.file.split('/').join(path.sep).endsWith(suffix));\n return hit === undefined ? '' : (hit.fileAbs === '' ? hit.file : hit.fileAbs);\n }\n\n /**\n * Deduped absolute parent dirs of the matched files. Dirs rather than files because the point is to say\n * \"the neighbourhood is here\" — a reviewer already has the file list; what it lacks is where to look for\n * the code AROUND the change.\n */\n private sourceDirsFor(repoRoot: string, files: readonly string[]): string[] {\n const dirs = new Set<string>();\n for (const f of files) dirs.add(path.resolve(repoRoot, path.dirname(f)));\n return [...dirs].sort();\n }\n\n /**\n * The pre-resolved context list, from two config keys with deliberately different jobs:\n *\n * `reviewContextPackages` — installed packages, resolved to their real on-disk dir. This is what deletes\n * the repeated `node_modules/@webpieces` greps: the tooling knows where a package resolved to, and the\n * reviewer does not.\n * `reviewContext` — the repo-specific escape hatch (`{label, path}`) for what tooling cannot possibly\n * guess, e.g. \"queue names live in terraform/services/\".\n *\n * Neither ever drops an entry: an unresolvable package or a missing path is LISTED, with the reason. A\n * silently shorter list is indistinguishable from a complete one.\n */\n private contextEntries(repoRoot: string, config: PrGateConfig): ContextEntry[] {\n const entries: ContextEntry[] = [];\n for (const pkg of config.reviewContextPackages) entries.push(this.resolvePackage(repoRoot, pkg));\n for (const item of config.reviewContext) {\n const abs = path.resolve(repoRoot, item.path);\n entries.push(fs.existsSync(abs)\n ? new ContextEntry(item.label, abs)\n : new ContextEntry(item.label, '', `(configured as \"${item.path}\" but missing)`));\n }\n return entries;\n }\n\n private resolvePackage(repoRoot: string, pkg: string): ContextEntry {\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: an unresolvable package is REPORTED as such, never fatal\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n const manifestPath = require.resolve(`${pkg}/package.json`, { paths: [repoRoot] });\n const dir = path.dirname(manifestPath);\n // webpieces-disable no-any-unknown -- opaque package.json, only `description` is read\n const meta = JSON.parse(fs.readFileSync(manifestPath, 'utf8')) as Record<string, unknown>;\n const desc = typeof meta['description'] === 'string' ? (meta['description'] as string) : '';\n return new ContextEntry(desc === '' ? pkg : `${pkg} — ${desc}`, dir);\n } catch (err: unknown) {\n const error = toError(err);\n void error; // an unresolvable package is REPORTED below, never fatal\n return new ContextEntry(pkg, '', '(not installed — configured in pr-gate.reviewContextPackages)');\n }\n }\n}\n"]}
|