@webpieces/pr-gate 0.4.593 → 0.4.594

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.593",
3
+ "version": "0.4.594",
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.593",
18
+ "@webpieces/rules-config": "0.4.594",
19
19
  "@inversifyjs/binding-decorators": "1.1.5",
20
20
  "inversify": "7.10.4",
21
21
  "reflect-metadata": "0.2.2"
@@ -0,0 +1,78 @@
1
+ import { ChecklistCommentRow } from './checklist-comment-row';
2
+ /**
3
+ * Hidden marker on the checklist review COMMENT — the "2nd comment" the PR description points at — so
4
+ * wp-finish can find and PATCH its own comment on every push instead of appending a new one. Versioned so
5
+ * the format can evolve without matching an old shape. v2 = the full roster (every DEFINED checklist,
6
+ * matched or not) + tri-state verdicts.
7
+ */
8
+ export declare const CHECKLIST_COMMENT_MARKER = "<!-- webpieces-checklists v2 -->";
9
+ /**
10
+ * Renders the 2nd PR comment: the reviewer checklist — full roster plus each reviewer's verbatim output.
11
+ *
12
+ * Split out of `Dashboard` when the PR-description/commit-body swap pushed that file over the 700-line
13
+ * cap, and the seam was already there to be found: this is ONE of the three surfaces the gated flow
14
+ * writes, it shares no rendering helper with the other two (it speaks `ChecklistCommentRow`, where the
15
+ * dashboard speaks `ChecklistRow`), and it owns the only size-fitting logic in the package. One class per
16
+ * surface is the shape the rest of this change assumes — see `Dashboard.renderPrBody` and
17
+ * `Dashboard.renderDetailComment`.
18
+ *
19
+ * `@injectable(bindingScopeValues.Singleton)` so it is drawn in the DI design and injected by type.
20
+ */
21
+ export declare class ChecklistCommentRenderer {
22
+ /**
23
+ * The ONE combined PR comment. Two halves, in this order:
24
+ *
25
+ * 1. A roll-up plus the FULL ROSTER — every DEFINED checklist as a checkbox, each with a sub-bullet
26
+ * stating exactly which globs fired against which files, or which did not and out of how many.
27
+ * Skipped checklists are listed on purpose: skipping is the normal, healthy outcome, and a comment
28
+ * that names only the reviewers that fired cannot distinguish "evaluated and irrelevant" from
29
+ * "never wired up" — nor answer "why did the DB reviewer run on my frontend PR?".
30
+ * 2. One section per reviewer that RAN, carrying its full `output` — the depth a verdict line throws
31
+ * away. Overridden first, then warned, then passed: a reader should meet the exceptions first.
32
+ *
33
+ * Idempotent: keyed by the hidden marker so wp-finish PATCHes this same comment on every push.
34
+ */
35
+ render(rows: readonly ChecklistCommentRow[], provenanceVerified: boolean, baseResolved: boolean): string;
36
+ /**
37
+ * The closing note when no reviewer produced a verdict. The original wording — "every configured
38
+ * checklist was evaluated and none of them applied" — is an all-clear, and it becomes FALSE the moment a
39
+ * checklist did apply and was declined. That sentence under a PR nobody reviewed is precisely the
40
+ * misreport this feature could otherwise introduce, so the declined case gets its own words.
41
+ */
42
+ private nothingRanNote;
43
+ private rollupHeader;
44
+ private rollupCounts;
45
+ private rosterBullet;
46
+ /**
47
+ * Did a reviewer actually produce a verdict for this row?
48
+ *
49
+ * `row.ran` is really "this checklist APPLIED to the diff" — for a required checklist the two are the
50
+ * same thing, because the PR cannot open otherwise, and the field was named before optional checklists
51
+ * existed. For a DECLINED optional one they diverge, and using `ran` alone would put a checked box and a
52
+ * reviewer section on a review that nobody performed.
53
+ */
54
+ private reviewerRan;
55
+ private declined;
56
+ private optionalTag;
57
+ /**
58
+ * Whether the reviewer demonstrably opened the diff, read from its own transcript. A QUALITY signal and
59
+ * never a blocker (see SubagentProvenanceService.evidenceFor) — but published, because "wrote a verdict
60
+ * without reading the change" is exactly what a reader of this comment would want to weigh.
61
+ */
62
+ private evidenceSuffix;
63
+ private verdictEmoji;
64
+ private verdictWords;
65
+ /**
66
+ * WHY this checklist ran or did not — the line that answers "why was this reviewer involved?". Branches
67
+ * on `configuredPatterns`, NEVER on `firedPatterns.length`: a patternless checklist and a skipped one
68
+ * both fired zero globs and they mean opposite things, so keying off the fired list would tell every
69
+ * skipped checklist's reader that the whole diff had been in its scope.
70
+ */
71
+ private whyLine;
72
+ private asCode;
73
+ private ranOrdered;
74
+ private rankOf;
75
+ private commentSection;
76
+ private fitComment;
77
+ private longestBodyIndex;
78
+ }
@@ -0,0 +1,272 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ChecklistCommentRenderer = exports.CHECKLIST_COMMENT_MARKER = void 0;
4
+ const tslib_1 = require("tslib");
5
+ const rules_config_1 = require("@webpieces/rules-config");
6
+ const inversify_1 = require("inversify");
7
+ /**
8
+ * Hidden marker on the checklist review COMMENT — the "2nd comment" the PR description points at — so
9
+ * wp-finish can find and PATCH its own comment on every push instead of appending a new one. Versioned so
10
+ * the format can evolve without matching an old shape. v2 = the full roster (every DEFINED checklist,
11
+ * matched or not) + tri-state verdicts.
12
+ */
13
+ exports.CHECKLIST_COMMENT_MARKER = '<!-- webpieces-checklists v2 -->';
14
+ const COMMENT_LIMIT = 65000; // under GitHub's 65536-char cap, with headroom for the marker + roll-up.
15
+ // One checklist section for the combined comment (heading + verbatim reviewer output), so oversize
16
+ // truncation can shrink the longest BODY without ever dropping a verdict heading.
17
+ class CommentSection {
18
+ heading;
19
+ body;
20
+ constructor(heading, body) {
21
+ this.heading = heading;
22
+ this.body = body;
23
+ }
24
+ }
25
+ /**
26
+ * Renders the 2nd PR comment: the reviewer checklist — full roster plus each reviewer's verbatim output.
27
+ *
28
+ * Split out of `Dashboard` when the PR-description/commit-body swap pushed that file over the 700-line
29
+ * cap, and the seam was already there to be found: this is ONE of the three surfaces the gated flow
30
+ * writes, it shares no rendering helper with the other two (it speaks `ChecklistCommentRow`, where the
31
+ * dashboard speaks `ChecklistRow`), and it owns the only size-fitting logic in the package. One class per
32
+ * surface is the shape the rest of this change assumes — see `Dashboard.renderPrBody` and
33
+ * `Dashboard.renderDetailComment`.
34
+ *
35
+ * `@injectable(bindingScopeValues.Singleton)` so it is drawn in the DI design and injected by type.
36
+ */
37
+ let ChecklistCommentRenderer = class ChecklistCommentRenderer {
38
+ /**
39
+ * The ONE combined PR comment. Two halves, in this order:
40
+ *
41
+ * 1. A roll-up plus the FULL ROSTER — every DEFINED checklist as a checkbox, each with a sub-bullet
42
+ * stating exactly which globs fired against which files, or which did not and out of how many.
43
+ * Skipped checklists are listed on purpose: skipping is the normal, healthy outcome, and a comment
44
+ * that names only the reviewers that fired cannot distinguish "evaluated and irrelevant" from
45
+ * "never wired up" — nor answer "why did the DB reviewer run on my frontend PR?".
46
+ * 2. One section per reviewer that RAN, carrying its full `output` — the depth a verdict line throws
47
+ * away. Overridden first, then warned, then passed: a reader should meet the exceptions first.
48
+ *
49
+ * Idempotent: keyed by the hidden marker so wp-finish PATCHes this same comment on every push.
50
+ */
51
+ render(rows, provenanceVerified, baseResolved) {
52
+ const ran = this.ranOrdered(rows);
53
+ const prov = provenanceVerified
54
+ ? '_Each reviewer ran as its own independent subagent, verified from the Claude Code harness._'
55
+ : '_⚠️ Reviewer provenance was NOT verified (no Claude Code session) — treat these as unverified._';
56
+ // The roster lives in the HEADER, never in a section: fitComment only ever shrinks section bodies,
57
+ // so a roster line can never be the thing an oversize comment silently drops.
58
+ const lines = [exports.CHECKLIST_COMMENT_MARKER, this.rollupHeader(rows, baseResolved)];
59
+ // No reviewer ran ⇒ no provenance claim to make. Printing one either way would attest to nothing.
60
+ if (ran.length > 0)
61
+ lines.push(prov);
62
+ lines.push('', `### Checklists (all ${rows.length})`);
63
+ for (const row of rows)
64
+ lines.push(this.rosterBullet(row));
65
+ const header = lines.join('\n');
66
+ if (ran.length === 0) {
67
+ return `${header}\n\n${this.nothingRanNote(rows)}`;
68
+ }
69
+ return this.fitComment(`${header}\n\n### Reviews that ran`, ran.map((r) => this.commentSection(r)));
70
+ }
71
+ /**
72
+ * The closing note when no reviewer produced a verdict. The original wording — "every configured
73
+ * checklist was evaluated and none of them applied" — is an all-clear, and it becomes FALSE the moment a
74
+ * checklist did apply and was declined. That sentence under a PR nobody reviewed is precisely the
75
+ * misreport this feature could otherwise introduce, so the declined case gets its own words.
76
+ */
77
+ nothingRanNote(rows) {
78
+ const declined = rows.filter((r) => this.declined(r));
79
+ if (declined.length === 0) {
80
+ return '_No reviewer had to run on this diff — every configured checklist was evaluated and none of them applied._';
81
+ }
82
+ return (`_No reviewer ran. ${declined.length} OPTIONAL checklist(s) DID apply to this diff and were not ` +
83
+ `run; the rest were evaluated and did not apply._`);
84
+ }
85
+ // The roll-up line. `baseResolved:false` replaces it entirely: with no fork point the changed-file set is
86
+ // EMPTY, so nothing matched — including patternless ALWAYS-RUNS checklists — and reporting that as
87
+ // "all skipped ✅" would post a green all-clear for a PR where nothing was actually evaluated.
88
+ rollupHeader(rows, baseResolved) {
89
+ if (!baseResolved) {
90
+ return (`## 🔍 Company review checklists — ⚠️ NOT EVALUATED (${rows.length} defined)\n` +
91
+ `_No diff base (fork point of main) could be resolved, so no checklist was matched against ` +
92
+ `anything. This is **not** an all-clear._`);
93
+ }
94
+ const ran = rows.filter((r) => this.reviewerRan(r));
95
+ const declined = rows.filter((r) => this.declined(r));
96
+ const skipped = rows.length - ran.length - declined.length;
97
+ const parts = [];
98
+ for (const pair of this.rollupCounts(ran))
99
+ parts.push(pair);
100
+ const breakdown = parts.length > 0 ? ` (${parts.join(' · ')})` : '';
101
+ const skip = skipped > 0 ? ` · ${skipped} skipped ✅` : '';
102
+ // Counted SEPARATELY from "skipped", and without a ✅. A declined optional review is a legitimate
103
+ // outcome, but it is not the same good news as a checklist that had nothing to look at — folding the
104
+ // two together would let a PR that declined every optional review read as fully covered.
105
+ const notRun = declined.length > 0 ? ` · ${declined.length} optional not run` : '';
106
+ return `## 🔍 Company review checklists — ${rows.length} defined · ${ran.length} ran${breakdown}${skip}${notRun}`;
107
+ }
108
+ // `🟢 2 · 🟡 1` — only the non-zero buckets, so a clean run reads as one number rather than four.
109
+ rollupCounts(ran) {
110
+ const counts = [];
111
+ const emojiFor = ['🟢', '🟡', '🟠'];
112
+ const statusFor = [rules_config_1.CK_PASS, rules_config_1.CK_WARN, rules_config_1.CK_OVERRIDDEN];
113
+ statusFor.forEach((status, i) => {
114
+ const n = ran.filter((r) => r.status === status).length;
115
+ if (n > 0)
116
+ counts.push(`${emojiFor[i]} ${n}`);
117
+ });
118
+ return counts;
119
+ }
120
+ // One roster line + its why sub-bullet. A checked box means a reviewer ran; an unchecked one means the
121
+ // checklist was evaluated and did not apply, which the words state as the good news it is.
122
+ rosterBullet(row) {
123
+ const box = this.reviewerRan(row) ? '- [x]' : '- [ ]';
124
+ return (`${box} ${this.verdictEmoji(row)} **${row.subagent}**${this.optionalTag(row)} — ` +
125
+ `${this.verdictWords(row)}${this.evidenceSuffix(row)}\n` +
126
+ ` - ${this.whyLine(row)}`);
127
+ }
128
+ /**
129
+ * Did a reviewer actually produce a verdict for this row?
130
+ *
131
+ * `row.ran` is really "this checklist APPLIED to the diff" — for a required checklist the two are the
132
+ * same thing, because the PR cannot open otherwise, and the field was named before optional checklists
133
+ * existed. For a DECLINED optional one they diverge, and using `ran` alone would put a checked box and a
134
+ * reviewer section on a review that nobody performed.
135
+ */
136
+ reviewerRan(row) {
137
+ return row.ran && !this.declined(row);
138
+ }
139
+ // Applied, optional, and carrying no verdict — i.e. the human was offered this review and said no (or
140
+ // `--no-optional` skipped the offer). Never true of a required checklist: one of those with no verdict
141
+ // does not reach a PR at all.
142
+ declined(row) {
143
+ return row.ran && !row.required && (row.status === rules_config_1.CK_MISSING || row.status === '');
144
+ }
145
+ // Marks which rows the human could have declined. Without it a reader cannot tell a review that was
146
+ // skippable from one that simply passed, and so cannot judge how much this PR was actually reviewed.
147
+ optionalTag(row) {
148
+ return row.required ? '' : ' _(optional)_';
149
+ }
150
+ /**
151
+ * Whether the reviewer demonstrably opened the diff, read from its own transcript. A QUALITY signal and
152
+ * never a blocker (see SubagentProvenanceService.evidenceFor) — but published, because "wrote a verdict
153
+ * without reading the change" is exactly what a reader of this comment would want to weigh.
154
+ */
155
+ evidenceSuffix(row) {
156
+ if (!this.reviewerRan(row) || row.diffRead === '')
157
+ return '';
158
+ return row.diffRead === 'yes' ? ' _(diff read ✓)_' : ' _(⚠️ no diff read recorded)_';
159
+ }
160
+ verdictEmoji(row) {
161
+ if (!this.reviewerRan(row))
162
+ return '⚪';
163
+ if (row.status === rules_config_1.CK_PASS)
164
+ return '🟢';
165
+ if (row.status === rules_config_1.CK_WARN)
166
+ return '🟡';
167
+ if (row.status === rules_config_1.CK_OVERRIDDEN)
168
+ return '🟠';
169
+ if (row.status === rules_config_1.CK_FAIL)
170
+ return '🔴';
171
+ return '⚪';
172
+ }
173
+ // SHORT words for a roster line / section heading. Short on purpose: the reviewer's own output and any
174
+ // override justification get their own section below, and a roster exists to be scanned.
175
+ verdictWords(row) {
176
+ // Two different unchecked boxes, two different sentences. "Not applicable" is the diff's doing;
177
+ // "not run" is a person's, and reporting the second as the first would quietly credit a review that
178
+ // a human deliberately declined.
179
+ if (this.declined(row))
180
+ return 'OPTIONAL — applied to this diff but was NOT run (not selected)';
181
+ if (!row.ran)
182
+ return 'skipped, not applicable to this diff (expected ✅)';
183
+ if (row.status === rules_config_1.CK_PASS)
184
+ return 'passed';
185
+ if (row.status === rules_config_1.CK_WARN)
186
+ return 'passed with concerns';
187
+ if (row.status === rules_config_1.CK_OVERRIDDEN)
188
+ return 'OVERRIDDEN — shipped with a stated justification';
189
+ if (row.status === rules_config_1.CK_FAIL)
190
+ return 'FAILED review';
191
+ if (row.status === rules_config_1.CK_MISSING)
192
+ return 'no verdict written';
193
+ return `unknown verdict (${row.status})`;
194
+ }
195
+ /**
196
+ * WHY this checklist ran or did not — the line that answers "why was this reviewer involved?". Branches
197
+ * on `configuredPatterns`, NEVER on `firedPatterns.length`: a patternless checklist and a skipped one
198
+ * both fired zero globs and they mean opposite things, so keying off the fired list would tell every
199
+ * skipped checklist's reader that the whole diff had been in its scope.
200
+ */
201
+ whyLine(row) {
202
+ const total = row.changedFileCount;
203
+ if (row.configuredPatterns.length === 0) {
204
+ // State the fact, not a suspicion. Patternless is a deliberate configuration — an always-runs
205
+ // gate (every PR names a ticket, every PR has an owner) is exactly what it is FOR — so telling
206
+ // every such row to "add `patterns` if that is not intended" nags the repos that meant it, on
207
+ // every PR, forever. A reader who wants to know whether it was intended can read the config.
208
+ return (`ALWAYS RUNS (no patterns) — whole diff in scope, ${total} changed file(s): ` +
209
+ `${(0, rules_config_1.formatFileList)(row.matchedFiles)}`);
210
+ }
211
+ const configured = this.asCode(row.configuredPatterns);
212
+ if (row.firedPatterns.length === 0) {
213
+ return `${configured} matched 0 of ${total} changed file(s)`;
214
+ }
215
+ return (`matched ${this.asCode(row.firedPatterns)} → ${row.matchedFiles.length} of ${total} ` +
216
+ `changed file(s): ${(0, rules_config_1.formatFileList)(row.matchedFiles)}`);
217
+ }
218
+ asCode(patterns) {
219
+ return patterns.map((p) => `\`${p}\``).join(', ');
220
+ }
221
+ // Reviewers that ran, exceptions first (overridden → warned → passed) so a reader meets what needs
222
+ // attention before a wall of green.
223
+ ranOrdered(rows) {
224
+ const rank = [rules_config_1.CK_OVERRIDDEN, rules_config_1.CK_WARN, rules_config_1.CK_PASS];
225
+ return rows
226
+ .filter((r) => this.reviewerRan(r))
227
+ .slice()
228
+ .sort((a, b) => this.rankOf(rank, a.status) - this.rankOf(rank, b.status));
229
+ }
230
+ rankOf(rank, status) {
231
+ const idx = rank.indexOf(status);
232
+ return idx < 0 ? rank.length : idx;
233
+ }
234
+ commentSection(row) {
235
+ const heading = `#### ${this.verdictEmoji(row)} ${row.subagent} — ${this.verdictWords(row)}`;
236
+ const body = row.detail.trim() !== '' ? row.detail.trim() : '_(reviewer recorded no output)_';
237
+ return new CommentSection(heading, body);
238
+ }
239
+ // Keep the comment under GitHub's size cap by shrinking the LONGEST section body first (so a short
240
+ // overridden note is never cut to make room for a long passing one), never dropping a verdict heading.
241
+ fitComment(header, sections) {
242
+ const assemble = () => `${header}\n\n${sections.map((s) => `${s.heading}\n\n${s.body}`).join('\n\n')}`;
243
+ const trunc = '\n\n…_[truncated to fit the GitHub comment size limit]_';
244
+ let out = assemble();
245
+ while (out.length > COMMENT_LIMIT) {
246
+ const idx = this.longestBodyIndex(sections);
247
+ if (idx < 0 || sections[idx].body.length <= trunc.length + 1)
248
+ break;
249
+ const over = out.length - COMMENT_LIMIT;
250
+ const keep = Math.max(0, sections[idx].body.length - over - trunc.length - 8);
251
+ sections[idx].body = sections[idx].body.slice(0, keep).trimEnd() + trunc;
252
+ out = assemble();
253
+ }
254
+ return out;
255
+ }
256
+ longestBodyIndex(sections) {
257
+ let idx = -1;
258
+ let max = -1;
259
+ sections.forEach((s, i) => {
260
+ if (s.body.length > max) {
261
+ max = s.body.length;
262
+ idx = i;
263
+ }
264
+ });
265
+ return idx;
266
+ }
267
+ };
268
+ exports.ChecklistCommentRenderer = ChecklistCommentRenderer;
269
+ exports.ChecklistCommentRenderer = ChecklistCommentRenderer = tslib_1.__decorate([
270
+ (0, inversify_1.injectable)(inversify_1.bindingScopeValues.Singleton)
271
+ ], ChecklistCommentRenderer);
272
+ //# sourceMappingURL=checklist-comment-renderer.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"checklist-comment-renderer.js","sourceRoot":"","sources":["../../../../../../packages/tooling/pr-gate/src/dashboard/checklist-comment-renderer.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n formatFileList, CK_PASS, CK_WARN, CK_OVERRIDDEN, CK_FAIL, CK_MISSING,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { ChecklistCommentRow } from './checklist-comment-row';\n\n/**\n * Hidden marker on the checklist review COMMENT — the \"2nd comment\" the PR description points at — so\n * wp-finish can find and PATCH its own comment on every push instead of appending a new one. Versioned so\n * the format can evolve without matching an old shape. v2 = the full roster (every DEFINED checklist,\n * matched or not) + tri-state verdicts.\n */\nexport const CHECKLIST_COMMENT_MARKER = '<!-- webpieces-checklists v2 -->';\n\nconst COMMENT_LIMIT = 65000; // under GitHub's 65536-char cap, with headroom for the marker + roll-up.\n\n// One checklist section for the combined comment (heading + verbatim reviewer output), so oversize\n// truncation can shrink the longest BODY without ever dropping a verdict heading.\nclass CommentSection {\n heading: string;\n body: string;\n\n constructor(heading: string, body: string) {\n this.heading = heading;\n this.body = body;\n }\n}\n\n/**\n * Renders the 2nd PR comment: the reviewer checklist — full roster plus each reviewer's verbatim output.\n *\n * Split out of `Dashboard` when the PR-description/commit-body swap pushed that file over the 700-line\n * cap, and the seam was already there to be found: this is ONE of the three surfaces the gated flow\n * writes, it shares no rendering helper with the other two (it speaks `ChecklistCommentRow`, where the\n * dashboard speaks `ChecklistRow`), and it owns the only size-fitting logic in the package. One class per\n * surface is the shape the rest of this change assumes — see `Dashboard.renderPrBody` and\n * `Dashboard.renderDetailComment`.\n *\n * `@injectable(bindingScopeValues.Singleton)` so it is drawn in the DI design and injected by type.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class ChecklistCommentRenderer {\n /**\n * The ONE combined PR comment. Two halves, in this order:\n *\n * 1. A roll-up plus the FULL ROSTER — every DEFINED checklist as a checkbox, each with a sub-bullet\n * stating exactly which globs fired against which files, or which did not and out of how many.\n * Skipped checklists are listed on purpose: skipping is the normal, healthy outcome, and a comment\n * that names only the reviewers that fired cannot distinguish \"evaluated and irrelevant\" from\n * \"never wired up\" — nor answer \"why did the DB reviewer run on my frontend PR?\".\n * 2. One section per reviewer that RAN, carrying its full `output` — the depth a verdict line throws\n * away. Overridden first, then warned, then passed: a reader should meet the exceptions first.\n *\n * Idempotent: keyed by the hidden marker so wp-finish PATCHes this same comment on every push.\n */\n render(\n rows: readonly ChecklistCommentRow[],\n provenanceVerified: boolean,\n baseResolved: boolean,\n ): string {\n const ran = this.ranOrdered(rows);\n const prov = provenanceVerified\n ? '_Each reviewer ran as its own independent subagent, verified from the Claude Code harness._'\n : '_⚠️ Reviewer provenance was NOT verified (no Claude Code session) — treat these as unverified._';\n // The roster lives in the HEADER, never in a section: fitComment only ever shrinks section bodies,\n // so a roster line can never be the thing an oversize comment silently drops.\n const lines: string[] = [CHECKLIST_COMMENT_MARKER, this.rollupHeader(rows, baseResolved)];\n // No reviewer ran ⇒ no provenance claim to make. Printing one either way would attest to nothing.\n if (ran.length > 0) lines.push(prov);\n lines.push('', `### Checklists (all ${rows.length})`);\n for (const row of rows) lines.push(this.rosterBullet(row));\n const header = lines.join('\\n');\n if (ran.length === 0) {\n return `${header}\\n\\n${this.nothingRanNote(rows)}`;\n }\n return this.fitComment(\n `${header}\\n\\n### Reviews that ran`,\n ran.map((r: ChecklistCommentRow): CommentSection => this.commentSection(r)),\n );\n }\n\n /**\n * The closing note when no reviewer produced a verdict. The original wording — \"every configured\n * checklist was evaluated and none of them applied\" — is an all-clear, and it becomes FALSE the moment a\n * checklist did apply and was declined. That sentence under a PR nobody reviewed is precisely the\n * misreport this feature could otherwise introduce, so the declined case gets its own words.\n */\n private nothingRanNote(rows: readonly ChecklistCommentRow[]): string {\n const declined = rows.filter((r: ChecklistCommentRow): boolean => this.declined(r));\n if (declined.length === 0) {\n return '_No reviewer had to run on this diff — every configured checklist was evaluated and none of them applied._';\n }\n return (\n `_No reviewer ran. ${declined.length} OPTIONAL checklist(s) DID apply to this diff and were not ` +\n `run; the rest were evaluated and did not apply._`\n );\n }\n\n // The roll-up line. `baseResolved:false` replaces it entirely: with no fork point the changed-file set is\n // EMPTY, so nothing matched — including patternless ALWAYS-RUNS checklists — and reporting that as\n // \"all skipped ✅\" would post a green all-clear for a PR where nothing was actually evaluated.\n private rollupHeader(rows: readonly ChecklistCommentRow[], baseResolved: boolean): string {\n if (!baseResolved) {\n return (\n `## 🔍 Company review checklists — ⚠️ NOT EVALUATED (${rows.length} defined)\\n` +\n `_No diff base (fork point of main) could be resolved, so no checklist was matched against ` +\n `anything. This is **not** an all-clear._`\n );\n }\n const ran = rows.filter((r: ChecklistCommentRow): boolean => this.reviewerRan(r));\n const declined = rows.filter((r: ChecklistCommentRow): boolean => this.declined(r));\n const skipped = rows.length - ran.length - declined.length;\n const parts: string[] = [];\n for (const pair of this.rollupCounts(ran)) parts.push(pair);\n const breakdown = parts.length > 0 ? ` (${parts.join(' · ')})` : '';\n const skip = skipped > 0 ? ` · ${skipped} skipped ✅` : '';\n // Counted SEPARATELY from \"skipped\", and without a ✅. A declined optional review is a legitimate\n // outcome, but it is not the same good news as a checklist that had nothing to look at — folding the\n // two together would let a PR that declined every optional review read as fully covered.\n const notRun = declined.length > 0 ? ` · ${declined.length} optional not run` : '';\n return `## 🔍 Company review checklists — ${rows.length} defined · ${ran.length} ran${breakdown}${skip}${notRun}`;\n }\n\n // `🟢 2 · 🟡 1` — only the non-zero buckets, so a clean run reads as one number rather than four.\n private rollupCounts(ran: readonly ChecklistCommentRow[]): string[] {\n const counts: string[] = [];\n const emojiFor: string[] = ['🟢', '🟡', '🟠'];\n const statusFor: string[] = [CK_PASS, CK_WARN, CK_OVERRIDDEN];\n statusFor.forEach((status: string, i: number): void => {\n const n = ran.filter((r: ChecklistCommentRow): boolean => r.status === status).length;\n if (n > 0) counts.push(`${emojiFor[i]} ${n}`);\n });\n return counts;\n }\n\n // One roster line + its why sub-bullet. A checked box means a reviewer ran; an unchecked one means the\n // checklist was evaluated and did not apply, which the words state as the good news it is.\n private rosterBullet(row: ChecklistCommentRow): string {\n const box = this.reviewerRan(row) ? '- [x]' : '- [ ]';\n return (\n `${box} ${this.verdictEmoji(row)} **${row.subagent}**${this.optionalTag(row)} — ` +\n `${this.verdictWords(row)}${this.evidenceSuffix(row)}\\n` +\n ` - ${this.whyLine(row)}`\n );\n }\n\n /**\n * Did a reviewer actually produce a verdict for this row?\n *\n * `row.ran` is really \"this checklist APPLIED to the diff\" — for a required checklist the two are the\n * same thing, because the PR cannot open otherwise, and the field was named before optional checklists\n * existed. For a DECLINED optional one they diverge, and using `ran` alone would put a checked box and a\n * reviewer section on a review that nobody performed.\n */\n private reviewerRan(row: ChecklistCommentRow): boolean {\n return row.ran && !this.declined(row);\n }\n\n // Applied, optional, and carrying no verdict — i.e. the human was offered this review and said no (or\n // `--no-optional` skipped the offer). Never true of a required checklist: one of those with no verdict\n // does not reach a PR at all.\n private declined(row: ChecklistCommentRow): boolean {\n return row.ran && !row.required && (row.status === CK_MISSING || row.status === '');\n }\n\n // Marks which rows the human could have declined. Without it a reader cannot tell a review that was\n // skippable from one that simply passed, and so cannot judge how much this PR was actually reviewed.\n private optionalTag(row: ChecklistCommentRow): string {\n return row.required ? '' : ' _(optional)_';\n }\n\n /**\n * Whether the reviewer demonstrably opened the diff, read from its own transcript. A QUALITY signal and\n * never a blocker (see SubagentProvenanceService.evidenceFor) — but published, because \"wrote a verdict\n * without reading the change\" is exactly what a reader of this comment would want to weigh.\n */\n private evidenceSuffix(row: ChecklistCommentRow): string {\n if (!this.reviewerRan(row) || row.diffRead === '') return '';\n return row.diffRead === 'yes' ? ' _(diff read ✓)_' : ' _(⚠️ no diff read recorded)_';\n }\n\n private verdictEmoji(row: ChecklistCommentRow): string {\n if (!this.reviewerRan(row)) return '⚪';\n if (row.status === CK_PASS) return '🟢';\n if (row.status === CK_WARN) return '🟡';\n if (row.status === CK_OVERRIDDEN) return '🟠';\n if (row.status === CK_FAIL) return '🔴';\n return '⚪';\n }\n\n // SHORT words for a roster line / section heading. Short on purpose: the reviewer's own output and any\n // override justification get their own section below, and a roster exists to be scanned.\n private verdictWords(row: ChecklistCommentRow): string {\n // Two different unchecked boxes, two different sentences. \"Not applicable\" is the diff's doing;\n // \"not run\" is a person's, and reporting the second as the first would quietly credit a review that\n // a human deliberately declined.\n if (this.declined(row)) return 'OPTIONAL — applied to this diff but was NOT run (not selected)';\n if (!row.ran) return 'skipped, not applicable to this diff (expected ✅)';\n if (row.status === CK_PASS) return 'passed';\n if (row.status === CK_WARN) return 'passed with concerns';\n if (row.status === CK_OVERRIDDEN) return 'OVERRIDDEN — shipped with a stated justification';\n if (row.status === CK_FAIL) return 'FAILED review';\n if (row.status === CK_MISSING) return 'no verdict written';\n return `unknown verdict (${row.status})`;\n }\n\n /**\n * WHY this checklist ran or did not — the line that answers \"why was this reviewer involved?\". Branches\n * on `configuredPatterns`, NEVER on `firedPatterns.length`: a patternless checklist and a skipped one\n * both fired zero globs and they mean opposite things, so keying off the fired list would tell every\n * skipped checklist's reader that the whole diff had been in its scope.\n */\n private whyLine(row: ChecklistCommentRow): string {\n const total = row.changedFileCount;\n if (row.configuredPatterns.length === 0) {\n // State the fact, not a suspicion. Patternless is a deliberate configuration — an always-runs\n // gate (every PR names a ticket, every PR has an owner) is exactly what it is FOR — so telling\n // every such row to \"add `patterns` if that is not intended\" nags the repos that meant it, on\n // every PR, forever. A reader who wants to know whether it was intended can read the config.\n return (\n `ALWAYS RUNS (no patterns) — whole diff in scope, ${total} changed file(s): ` +\n `${formatFileList(row.matchedFiles)}`\n );\n }\n const configured = this.asCode(row.configuredPatterns);\n if (row.firedPatterns.length === 0) {\n return `${configured} matched 0 of ${total} changed file(s)`;\n }\n return (\n `matched ${this.asCode(row.firedPatterns)} → ${row.matchedFiles.length} of ${total} ` +\n `changed file(s): ${formatFileList(row.matchedFiles)}`\n );\n }\n\n private asCode(patterns: readonly string[]): string {\n return patterns.map((p: string): string => `\\`${p}\\``).join(', ');\n }\n\n // Reviewers that ran, exceptions first (overridden → warned → passed) so a reader meets what needs\n // attention before a wall of green.\n private ranOrdered(rows: readonly ChecklistCommentRow[]): ChecklistCommentRow[] {\n const rank: string[] = [CK_OVERRIDDEN, CK_WARN, CK_PASS];\n return rows\n .filter((r: ChecklistCommentRow): boolean => this.reviewerRan(r))\n .slice()\n .sort(\n (a: ChecklistCommentRow, b: ChecklistCommentRow): number =>\n this.rankOf(rank, a.status) - this.rankOf(rank, b.status),\n );\n }\n\n private rankOf(rank: readonly string[], status: string): number {\n const idx = rank.indexOf(status);\n return idx < 0 ? rank.length : idx;\n }\n\n private commentSection(row: ChecklistCommentRow): CommentSection {\n const heading = `#### ${this.verdictEmoji(row)} ${row.subagent} — ${this.verdictWords(row)}`;\n const body =\n row.detail.trim() !== '' ? row.detail.trim() : '_(reviewer recorded no output)_';\n return new CommentSection(heading, body);\n }\n\n // Keep the comment under GitHub's size cap by shrinking the LONGEST section body first (so a short\n // overridden note is never cut to make room for a long passing one), never dropping a verdict heading.\n private fitComment(header: string, sections: CommentSection[]): string {\n const assemble = (): string =>\n `${header}\\n\\n${sections.map((s: CommentSection): string => `${s.heading}\\n\\n${s.body}`).join('\\n\\n')}`;\n const trunc = '\\n\\n…_[truncated to fit the GitHub comment size limit]_';\n let out = assemble();\n while (out.length > COMMENT_LIMIT) {\n const idx = this.longestBodyIndex(sections);\n if (idx < 0 || sections[idx].body.length <= trunc.length + 1) break;\n const over = out.length - COMMENT_LIMIT;\n const keep = Math.max(0, sections[idx].body.length - over - trunc.length - 8);\n sections[idx].body = sections[idx].body.slice(0, keep).trimEnd() + trunc;\n out = assemble();\n }\n return out;\n }\n\n private longestBodyIndex(sections: readonly CommentSection[]): number {\n let idx = -1;\n let max = -1;\n sections.forEach((s: CommentSection, i: number): void => {\n if (s.body.length > max) {\n max = s.body.length;\n idx = i;\n }\n });\n return idx;\n }\n}\n"]}
@@ -1,6 +1,13 @@
1
1
  import { GateDefinition, ReviewJson } from '@webpieces/rules-config';
2
- import { ChecklistCommentRow } from './checklist-comment-row';
3
- export declare const CHECKLIST_COMMENT_MARKER = "<!-- webpieces-checklists v2 -->";
2
+ /**
3
+ * Hidden marker on the FULL-DASHBOARD comment — the "1st comment" the PR description points at — so
4
+ * finish PATCHes its own comment on every push instead of appending a new one, exactly like the checklist
5
+ * comment (see ChecklistCommentRenderer, which owns the 2nd).
6
+ *
7
+ * This comment did not exist before the PR description became the git-log body: the dashboard WAS the
8
+ * description. Moving it here is what keeps a risk table out of every squash commit on main.
9
+ */
10
+ export declare const DETAIL_COMMENT_MARKER = "<!-- webpieces-pr-detail v1 -->";
4
11
  export declare class GateResult {
5
12
  name: string;
6
13
  warningColor: string;
@@ -29,70 +36,77 @@ export declare class DashboardInput {
29
36
  mainHead: string;
30
37
  review: ReviewJson;
31
38
  checklists: ChecklistRow[];
32
- constructor(title: string, gateResults: GateResult[], disables: DisableCounts, buildPassed: boolean, forkPoint: string, featureHead: string, mainHead: string, review: ReviewJson, checklists?: ChecklistRow[]);
39
+ /**
40
+ * `commands.pr-gate.buildCommand` VERBATIM, named in the PR-body footer so `git log` records WHICH
41
+ * command vouched for the commit. Read from config rather than hard-coded because the footer used to
42
+ * assert "build ran via nx affected" on every repo, including those whose buildCommand is not nx.
43
+ *
44
+ * REQUIRED, with no default, and `checklists` lost its `= []` for the same reason. A defaulted
45
+ * `buildCommand: string = ''` let every pre-existing 9-argument construction keep compiling while
46
+ * silently rendering a footer that claims nothing — an absence that quietly means "no build was
47
+ * named", which is the widening-by-omission the compatibility policy calls out. Making it required
48
+ * means every caller states what vouched for the commit, and the empty string stays available for a
49
+ * repo that genuinely has no build command, but only when someone writes it down.
50
+ */
51
+ buildCommand: string;
52
+ constructor(title: string, gateResults: GateResult[], disables: DisableCounts, buildPassed: boolean, forkPoint: string, featureHead: string, mainHead: string, review: ReviewJson, checklists: ChecklistRow[], buildCommand: string);
33
53
  }
34
54
  /** Renders the PR-gate dashboard markdown (gates × changed files, disables, risk, 3-point hashes). */
35
55
  export declare class Dashboard {
36
56
  computeGateResults(gates: GateDefinition[], changedFiles: string[]): GateResult[];
37
57
  countAddedDisables(patch: string): DisableCounts;
38
- renderDashboard(input: DashboardInput): string;
39
58
  /**
40
- * The ONE combined PR comment. Two halves, in this order:
59
+ * The FULL dashboard — every row (green included), the whole summary, the 3-point hash points.
41
60
  *
42
- * 1. A roll-up plus the FULL ROSTER — every DEFINED checklist as a checkbox, each with a sub-bullet
43
- * stating exactly which globs fired against which files, or which did not and out of how many.
44
- * Skipped checklists are listed on purpose: skipping is the normal, healthy outcome, and a comment
45
- * that names only the reviewers that fired cannot distinguish "evaluated and irrelevant" from
46
- * "never wired up" — nor answer "why did the DB reviewer run on my frontend PR?".
47
- * 2. One section per reviewer that RAN, carrying its full `output` — the depth a verdict line throws
48
- * away. Overridden first, then warned, then passed: a reader should meet the exceptions first.
61
+ * This is the **1st PR comment**, not the PR description. It used to be the description, and that is
62
+ * exactly what put it into main's history: GitHub's `squash_merge_commit_message: PR_BODY` copies the
63
+ * description verbatim into the squash commit, so every `git log` entry carried the risk table, the
64
+ * hash points and the gate token. The description now holds {@link renderPrBody}'s compact form, and
65
+ * everything long-form lives here where a reader can open it and `git log` never sees it.
49
66
  *
50
- * Idempotent: keyed by the hidden marker so wp-finish PATCHes this same comment on every push.
67
+ * Machine-facing content belongs HERE too — this comment carries the HMAC gate token. That is the
68
+ * whole rule that keeps the description clean: an HTML comment is invisible in rendered markdown but
69
+ * perfectly visible in `git log`, so nothing hidden may live in the description.
51
70
  */
52
- renderChecklistComment(rows: readonly ChecklistCommentRow[], provenanceVerified: boolean, baseResolved: boolean): string;
71
+ renderDetailComment(input: DashboardInput): string;
53
72
  /**
54
- * The closing note when no reviewer produced a verdict. The original wording — "every configured
55
- * checklist was evaluated and none of them applied" — is an all-clear, and it becomes FALSE the moment a
56
- * checklist did apply and was declined. That sentence under a PR nobody reviewed is precisely the
57
- * misreport this feature could otherwise introduce, so the declined case gets its own words.
58
- */
59
- private nothingRanNote;
60
- private rollupHeader;
61
- private rollupCounts;
62
- private rosterBullet;
63
- /**
64
- * Did a reviewer actually produce a verdict for this row?
73
+ * The PR DESCRIPTION — which is also, byte for byte, the squash-commit body that lands in main.
65
74
  *
66
- * `row.ran` is really "this checklist APPLIED to the diff" — for a required checklist the two are the
67
- * same thing, because the PR cannot open otherwise, and the field was named before optional checklists
68
- * existed. For a DECLINED optional one they diverge, and using `ran` alone would put a checked box and a
69
- * reviewer section on a review that nobody performed.
70
- */
71
- private reviewerRan;
72
- private declined;
73
- private optionalTag;
74
- /**
75
- * Whether the reviewer demonstrably opened the diff, read from its own transcript. A QUALITY signal and
76
- * never a blocker (see SubagentProvenanceService.evidenceFor) — but published, because "wrote a verdict
77
- * without reading the change" is exactly what a reader of this comment would want to weigh.
75
+ * ─── Why one string serves both ─────────────────────────────────────────────────────────────────
76
+ * There used to be two: a long dashboard in the description and this compact form passed to
77
+ * `gh pr merge --body-file`. That made the good `git log` reachable ONLY through an explicit
78
+ * `--body-file` merge, because every other route (the GitHub Merge button, a bare `gh pr merge`)
79
+ * takes its body from the repo's `squash_merge_commit_message`, and on `PR_BODY` that copied the
80
+ * whole dashboard into history. Two repos ran that way for months.
81
+ *
82
+ * Making the DESCRIPTION the compact form inverts it: `PR_BODY` now yields exactly the right commit,
83
+ * so the UI button, a bare `gh pr merge`, `wp-land-pr` and finish's own auto-merge all converge on
84
+ * identical bytes. The consumer requirement is two repo settings — `squash_merge_commit_title:
85
+ * PR_TITLE` and `squash_merge_commit_message: PR_BODY` — not a config key and not a command anyone
86
+ * has to remember. That convergence is the point, and `pr-body-is-merge-body.spec.ts` pins it.
87
+ *
88
+ * ─── The shape, and why each part earns its line ────────────────────────────────────────────────
89
+ * The URL leads, labelled `(for git log)` so nobody deletes it as redundant while reading the PR on
90
+ * GitHub — on the page it is obviously the page you are on; in `git log` it is the only way back.
91
+ * Then the risk score (always). Then ONLY the non-green flags: a commit log should surface what
92
+ * stands out, and the green rows are in the 1st comment. The last bullet is always the pointer to
93
+ * that comment, so a reader of main's history is never left thinking this is all there was. Then the
94
+ * summary capped at 4 sentences, then the footer naming the build command that vouched for it.
95
+ *
96
+ * The hidden gate-token marker is appended AFTER this by the caller, so it is the very last line of
97
+ * the description and therefore of the commit. It stays an HTML comment — invisible on the PR page,
98
+ * visible in `git log` — which is a deliberate accepted trade: the alternative was reshaping a live
99
+ * CI-critical surface that two consumer repos verify on every PR, to save one line of history.
78
100
  */
79
- private evidenceSuffix;
80
- private verdictEmoji;
81
- private verdictWords;
101
+ renderPrBody(input: DashboardInput, prUrl: string): string;
82
102
  /**
83
- * WHY this checklist ran or did not — the line that answers "why was this reviewer involved?". Branches
84
- * on `configuredPatterns`, NEVER on `firedPatterns.length`: a patternless checklist and a skipped one
85
- * both fired zero globs and they mean opposite things, so keying off the fired list would tell every
86
- * skipped checklist's reader that the whole diff had been in its scope.
103
+ * The footer, naming the build command from `commands.pr-gate.buildCommand` VERBATIM.
104
+ *
105
+ * No backticks: this string's primary home is `git log` in a terminal, where markdown ticks are
106
+ * literal punctuation. "run locally" is the honest claim — this is the LOCAL gate's receipt, and the
107
+ * server-side proof is the gate token in the 1st comment, not this sentence.
87
108
  */
88
- private whyLine;
89
- private asCode;
90
- private ranOrdered;
91
- private rankOf;
92
- private commentSection;
93
- private fitComment;
94
- private longestBodyIndex;
95
- renderCommitBody(input: DashboardInput, prUrl: string): string;
109
+ private prBodyFooter;
96
110
  private nonGreenFlags;
97
111
  private checklistStatusText;
98
112
  private firstSentences;