@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.
- package/package.json +1 -1
- package/src/review-json-data.d.ts +98 -0
- package/src/review-json-data.js +187 -0
- package/src/review-json-data.js.map +1 -0
- package/src/review-json.d.ts +67 -94
- package/src/review-json.js +156 -210
- package/src/review-json.js.map +1 -1
package/src/review-json.js
CHANGED
|
@@ -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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
exports.
|
|
21
|
-
exports.
|
|
22
|
-
exports.
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
|
252
|
-
*
|
|
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
|
-
*
|
|
255
|
-
*
|
|
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] =
|
|
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 !==
|
|
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,
|
|
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,
|
|
454
|
-
if (result.status ===
|
|
455
|
-
return new ChecklistVerdict(req.id,
|
|
456
|
-
if (result.status ===
|
|
457
|
-
return new ChecklistVerdict(req.id,
|
|
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,
|
|
460
|
-
return new ChecklistVerdict(req.id,
|
|
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 ===
|
|
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 ===
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
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 ===
|
|
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":"${
|
|
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": "${
|
|
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} ${
|
|
522
|
-
`${indent} ${
|
|
523
|
-
`${indent} ${
|
|
524
|
-
`${indent}Prefer "${
|
|
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 (
|
|
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 ${
|
|
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) {
|