@webpieces/rules-config 0.4.534 → 0.4.536

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.
@@ -13,181 +13,23 @@ const constants_1 = require("./constants");
13
13
  const state_dir_1 = require("./state-dir");
14
14
  const inform_ai_error_1 = require("./inform-ai-error");
15
15
  const to_error_1 = require("./to-error");
16
- // The three colors a reviewer subagent may report in `review-<id>.json`. A TRI-state, not a boolean,
17
- // because the boolean it replaced gave a reviewer no way to say "this passes, but a human should look at
18
- // X" the only way to raise a concern was to FAIL the PR and then override your own failure, which reads
19
- // on the dashboard as a deliberately-accepted defect rather than as a note.
20
- exports.VERDICT_GREEN = 'green';
21
- exports.VERDICT_YELLOW = 'yellow';
22
- exports.VERDICT_RED = 'red';
23
- exports.VERDICT_STATUSES = [exports.VERDICT_GREEN, exports.VERDICT_YELLOW, exports.VERDICT_RED];
24
- // The verdict a reviewer SUBAGENT writes into `.webpieces/pr-review/<branch>/review-<id>.json`, one per
25
- // matched checklist. One file per checklist so N concurrent reviewer subagents never clobber a shared
26
- // file. It records the OUTCOME:
27
- // status:'green' → PASS
28
- // status:'yellow' → WARN (passes; the concern is published on the PR, nothing is blocked)
29
- // status:'red' + override non-empty OVERRIDDEN (pass; the free-text justification reaches the PR)
30
- // status:'red' + no override → FAIL (refuse; `output` is printed verbatim)
31
- // `override` is deliberately free text, not a boolean it forces the ship-anyway decision to be stated
32
- // in words and surfaces it on the dashboard, where a human sees it. Data-only (per CLAUDE.md).
33
- class ChecklistResult {
34
- id;
35
- status; // one of VERDICT_STATUSES; anything else is reported via `problem`
36
- output; // what the reviewer found; printed verbatim when the checklist fails
37
- override; // '' = no override; non-empty = ship-anyway justification (renders 🟠 overridden)
38
- // '' = a well-formed verdict. Non-empty = the file exists and parses but its verdict cannot be READ
39
- // (most often: it still uses the removed `success` field). Carried as data rather than thrown so the
40
- // complaint can be reported by BOTH wp-review-upsert-pr and wp-finish-upsert-pr in identical words, and so a
41
- // legacy file is never silently mistaken for a missing one.
42
- problem;
43
- // eslint-disable-next-line @typescript-eslint/max-params
44
- constructor(id, status, output, override, problem = '') {
45
- this.id = id;
46
- this.status = status;
47
- this.output = output;
48
- this.override = override;
49
- this.problem = problem;
50
- }
51
- }
52
- exports.ChecklistResult = ChecklistResult;
53
- // What the pr-gate command computed from the diff: a checklist this branch MATCHED (its patterns hit the
54
- // diff, so its reviewer subagent must run). Drives review-<id>.json enforcement, provenance, the schema
55
- // hint, and the dashboard. Data-only.
56
- class RequiredChecklist {
57
- id; // = subagent name; keys review-<id>.json
58
- subagent; // reviewer agent that must run (agentType the harness stamps)
59
- doc; // REPO-RELATIVE guidance doc the reviewer reads ('' → it just reads the diff)
60
- matchedFiles; // the changed files that matched it (for the dashboard + hint)
61
- // Which of the checklist's OWN globs actually fired. Printed so a reviewer can judge how coarse the
62
- // match was — a precise `db/migrations/**` hit means something different from a blanket `**` — and the
63
- // template tells reviewers that matching IS deliberately coarse. [] = no patterns (matches every PR).
64
- matchedPatterns;
65
- // eslint-disable-next-line @typescript-eslint/max-params
66
- constructor(id, subagent, doc, matchedFiles, matchedPatterns = []) {
67
- this.id = id;
68
- this.subagent = subagent;
69
- this.doc = doc;
70
- this.matchedFiles = matchedFiles;
71
- this.matchedPatterns = matchedPatterns;
72
- }
73
- }
74
- exports.RequiredChecklist = RequiredChecklist;
75
- /**
76
- * The per-PR facts every reviewer subagent needs GIVEN to it, alongside its own checklist: the exact base
77
- * sha the gate diffs against and the file holding the complete changed-file set. Both used to live only in
78
- * a doc the printed instruction told the AI to go read, one indirection away from the instruction to hand
79
- * them over — so the printed block could not stand on its own. Data-only; empty = omit those lines.
80
- */
81
- class ChecklistReviewContext {
82
- baseSha; // the 3-point merge-base sha
83
- prContextPath; // path of pr-context.json — the AUTHORITATIVE full changed-file set
84
- /**
85
- * The exact command that reproduces ONE file's diff, with a `-- <file>` tail — NOT assembled by the
86
- * caller. This used to be hardcoded as `git diff <baseSha> HEAD -- <file>`, which returns NOTHING on a
87
- * dirty tree because the changed-file set is computed base→working-tree. See DiffBasis, which derives
88
- * this string from the same range the file set came from.
89
- */
90
- fileDiffCommand;
91
- diffDir; // dir of the MATERIALIZED diff (diff/ALL.diff + diff/files/…); '' when not written
92
- dirty; // true ⇒ the range includes uncommitted + untracked work, and must be said out loud
93
- // eslint-disable-next-line @typescript-eslint/max-params
94
- constructor(baseSha = '', prContextPath = '', fileDiffCommand = '', diffDir = '', dirty = false) {
95
- this.baseSha = baseSha;
96
- this.prContextPath = prContextPath;
97
- this.fileDiffCommand = fileDiffCommand;
98
- this.diffDir = diffDir;
99
- this.dirty = dirty;
100
- }
101
- }
102
- exports.ChecklistReviewContext = ChecklistReviewContext;
103
- // The AI-authored review for a PR. The AI writes review.json itself between `wp-start-upsert-pr` (which
104
- // prints the schema) and `wp-finish-upsert-pr` (which reads it); reviewer subagents write the per-checklist
105
- // review-<id>.json files. Data-only (per CLAUDE.md).
106
- class ReviewJson {
107
- title; // human PR title describing the change; used as the `gh pr` title (empty → caller falls back)
108
- riskScore; // 0–100, drives the risk bar
109
- riskLevel; // 'green' | 'yellow' | 'red'
110
- riskEmoji; // '🟢' | '🟡' | '🔴' — derived from riskLevel when omitted
111
- summary; // rendered in the dashboard Summary section
112
- violations; // pattern/architecture violations; length = the Pattern Violations count
113
- risks;
114
- filesToReview;
115
- results; // resolved per-checklist verdicts (from review-<id>.json); [] when none
116
- // eslint-disable-next-line @typescript-eslint/max-params
117
- constructor(title, riskScore, riskLevel, riskEmoji, summary, violations, risks, filesToReview, results = []) {
118
- this.title = title;
119
- this.riskScore = riskScore;
120
- this.riskLevel = riskLevel;
121
- this.riskEmoji = riskEmoji;
122
- this.summary = summary;
123
- this.violations = violations;
124
- this.risks = risks;
125
- this.filesToReview = filesToReview;
126
- this.results = results;
127
- }
128
- }
129
- exports.ReviewJson = ReviewJson;
130
- // A checklist's resolved outcome, shared by review.json enforcement and the dashboard so both agree.
131
- // PASS, WARN and OVERRIDDEN all ship; FAIL, MISSING and BAD_FORMAT all refuse the PR.
132
- exports.CK_PASS = 'pass'; // review-<id>.json status:'green'
133
- exports.CK_WARN = 'warn'; // review-<id>.json status:'yellow' → 🟡 passes WITH concerns
134
- exports.CK_OVERRIDDEN = 'overridden'; // review-<id>.json status:'red' + non-empty override → 🟠
135
- exports.CK_FAIL = 'fail'; // review-<id>.json status:'red' + no override → refuse
136
- exports.CK_MISSING = 'missing'; // no review-<id>.json written → refuse
137
- exports.CK_BAD_FORMAT = 'bad-format'; // written, but its verdict is unreadable (e.g. legacy `success`)
138
- class ChecklistVerdict {
139
- id;
140
- status; // one of CK_PASS | CK_WARN | CK_OVERRIDDEN | CK_FAIL | CK_MISSING | CK_BAD_FORMAT
141
- detail; // reviewer output / override justification / format complaint (dashboard + errors)
142
- constructor(id, status, detail) {
143
- this.id = id;
144
- this.status = status;
145
- this.detail = detail;
146
- }
147
- }
148
- exports.ChecklistVerdict = ChecklistVerdict;
149
- // The PR's diff context, written by wp-start-upsert-pr into `.webpieces/pr-review/<branch>/pr-context.json`
150
- // so a reviewer subagent knows the exact 3-point base the gate used and the full changed-file set — then
151
- // reads any file's actual diff with `git diff <base> HEAD -- <file>`. This is what lets a checklist match
152
- // coarsely by path (in the config) while the subagent makes the fine, content-level judgment. Data-only.
153
- class PrContext {
154
- base; // the 3-point merge-base sha the gate diffs against
155
- /**
156
- * The real HEAD sha. This was once the literal string 'HEAD', which is not a fact — it cannot be
157
- * compared later to detect that the tree moved under a review, and it reads as a range that was never
158
- * actually diffed. Its only reader (reviewContextFor) takes `base`, so recording the sha is free.
159
- */
160
- head;
161
- changedFiles; // every file changed in the range (NOT tsOnly — includes .sql/.gql/Dockerfile/…)
162
- dirty; // true ⇒ changedFiles includes uncommitted + untracked work
163
- dirtyFiles; // exactly which paths are uncommitted/untracked — why `dirty` is true
164
- diffCommand; // the command that reproduces the WHOLE diff (see DiffBasis; correct when dirty)
165
- diffDir; // dir holding the materialized per-file diffs + ALL.diff; '' when not materialized
166
- generatedAt; // ISO timestamp, so a stale context is detectable rather than silently trusted
167
- /**
168
- * Main's head as this clone last saw it — the THIRD hash point, matching the trio the 3-point merge
169
- * records in `merge-info/<branch>/updatemain-hashes.json`. `base`/`head` above are points A and B
170
- * under the review side's older names.
171
- *
172
- * The review side used to record only A and B, so nothing could answer "did main move while this was
173
- * under review?" — the question you most want answered when a review looks stale. '' when origin/main
174
- * is unresolvable. Purely informational; nothing gates on it.
175
- */
176
- hashMainHead;
177
- // eslint-disable-next-line @typescript-eslint/max-params
178
- constructor(base, head, changedFiles, dirty = false, dirtyFiles = [], diffCommand = '', diffDir = '', generatedAt = '', hashMainHead = '') {
179
- this.hashMainHead = hashMainHead;
180
- this.base = base;
181
- this.head = head;
182
- this.changedFiles = changedFiles;
183
- this.dirty = dirty;
184
- this.dirtyFiles = dirtyFiles;
185
- this.diffCommand = diffCommand;
186
- this.diffDir = diffDir;
187
- this.generatedAt = generatedAt;
188
- }
189
- }
190
- exports.PrContext = PrContext;
16
+ const review_json_data_1 = require("./review-json-data");
17
+ Object.defineProperty(exports, "VERDICT_GREEN", { enumerable: true, get: function () { return review_json_data_1.VERDICT_GREEN; } });
18
+ Object.defineProperty(exports, "VERDICT_YELLOW", { enumerable: true, get: function () { return review_json_data_1.VERDICT_YELLOW; } });
19
+ Object.defineProperty(exports, "VERDICT_RED", { enumerable: true, get: function () { return review_json_data_1.VERDICT_RED; } });
20
+ Object.defineProperty(exports, "VERDICT_STATUSES", { enumerable: true, get: function () { return review_json_data_1.VERDICT_STATUSES; } });
21
+ Object.defineProperty(exports, "ChecklistResult", { enumerable: true, get: function () { return review_json_data_1.ChecklistResult; } });
22
+ Object.defineProperty(exports, "RequiredChecklist", { enumerable: true, get: function () { return review_json_data_1.RequiredChecklist; } });
23
+ Object.defineProperty(exports, "ChecklistReviewContext", { enumerable: true, get: function () { return review_json_data_1.ChecklistReviewContext; } });
24
+ Object.defineProperty(exports, "ReviewJson", { enumerable: true, get: function () { return review_json_data_1.ReviewJson; } });
25
+ Object.defineProperty(exports, "CK_PASS", { enumerable: true, get: function () { return review_json_data_1.CK_PASS; } });
26
+ Object.defineProperty(exports, "CK_WARN", { enumerable: true, get: function () { return review_json_data_1.CK_WARN; } });
27
+ Object.defineProperty(exports, "CK_OVERRIDDEN", { enumerable: true, get: function () { return review_json_data_1.CK_OVERRIDDEN; } });
28
+ Object.defineProperty(exports, "CK_FAIL", { enumerable: true, get: function () { return review_json_data_1.CK_FAIL; } });
29
+ Object.defineProperty(exports, "CK_MISSING", { enumerable: true, get: function () { return review_json_data_1.CK_MISSING; } });
30
+ Object.defineProperty(exports, "CK_BAD_FORMAT", { enumerable: true, get: function () { return review_json_data_1.CK_BAD_FORMAT; } });
31
+ Object.defineProperty(exports, "ChecklistVerdict", { enumerable: true, get: function () { return review_json_data_1.ChecklistVerdict; } });
32
+ Object.defineProperty(exports, "PrContext", { enumerable: true, get: function () { return review_json_data_1.PrContext; } });
191
33
  const RISK_LEVELS = ['green', 'yellow', 'red'];
192
34
  const EMOJI_FOR_LEVEL = { green: '🟢', yellow: '🟡', red: '🔴' };
193
35
  // Where `wp-finish-upsert-pr` retires the review.json it just consumed, and the note it stamps on the way.
@@ -200,6 +42,15 @@ const ARCHIVE_NOTE = 'ARCHIVE — this is the review from the PREVIOUS wp-finish
200
42
  'update, which has since moved. If you are reviewing again, write a FRESH review.json at the path ' +
201
43
  'pnpm wp-review-upsert-pr prints; do not copy this file\'s title, summary or risk level forward without ' +
202
44
  're-deciding each one. Overwritten by every finish, so only the most recent review is ever here.';
45
+ // The same stamp, for a retired per-checklist verdict. Verdict files get their OWN wording because the two
46
+ // archives answer different questions: old-review.json holds a description of the code, this holds a
47
+ // REVIEWER'S DECISION. The one thing that must not happen is a reader treating an archived red as the live
48
+ // verdict — the whole reason the file was moved rather than copied — so the note says that outright.
49
+ const CHECKLIST_ARCHIVE_NOTE = 'ARCHIVE — this is a checklist verdict from a PREVIOUS reviewer run on this branch, kept for audit ' +
50
+ 'purposes only. It is NOT a live verdict and must never be read back as one: it was RETIRED because it ' +
51
+ 'refused the PR, and the gate moved it here so the only way forward is a FRESH review-<id>.json written ' +
52
+ 'by a real reviewer run. Do not copy its status back onto the live path to get past the gate. ' +
53
+ 'Overwritten by every retirement, so only the most recently retired verdict is ever here.';
203
54
  /** Locates + loads/validates the AI-authored review.json. `@injectable(bindingScopeValues.Singleton)` so it's drawn in the design. */
204
55
  let ReviewJsonService = class ReviewJsonService {
205
56
  dotDir;
@@ -243,24 +94,31 @@ let ReviewJsonService = class ReviewJsonService {
243
94
  return '';
244
95
  const archivePath = this.oldReviewJsonPath(reviewJsonFilePath);
245
96
  const raw = fs.readFileSync(reviewJsonFilePath, 'utf8');
246
- fs.writeFileSync(archivePath, this.archivedBody(raw));
97
+ fs.writeFileSync(archivePath, this.archivedBody(raw, ARCHIVE_NOTE));
247
98
  fs.rmSync(reviewJsonFilePath);
248
99
  return archivePath;
249
100
  }
250
101
  /**
251
- * The archived bytes: the original review with the AUDIT-ONLY note as its FIRST key, so anything that
252
- * opens the file — human or AI — reads what it is before it reads any of its content.
102
+ * The archived bytes: the original JSON with an AUDIT-ONLY note as its FIRST key, so anything that opens
103
+ * the file — human or AI — reads what it is before it reads any of its content.
253
104
  *
254
- * Falls back to the raw bytes when they do not parse. By here `loadReviewJson` has already accepted the
255
- * file, so that is close to impossible; preserving the original beats losing it to a stamping failure.
105
+ * `note` is a parameter rather than a constant because two different files are archived here (review.json
106
+ * and review-<id>.json) and they need to say different things, while the stamping MECHANICS parse,
107
+ * note first, original keys in order, fall back to raw — are identical. One implementation, two texts;
108
+ * a second copy of this method would be the thing that drifts.
109
+ *
110
+ * Falls back to the raw bytes when they do not parse. For review.json `loadReviewJson` has already
111
+ * accepted the file so that is close to impossible, but a verdict file is written by a subagent and may
112
+ * be half-written or not an object at all — and preserving the original always beats losing it to a
113
+ * stamping failure, since the archive exists precisely to be the record.
256
114
  */
257
- archivedBody(raw) {
115
+ archivedBody(raw, note) {
258
116
  const parsed = this.tryParseObject(raw);
259
117
  if (parsed === null)
260
118
  return raw;
261
119
  // webpieces-disable no-any-unknown -- re-serializing opaque review fields verbatim; only the key ORDER is ours
262
120
  const stamped = {};
263
- stamped[ARCHIVE_NOTE_KEY] = ARCHIVE_NOTE;
121
+ stamped[ARCHIVE_NOTE_KEY] = note;
264
122
  for (const key of Object.keys(parsed))
265
123
  stamped[key] = parsed[key];
266
124
  return JSON.stringify(stamped, null, 2) + '\n';
@@ -314,7 +172,7 @@ let ReviewJsonService = class ReviewJsonService {
314
172
  reviewContextFor(repoRoot, featureName) {
315
173
  const p = this.prContextPath(repoRoot, featureName);
316
174
  if (!fs.existsSync(p))
317
- return new ChecklistReviewContext();
175
+ return new review_json_data_1.ChecklistReviewContext();
318
176
  // webpieces-disable no-unmanaged-exceptions -- chokepoint: an unreadable context file degrades to fewer printed lines, never a crash
319
177
  // eslint-disable-next-line @webpieces/no-unmanaged-exceptions
320
178
  try {
@@ -327,12 +185,12 @@ let ReviewJsonService = class ReviewJsonService {
327
185
  const cmd = typeof raw['diffCommand'] === 'string' ? raw['diffCommand'] : '';
328
186
  const diffDir = typeof raw['diffDir'] === 'string' ? raw['diffDir'] : '';
329
187
  const dirty = raw['dirty'] === true;
330
- return new ChecklistReviewContext(base, p, cmd === '' ? '' : `${cmd} -- <file>`, diffDir, dirty);
188
+ return new review_json_data_1.ChecklistReviewContext(base, p, cmd === '' ? '' : `${cmd} -- <file>`, diffDir, dirty);
331
189
  }
332
190
  catch (err) {
333
191
  const error = (0, to_error_1.toError)(err);
334
192
  void error;
335
- return new ChecklistReviewContext('', p);
193
+ return new review_json_data_1.ChecklistReviewContext('', p);
336
194
  }
337
195
  }
338
196
  // Copy-paste schema both commands print. `required` is the set of checklists the diff MATCHED; empty
@@ -367,6 +225,43 @@ let ReviewJsonService = class ReviewJsonService {
367
225
  checklistResultPath(reviewJsonFilePath, checklistId) {
368
226
  return path.join(path.dirname(reviewJsonFilePath), `review-${checklistId}.json`);
369
227
  }
228
+ /**
229
+ * Where a RETIRED verdict for one checklist goes: `review-<id>.json.old`, beside the live path.
230
+ *
231
+ * Mirrors {@link oldReviewJsonPath} deliberately, including its single-slot rule: ALWAYS the same path,
232
+ * so it holds the last retired verdict and only the last one. A series (`.old.old`, `.old.1`) would read
233
+ * as though the number of retirements meant something, and nothing downstream can interpret that — the
234
+ * one fact worth keeping is "this checklist refused before, here is what it said".
235
+ */
236
+ oldChecklistResultPath(reviewJsonFilePath, checklistId) {
237
+ return `${this.checklistResultPath(reviewJsonFilePath, checklistId)}.old`;
238
+ }
239
+ /**
240
+ * Retire one checklist's verdict: MOVE review-<id>.json to review-<id>.json.old, stamped with a note
241
+ * saying what it is. Returns the archive path, or '' when there was nothing to archive.
242
+ *
243
+ * The point is the MOVE, exactly as in {@link archiveReviewJson}. A red verdict left on the live path is
244
+ * re-read by the next run and re-reported as the CURRENT state of the branch, so the branch keeps being
245
+ * refused for a finding that may already be fixed — and the fix, when it comes, silently overwrites the
246
+ * only record that the gate ever refused anything. Moving it makes the refusal durable and makes a fresh
247
+ * reviewer run the only way forward, which is the honest requirement: the old verdict judged code that
248
+ * has since changed.
249
+ *
250
+ * Safe by construction for RED verdicts specifically, which is why the caller only archives on CK_FAIL:
251
+ * a red verdict is never reusable — it always blocks — so nothing is lost by moving it. Green and yellow
252
+ * verdicts ARE deliberately reused across finish attempts, and retiring one would force a needless (and
253
+ * expensive) subagent re-run.
254
+ */
255
+ archiveChecklistResult(reviewJsonFilePath, checklistId) {
256
+ const livePath = this.checklistResultPath(reviewJsonFilePath, checklistId);
257
+ if (!fs.existsSync(livePath))
258
+ return '';
259
+ const archivePath = this.oldChecklistResultPath(reviewJsonFilePath, checklistId);
260
+ const raw = fs.readFileSync(livePath, 'utf8');
261
+ fs.writeFileSync(archivePath, this.archivedBody(raw, CHECKLIST_ARCHIVE_NOTE));
262
+ fs.rmSync(livePath);
263
+ return archivePath;
264
+ }
370
265
  /**
371
266
  * Load + validate the AI-authored review.json. Throws InformAiError (with the schema) when missing,
372
267
  * unparseable, or structurally wrong. `required` is the set of checklists the diff matched: every one
@@ -410,7 +305,7 @@ let ReviewJsonService = class ReviewJsonService {
410
305
  ? raw['riskEmoji']
411
306
  : (EMOJI_FOR_LEVEL[level] ?? '🟡');
412
307
  const summary = typeof raw['summary'] === 'string' ? raw['summary'] : '';
413
- return new ReviewJson(title, riskScore, level, emoji, summary, this.asStringArray(raw['violations']), this.asStringArray(raw['risks']), this.asStringArray(raw['filesToReview']), results);
308
+ return new review_json_data_1.ReviewJson(title, riskScore, level, emoji, summary, this.asStringArray(raw['violations']), this.asStringArray(raw['risks']), this.asStringArray(raw['filesToReview']), results);
414
309
  }
415
310
  /**
416
311
  * The checklists that still OWE a verdict: no review-<id>.json at all, a malformed one, or one whose
@@ -424,12 +319,63 @@ let ReviewJsonService = class ReviewJsonService {
424
319
  // CK_WARN must be listed here beside PASS/OVERRIDDEN. A yellow verdict SHIPS — leaving it out
425
320
  // would mark the checklist owed forever, so `outstanding` never empties and the PR is refused
426
321
  // permanently no matter how many times the reviewer runs.
427
- return status !== exports.CK_PASS && status !== exports.CK_WARN && status !== exports.CK_OVERRIDDEN;
322
+ return status !== review_json_data_1.CK_PASS && status !== review_json_data_1.CK_WARN && status !== review_json_data_1.CK_OVERRIDDEN;
428
323
  });
429
324
  }
325
+ /**
326
+ * The checklists that REFUSED: a reviewer ran, judged the change, and said no (CK_FAIL — status red with
327
+ * no override). A strict subset of {@link pendingChecklists}, split out because it demands a completely
328
+ * different action from the reader.
329
+ *
330
+ * Public so every command agrees on the set. When "refused" was computed ad hoc, a refusal and a
331
+ * never-ran reviewer landed in one bucket and produced one message — "you MUST run these N reviewer
332
+ * subagent(s)" — handed to an AI, which obediently re-spawned a reviewer that had already answered. It
333
+ * refused again for the same reason, and the loop cost a full subagent run per pass while the reviewer's
334
+ * actual finding was never shown to anyone. A refusal is a RESULT, not a missing step.
335
+ */
336
+ refusedChecklists(required, results) {
337
+ return required.filter((req) => this.resolveVerdict(req, results).status === review_json_data_1.CK_FAIL);
338
+ }
339
+ /**
340
+ * THE renderer for "this reviewer refused" — one wording, wherever the refusal surfaces. It exists as a
341
+ * method because the text was previously inlined in {@link requiredChecklistErrors}, reachable only
342
+ * through review.json validation, while the command layer refused earlier with its own generic message.
343
+ * Two messages for one event is how the useful one became unreachable; there is now exactly one.
344
+ *
345
+ * It always quotes the reviewer's own `output` verbatim: the finding is the whole point, and an error
346
+ * that names a checklist without saying what it objected to gives the reader nothing to fix.
347
+ *
348
+ * `archivedPath` non-empty ⇒ the verdict has just been RETIRED (moved) to that path, so the message must
349
+ * change in two ways. It says where the record went — otherwise the move reads as data loss — and,
350
+ * critically, it must NOT tell the reader to "set override in review-<id>.json", because that file no
351
+ * longer exists. The escape hatch is therefore worded as writing a FRESH verdict file (the body can be
352
+ * copied back out of the archive) with a human-authored override.
353
+ */
354
+ refusalError(req, verdict, archivedPath = '') {
355
+ const finding = `${verdict.detail.split('\n').join('\n ')}\n`;
356
+ const head = `Checklist "${req.id}" FAILED review (status:"${review_json_data_1.VERDICT_RED}"). The reviewer (${req.subagent}) wrote:\n ` + finding;
357
+ if (archivedPath === '') {
358
+ return head +
359
+ ` Fix it, then re-run; or set a non-empty "override" in ${this.checklistFileName(req.id)} to ship anyway with a stated justification.`;
360
+ }
361
+ // Re-spawning is the LAST thing said, and only after the finding, because an instruction to spawn a
362
+ // subagent is the one line an AI acts on first — see refusedChecklists for what that cost.
363
+ return head +
364
+ ` That verdict has been RETIRED to ${archivedPath} (audit only — it is not a live verdict).\n` +
365
+ ` A FRESH ${this.checklistFileName(req.id)} is now required. Fix the finding first, then have the ` +
366
+ `"${req.subagent}" subagent review again and write a new verdict.\n` +
367
+ ` To ship anyway, a HUMAN must decide it: write a fresh ${this.checklistFileName(req.id)} (you may copy the ` +
368
+ `body back from the archive) carrying a non-empty, human-authored "override" justification.`;
369
+ }
430
370
  // Read the per-checklist verdict files `review-<id>.json` beside review.json — one per matched checklist.
431
371
  // A missing file is simply absent from the result (→ counts as MISSING for that checklist); a malformed
432
372
  // one is skipped (a stale review-<id>.json never wedges the branch).
373
+ //
374
+ // It looks up the EXACT `review-<id>.json` name per required id — never a directory scan, never a prefix
375
+ // match. That is what guarantees an archived `review-<id>.json.old` can never resolve as a live verdict:
376
+ // the retired file sits right beside the live path, and a scan that swept the directory would hand a
377
+ // RETIRED refusal (or worse, a retired pass) back as the current state, undoing the whole point of the
378
+ // move in {@link archiveChecklistResult}.
433
379
  loadChecklistResults(reviewJsonFilePath, required) {
434
380
  const results = [];
435
381
  for (const req of required) {
@@ -448,16 +394,16 @@ let ReviewJsonService = class ReviewJsonService {
448
394
  resolveVerdict(req, results) {
449
395
  const result = results.find((r) => r.id === req.id);
450
396
  if (!result)
451
- return new ChecklistVerdict(req.id, exports.CK_MISSING, '');
397
+ return new review_json_data_1.ChecklistVerdict(req.id, review_json_data_1.CK_MISSING, '');
452
398
  if (result.problem !== '')
453
- return new ChecklistVerdict(req.id, exports.CK_BAD_FORMAT, result.problem);
454
- if (result.status === exports.VERDICT_GREEN)
455
- return new ChecklistVerdict(req.id, exports.CK_PASS, result.output);
456
- if (result.status === exports.VERDICT_YELLOW)
457
- return new ChecklistVerdict(req.id, exports.CK_WARN, result.output);
399
+ return new review_json_data_1.ChecklistVerdict(req.id, review_json_data_1.CK_BAD_FORMAT, result.problem);
400
+ if (result.status === review_json_data_1.VERDICT_GREEN)
401
+ return new review_json_data_1.ChecklistVerdict(req.id, review_json_data_1.CK_PASS, result.output);
402
+ if (result.status === review_json_data_1.VERDICT_YELLOW)
403
+ return new review_json_data_1.ChecklistVerdict(req.id, review_json_data_1.CK_WARN, result.output);
458
404
  if (result.override.trim() !== '')
459
- return new ChecklistVerdict(req.id, exports.CK_OVERRIDDEN, result.override.trim());
460
- return new ChecklistVerdict(req.id, exports.CK_FAIL, result.output);
405
+ return new review_json_data_1.ChecklistVerdict(req.id, review_json_data_1.CK_OVERRIDDEN, result.override.trim());
406
+ return new review_json_data_1.ChecklistVerdict(req.id, review_json_data_1.CK_FAIL, result.output);
461
407
  }
462
408
  /**
463
409
  * One loud complaint per checklist whose verdict file EXISTS but cannot be read as a verdict — almost
@@ -470,7 +416,7 @@ let ReviewJsonService = class ReviewJsonService {
470
416
  const errors = [];
471
417
  for (const req of required) {
472
418
  const verdict = this.resolveVerdict(req, results);
473
- if (verdict.status === exports.CK_BAD_FORMAT)
419
+ if (verdict.status === review_json_data_1.CK_BAD_FORMAT)
474
420
  errors.push(verdict.detail);
475
421
  }
476
422
  return errors;
@@ -484,16 +430,16 @@ let ReviewJsonService = class ReviewJsonService {
484
430
  const verdict = this.resolveVerdict(req, results);
485
431
  // CK_WARN ('yellow' — passed with concerns) is deliberately absent from this chain: it SHIPS.
486
432
  // The concern still reaches the PR, published in the checklist comment. Do not "fix" this.
487
- if (verdict.status === exports.CK_FAIL) {
488
- errors.push(`Checklist "${req.id}" FAILED review (status:"${exports.VERDICT_RED}"). The reviewer (${req.subagent}) wrote:\n ` +
489
- `${verdict.detail.split('\n').join('\n ')}\n` +
490
- ` Fix it, then re-run; or set a non-empty "override" in ${this.checklistFileName(req.id)} to ship anyway with a stated justification.`);
433
+ if (verdict.status === review_json_data_1.CK_FAIL) {
434
+ // Through the ONE renderer, so this path and the command layer's refusal say the same thing.
435
+ // No archive path here: this is validation, not the act of retiring the verdict.
436
+ errors.push(this.refusalError(req, verdict));
491
437
  }
492
- else if (verdict.status === exports.CK_MISSING) {
438
+ else if (verdict.status === review_json_data_1.CK_MISSING) {
493
439
  const doc = req.doc.trim() !== '' ? ` Read: ${req.doc}.` : '';
494
440
  errors.push(`Checklist "${req.id}" MATCHED this diff but has no verdict. Spawn the "${req.subagent}" subagent to review it, ` +
495
441
  `then write ${this.checklistFileName(req.id)} with ` +
496
- `{"id":"${req.id}","status":"${exports.VERDICT_GREEN}","output":"…","override":""}.${doc}`);
442
+ `{"id":"${req.id}","status":"${review_json_data_1.VERDICT_GREEN}","output":"…","override":""}.${doc}`);
497
443
  }
498
444
  }
499
445
  return errors;
@@ -516,12 +462,12 @@ let ReviewJsonService = class ReviewJsonService {
516
462
  */
517
463
  verdictSchemaFor(id, verdictPath = '', indent = ' ') {
518
464
  const lines = [
519
- `${indent}{ "id": "${id}", "status": "${exports.VERDICT_GREEN} | ${exports.VERDICT_YELLOW} | ${exports.VERDICT_RED}", ` +
465
+ `${indent}{ "id": "${id}", "status": "${review_json_data_1.VERDICT_GREEN} | ${review_json_data_1.VERDICT_YELLOW} | ${review_json_data_1.VERDICT_RED}", ` +
520
466
  `"output": "what you checked / found", "override": "" }`,
521
- `${indent} ${exports.VERDICT_GREEN} → passes, nothing to flag`,
522
- `${indent} ${exports.VERDICT_YELLOW} → passes WITH CONCERNS; nothing is blocked and the concern is published on the PR`,
523
- `${indent} ${exports.VERDICT_RED} → REFUSES the PR (set a non-empty "override" to ship anyway with a stated justification)`,
524
- `${indent}Prefer "${exports.VERDICT_YELLOW}" over red-plus-override when the change is acceptable but worth a human's`,
467
+ `${indent} ${review_json_data_1.VERDICT_GREEN} → passes, nothing to flag`,
468
+ `${indent} ${review_json_data_1.VERDICT_YELLOW} → passes WITH CONCERNS; nothing is blocked and the concern is published on the PR`,
469
+ `${indent} ${review_json_data_1.VERDICT_RED} → REFUSES the PR (set a non-empty "override" to ship anyway with a stated justification)`,
470
+ `${indent}Prefer "${review_json_data_1.VERDICT_YELLOW}" over red-plus-override when the change is acceptable but worth a human's`,
525
471
  `${indent}attention — an override reads as a deliberately-accepted defect, a yellow reads as a note.`,
526
472
  ];
527
473
  if (verdictPath !== '')
@@ -549,7 +495,7 @@ let ReviewJsonService = class ReviewJsonService {
549
495
  const output = typeof raw['output'] === 'string' ? raw['output'] : '';
550
496
  const override = typeof raw['override'] === 'string' ? raw['override'] : '';
551
497
  const status = typeof raw['status'] === 'string' ? raw['status'].trim().toLowerCase() : '';
552
- return new ChecklistResult(id, status, output, override, this.statusProblem(filePath, id, status, raw));
498
+ return new review_json_data_1.ChecklistResult(id, status, output, override, this.statusProblem(filePath, id, status, raw));
553
499
  }
554
500
  catch (err) {
555
501
  const error = (0, to_error_1.toError)(err);
@@ -566,7 +512,7 @@ let ReviewJsonService = class ReviewJsonService {
566
512
  // webpieces-disable no-any-unknown -- opaque parsed JSON; only tested for key presence here
567
513
  statusProblem(filePath, id, status, raw) {
568
514
  // webpieces-disable no-any-unknown -- comparing against the readonly literal tuple of valid colors
569
- if (exports.VERDICT_STATUSES.includes(status))
515
+ if (review_json_data_1.VERDICT_STATUSES.includes(status))
570
516
  return '';
571
517
  // The ONE renderer — see verdictSchemaFor. A second copy here is what let the old `success` shape
572
518
  // survive in print after it was removed from the parser.
@@ -576,7 +522,7 @@ let ReviewJsonService = class ReviewJsonService {
576
522
  `"status" — there is no compatibility mode. Rewrite the file as:\n${shape}`;
577
523
  }
578
524
  return `Checklist "${id}" wrote a verdict with no valid "status" (got ${JSON.stringify(status)}). ` +
579
- `It must be exactly one of ${exports.VERDICT_STATUSES.join(', ')}:\n${shape}`;
525
+ `It must be exactly one of ${review_json_data_1.VERDICT_STATUSES.join(', ')}:\n${shape}`;
580
526
  }
581
527
  // webpieces-disable no-any-unknown -- opaque parsed JSON value, narrowed to string[] here
582
528
  asStringArray(value) {