@webpieces/pr-gate 0.4.564 → 0.4.565

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.
Files changed (31) hide show
  1. package/package.json +2 -2
  2. package/src/dashboard/checklist-comment-row.d.ts +42 -0
  3. package/src/dashboard/checklist-comment-row.js +59 -0
  4. package/src/dashboard/checklist-comment-row.js.map +1 -0
  5. package/src/dashboard/dashboard.d.ts +19 -29
  6. package/src/dashboard/dashboard.js +57 -53
  7. package/src/dashboard/dashboard.js.map +1 -1
  8. package/src/scripts/commands/finish-upsert-pr-command.d.ts +9 -0
  9. package/src/scripts/commands/finish-upsert-pr-command.js +24 -4
  10. package/src/scripts/commands/finish-upsert-pr-command.js.map +1 -1
  11. package/src/scripts/commands/review-upsert-pr-command.d.ts +12 -1
  12. package/src/scripts/commands/review-upsert-pr-command.js +20 -4
  13. package/src/scripts/commands/review-upsert-pr-command.js.map +1 -1
  14. package/src/scripts/pr-gate-app.d.ts +2 -2
  15. package/src/scripts/pr-gate-app.js +2 -2
  16. package/src/scripts/pr-gate-app.js.map +1 -1
  17. package/src/scripts/workflow/checklist-detector.js +1 -1
  18. package/src/scripts/workflow/checklist-detector.js.map +1 -1
  19. package/src/scripts/workflow/checklist-scanner.d.ts +21 -1
  20. package/src/scripts/workflow/checklist-scanner.js +31 -3
  21. package/src/scripts/workflow/checklist-scanner.js.map +1 -1
  22. package/src/scripts/workflow/review-report.d.ts +62 -1
  23. package/src/scripts/workflow/review-report.js +165 -34
  24. package/src/scripts/workflow/review-report.js.map +1 -1
  25. package/src/scripts/workflow/reviewer-briefing-builder.js +1 -0
  26. package/src/scripts/workflow/reviewer-briefing-builder.js.map +1 -1
  27. package/src/scripts/workflow/reviewer-verdict-gate.d.ts +10 -0
  28. package/src/scripts/workflow/reviewer-verdict-gate.js +17 -1
  29. package/src/scripts/workflow/reviewer-verdict-gate.js.map +1 -1
  30. package/src/scripts/wp-review-upsert-pr.js +9 -2
  31. package/src/scripts/wp-review-upsert-pr.js.map +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webpieces/pr-gate",
3
- "version": "0.4.564",
3
+ "version": "0.4.565",
4
4
  "description": "Gated PR system: 3-point squash-merge, merge validation gate, and red/yellow/green PR dashboard. Standalone scripts, no Nx dependency required.",
5
5
  "type": "commonjs",
6
6
  "main": "./src/index.js",
@@ -26,7 +26,7 @@
26
26
  "directory": "packages/tooling/pr-gate"
27
27
  },
28
28
  "dependencies": {
29
- "@webpieces/rules-config": "0.4.564",
29
+ "@webpieces/rules-config": "0.4.565",
30
30
  "@inversifyjs/binding-decorators": "1.1.5",
31
31
  "inversify": "7.10.4",
32
32
  "reflect-metadata": "0.2.2"
@@ -0,0 +1,42 @@
1
+ /**
2
+ * One roster line of the checklist COMMENT: a defined checklist, whether its reviewer ran, and the evidence
3
+ * for WHY. Deliberately separate from {@link ChecklistRow} rather than five more fields on it — ChecklistRow
4
+ * also feeds the PR body and the squash-commit body, and roster evidence has no business travelling into
5
+ * main's git history or through every DashboardInput construction site.
6
+ *
7
+ * Kept in pr-gate, not rules-config: the shape of a GitHub comment is this package's concern, and
8
+ * rules-config is the dependency, not the dependent. Its own FILE (rather than sitting in dashboard.ts)
9
+ * per the one-class-per-file convention — dashboard.ts is a large renderer against a hard max-file-lines
10
+ * limit, while this is a data class its consumers construct directly: finish builds these, Dashboard only
11
+ * reads them.
12
+ */
13
+ export declare class ChecklistCommentRow {
14
+ subagent: string;
15
+ status: string;
16
+ detail: string;
17
+ ran: boolean;
18
+ configuredPatterns: string[];
19
+ firedPatterns: string[];
20
+ matchedFiles: string[];
21
+ changedFileCount: number;
22
+ /**
23
+ * Whether this reviewer's own transcript shows it OPENING the extracted diff.
24
+ *
25
+ * '' = not assessed (no Claude Code session, or nothing was materialized) and prints nothing — "no
26
+ * evidence recorded" and "evidence says it never looked" are different claims, and conflating them
27
+ * would accuse a reviewer that ran perfectly well in CI. 'yes' | 'no' otherwise. Defaulted, so every
28
+ * existing construction site is unchanged.
29
+ */
30
+ diffRead: string;
31
+ /**
32
+ * The checklist's configured `required`. Published because "no verdict" means two opposite things and the
33
+ * roster is the only place a reader can tell them apart: a REQUIRED checklist with no verdict could not
34
+ * have opened this PR at all, so seeing one means something went wrong; an OPTIONAL one with no verdict
35
+ * is a review the human was offered and declined, which is the feature working.
36
+ *
37
+ * Defaulted to the blocking value so every existing construction site is unchanged and a row that forgets
38
+ * to set it is reported as the stricter of the two.
39
+ */
40
+ required: boolean;
41
+ constructor(subagent: string, status: string, detail: string, ran: boolean, configuredPatterns: string[], firedPatterns: string[], matchedFiles: string[], changedFileCount: number);
42
+ }
@@ -0,0 +1,59 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.ChecklistCommentRow = void 0;
4
+ /**
5
+ * One roster line of the checklist COMMENT: a defined checklist, whether its reviewer ran, and the evidence
6
+ * for WHY. Deliberately separate from {@link ChecklistRow} rather than five more fields on it — ChecklistRow
7
+ * also feeds the PR body and the squash-commit body, and roster evidence has no business travelling into
8
+ * main's git history or through every DashboardInput construction site.
9
+ *
10
+ * Kept in pr-gate, not rules-config: the shape of a GitHub comment is this package's concern, and
11
+ * rules-config is the dependency, not the dependent. Its own FILE (rather than sitting in dashboard.ts)
12
+ * per the one-class-per-file convention — dashboard.ts is a large renderer against a hard max-file-lines
13
+ * limit, while this is a data class its consumers construct directly: finish builds these, Dashboard only
14
+ * reads them.
15
+ */
16
+ class ChecklistCommentRow {
17
+ subagent; // the reviewer / checklist id
18
+ status; // CK_* verdict; '' when it did not run
19
+ detail; // verbatim reviewer output
20
+ ran; // false = skipped, which is a NORMAL, healthy outcome
21
+ // The checklist's CONFIGURED globs. The only safe signal for "always runs": a patternless checklist and
22
+ // a skipped one both have an empty `firedPatterns`, and they mean opposite things.
23
+ configuredPatterns;
24
+ firedPatterns; // which configured globs actually hit a changed file
25
+ matchedFiles;
26
+ changedFileCount; // how many files were considered at all — "0 of N" needs the N
27
+ /**
28
+ * Whether this reviewer's own transcript shows it OPENING the extracted diff.
29
+ *
30
+ * '' = not assessed (no Claude Code session, or nothing was materialized) and prints nothing — "no
31
+ * evidence recorded" and "evidence says it never looked" are different claims, and conflating them
32
+ * would accuse a reviewer that ran perfectly well in CI. 'yes' | 'no' otherwise. Defaulted, so every
33
+ * existing construction site is unchanged.
34
+ */
35
+ diffRead = '';
36
+ /**
37
+ * The checklist's configured `required`. Published because "no verdict" means two opposite things and the
38
+ * roster is the only place a reader can tell them apart: a REQUIRED checklist with no verdict could not
39
+ * have opened this PR at all, so seeing one means something went wrong; an OPTIONAL one with no verdict
40
+ * is a review the human was offered and declined, which is the feature working.
41
+ *
42
+ * Defaulted to the blocking value so every existing construction site is unchanged and a row that forgets
43
+ * to set it is reported as the stricter of the two.
44
+ */
45
+ required = true;
46
+ // eslint-disable-next-line @typescript-eslint/max-params
47
+ constructor(subagent, status, detail, ran, configuredPatterns, firedPatterns, matchedFiles, changedFileCount) {
48
+ this.subagent = subagent;
49
+ this.status = status;
50
+ this.detail = detail;
51
+ this.ran = ran;
52
+ this.configuredPatterns = configuredPatterns;
53
+ this.firedPatterns = firedPatterns;
54
+ this.matchedFiles = matchedFiles;
55
+ this.changedFileCount = changedFileCount;
56
+ }
57
+ }
58
+ exports.ChecklistCommentRow = ChecklistCommentRow;
59
+ //# sourceMappingURL=checklist-comment-row.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"checklist-comment-row.js","sourceRoot":"","sources":["../../../../../../packages/tooling/pr-gate/src/dashboard/checklist-comment-row.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;;;;GAWG;AACH,MAAa,mBAAmB;IAC5B,QAAQ,CAAS,CAAC,8BAA8B;IAChD,MAAM,CAAS,CAAC,uCAAuC;IACvD,MAAM,CAAS,CAAC,2BAA2B;IAC3C,GAAG,CAAU,CAAC,sDAAsD;IACpE,wGAAwG;IACxG,mFAAmF;IACnF,kBAAkB,CAAW;IAC7B,aAAa,CAAW,CAAC,qDAAqD;IAC9E,YAAY,CAAW;IACvB,gBAAgB,CAAS,CAAC,+DAA+D;IACzF;;;;;;;OAOG;IACH,QAAQ,GAAW,EAAE,CAAC;IACtB;;;;;;;;OAQG;IACH,QAAQ,GAAY,IAAI,CAAC;IAEzB,yDAAyD;IACzD,YACI,QAAgB,EAChB,MAAc,EACd,MAAc,EACd,GAAY,EACZ,kBAA4B,EAC5B,aAAuB,EACvB,YAAsB,EACtB,gBAAwB;QAExB,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,GAAG,GAAG,GAAG,CAAC;QACf,IAAI,CAAC,kBAAkB,GAAG,kBAAkB,CAAC;QAC7C,IAAI,CAAC,aAAa,GAAG,aAAa,CAAC;QACnC,IAAI,CAAC,YAAY,GAAG,YAAY,CAAC;QACjC,IAAI,CAAC,gBAAgB,GAAG,gBAAgB,CAAC;IAC7C,CAAC;CACJ;AAnDD,kDAmDC","sourcesContent":["/**\n * One roster line of the checklist COMMENT: a defined checklist, whether its reviewer ran, and the evidence\n * for WHY. Deliberately separate from {@link ChecklistRow} rather than five more fields on it — ChecklistRow\n * also feeds the PR body and the squash-commit body, and roster evidence has no business travelling into\n * main's git history or through every DashboardInput construction site.\n *\n * Kept in pr-gate, not rules-config: the shape of a GitHub comment is this package's concern, and\n * rules-config is the dependency, not the dependent. Its own FILE (rather than sitting in dashboard.ts)\n * per the one-class-per-file convention — dashboard.ts is a large renderer against a hard max-file-lines\n * limit, while this is a data class its consumers construct directly: finish builds these, Dashboard only\n * reads them.\n */\nexport class ChecklistCommentRow {\n subagent: string; // the reviewer / checklist id\n status: string; // CK_* verdict; '' when it did not run\n detail: string; // verbatim reviewer output\n ran: boolean; // false = skipped, which is a NORMAL, healthy outcome\n // The checklist's CONFIGURED globs. The only safe signal for \"always runs\": a patternless checklist and\n // a skipped one both have an empty `firedPatterns`, and they mean opposite things.\n configuredPatterns: string[];\n firedPatterns: string[]; // which configured globs actually hit a changed file\n matchedFiles: string[];\n changedFileCount: number; // how many files were considered at all — \"0 of N\" needs the N\n /**\n * Whether this reviewer's own transcript shows it OPENING the extracted diff.\n *\n * '' = not assessed (no Claude Code session, or nothing was materialized) and prints nothing — \"no\n * evidence recorded\" and \"evidence says it never looked\" are different claims, and conflating them\n * would accuse a reviewer that ran perfectly well in CI. 'yes' | 'no' otherwise. Defaulted, so every\n * existing construction site is unchanged.\n */\n diffRead: string = '';\n /**\n * The checklist's configured `required`. Published because \"no verdict\" means two opposite things and the\n * roster is the only place a reader can tell them apart: a REQUIRED checklist with no verdict could not\n * have opened this PR at all, so seeing one means something went wrong; an OPTIONAL one with no verdict\n * is a review the human was offered and declined, which is the feature working.\n *\n * Defaulted to the blocking value so every existing construction site is unchanged and a row that forgets\n * to set it is reported as the stricter of the two.\n */\n required: boolean = true;\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(\n subagent: string,\n status: string,\n detail: string,\n ran: boolean,\n configuredPatterns: string[],\n firedPatterns: string[],\n matchedFiles: string[],\n changedFileCount: number,\n ) {\n this.subagent = subagent;\n this.status = status;\n this.detail = detail;\n this.ran = ran;\n this.configuredPatterns = configuredPatterns;\n this.firedPatterns = firedPatterns;\n this.matchedFiles = matchedFiles;\n this.changedFileCount = changedFileCount;\n }\n}\n"]}
@@ -1,4 +1,5 @@
1
1
  import { GateDefinition, ReviewJson } from '@webpieces/rules-config';
2
+ import { ChecklistCommentRow } from './checklist-comment-row';
2
3
  export declare const CHECKLIST_COMMENT_MARKER = "<!-- webpieces-checklists v2 -->";
3
4
  export declare class GateResult {
4
5
  name: string;
@@ -12,35 +13,6 @@ export declare class ChecklistRow {
12
13
  detail: string;
13
14
  constructor(title: string, status: string, detail?: string);
14
15
  }
15
- /**
16
- * One roster line of the checklist COMMENT: a defined checklist, whether its reviewer ran, and the evidence
17
- * for WHY. Deliberately separate from {@link ChecklistRow} rather than five more fields on it — ChecklistRow
18
- * also feeds the PR body and the squash-commit body, and roster evidence has no business travelling into
19
- * main's git history or through every DashboardInput construction site.
20
- *
21
- * Kept in pr-gate, not rules-config: the shape of a GitHub comment is this package's concern, and
22
- * rules-config is the dependency, not the dependent.
23
- */
24
- export declare class ChecklistCommentRow {
25
- subagent: string;
26
- status: string;
27
- detail: string;
28
- ran: boolean;
29
- configuredPatterns: string[];
30
- firedPatterns: string[];
31
- matchedFiles: string[];
32
- changedFileCount: number;
33
- /**
34
- * Whether this reviewer's own transcript shows it OPENING the extracted diff.
35
- *
36
- * '' = not assessed (no Claude Code session, or nothing was materialized) and prints nothing — "no
37
- * evidence recorded" and "evidence says it never looked" are different claims, and conflating them
38
- * would accuse a reviewer that ran perfectly well in CI. 'yes' | 'no' otherwise. Defaulted, so every
39
- * existing construction site is unchanged.
40
- */
41
- diffRead: string;
42
- constructor(subagent: string, status: string, detail: string, ran: boolean, configuredPatterns: string[], firedPatterns: string[], matchedFiles: string[], changedFileCount: number);
43
- }
44
16
  export declare class DisableCounts {
45
17
  webpiecesCount: number;
46
18
  eslintCount: number;
@@ -78,9 +50,27 @@ export declare class Dashboard {
78
50
  * Idempotent: keyed by the hidden marker so wp-finish PATCHes this same comment on every push.
79
51
  */
80
52
  renderChecklistComment(rows: readonly ChecklistCommentRow[], provenanceVerified: boolean, baseResolved: boolean): string;
53
+ /**
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;
81
60
  private rollupHeader;
82
61
  private rollupCounts;
83
62
  private rosterBullet;
63
+ /**
64
+ * Did a reviewer actually produce a verdict for this row?
65
+ *
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;
84
74
  /**
85
75
  * Whether the reviewer demonstrably opened the diff, read from its own transcript. A QUALITY signal and
86
76
  * never a blocker (see SubagentProvenanceService.evidenceFor) — but published, because "wrote a verdict
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.Dashboard = exports.DashboardInput = exports.DisableCounts = exports.ChecklistCommentRow = exports.ChecklistRow = exports.GateResult = exports.CHECKLIST_COMMENT_MARKER = void 0;
3
+ exports.Dashboard = exports.DashboardInput = exports.DisableCounts = exports.ChecklistRow = exports.GateResult = exports.CHECKLIST_COMMENT_MARKER = void 0;
4
4
  const tslib_1 = require("tslib");
5
5
  const rules_config_1 = require("@webpieces/rules-config");
6
6
  const inversify_1 = require("inversify");
@@ -47,48 +47,6 @@ class ChecklistRow {
47
47
  }
48
48
  }
49
49
  exports.ChecklistRow = ChecklistRow;
50
- /**
51
- * One roster line of the checklist COMMENT: a defined checklist, whether its reviewer ran, and the evidence
52
- * for WHY. Deliberately separate from {@link ChecklistRow} rather than five more fields on it — ChecklistRow
53
- * also feeds the PR body and the squash-commit body, and roster evidence has no business travelling into
54
- * main's git history or through every DashboardInput construction site.
55
- *
56
- * Kept in pr-gate, not rules-config: the shape of a GitHub comment is this package's concern, and
57
- * rules-config is the dependency, not the dependent.
58
- */
59
- class ChecklistCommentRow {
60
- subagent; // the reviewer / checklist id
61
- status; // CK_* verdict; '' when it did not run
62
- detail; // verbatim reviewer output
63
- ran; // false = skipped, which is a NORMAL, healthy outcome
64
- // The checklist's CONFIGURED globs. The only safe signal for "always runs": a patternless checklist and
65
- // a skipped one both have an empty `firedPatterns`, and they mean opposite things.
66
- configuredPatterns;
67
- firedPatterns; // which configured globs actually hit a changed file
68
- matchedFiles;
69
- changedFileCount; // how many files were considered at all — "0 of N" needs the N
70
- /**
71
- * Whether this reviewer's own transcript shows it OPENING the extracted diff.
72
- *
73
- * '' = not assessed (no Claude Code session, or nothing was materialized) and prints nothing — "no
74
- * evidence recorded" and "evidence says it never looked" are different claims, and conflating them
75
- * would accuse a reviewer that ran perfectly well in CI. 'yes' | 'no' otherwise. Defaulted, so every
76
- * existing construction site is unchanged.
77
- */
78
- diffRead = '';
79
- // eslint-disable-next-line @typescript-eslint/max-params
80
- constructor(subagent, status, detail, ran, configuredPatterns, firedPatterns, matchedFiles, changedFileCount) {
81
- this.subagent = subagent;
82
- this.status = status;
83
- this.detail = detail;
84
- this.ran = ran;
85
- this.configuredPatterns = configuredPatterns;
86
- this.firedPatterns = firedPatterns;
87
- this.matchedFiles = matchedFiles;
88
- this.changedFileCount = changedFileCount;
89
- }
90
- }
91
- exports.ChecklistCommentRow = ChecklistCommentRow;
92
50
  /**
93
51
  * One severity bucket of the rolled-up **Checklists** dashboard row: its emoji, the words that describe it,
94
52
  * and which checklists landed in it. Data-only.
@@ -239,11 +197,24 @@ let Dashboard = class Dashboard {
239
197
  lines.push(this.rosterBullet(row));
240
198
  const header = lines.join('\n');
241
199
  if (ran.length === 0) {
242
- return (`${header}\n\n_No reviewer had to run on this diff — every configured checklist was ` +
243
- `evaluated and none of them applied._`);
200
+ return `${header}\n\n${this.nothingRanNote(rows)}`;
244
201
  }
245
202
  return this.fitComment(`${header}\n\n### Reviews that ran`, ran.map((r) => this.commentSection(r)));
246
203
  }
204
+ /**
205
+ * The closing note when no reviewer produced a verdict. The original wording — "every configured
206
+ * checklist was evaluated and none of them applied" — is an all-clear, and it becomes FALSE the moment a
207
+ * checklist did apply and was declined. That sentence under a PR nobody reviewed is precisely the
208
+ * misreport this feature could otherwise introduce, so the declined case gets its own words.
209
+ */
210
+ nothingRanNote(rows) {
211
+ const declined = rows.filter((r) => this.declined(r));
212
+ if (declined.length === 0) {
213
+ return '_No reviewer had to run on this diff — every configured checklist was evaluated and none of them applied._';
214
+ }
215
+ return (`_No reviewer ran. ${declined.length} OPTIONAL checklist(s) DID apply to this diff and were not ` +
216
+ `run; the rest were evaluated and did not apply._`);
217
+ }
247
218
  // The roll-up line. `baseResolved:false` replaces it entirely: with no fork point the changed-file set is
248
219
  // EMPTY, so nothing matched — including patternless ALWAYS-RUNS checklists — and reporting that as
249
220
  // "all skipped ✅" would post a green all-clear for a PR where nothing was actually evaluated.
@@ -253,14 +224,19 @@ let Dashboard = class Dashboard {
253
224
  `_No diff base (fork point of main) could be resolved, so no checklist was matched against ` +
254
225
  `anything. This is **not** an all-clear._`);
255
226
  }
256
- const ran = rows.filter((r) => r.ran);
257
- const skipped = rows.length - ran.length;
227
+ const ran = rows.filter((r) => this.reviewerRan(r));
228
+ const declined = rows.filter((r) => this.declined(r));
229
+ const skipped = rows.length - ran.length - declined.length;
258
230
  const parts = [];
259
231
  for (const pair of this.rollupCounts(ran))
260
232
  parts.push(pair);
261
233
  const breakdown = parts.length > 0 ? ` (${parts.join(' · ')})` : '';
262
234
  const skip = skipped > 0 ? ` · ${skipped} skipped ✅` : '';
263
- return `## 🔍 Company review checklists — ${rows.length} defined · ${ran.length} ran${breakdown}${skip}`;
235
+ // Counted SEPARATELY from "skipped", and without a ✅. A declined optional review is a legitimate
236
+ // outcome, but it is not the same good news as a checklist that had nothing to look at — folding the
237
+ // two together would let a PR that declined every optional review read as fully covered.
238
+ const notRun = declined.length > 0 ? ` · ${declined.length} optional not run` : '';
239
+ return `## 🔍 Company review checklists — ${rows.length} defined · ${ran.length} ran${breakdown}${skip}${notRun}`;
264
240
  }
265
241
  // `🟢 2 · 🟡 1` — only the non-zero buckets, so a clean run reads as one number rather than four.
266
242
  rollupCounts(ran) {
@@ -277,22 +253,45 @@ let Dashboard = class Dashboard {
277
253
  // One roster line + its why sub-bullet. A checked box means a reviewer ran; an unchecked one means the
278
254
  // checklist was evaluated and did not apply, which the words state as the good news it is.
279
255
  rosterBullet(row) {
280
- const box = row.ran ? '- [x]' : '- [ ]';
281
- return (`${box} ${this.verdictEmoji(row)} **${row.subagent}** — ${this.verdictWords(row)}${this.evidenceSuffix(row)}\n` +
256
+ const box = this.reviewerRan(row) ? '- [x]' : '- [ ]';
257
+ return (`${box} ${this.verdictEmoji(row)} **${row.subagent}**${this.optionalTag(row)} — ` +
258
+ `${this.verdictWords(row)}${this.evidenceSuffix(row)}\n` +
282
259
  ` - ${this.whyLine(row)}`);
283
260
  }
261
+ /**
262
+ * Did a reviewer actually produce a verdict for this row?
263
+ *
264
+ * `row.ran` is really "this checklist APPLIED to the diff" — for a required checklist the two are the
265
+ * same thing, because the PR cannot open otherwise, and the field was named before optional checklists
266
+ * existed. For a DECLINED optional one they diverge, and using `ran` alone would put a checked box and a
267
+ * reviewer section on a review that nobody performed.
268
+ */
269
+ reviewerRan(row) {
270
+ return row.ran && !this.declined(row);
271
+ }
272
+ // Applied, optional, and carrying no verdict — i.e. the human was offered this review and said no (or
273
+ // `--no-optional` skipped the offer). Never true of a required checklist: one of those with no verdict
274
+ // does not reach a PR at all.
275
+ declined(row) {
276
+ return row.ran && !row.required && (row.status === rules_config_1.CK_MISSING || row.status === '');
277
+ }
278
+ // Marks which rows the human could have declined. Without it a reader cannot tell a review that was
279
+ // skippable from one that simply passed, and so cannot judge how much this PR was actually reviewed.
280
+ optionalTag(row) {
281
+ return row.required ? '' : ' _(optional)_';
282
+ }
284
283
  /**
285
284
  * Whether the reviewer demonstrably opened the diff, read from its own transcript. A QUALITY signal and
286
285
  * never a blocker (see SubagentProvenanceService.evidenceFor) — but published, because "wrote a verdict
287
286
  * without reading the change" is exactly what a reader of this comment would want to weigh.
288
287
  */
289
288
  evidenceSuffix(row) {
290
- if (!row.ran || row.diffRead === '')
289
+ if (!this.reviewerRan(row) || row.diffRead === '')
291
290
  return '';
292
291
  return row.diffRead === 'yes' ? ' _(diff read ✓)_' : ' _(⚠️ no diff read recorded)_';
293
292
  }
294
293
  verdictEmoji(row) {
295
- if (!row.ran)
294
+ if (!this.reviewerRan(row))
296
295
  return '⚪';
297
296
  if (row.status === rules_config_1.CK_PASS)
298
297
  return '🟢';
@@ -307,6 +306,11 @@ let Dashboard = class Dashboard {
307
306
  // SHORT words for a roster line / section heading. Short on purpose: the reviewer's own output and any
308
307
  // override justification get their own section below, and a roster exists to be scanned.
309
308
  verdictWords(row) {
309
+ // Two different unchecked boxes, two different sentences. "Not applicable" is the diff's doing;
310
+ // "not run" is a person's, and reporting the second as the first would quietly credit a review that
311
+ // a human deliberately declined.
312
+ if (this.declined(row))
313
+ return 'OPTIONAL — applied to this diff but was NOT run (not selected)';
310
314
  if (!row.ran)
311
315
  return 'skipped, not applicable to this diff (expected ✅)';
312
316
  if (row.status === rules_config_1.CK_PASS)
@@ -352,7 +356,7 @@ let Dashboard = class Dashboard {
352
356
  ranOrdered(rows) {
353
357
  const rank = [rules_config_1.CK_OVERRIDDEN, rules_config_1.CK_WARN, rules_config_1.CK_PASS];
354
358
  return rows
355
- .filter((r) => r.ran)
359
+ .filter((r) => this.reviewerRan(r))
356
360
  .slice()
357
361
  .sort((a, b) => this.rankOf(rank, a.status) - this.rankOf(rank, b.status));
358
362
  }