@webpieces/pr-gate 0.4.647 → 0.4.649
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.
|
|
3
|
+
"version": "0.4.649",
|
|
4
4
|
"description": "Gated PR system: 3-point squash-merge, merge validation gate, and red/yellow/green PR dashboard. Standalone scripts, no Nx dependency required.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"directory": "packages/tooling/pr-gate"
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@webpieces/rules-config": "0.4.
|
|
18
|
+
"@webpieces/rules-config": "0.4.649",
|
|
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
|
-
*
|
|
43
|
-
*
|
|
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
|
|
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
|
-
*
|
|
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
|
|
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,
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
97
|
-
*
|
|
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
|
-
|
|
100
|
-
|
|
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
|
-
|
|
104
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
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)
|
|
157
|
-
if (
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
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"]}
|