@webpieces/pr-gate 0.4.648 → 0.4.650

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webpieces/pr-gate",
3
- "version": "0.4.648",
3
+ "version": "0.4.650",
4
4
  "description": "Gated PR system: 3-point squash-merge, merge validation gate, and red/yellow/green PR dashboard. Standalone scripts, no Nx dependency required.",
5
5
  "type": "commonjs",
6
6
  "main": "./src/index.js",
@@ -15,7 +15,7 @@
15
15
  "directory": "packages/tooling/pr-gate"
16
16
  },
17
17
  "dependencies": {
18
- "@webpieces/rules-config": "0.4.648",
18
+ "@webpieces/rules-config": "0.4.650",
19
19
  "@inversifyjs/binding-decorators": "1.1.5",
20
20
  "inversify": "7.10.4",
21
21
  "reflect-metadata": "0.2.2"
@@ -39,10 +39,40 @@ export declare class ProvenanceEnforcer {
39
39
  private gitOut;
40
40
  enforce(required: readonly RequiredChecklist[], branch: string, repoRoot: string, config: PrGateConfig): ProvenanceReport;
41
41
  /**
42
- * What each credited reviewer actually read. Purely observational here {@link evidenceErrors} decides
43
- * whether any of it blocks, and by default none of it does.
42
+ * Refuse with an instruction that matches WHICH failure this is.
43
+ *
44
+ * The count is the number of CHECKLISTS at fault, not the number of message paragraphs. It used to be
45
+ * `errors.length`, which printed "1 checklist(s) require …" above a list of seven names.
46
+ *
47
+ * The split matters more than the count. A checklist with no verdict file needs a reviewer spawned. A
48
+ * checklist WITH one needs the opposite: the reviewer already ran, the verdict is on disk, and only the
49
+ * attribution failed — so telling an agent to "spawn each as its OWN subagent" makes it overwrite seven
50
+ * verdicts with runs that are stamped identically and refused identically. That is a loop that destroys
51
+ * evidence on every iteration, and it is what the previous single message guaranteed.
52
+ *
53
+ * The BLOCK decision is the STATUS, not the length of `missing`. Those agree today — but a
54
+ * `PROVENANCE_MISSING` that arrived carrying no names would otherwise be a silent pass: reported
55
+ * unverified and refused by nothing. Refusing on the status makes the unverified verdict block whatever
56
+ * else is true, and `detail` carries the wording when there are no names to word it from.
57
+ */
58
+ private refuse;
59
+ /**
60
+ * The verdict exists but nothing can prove WHICH branch its reviewer looked at.
61
+ *
62
+ * Reachable when a session rooted in the PRIMARY CLONE spawns reviewers for a branch that lives in a
63
+ * linked worktree: the harness stamps record 0 from the spawning session's cwd (the Agent tool has no
64
+ * cwd parameter), so every respawn is stamped identically and re-spawning cannot change the outcome.
65
+ * Saying so is the whole point of this message — the remedy is a session rooted in the worktree, or a
66
+ * reviewer that names this branch's own files.
44
67
  */
45
- private gatherEvidence;
68
+ private unattributedMessage;
69
+ /**
70
+ * WHERE this branch's review materials live — the paths BOTH provenance passes compare against.
71
+ *
72
+ * One object for crediting and for evidence: they ask different questions of the same files, and
73
+ * building the paths twice is how the two could drift apart.
74
+ */
75
+ private contextFor;
46
76
  /**
47
77
  * Write this round's audit record: `.webpieces/pr-review/<featureSlug>/provenance.json`, linking each
48
78
  * verdict to the transcript of the subagent that produced it.
@@ -63,11 +93,15 @@ export declare class ProvenanceEnforcer {
63
93
  private writeProvenanceRecord;
64
94
  private reviewerPathsFor;
65
95
  /**
66
- * WARN (default) or REFUSE (opt-in) on a reviewer that wrote a verdict without opening the diff.
96
+ * The reviewers that wrote a verdict without opening the diff — WARNED about (default) or returned for
97
+ * {@link refuse} to block on (opt-in).
67
98
  *
68
99
  * Default-warn because the signal is derived from undocumented Claude Code transcript internals: if the
69
100
  * format shifts, a blocking check wedges every PR in every consumer repo with no self-service recovery.
70
101
  * `requireDiffEvidence` lets a repo that has watched the warning promote it deliberately.
102
+ *
103
+ * Returns the agentTypes rather than a finished sentence so the caller can count CHECKLISTS at fault —
104
+ * a name can appear in this list and in `missing`, and it must be one checklist in the total, not two.
71
105
  */
72
- private evidenceErrors;
106
+ private blindReviewers;
73
107
  }
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ProvenanceEnforcer = exports.ProvenanceReport = void 0;
4
4
  const tslib_1 = require("tslib");
5
5
  const child_process_1 = require("child_process");
6
+ const fs = tslib_1.__importStar(require("fs"));
6
7
  const path = tslib_1.__importStar(require("path"));
7
8
  const rules_config_1 = require("@webpieces/rules-config");
8
9
  const inversify_1 = require("inversify");
@@ -67,44 +68,101 @@ let ProvenanceEnforcer = class ProvenanceEnforcer {
67
68
  // passes silently; no session id warns but passes; any missing reviewer throws so the PR does not open.
68
69
  // eslint-disable-next-line @typescript-eslint/max-params
69
70
  enforce(required, branch, repoRoot, config) {
70
- const errors = [];
71
71
  const report = new ProvenanceReport(true, []); // no reviewers to verify ⇒ vacuously verified
72
72
  const subagents = required.map((r) => r.subagent.trim()).filter((s) => s !== '');
73
+ const context = this.contextFor(repoRoot, required, branch);
73
74
  // verifyDistinct short-circuits to OK on an empty set, so this runs unconditionally: a repo with no
74
75
  // checklists still gets a provenance record naming the session and the main agent's own transcript.
75
- const result = this.provenance.verifyDistinct(subagents, branch);
76
+ const result = this.provenance.verifyDistinct(subagents, context);
76
77
  report.verified = result.status === rules_config_1.PROVENANCE_OK;
77
- if (result.status === rules_config_1.PROVENANCE_MISSING) {
78
- errors.push(result.detail);
79
- }
80
- else if (result.status === rules_config_1.PROVENANCE_SKIPPED) {
78
+ if (result.status === rules_config_1.PROVENANCE_SKIPPED)
81
79
  process.stderr.write(`⚠️ ${result.detail}\n`);
82
- }
83
- report.evidence = this.gatherEvidence(repoRoot, required, result, branch);
84
- errors.push(...this.evidenceErrors(report.evidence, config));
80
+ report.evidence = this.provenance.evidenceFor(context, result.agentIds);
81
+ const blind = this.blindReviewers(report.evidence, config);
85
82
  // BEFORE the throw below, deliberately. A refused round is the one most worth auditing, and a record
86
83
  // that only ever appeared on success could not answer what the reviewers did the time it was refused.
87
84
  this.writeProvenanceRecord(repoRoot, required, result, report.evidence);
88
- if (errors.length > 0) {
89
- 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` +
90
- errors.map((e) => ` • ${e}`).join('\n') +
91
- `\n\nSpawn the named reviewer subagent to review the checklist on THIS branch, then re-run.`);
92
- }
85
+ this.refuse(result, blind, context);
93
86
  return report;
94
87
  }
95
88
  /**
96
- * What each credited reviewer actually read. Purely observational here {@link evidenceErrors} decides
97
- * whether any of it blocks, and by default none of it does.
89
+ * Refuse with an instruction that matches WHICH failure this is.
90
+ *
91
+ * The count is the number of CHECKLISTS at fault, not the number of message paragraphs. It used to be
92
+ * `errors.length`, which printed "1 checklist(s) require …" above a list of seven names.
93
+ *
94
+ * The split matters more than the count. A checklist with no verdict file needs a reviewer spawned. A
95
+ * checklist WITH one needs the opposite: the reviewer already ran, the verdict is on disk, and only the
96
+ * attribution failed — so telling an agent to "spawn each as its OWN subagent" makes it overwrite seven
97
+ * verdicts with runs that are stamped identically and refused identically. That is a loop that destroys
98
+ * evidence on every iteration, and it is what the previous single message guaranteed.
99
+ *
100
+ * The BLOCK decision is the STATUS, not the length of `missing`. Those agree today — but a
101
+ * `PROVENANCE_MISSING` that arrived carrying no names would otherwise be a silent pass: reported
102
+ * unverified and refused by nothing. Refusing on the status makes the unverified verdict block whatever
103
+ * else is true, and `detail` carries the wording when there are no names to word it from.
98
104
  */
99
- // eslint-disable-next-line @typescript-eslint/max-params
100
- gatherEvidence(repoRoot, required, result, branch) {
105
+ refuse(result, blind, context) {
106
+ const missing = result.missing;
107
+ const neverRan = missing.filter((t) => !fs.existsSync(context.verdictPaths[t] ?? ''));
108
+ const unattributed = missing.filter((t) => fs.existsSync(context.verdictPaths[t] ?? ''));
109
+ const paragraphs = [];
110
+ if (result.status === rules_config_1.PROVENANCE_MISSING && missing.length === 0)
111
+ paragraphs.push(result.detail);
112
+ if (neverRan.length > 0) {
113
+ paragraphs.push(`these reviewer subagents did not run on this branch (spawn each as its OWN subagent — do not self-certify): ${neverRan.join(', ')}`);
114
+ }
115
+ if (unattributed.length > 0)
116
+ paragraphs.push(this.unattributedMessage(unattributed, context));
117
+ if (blind.length > 0) {
118
+ paragraphs.push(`these reviewers wrote a verdict without opening the diff (pr-gate.requireDiffEvidence is on): ${blind.join(', ')}`);
119
+ }
120
+ if (paragraphs.length === 0)
121
+ return;
122
+ // The UNION, because one checklist can be both unattributed and blind, and counting the paragraphs
123
+ // is what printed "1 checklist(s) require …" above a list of seven names. Floored at 1 so the
124
+ // nameless-MISSING case above cannot announce "0 checklist(s) failed" and then refuse.
125
+ const atFault = Math.max(new Set([...missing, ...blind]).size, 1);
126
+ throw new rules_config_1.InformAiError(`${atFault} checklist(s) failed the reviewer-provenance check — fix, then re-run pnpm wp-finish-upsert-pr:\n\n` +
127
+ paragraphs.map((e) => ` • ${e}`).join('\n\n'));
128
+ }
129
+ /**
130
+ * The verdict exists but nothing can prove WHICH branch its reviewer looked at.
131
+ *
132
+ * Reachable when a session rooted in the PRIMARY CLONE spawns reviewers for a branch that lives in a
133
+ * linked worktree: the harness stamps record 0 from the spawning session's cwd (the Agent tool has no
134
+ * cwd parameter), so every respawn is stamped identically and re-spawning cannot change the outcome.
135
+ * Saying so is the whole point of this message — the remedy is a session rooted in the worktree, or a
136
+ * reviewer that names this branch's own files.
137
+ */
138
+ unattributedMessage(unattributed, context) {
139
+ return `these reviewers WROTE a verdict for this branch that provenance cannot attribute to them: ${unattributed.join(', ')}\n` +
140
+ ` Do NOT re-spawn them. A re-spawn overwrites the verdict file it already wrote and is stamped with the\n` +
141
+ ` same cwd, so it is refused again — that loop destroys the verdicts and cannot change the outcome.\n` +
142
+ ` A reviewer is attributed when it names this branch's own files: its verdict\n` +
143
+ ` (${context.verdictPaths[unattributed[0]] ?? '<verdict path>'}) or the extracted diff (${context.diffDir}).\n` +
144
+ ` If the reviewers ran before pnpm wp-review-upsert-pr materialized that diff, re-run this from a Claude\n` +
145
+ ` Code session whose cwd IS the worktree holding ${context.branch} — that is the only cwd the harness will\n` +
146
+ ` stamp on them.`;
147
+ }
148
+ /**
149
+ * WHERE this branch's review materials live — the paths BOTH provenance passes compare against.
150
+ *
151
+ * One object for crediting and for evidence: they ask different questions of the same files, and
152
+ * building the paths twice is how the two could drift apart.
153
+ */
154
+ contextFor(repoRoot, required, branch) {
155
+ const featureName = this.aiBranchName.getFeatureName();
101
156
  const docPaths = {};
157
+ const verdictPaths = {};
102
158
  for (const req of required) {
103
- if (req.subagent.trim() !== '')
104
- docPaths[req.subagent] = req.doc.trim() === '' ? '' : path.resolve(repoRoot, req.doc);
159
+ const type = req.subagent.trim();
160
+ if (type === '')
161
+ continue;
162
+ docPaths[type] = req.doc.trim() === '' ? '' : path.resolve(repoRoot, req.doc);
163
+ verdictPaths[type] = this.reviewJsonService.checklistResultPath((0, rules_config_1.reviewJsonPath)(repoRoot, featureName), req.id);
105
164
  }
106
- const diffDir = path.join((0, rules_config_1.prDirFor)(repoRoot, this.aiBranchName.getFeatureName()), 'diff');
107
- return this.provenance.evidenceFor(new rules_config_1.EvidenceRequest(branch, result.agentIds, diffDir, docPaths));
165
+ return new rules_config_1.ReviewerContext(branch, path.join((0, rules_config_1.prDirFor)(repoRoot, featureName), 'diff'), docPaths, verdictPaths);
108
166
  }
109
167
  /**
110
168
  * Write this round's audit record: `.webpieces/pr-review/<featureSlug>/provenance.json`, linking each
@@ -143,23 +201,26 @@ let ProvenanceEnforcer = class ProvenanceEnforcer {
143
201
  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);
144
202
  }
145
203
  /**
146
- * WARN (default) or REFUSE (opt-in) on a reviewer that wrote a verdict without opening the diff.
204
+ * The reviewers that wrote a verdict without opening the diff — WARNED about (default) or returned for
205
+ * {@link refuse} to block on (opt-in).
147
206
  *
148
207
  * Default-warn because the signal is derived from undocumented Claude Code transcript internals: if the
149
208
  * format shifts, a blocking check wedges every PR in every consumer repo with no self-service recovery.
150
209
  * `requireDiffEvidence` lets a repo that has watched the warning promote it deliberately.
210
+ *
211
+ * Returns the agentTypes rather than a finished sentence so the caller can count CHECKLISTS at fault —
212
+ * a name can appear in this list and in `missing`, and it must be one checklist in the total, not two.
151
213
  */
152
- evidenceErrors(evidence, config) {
214
+ blindReviewers(evidence, config) {
153
215
  const blind = evidence.filter((e) => !e.readDiff);
154
216
  if (blind.length === 0)
155
217
  return [];
156
- const names = blind.map((e) => e.agentType).join(', ');
157
- if (!config.requireDiffEvidence) {
158
- process.stderr.write(`\n⚠️ ${blind.length} reviewer(s) wrote a verdict with no record of opening the extracted diff: ${names}\n` +
159
- ' Published on the PR as a note. Not blocking set pr-gate.requireDiffEvidence:true to make it one.\n');
160
- return [];
161
- }
162
- return [`these reviewers wrote a verdict without opening the diff (pr-gate.requireDiffEvidence is on): ${names}`];
218
+ const names = blind.map((e) => e.agentType);
219
+ if (config.requireDiffEvidence)
220
+ return names;
221
+ process.stderr.write(`\n⚠️ ${blind.length} reviewer(s) wrote a verdict with no record of opening the extracted diff: ${names.join(', ')}\n` +
222
+ ' Published on the PR as a note. Not blocking — set pr-gate.requireDiffEvidence:true to make it one.\n');
223
+ return [];
163
224
  }
164
225
  };
165
226
  exports.ProvenanceEnforcer = ProvenanceEnforcer;
@@ -1 +1 @@
1
- {"version":3,"file":"provenance-enforcer.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/provenance-enforcer.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawnSync } from 'child_process';\nimport * as path from 'path';\nimport {\n prDirFor, reviewJsonPath, RequiredChecklist, PrGateConfig, ReviewJsonService,\n SubagentProvenanceService, PROVENANCE_OK, PROVENANCE_MISSING, PROVENANCE_SKIPPED,\n ProvenanceResult, ReviewerEvidence, EvidenceRequest,\n ReviewProvenanceService, ProvenanceWriteRequest, ReviewerTranscript, ReviewerPaths, OfferedContext,\n ReviewerInstructionsService, InformAiError,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { AiBranchName } from './git-readAiBranchName';\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 */\nexport class 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/**\n * Proves each checklist verdict came from its OWN independently-run reviewer subagent, and records what\n * each one read.\n *\n * Split out of FinishUpsertPrCommand when that file crossed the 700-line cap, and it is the right seam\n * twice over: this is a self-contained integrity check with its own spec\n * (`finish-provenance-record.spec.ts`), and that spec previously had to reach through\n * `command as unknown as { enforceProvenance(...) }` to get at it. Now `enforce` is simply public.\n *\n * `@injectable(bindingScopeValues.Singleton)` so it is drawn in the DI design and injected by type.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class ProvenanceEnforcer {\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(\n private readonly aiBranchName: AiBranchName,\n private readonly provenance: SubagentProvenanceService,\n private readonly provenanceRecord: ReviewProvenanceService,\n private readonly reviewerInstructions: ReviewerInstructionsService,\n private readonly reviewJsonService: ReviewJsonService,\n ) {}\n\n /**\n * Archive this round's provenance.json alongside the archived review.json, so an archived review keeps\n * the transcript links belonging to the round that produced it — a record overwritten by the NEXT\n * round audits nothing.\n *\n * Here rather than in the command because this class owns the record; the command only knows WHEN to\n * archive, not what the record is.\n */\n archiveRecord(prDir: string): void {\n this.provenanceRecord.archive(prDir);\n }\n\n // HEAD sha for the audit record. Failure-tolerant ('' rather than a throw): a missing sha degrades\n // the audit trail, and refusing a PR over it would be the wrong trade — see writeProvenanceRecord.\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 // 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 enforce(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/<featureSlug>/provenance.json`, linking each\n * verdict to the transcript of the subagent that produced it.\n *\n * `<featureSlug>`, not `<branch>`: the dir is named by `AiBranchName.getFeatureName()`, which\n * dash-sanitizes the branch. Calling it `<branch>` is what led a bug reporter to conclude the gate\n * must be comparing a dash-form name against a slash-form git branch — see\n * ProvenanceWriteRequest.featureSlug, whose field carried the same mislabel.\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"]}
1
+ {"version":3,"file":"provenance-enforcer.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/provenance-enforcer.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawnSync } from 'child_process';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport {\n prDirFor, reviewJsonPath, RequiredChecklist, PrGateConfig, ReviewJsonService,\n SubagentProvenanceService, PROVENANCE_OK, PROVENANCE_MISSING, PROVENANCE_SKIPPED,\n ProvenanceResult, ReviewerEvidence, ReviewerContext,\n ReviewProvenanceService, ProvenanceWriteRequest, ReviewerTranscript, ReviewerPaths, OfferedContext,\n ReviewerInstructionsService, InformAiError,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { AiBranchName } from './git-readAiBranchName';\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 */\nexport class 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/**\n * Proves each checklist verdict came from its OWN independently-run reviewer subagent, and records what\n * each one read.\n *\n * Split out of FinishUpsertPrCommand when that file crossed the 700-line cap, and it is the right seam\n * twice over: this is a self-contained integrity check with its own spec\n * (`finish-provenance-record.spec.ts`), and that spec previously had to reach through\n * `command as unknown as { enforceProvenance(...) }` to get at it. Now `enforce` is simply public.\n *\n * `@injectable(bindingScopeValues.Singleton)` so it is drawn in the DI design and injected by type.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class ProvenanceEnforcer {\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(\n private readonly aiBranchName: AiBranchName,\n private readonly provenance: SubagentProvenanceService,\n private readonly provenanceRecord: ReviewProvenanceService,\n private readonly reviewerInstructions: ReviewerInstructionsService,\n private readonly reviewJsonService: ReviewJsonService,\n ) {}\n\n /**\n * Archive this round's provenance.json alongside the archived review.json, so an archived review keeps\n * the transcript links belonging to the round that produced it — a record overwritten by the NEXT\n * round audits nothing.\n *\n * Here rather than in the command because this class owns the record; the command only knows WHEN to\n * archive, not what the record is.\n */\n archiveRecord(prDir: string): void {\n this.provenanceRecord.archive(prDir);\n }\n\n // HEAD sha for the audit record. Failure-tolerant ('' rather than a throw): a missing sha degrades\n // the audit trail, and refusing a PR over it would be the wrong trade — see writeProvenanceRecord.\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 // 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 enforce(required: readonly RequiredChecklist[], branch: string, repoRoot: string, config: PrGateConfig): ProvenanceReport {\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 const context = this.contextFor(repoRoot, required, branch);\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, context);\n report.verified = result.status === PROVENANCE_OK;\n if (result.status === PROVENANCE_SKIPPED) process.stderr.write(`⚠️ ${result.detail}\\n`);\n report.evidence = this.provenance.evidenceFor(context, result.agentIds);\n const blind = this.blindReviewers(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 this.refuse(result, blind, context);\n return report;\n }\n\n /**\n * Refuse — with an instruction that matches WHICH failure this is.\n *\n * The count is the number of CHECKLISTS at fault, not the number of message paragraphs. It used to be\n * `errors.length`, which printed \"1 checklist(s) require …\" above a list of seven names.\n *\n * The split matters more than the count. A checklist with no verdict file needs a reviewer spawned. A\n * checklist WITH one needs the opposite: the reviewer already ran, the verdict is on disk, and only the\n * attribution failed — so telling an agent to \"spawn each as its OWN subagent\" makes it overwrite seven\n * verdicts with runs that are stamped identically and refused identically. That is a loop that destroys\n * evidence on every iteration, and it is what the previous single message guaranteed.\n *\n * The BLOCK decision is the STATUS, not the length of `missing`. Those agree today — but a\n * `PROVENANCE_MISSING` that arrived carrying no names would otherwise be a silent pass: reported\n * unverified and refused by nothing. Refusing on the status makes the unverified verdict block whatever\n * else is true, and `detail` carries the wording when there are no names to word it from.\n */\n private refuse(result: ProvenanceResult, blind: readonly string[], context: ReviewerContext): void {\n const missing = result.missing;\n const neverRan = missing.filter((t: string): boolean => !fs.existsSync(context.verdictPaths[t] ?? ''));\n const unattributed = missing.filter((t: string): boolean => fs.existsSync(context.verdictPaths[t] ?? ''));\n const paragraphs: string[] = [];\n if (result.status === PROVENANCE_MISSING && missing.length === 0) paragraphs.push(result.detail);\n if (neverRan.length > 0) {\n paragraphs.push(\n `these reviewer subagents did not run on this branch (spawn each as its OWN subagent — do not self-certify): ${neverRan.join(', ')}`);\n }\n if (unattributed.length > 0) paragraphs.push(this.unattributedMessage(unattributed, context));\n if (blind.length > 0) {\n paragraphs.push(`these reviewers wrote a verdict without opening the diff (pr-gate.requireDiffEvidence is on): ${blind.join(', ')}`);\n }\n if (paragraphs.length === 0) return;\n // The UNION, because one checklist can be both unattributed and blind, and counting the paragraphs\n // is what printed \"1 checklist(s) require …\" above a list of seven names. Floored at 1 so the\n // nameless-MISSING case above cannot announce \"0 checklist(s) failed\" and then refuse.\n const atFault = Math.max(new Set([...missing, ...blind]).size, 1);\n throw new InformAiError(\n `${atFault} checklist(s) failed the reviewer-provenance check — fix, then re-run pnpm wp-finish-upsert-pr:\\n\\n` +\n paragraphs.map((e: string): string => ` • ${e}`).join('\\n\\n'));\n }\n\n /**\n * The verdict exists but nothing can prove WHICH branch its reviewer looked at.\n *\n * Reachable when a session rooted in the PRIMARY CLONE spawns reviewers for a branch that lives in a\n * linked worktree: the harness stamps record 0 from the spawning session's cwd (the Agent tool has no\n * cwd parameter), so every respawn is stamped identically and re-spawning cannot change the outcome.\n * Saying so is the whole point of this message — the remedy is a session rooted in the worktree, or a\n * reviewer that names this branch's own files.\n */\n private unattributedMessage(unattributed: readonly string[], context: ReviewerContext): string {\n return `these reviewers WROTE a verdict for this branch that provenance cannot attribute to them: ${unattributed.join(', ')}\\n` +\n ` Do NOT re-spawn them. A re-spawn overwrites the verdict file it already wrote and is stamped with the\\n` +\n ` same cwd, so it is refused again — that loop destroys the verdicts and cannot change the outcome.\\n` +\n ` A reviewer is attributed when it names this branch's own files: its verdict\\n` +\n ` (${context.verdictPaths[unattributed[0]] ?? '<verdict path>'}) or the extracted diff (${context.diffDir}).\\n` +\n ` If the reviewers ran before pnpm wp-review-upsert-pr materialized that diff, re-run this from a Claude\\n` +\n ` Code session whose cwd IS the worktree holding ${context.branch} — that is the only cwd the harness will\\n` +\n ` stamp on them.`;\n }\n\n /**\n * WHERE this branch's review materials live — the paths BOTH provenance passes compare against.\n *\n * One object for crediting and for evidence: they ask different questions of the same files, and\n * building the paths twice is how the two could drift apart.\n */\n private contextFor(repoRoot: string, required: readonly RequiredChecklist[], branch: string): ReviewerContext {\n const featureName = this.aiBranchName.getFeatureName();\n const docPaths: Record<string, string> = {};\n const verdictPaths: Record<string, string> = {};\n for (const req of required) {\n const type = req.subagent.trim();\n if (type === '') continue;\n docPaths[type] = req.doc.trim() === '' ? '' : path.resolve(repoRoot, req.doc);\n verdictPaths[type] = this.reviewJsonService.checklistResultPath(reviewJsonPath(repoRoot, featureName), req.id);\n }\n return new ReviewerContext(branch, path.join(prDirFor(repoRoot, featureName), 'diff'), docPaths, verdictPaths);\n }\n\n /**\n * Write this round's audit record: `.webpieces/pr-review/<featureSlug>/provenance.json`, linking each\n * verdict to the transcript of the subagent that produced it.\n *\n * `<featureSlug>`, not `<branch>`: the dir is named by `AiBranchName.getFeatureName()`, which\n * dash-sanitizes the branch. Calling it `<branch>` is what led a bug reporter to conclude the gate\n * must be comparing a dash-form name against a slash-form git branch — see\n * ProvenanceWriteRequest.featureSlug, whose field carried the same mislabel.\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 * The reviewers that wrote a verdict without opening the diff — WARNED about (default) or returned for\n * {@link refuse} to block on (opt-in).\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 * Returns the agentTypes rather than a finished sentence so the caller can count CHECKLISTS at fault —\n * a name can appear in this list and in `missing`, and it must be one checklist in the total, not two.\n */\n private blindReviewers(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);\n if (config.requireDiffEvidence) return names;\n process.stderr.write(\n `\\n⚠️ ${blind.length} reviewer(s) wrote a verdict with no record of opening the extracted diff: ${names.join(', ')}\\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}\n"]}