@webpieces/rules-config 0.4.481 → 0.4.483
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/checklist-config.d.ts +32 -1
- package/src/checklist-config.js +86 -11
- package/src/checklist-config.js.map +1 -1
- package/src/checklist-instructions.d.ts +35 -0
- package/src/checklist-instructions.js +110 -0
- package/src/checklist-instructions.js.map +1 -0
- package/src/checklist-manifest.d.ts +26 -7
- package/src/checklist-manifest.js +88 -28
- package/src/checklist-manifest.js.map +1 -1
- package/src/commands-config.d.ts +3 -0
- package/src/commands-config.js +15 -1
- package/src/commands-config.js.map +1 -1
- package/src/index.d.ts +3 -2
- package/src/index.js +9 -3
- package/src/index.js.map +1 -1
- package/src/pr-gate-config.d.ts +3 -2
- package/src/pr-gate-config.js +30 -14
- package/src/pr-gate-config.js.map +1 -1
- package/src/pr-gate-section-validators.d.ts +22 -0
- package/src/pr-gate-section-validators.js +93 -0
- package/src/pr-gate-section-validators.js.map +1 -0
- package/src/review-json.d.ts +29 -4
- package/src/review-json.js +71 -38
- package/src/review-json.js.map +1 -1
- package/src/validate-config.d.ts +2 -1
- package/src/validate-config.js +43 -33
- package/src/validate-config.js.map +1 -1
- package/templates/webpieces.review-checklists.md +44 -5
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pr-gate-section-validators.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/pr-gate-section-validators.ts"],"names":[],"mappings":"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","sourcesContent":["import { ChecklistManifestService } from './checklist-manifest';\nimport { ChecklistDefinition, ChecklistSource, RawChecklistItem, toChecklist } from './checklist-config';\n\n// The two `pr-gate` sub-sections whose validation is bulky enough to own a file: the review `checklists`\n// (two accepted shapes) and the one rationale key that is rejected outright. Split out of validate-config.ts\n// only for size; loadAndValidate still reaches both through validatePrGateSection.\n\n// The `*Why` convention (buildCommandWhy, mergeModeWhy, gatesWhy…) is free-form rationale a consumer keeps\n// beside a field, and pr-gate tolerates any of them — EXCEPT this one. See validateNoGateSaltRationale.\nconst GATE_SALT_WHY = 'gateSaltWhy';\n\n/**\n * Reject `gateSaltWhy` outright, and say why, so the next validate on upgrade FORCES its removal.\n *\n * webpieces.config.json is one of the first files a coding agent reads. A rationale note next to `gateSalt`\n * necessarily explains what the token protects, that the salt is committed, and therefore how to forge it —\n * i.e. it is a bypass how-to, sitting in the most-read file in the repo, defeating the only thing an\n * obscurity-grade mechanism has going for it. The rationale belongs in the webpieces source (pr-gate-config.ts\n * documents it in full for humans reading the tooling), never in consumer config. Every other `*Why` key\n * stays allowed; this is not a general ban on documenting your config.\n */\n// webpieces-disable no-any-unknown -- the already-narrowed opaque pr-gate section; only key PRESENCE is read\n// webpieces-disable no-function-outside-class -- module-level config validator, matches the rest of this file\nexport function validateNoGateSaltRationale(s: Record<string, unknown>): string[] {\n if (!(GATE_SALT_WHY in s)) return [];\n return [\n `[pr-gate] DELETE the \"${GATE_SALT_WHY}\" key from webpieces.config.json. It is a rationale note next to ` +\n `\"gateSalt\", which means it spells out what the gate token protects and that the salt is committed — a ` +\n `bypass how-to in the file a coding agent reads first. The mechanism is obscurity-grade; documenting it ` +\n `here removes the obscurity. Nothing else needs changing: the reasoning is already documented in the ` +\n `webpieces source (PrGateConfig.gateSalt) for humans reading the tooling.`,\n ];\n}\n\n/**\n * The `checklists` section of a pr-gate config. TWO shapes are accepted:\n * - an ARRAY of { subagent, doc?, patterns? } — the PRIMARY form, right here in webpieces.config.json\n * where it is greppable, schemable and readable by any tool. Each `doc` resolves REPO-relative.\n * - `{ \"doc\": \"...\" }` — the LEGACY form, where the same array lives in an HTML comment inside that doc\n * and each entry's `doc` resolves relative to it. Still accepted; not recommended for new repos.\n * Either way the entries themselves are validated by ChecklistManifestService, so both shapes get the\n * identical subagent/doc/patterns checks. Exported so the isolated validate-checklist-docs target reuses\n * it. `repoRoot` (when known) lets the doc + reviewer-agent existence checks run.\n */\n// webpieces-disable no-any-unknown -- `value` is opaque consumer JSON until narrowed below\n// webpieces-disable no-function-outside-class -- module-level config validator, matches the rest of this file\nexport function validateChecklistsSection(value: unknown, repoRoot?: string): string[] {\n if (Array.isArray(value)) return validateChecklistArray(value, repoRoot);\n if (typeof value !== 'object' || value === null) {\n return [`[pr-gate] \"checklists\" must be an array of { \"subagent\", \"doc\"?, \"patterns\"? }, or the legacy object { \"doc\": \"<path to the review manifest doc>\" }.`];\n }\n // webpieces-disable no-any-unknown -- narrowing the opaque checklists section\n const doc = (value as Record<string, unknown>)['doc'];\n if (typeof doc !== 'string' || doc.trim() === '') {\n return [`[pr-gate] \"checklists.doc\" must be a non-empty string — the repo-relative markdown doc carrying the <!-- webpieces:checklists [...] --> manifest. (Preferred alternative: drop the object and put the checklist ARRAY directly in \"checklists\".)`];\n }\n if (repoRoot === undefined) return [];\n return new ChecklistManifestService().validate(repoRoot, new ChecklistSource([], doc));\n}\n\n// The array (primary) shape: structurally check each entry HERE — a bad `patterns` or a non-object entry is\n// a config-file typo and deserves a `checklists[i]` message — then hand the narrowed defs to the one\n// validator both shapes share.\n// webpieces-disable no-any-unknown -- opaque consumer JSON entries, narrowed per-field below\n// webpieces-disable no-function-outside-class -- module-level config validator, matches the rest of this file\nfunction validateChecklistArray(value: readonly unknown[], repoRoot?: string): string[] {\n const errors: string[] = [];\n const items: RawChecklistItem[] = [];\n // webpieces-disable no-any-unknown -- each array entry is opaque consumer JSON, narrowed field-by-field below\n value.forEach((entry: unknown, i: number): void => {\n if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) {\n errors.push(`[pr-gate] checklists[${i}] must be an object { \"subagent\", \"doc\"?, \"patterns\"? }.`);\n return;\n }\n // webpieces-disable no-any-unknown -- narrowing one opaque checklist entry\n const e = entry as Record<string, unknown>;\n if (e['doc'] !== undefined && typeof e['doc'] !== 'string') {\n errors.push(`[pr-gate] checklists[${i}].doc must be a string — a REPO-relative path to the reviewer's guidance doc (omit it and the reviewer just reads the diff).`);\n }\n // webpieces-disable no-any-unknown -- opaque array element, narrowed by the typeof guard\n if (e['patterns'] !== undefined && !(Array.isArray(e['patterns']) && e['patterns'].every((p: unknown): boolean => typeof p === 'string'))) {\n errors.push(`[pr-gate] checklists[${i}].patterns must be a string[] of path globs (omit or [] to run on every PR).`);\n }\n items.push(e as RawChecklistItem);\n });\n if (repoRoot === undefined) return errors;\n const defs = items.map((item: RawChecklistItem): ChecklistDefinition => toChecklist(item, ''));\n return [...errors, ...new ChecklistManifestService().validate(repoRoot, new ChecklistSource(defs, ''))];\n}\n\n"]}
|
package/src/review-json.d.ts
CHANGED
|
@@ -10,7 +10,19 @@ export declare class RequiredChecklist {
|
|
|
10
10
|
subagent: string;
|
|
11
11
|
doc: string;
|
|
12
12
|
matchedFiles: string[];
|
|
13
|
-
|
|
13
|
+
matchedPatterns: string[];
|
|
14
|
+
constructor(id: string, subagent: string, doc: string, matchedFiles: string[], matchedPatterns?: string[]);
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* The per-PR facts every reviewer subagent needs GIVEN to it, alongside its own checklist: the exact base
|
|
18
|
+
* sha the gate diffs against and the file holding the complete changed-file set. Both used to live only in
|
|
19
|
+
* a doc the printed instruction told the AI to go read, one indirection away from the instruction to hand
|
|
20
|
+
* them over — so the printed block could not stand on its own. Data-only; empty = omit those lines.
|
|
21
|
+
*/
|
|
22
|
+
export declare class ChecklistReviewContext {
|
|
23
|
+
baseSha: string;
|
|
24
|
+
prContextPath: string;
|
|
25
|
+
constructor(baseSha?: string, prContextPath?: string);
|
|
14
26
|
}
|
|
15
27
|
export declare class ReviewJson {
|
|
16
28
|
title: string;
|
|
@@ -46,9 +58,15 @@ export declare class ReviewJsonService {
|
|
|
46
58
|
reviewJsonPath(repoRoot: string, featureName: string): string;
|
|
47
59
|
prContextPath(repoRoot: string, featureName: string): string;
|
|
48
60
|
writePrContext(repoRoot: string, featureName: string, context: PrContext): string;
|
|
49
|
-
|
|
61
|
+
/**
|
|
62
|
+
* The review context for a feature, recovered from the pr-context.json wp-start-upsert-pr already wrote.
|
|
63
|
+
* Lets wp-finish-upsert-pr's "you still owe me review-<id>.json" message inline the SAME self-sufficient
|
|
64
|
+
* per-reviewer block start printed, instead of a checklist name and an indirection. Empty when the file
|
|
65
|
+
* is absent or unreadable — the block then just omits those lines.
|
|
66
|
+
*/
|
|
67
|
+
reviewContextFor(repoRoot: string, featureName: string): ChecklistReviewContext;
|
|
68
|
+
reviewJsonSchemaHint(filePath: string): string;
|
|
50
69
|
checklistResultPath(reviewJsonFilePath: string, checklistId: string): string;
|
|
51
|
-
private requiredChecklistHint;
|
|
52
70
|
/**
|
|
53
71
|
* Load + validate the AI-authored review.json. Throws InformAiError (with the schema) when missing,
|
|
54
72
|
* unparseable, or structurally wrong. `required` is the set of checklists the diff matched: every one
|
|
@@ -56,6 +74,13 @@ export declare class ReviewJsonService {
|
|
|
56
74
|
* alongside the usual ones so the AI gets ONE message.
|
|
57
75
|
*/
|
|
58
76
|
loadReviewJson(filePath: string, required?: readonly RequiredChecklist[]): ReviewJson;
|
|
77
|
+
/**
|
|
78
|
+
* The checklists that still OWE a verdict: no review-<id>.json at all, a malformed one, or one whose
|
|
79
|
+
* verdict is an un-overridden FAIL. This is the set every message lists — a checklist already PASSed or
|
|
80
|
+
* OVERRIDDEN on this branch is deliberately NOT re-listed, because re-instructing it invites a redundant
|
|
81
|
+
* second run and reads as though the earlier verdict did not count.
|
|
82
|
+
*/
|
|
83
|
+
pendingChecklists(required: readonly RequiredChecklist[], results: readonly ChecklistResult[]): RequiredChecklist[];
|
|
59
84
|
loadChecklistResults(reviewJsonFilePath: string, required: readonly RequiredChecklist[]): ChecklistResult[];
|
|
60
85
|
resolveVerdict(req: RequiredChecklist, results: readonly ChecklistResult[]): ChecklistVerdict;
|
|
61
86
|
private requiredChecklistErrors;
|
|
@@ -66,5 +91,5 @@ export declare class ReviewJsonService {
|
|
|
66
91
|
}
|
|
67
92
|
export declare function prDirFor(repoRoot: string, featureName: string): string;
|
|
68
93
|
export declare function reviewJsonPath(repoRoot: string, featureName: string): string;
|
|
69
|
-
export declare function reviewJsonSchemaHint(filePath: string
|
|
94
|
+
export declare function reviewJsonSchemaHint(filePath: string): string;
|
|
70
95
|
export declare function loadReviewJson(filePath: string, required?: readonly RequiredChecklist[]): ReviewJson;
|
package/src/review-json.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.ReviewJsonService = exports.PrContext = exports.ChecklistVerdict = exports.CK_MISSING = exports.CK_FAIL = exports.CK_OVERRIDDEN = exports.CK_PASS = exports.ReviewJson = exports.RequiredChecklist = exports.ChecklistResult = void 0;
|
|
3
|
+
exports.ReviewJsonService = exports.PrContext = exports.ChecklistVerdict = exports.CK_MISSING = exports.CK_FAIL = exports.CK_OVERRIDDEN = exports.CK_PASS = exports.ReviewJson = exports.ChecklistReviewContext = exports.RequiredChecklist = exports.ChecklistResult = void 0;
|
|
4
4
|
exports.prDirFor = prDirFor;
|
|
5
5
|
exports.reviewJsonPath = reviewJsonPath;
|
|
6
6
|
exports.reviewJsonSchemaHint = reviewJsonSchemaHint;
|
|
@@ -39,16 +39,37 @@ exports.ChecklistResult = ChecklistResult;
|
|
|
39
39
|
class RequiredChecklist {
|
|
40
40
|
id; // = subagent name; keys review-<id>.json
|
|
41
41
|
subagent; // reviewer agent that must run (agentType the harness stamps)
|
|
42
|
-
doc; // guidance doc the reviewer reads ('' → it reads the manifest doc)
|
|
42
|
+
doc; // REPO-RELATIVE guidance doc the reviewer reads ('' → it reads the manifest doc)
|
|
43
43
|
matchedFiles; // the changed files that matched it (for the dashboard + hint)
|
|
44
|
-
|
|
44
|
+
// Which of the checklist's OWN globs actually fired. Printed so a reviewer can judge how coarse the
|
|
45
|
+
// match was — a precise `db/migrations/**` hit means something different from a blanket `**` — and the
|
|
46
|
+
// template tells reviewers that matching IS deliberately coarse. [] = no patterns (matches every PR).
|
|
47
|
+
matchedPatterns;
|
|
48
|
+
// eslint-disable-next-line @typescript-eslint/max-params
|
|
49
|
+
constructor(id, subagent, doc, matchedFiles, matchedPatterns = []) {
|
|
45
50
|
this.id = id;
|
|
46
51
|
this.subagent = subagent;
|
|
47
52
|
this.doc = doc;
|
|
48
53
|
this.matchedFiles = matchedFiles;
|
|
54
|
+
this.matchedPatterns = matchedPatterns;
|
|
49
55
|
}
|
|
50
56
|
}
|
|
51
57
|
exports.RequiredChecklist = RequiredChecklist;
|
|
58
|
+
/**
|
|
59
|
+
* The per-PR facts every reviewer subagent needs GIVEN to it, alongside its own checklist: the exact base
|
|
60
|
+
* sha the gate diffs against and the file holding the complete changed-file set. Both used to live only in
|
|
61
|
+
* a doc the printed instruction told the AI to go read, one indirection away from the instruction to hand
|
|
62
|
+
* them over — so the printed block could not stand on its own. Data-only; empty = omit those lines.
|
|
63
|
+
*/
|
|
64
|
+
class ChecklistReviewContext {
|
|
65
|
+
baseSha; // the 3-point merge-base sha; `git diff <baseSha> HEAD -- <file>`
|
|
66
|
+
prContextPath; // path of pr-context.json — the AUTHORITATIVE full changed-file set
|
|
67
|
+
constructor(baseSha = '', prContextPath = '') {
|
|
68
|
+
this.baseSha = baseSha;
|
|
69
|
+
this.prContextPath = prContextPath;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
exports.ChecklistReviewContext = ChecklistReviewContext;
|
|
52
73
|
// The AI-authored review for a PR. The AI writes review.json itself between `wp-start-upsert-pr` (which
|
|
53
74
|
// prints the schema) and `wp-finish-upsert-pr` (which reads it); reviewer subagents write the per-checklist
|
|
54
75
|
// review-<id>.json files. Data-only (per CLAUDE.md).
|
|
@@ -132,10 +153,34 @@ let ReviewJsonService = class ReviewJsonService {
|
|
|
132
153
|
fs.writeFileSync(p, JSON.stringify(context, null, 2) + '\n');
|
|
133
154
|
return p;
|
|
134
155
|
}
|
|
156
|
+
/**
|
|
157
|
+
* The review context for a feature, recovered from the pr-context.json wp-start-upsert-pr already wrote.
|
|
158
|
+
* Lets wp-finish-upsert-pr's "you still owe me review-<id>.json" message inline the SAME self-sufficient
|
|
159
|
+
* per-reviewer block start printed, instead of a checklist name and an indirection. Empty when the file
|
|
160
|
+
* is absent or unreadable — the block then just omits those lines.
|
|
161
|
+
*/
|
|
162
|
+
reviewContextFor(repoRoot, featureName) {
|
|
163
|
+
const p = this.prContextPath(repoRoot, featureName);
|
|
164
|
+
if (!fs.existsSync(p))
|
|
165
|
+
return new ChecklistReviewContext();
|
|
166
|
+
// webpieces-disable no-unmanaged-exceptions -- chokepoint: an unreadable context file degrades to fewer printed lines, never a crash
|
|
167
|
+
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
168
|
+
try {
|
|
169
|
+
// webpieces-disable no-any-unknown -- parsed JSON is opaque until narrowed on the next line
|
|
170
|
+
const raw = JSON.parse(fs.readFileSync(p, 'utf8'));
|
|
171
|
+
const base = typeof raw['base'] === 'string' ? raw['base'] : '';
|
|
172
|
+
return new ChecklistReviewContext(base, p);
|
|
173
|
+
}
|
|
174
|
+
catch (err) {
|
|
175
|
+
const error = (0, to_error_1.toError)(err);
|
|
176
|
+
void error;
|
|
177
|
+
return new ChecklistReviewContext('', p);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
135
180
|
// Copy-paste schema both commands print. `required` is the set of checklists the diff MATCHED; empty
|
|
136
181
|
// ⇒ output identical to a repo with no checklists. Non-empty ⇒ appends per-checklist instructions
|
|
137
182
|
// naming the reviewer subagent + doc + the review-<id>.json to write.
|
|
138
|
-
reviewJsonSchemaHint(filePath
|
|
183
|
+
reviewJsonSchemaHint(filePath) {
|
|
139
184
|
return (`Write your PR review to:\n ${filePath}\n\n` +
|
|
140
185
|
`with this exact JSON shape (riskEmoji optional — derived from riskLevel):\n\n` +
|
|
141
186
|
`{\n` +
|
|
@@ -146,35 +191,12 @@ let ReviewJsonService = class ReviewJsonService {
|
|
|
146
191
|
` "violations": ["pattern/architecture violations you found (empty array if none)"],\n` +
|
|
147
192
|
` "risks": ["notable risks (empty array if none)"],\n` +
|
|
148
193
|
` "filesToReview": ["paths a human should look at (empty array if none)"]\n` +
|
|
149
|
-
`}`
|
|
150
|
-
this.requiredChecklistHint(filePath, required));
|
|
194
|
+
`}`);
|
|
151
195
|
}
|
|
152
196
|
// The per-checklist review file path that sits beside review.json: review-<id>.json.
|
|
153
197
|
checklistResultPath(reviewJsonFilePath, checklistId) {
|
|
154
198
|
return path.join(path.dirname(reviewJsonFilePath), `review-${checklistId}.json`);
|
|
155
199
|
}
|
|
156
|
-
// The matched-checklist instruction block, appended ONLY when the diff matched a checklist. Each matched
|
|
157
|
-
// checklist must be reviewed by its OWN subagent (a distinct one), which writes review-<id>.json.
|
|
158
|
-
requiredChecklistHint(reviewJsonFilePath, required) {
|
|
159
|
-
if (required.length === 0)
|
|
160
|
-
return '';
|
|
161
|
-
const lines = ['', '', 'This diff MATCHED company review checklist(s). Spawn EACH named subagent as a SEPARATE'];
|
|
162
|
-
lines.push('subagent (a different one per checklist — the coding agent may NOT self-certify). Each reads its');
|
|
163
|
-
lines.push('doc + the diff and writes its own review-<id>.json with this shape:');
|
|
164
|
-
lines.push('');
|
|
165
|
-
lines.push(' { "id": "<id>", "success": true, "output": "what you checked / found", "override": "" }');
|
|
166
|
-
lines.push(' (success:false with no non-empty "override" refuses the PR; a non-empty override ships it 🟡.)');
|
|
167
|
-
lines.push('');
|
|
168
|
-
for (const req of required) {
|
|
169
|
-
lines.push(` • [${req.id}] reviewer subagent: ${req.subagent}`);
|
|
170
|
-
lines.push(` write: ${this.checklistResultPath(reviewJsonFilePath, req.id)}`);
|
|
171
|
-
if (req.doc.trim() !== '')
|
|
172
|
-
lines.push(` doc to read: ${req.doc}`);
|
|
173
|
-
if (req.matchedFiles.length > 0)
|
|
174
|
-
lines.push(` matched: ${req.matchedFiles.slice(0, 5).join(', ')}`);
|
|
175
|
-
}
|
|
176
|
-
return lines.join('\n');
|
|
177
|
-
}
|
|
178
200
|
/**
|
|
179
201
|
* Load + validate the AI-authored review.json. Throws InformAiError (with the schema) when missing,
|
|
180
202
|
* unparseable, or structurally wrong. `required` is the set of checklists the diff matched: every one
|
|
@@ -184,12 +206,12 @@ let ReviewJsonService = class ReviewJsonService {
|
|
|
184
206
|
// webpieces-disable max-lines-new-methods -- one cohesive load+validate pass over the review fields
|
|
185
207
|
loadReviewJson(filePath, required = []) {
|
|
186
208
|
if (!fs.existsSync(filePath)) {
|
|
187
|
-
throw new inform_ai_error_1.InformAiError(`Required review.json not found.\n\n${this.reviewJsonSchemaHint(filePath
|
|
209
|
+
throw new inform_ai_error_1.InformAiError(`Required review.json not found.\n\n${this.reviewJsonSchemaHint(filePath)}\n\n` +
|
|
188
210
|
`Then re-run: pnpm wp-finish-upsert-pr`);
|
|
189
211
|
}
|
|
190
|
-
const raw = this.parseReviewJson(fs.readFileSync(filePath, 'utf8'), filePath
|
|
212
|
+
const raw = this.parseReviewJson(fs.readFileSync(filePath, 'utf8'), filePath);
|
|
191
213
|
if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
|
|
192
|
-
throw new inform_ai_error_1.InformAiError(`review.json must be a JSON object.\n\n${this.reviewJsonSchemaHint(filePath
|
|
214
|
+
throw new inform_ai_error_1.InformAiError(`review.json must be a JSON object.\n\n${this.reviewJsonSchemaHint(filePath)}`);
|
|
193
215
|
}
|
|
194
216
|
const errors = [];
|
|
195
217
|
const riskScore = raw['riskScore'];
|
|
@@ -210,7 +232,7 @@ let ReviewJsonService = class ReviewJsonService {
|
|
|
210
232
|
if (errors.length > 0) {
|
|
211
233
|
throw new inform_ai_error_1.InformAiError(`review.json has ${errors.length} error(s) — fix ALL, then re-run pnpm wp-finish-upsert-pr:\n\n` +
|
|
212
234
|
errors.map((e) => ` • ${e}`).join('\n') +
|
|
213
|
-
`\n\n${this.reviewJsonSchemaHint(filePath
|
|
235
|
+
`\n\n${this.reviewJsonSchemaHint(filePath)}`);
|
|
214
236
|
}
|
|
215
237
|
const level = riskLevel;
|
|
216
238
|
const emoji = typeof raw['riskEmoji'] === 'string' && raw['riskEmoji'] !== ''
|
|
@@ -219,6 +241,18 @@ let ReviewJsonService = class ReviewJsonService {
|
|
|
219
241
|
const summary = typeof raw['summary'] === 'string' ? raw['summary'] : '';
|
|
220
242
|
return new ReviewJson(title, riskScore, level, emoji, summary, this.asStringArray(raw['violations']), this.asStringArray(raw['risks']), this.asStringArray(raw['filesToReview']), results);
|
|
221
243
|
}
|
|
244
|
+
/**
|
|
245
|
+
* The checklists that still OWE a verdict: no review-<id>.json at all, a malformed one, or one whose
|
|
246
|
+
* verdict is an un-overridden FAIL. This is the set every message lists — a checklist already PASSed or
|
|
247
|
+
* OVERRIDDEN on this branch is deliberately NOT re-listed, because re-instructing it invites a redundant
|
|
248
|
+
* second run and reads as though the earlier verdict did not count.
|
|
249
|
+
*/
|
|
250
|
+
pendingChecklists(required, results) {
|
|
251
|
+
return required.filter((req) => {
|
|
252
|
+
const status = this.resolveVerdict(req, results).status;
|
|
253
|
+
return status !== exports.CK_PASS && status !== exports.CK_OVERRIDDEN;
|
|
254
|
+
});
|
|
255
|
+
}
|
|
222
256
|
// Read the per-checklist verdict files `review-<id>.json` beside review.json — one per matched checklist.
|
|
223
257
|
// A missing file is simply absent from the result (→ counts as MISSING for that checklist); a malformed
|
|
224
258
|
// one is skipped (a stale review-<id>.json never wedges the branch).
|
|
@@ -297,10 +331,9 @@ let ReviewJsonService = class ReviewJsonService {
|
|
|
297
331
|
// webpieces-disable no-any-unknown -- element of an opaque JSON array, narrowed by the type guard
|
|
298
332
|
return value.filter((v) => typeof v === 'string');
|
|
299
333
|
}
|
|
300
|
-
// Parse opaque AI-authored JSON, converting a SyntaxError into a readable InformAiError.
|
|
301
|
-
// is threaded through so a JSON syntax error still prints the checklist instructions the AI needs.
|
|
334
|
+
// Parse opaque AI-authored JSON, converting a SyntaxError into a readable InformAiError.
|
|
302
335
|
// webpieces-disable no-any-unknown -- returns the opaque parsed object; loadReviewJson narrows each field
|
|
303
|
-
parseReviewJson(raw, filePath
|
|
336
|
+
parseReviewJson(raw, filePath) {
|
|
304
337
|
// webpieces-disable no-unmanaged-exceptions -- chokepoint: convert JSON.parse SyntaxError to an InformAiError for the AI
|
|
305
338
|
// eslint-disable-next-line @webpieces/no-unmanaged-exceptions
|
|
306
339
|
try {
|
|
@@ -309,7 +342,7 @@ let ReviewJsonService = class ReviewJsonService {
|
|
|
309
342
|
}
|
|
310
343
|
catch (err) {
|
|
311
344
|
const error = (0, to_error_1.toError)(err);
|
|
312
|
-
throw new inform_ai_error_1.InformAiError(`review.json is not valid JSON (${error.message}).\n\n${this.reviewJsonSchemaHint(filePath
|
|
345
|
+
throw new inform_ai_error_1.InformAiError(`review.json is not valid JSON (${error.message}).\n\n${this.reviewJsonSchemaHint(filePath)}\n\n` +
|
|
313
346
|
`Then re-run: pnpm wp-finish-upsert-pr`);
|
|
314
347
|
}
|
|
315
348
|
}
|
|
@@ -329,8 +362,8 @@ function reviewJsonPath(repoRoot, featureName) {
|
|
|
329
362
|
return reviewJsonSvc.reviewJsonPath(repoRoot, featureName);
|
|
330
363
|
}
|
|
331
364
|
// webpieces-disable no-function-outside-class -- temporary back-compat delegator to ReviewJsonService; removed once consumers inject it
|
|
332
|
-
function reviewJsonSchemaHint(filePath
|
|
333
|
-
return reviewJsonSvc.reviewJsonSchemaHint(filePath
|
|
365
|
+
function reviewJsonSchemaHint(filePath) {
|
|
366
|
+
return reviewJsonSvc.reviewJsonSchemaHint(filePath);
|
|
334
367
|
}
|
|
335
368
|
// webpieces-disable no-function-outside-class -- temporary back-compat delegator to ReviewJsonService; removed once consumers inject it
|
|
336
369
|
function loadReviewJson(filePath, required = []) {
|
package/src/review-json.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"review-json.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/review-json.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { WEBPIECES_TMP_DIR, PR_REVIEW_DIR } from './constants';\nimport { InformAiError } from './inform-ai-error';\nimport { toError } from './to-error';\n\n// The verdict a reviewer SUBAGENT writes into `.webpieces/pr-review/<branch>/review-<id>.json`, one per\n// matched checklist. One file per checklist so N concurrent reviewer subagents never clobber a shared\n// file. It records the OUTCOME:\n// success:true → PASS\n// success:false + override non-empty → OVERRIDDEN (pass; the free-text justification reaches the PR)\n// success:false + no override → FAIL (refuse; `output` is printed verbatim)\n// `override` is deliberately free text, not a boolean — it forces the ship-anyway decision to be stated\n// in words and surfaces it on the dashboard, where a human sees it. Data-only (per CLAUDE.md).\nexport class ChecklistResult {\n id: string;\n success: boolean;\n output: string; // what the reviewer found; printed verbatim when the checklist fails\n override: string; // '' = no override; non-empty = ship-anyway justification (renders 🟡 overridden)\n\n constructor(id: string, success: boolean, output: string, override: string) {\n this.id = id;\n this.success = success;\n this.output = output;\n this.override = override;\n }\n}\n\n// What the pr-gate command computed from the diff: a checklist this branch MATCHED (its patterns hit the\n// diff, so its reviewer subagent must run). Drives review-<id>.json enforcement, provenance, the schema\n// hint, and the dashboard. Data-only.\nexport class RequiredChecklist {\n id: string; // = subagent name; keys review-<id>.json\n subagent: string; // reviewer agent that must run (agentType the harness stamps)\n doc: string; // guidance doc the reviewer reads ('' → it reads the manifest doc)\n matchedFiles: string[]; // the changed files that matched it (for the dashboard + hint)\n\n constructor(id: string, subagent: string, doc: string, matchedFiles: string[]) {\n this.id = id;\n this.subagent = subagent;\n this.doc = doc;\n this.matchedFiles = matchedFiles;\n }\n}\n\n// The AI-authored review for a PR. The AI writes review.json itself between `wp-start-upsert-pr` (which\n// prints the schema) and `wp-finish-upsert-pr` (which reads it); reviewer subagents write the per-checklist\n// review-<id>.json files. Data-only (per CLAUDE.md).\nexport class ReviewJson {\n title: string; // human PR title describing the change; used as the `gh pr` title (empty → caller falls back)\n riskScore: number; // 0–100, drives the risk bar\n riskLevel: string; // 'green' | 'yellow' | 'red'\n riskEmoji: string; // '🟢' | '🟡' | '🔴' — derived from riskLevel when omitted\n summary: string; // rendered in the dashboard Summary section\n violations: string[]; // pattern/architecture violations; length = the Pattern Violations count\n risks: string[];\n filesToReview: string[];\n results: ChecklistResult[]; // resolved per-checklist verdicts (from review-<id>.json); [] when none\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(\n title: string,\n riskScore: number,\n riskLevel: string,\n riskEmoji: string,\n summary: string,\n violations: string[],\n risks: string[],\n filesToReview: string[],\n results: ChecklistResult[] = [],\n ) {\n this.title = title;\n this.riskScore = riskScore;\n this.riskLevel = riskLevel;\n this.riskEmoji = riskEmoji;\n this.summary = summary;\n this.violations = violations;\n this.risks = risks;\n this.filesToReview = filesToReview;\n this.results = results;\n }\n}\n\n// A checklist's resolved outcome, shared by review.json enforcement and the dashboard so both agree.\nexport const CK_PASS = 'pass'; // review-<id>.json success:true\nexport const CK_OVERRIDDEN = 'overridden'; // review-<id>.json success:false + non-empty override → 🟡\nexport const CK_FAIL = 'fail'; // review-<id>.json success:false + no override → refuse\nexport const CK_MISSING = 'missing'; // no review-<id>.json written → refuse\n\nexport class ChecklistVerdict {\n id: string;\n status: string; // one of CK_PASS | CK_OVERRIDDEN | CK_FAIL | CK_MISSING\n detail: string; // reviewer output / override justification (for the dashboard + errors)\n\n constructor(id: string, status: string, detail: string) {\n this.id = id;\n this.status = status;\n this.detail = detail;\n }\n}\n\n// The PR's diff context, written by wp-start-upsert-pr into `.webpieces/pr-review/<branch>/pr-context.json`\n// so a reviewer subagent knows the exact 3-point base the gate used and the full changed-file set — then\n// reads any file's actual diff with `git diff <base> HEAD -- <file>`. This is what lets a checklist match\n// coarsely by path (in the manifest) while the subagent makes the fine, content-level judgment. Data-only.\nexport class PrContext {\n base: string; // the 3-point merge-base sha the gate diffs against\n head: string; // HEAD sha\n changedFiles: string[]; // every file changed base..head (NOT tsOnly — includes .sql/.gql/Dockerfile/…)\n\n constructor(base: string, head: string, changedFiles: string[]) {\n this.base = base;\n this.head = head;\n this.changedFiles = changedFiles;\n }\n}\n\nconst RISK_LEVELS = ['green', 'yellow', 'red'] as const;\nconst EMOJI_FOR_LEVEL: Record<string, string> = { green: '🟢', yellow: '🟡', red: '🔴' };\n\n/** Locates + loads/validates the AI-authored review.json. `@injectable(bindingScopeValues.Singleton)` so it's drawn in the design. */\n@injectable(bindingScopeValues.Singleton)\nexport class ReviewJsonService {\n // The per-feature PR working dir: `.webpieces/pr-review/<feature>`.\n prDirFor(repoRoot: string, featureName: string): string {\n return path.join(repoRoot, WEBPIECES_TMP_DIR, PR_REVIEW_DIR, featureName);\n }\n\n // Absolute path of the review.json for a feature — beside pr-body.md, keyed by branch name.\n reviewJsonPath(repoRoot: string, featureName: string): string {\n return path.join(this.prDirFor(repoRoot, featureName), 'review.json');\n }\n\n // Absolute path of the pr-context.json for a feature (the diff base/head + changed files).\n prContextPath(repoRoot: string, featureName: string): string {\n return path.join(this.prDirFor(repoRoot, featureName), 'pr-context.json');\n }\n\n // Persist the PR's diff context so reviewer subagents can read the changed-file set + the exact base\n // sha (then `git diff <base> HEAD -- <file>` for content). Returns the file path written.\n writePrContext(repoRoot: string, featureName: string, context: PrContext): string {\n const dir = this.prDirFor(repoRoot, featureName);\n fs.mkdirSync(dir, { recursive: true });\n const p = this.prContextPath(repoRoot, featureName);\n fs.writeFileSync(p, JSON.stringify(context, null, 2) + '\\n');\n return p;\n }\n\n // Copy-paste schema both commands print. `required` is the set of checklists the diff MATCHED; empty\n // ⇒ output identical to a repo with no checklists. Non-empty ⇒ appends per-checklist instructions\n // naming the reviewer subagent + doc + the review-<id>.json to write.\n reviewJsonSchemaHint(filePath: string, required: readonly RequiredChecklist[] = []): string {\n return (\n `Write your PR review to:\\n ${filePath}\\n\\n` +\n `with this exact JSON shape (riskEmoji optional — derived from riskLevel):\\n\\n` +\n `{\\n` +\n ` \"title\": \"concise PR title describing the change (imperative, no branch names)\",\\n` +\n ` \"riskScore\": 0, // integer 0–100 (higher = riskier)\\n` +\n ` \"riskLevel\": \"green | yellow | red\",\\n` +\n ` \"summary\": \"5–10 sentence review summary\",\\n` +\n ` \"violations\": [\"pattern/architecture violations you found (empty array if none)\"],\\n` +\n ` \"risks\": [\"notable risks (empty array if none)\"],\\n` +\n ` \"filesToReview\": [\"paths a human should look at (empty array if none)\"]\\n` +\n `}` +\n this.requiredChecklistHint(filePath, required)\n );\n }\n\n // The per-checklist review file path that sits beside review.json: review-<id>.json.\n checklistResultPath(reviewJsonFilePath: string, checklistId: string): string {\n return path.join(path.dirname(reviewJsonFilePath), `review-${checklistId}.json`);\n }\n\n // The matched-checklist instruction block, appended ONLY when the diff matched a checklist. Each matched\n // checklist must be reviewed by its OWN subagent (a distinct one), which writes review-<id>.json.\n private requiredChecklistHint(reviewJsonFilePath: string, required: readonly RequiredChecklist[]): string {\n if (required.length === 0) return '';\n const lines: string[] = ['', '', 'This diff MATCHED company review checklist(s). Spawn EACH named subagent as a SEPARATE'];\n lines.push('subagent (a different one per checklist — the coding agent may NOT self-certify). Each reads its');\n lines.push('doc + the diff and writes its own review-<id>.json with this shape:');\n lines.push('');\n lines.push(' { \"id\": \"<id>\", \"success\": true, \"output\": \"what you checked / found\", \"override\": \"\" }');\n lines.push(' (success:false with no non-empty \"override\" refuses the PR; a non-empty override ships it 🟡.)');\n lines.push('');\n for (const req of required) {\n lines.push(` • [${req.id}] reviewer subagent: ${req.subagent}`);\n lines.push(` write: ${this.checklistResultPath(reviewJsonFilePath, req.id)}`);\n if (req.doc.trim() !== '') lines.push(` doc to read: ${req.doc}`);\n if (req.matchedFiles.length > 0) lines.push(` matched: ${req.matchedFiles.slice(0, 5).join(', ')}`);\n }\n return lines.join('\\n');\n }\n\n /**\n * Load + validate the AI-authored review.json. Throws InformAiError (with the schema) when missing,\n * unparseable, or structurally wrong. `required` is the set of checklists the diff matched: every one\n * must have a well-formed, passing (or overridden) review-<id>.json or a validation error is raised\n * alongside the usual ones so the AI gets ONE message.\n */\n // webpieces-disable max-lines-new-methods -- one cohesive load+validate pass over the review fields\n loadReviewJson(filePath: string, required: readonly RequiredChecklist[] = []): ReviewJson {\n if (!fs.existsSync(filePath)) {\n throw new InformAiError(\n `Required review.json not found.\\n\\n${this.reviewJsonSchemaHint(filePath, required)}\\n\\n` +\n `Then re-run: pnpm wp-finish-upsert-pr`,\n );\n }\n\n const raw = this.parseReviewJson(fs.readFileSync(filePath, 'utf8'), filePath, required);\n if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {\n throw new InformAiError(`review.json must be a JSON object.\\n\\n${this.reviewJsonSchemaHint(filePath, required)}`);\n }\n\n const errors: string[] = [];\n\n const riskScore = raw['riskScore'];\n if (typeof riskScore !== 'number' || !Number.isFinite(riskScore) || riskScore < 0 || riskScore > 100) {\n errors.push(`\"riskScore\" must be a number 0–100, got ${JSON.stringify(riskScore)}.`);\n }\n\n const riskLevel = raw['riskLevel'];\n if (typeof riskLevel !== 'string' || !RISK_LEVELS.includes(riskLevel as typeof RISK_LEVELS[number])) {\n errors.push(`\"riskLevel\" must be one of: ${RISK_LEVELS.join(', ')}.`);\n }\n\n const title = typeof raw['title'] === 'string' ? (raw['title'] as string).trim() : '';\n if (title === '') {\n errors.push('\"title\" must be a non-empty, imperative PR title describing the change (no branch names).');\n }\n\n const results = this.loadChecklistResults(filePath, required);\n for (const err of this.requiredChecklistErrors(required, results)) errors.push(err);\n\n if (errors.length > 0) {\n throw new InformAiError(\n `review.json has ${errors.length} error(s) — fix ALL, then re-run pnpm wp-finish-upsert-pr:\\n\\n` +\n errors.map((e: string): string => ` • ${e}`).join('\\n') +\n `\\n\\n${this.reviewJsonSchemaHint(filePath, required)}`,\n );\n }\n\n const level = riskLevel as string;\n const emoji = typeof raw['riskEmoji'] === 'string' && raw['riskEmoji'] !== ''\n ? (raw['riskEmoji'] as string)\n : (EMOJI_FOR_LEVEL[level] ?? '🟡');\n const summary = typeof raw['summary'] === 'string' ? (raw['summary'] as string) : '';\n\n return new ReviewJson(\n title,\n riskScore as number,\n level,\n emoji,\n summary,\n this.asStringArray(raw['violations']),\n this.asStringArray(raw['risks']),\n this.asStringArray(raw['filesToReview']),\n results,\n );\n }\n\n // Read the per-checklist verdict files `review-<id>.json` beside review.json — one per matched checklist.\n // A missing file is simply absent from the result (→ counts as MISSING for that checklist); a malformed\n // one is skipped (a stale review-<id>.json never wedges the branch).\n loadChecklistResults(reviewJsonFilePath: string, required: readonly RequiredChecklist[]): ChecklistResult[] {\n const results: ChecklistResult[] = [];\n for (const req of required) {\n const p = this.checklistResultPath(reviewJsonFilePath, req.id);\n if (!fs.existsSync(p)) continue;\n const parsed = this.parseChecklistResult(p, req.id);\n if (parsed) results.push(parsed);\n }\n return results;\n }\n\n // Resolve ONE checklist's verdict from its review-<id>.json. Central so review.json enforcement AND the\n // finish-command dashboard agree on the outcome.\n resolveVerdict(req: RequiredChecklist, results: readonly ChecklistResult[]): ChecklistVerdict {\n const result = results.find((r: ChecklistResult): boolean => r.id === req.id);\n if (!result) return new ChecklistVerdict(req.id, CK_MISSING, '');\n if (result.success) return new ChecklistVerdict(req.id, CK_PASS, result.output);\n if (result.override.trim() !== '') return new ChecklistVerdict(req.id, CK_OVERRIDDEN, result.override.trim());\n return new ChecklistVerdict(req.id, CK_FAIL, result.output);\n }\n\n // Every matched checklist whose verdict is FAIL (reviewed, found a problem, no override) or MISSING (no\n // review-<id>.json written) → one error each, printing the reviewer's `output` verbatim.\n private requiredChecklistErrors(required: readonly RequiredChecklist[], results: readonly ChecklistResult[]): string[] {\n const errors: string[] = [];\n for (const req of required) {\n const verdict = this.resolveVerdict(req, results);\n if (verdict.status === CK_FAIL) {\n errors.push(\n `Checklist \"${req.id}\" FAILED review. The reviewer (${req.subagent}) wrote:\\n ` +\n `${verdict.detail.split('\\n').join('\\n ')}\\n` +\n ` Fix it, then re-run; or set a non-empty \"override\" in ${this.checklistFileName(req.id)} to ship anyway with a stated justification.`,\n );\n } else if (verdict.status === CK_MISSING) {\n const doc = req.doc.trim() !== '' ? ` Read: ${req.doc}.` : '';\n errors.push(\n `Checklist \"${req.id}\" MATCHED this diff but has no verdict. Spawn the \"${req.subagent}\" subagent to review it, ` +\n `then write ${this.checklistFileName(req.id)} with {\"id\":\"${req.id}\",\"success\":true,\"output\":\"…\"}.${doc}`,\n );\n }\n }\n return errors;\n }\n\n private checklistFileName(checklistId: string): string {\n return `review-${checklistId}.json`;\n }\n\n // Parse one review-<id>.json into a ChecklistResult, or null when malformed. Tolerant: missing\n // `success` counts as false (fail-closed), `output`/`override` default to ''.\n // webpieces-disable no-any-unknown -- opaque parsed JSON, narrowed field-by-field\n private parseChecklistResult(filePath: string, id: string): ChecklistResult | null {\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: a malformed per-checklist file is skipped, not fatal\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n // webpieces-disable no-any-unknown -- parsed JSON is opaque until narrowed below\n const raw = JSON.parse(fs.readFileSync(filePath, 'utf8')) as Record<string, unknown>;\n if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) return null;\n const success = raw['success'] === true;\n const output = typeof raw['output'] === 'string' ? (raw['output'] as string) : '';\n const override = typeof raw['override'] === 'string' ? (raw['override'] as string) : '';\n return new ChecklistResult(id, success, output, override);\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return null;\n }\n }\n\n // webpieces-disable no-any-unknown -- opaque parsed JSON value, narrowed to string[] here\n private asStringArray(value: unknown): string[] {\n if (!Array.isArray(value)) return [];\n // webpieces-disable no-any-unknown -- element of an opaque JSON array, narrowed by the type guard\n return value.filter((v: unknown): v is string => typeof v === 'string');\n }\n\n // Parse opaque AI-authored JSON, converting a SyntaxError into a readable InformAiError. `required`\n // is threaded through so a JSON syntax error still prints the checklist instructions the AI needs.\n // webpieces-disable no-any-unknown -- returns the opaque parsed object; loadReviewJson narrows each field\n private parseReviewJson(raw: string, filePath: string, required: readonly RequiredChecklist[]): Record<string, unknown> {\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: convert JSON.parse SyntaxError to an InformAiError for the AI\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n // webpieces-disable no-any-unknown -- parsed JSON is opaque until narrowed by the caller\n return JSON.parse(raw) as Record<string, unknown>;\n } catch (err: unknown) {\n const error = toError(err);\n throw new InformAiError(\n `review.json is not valid JSON (${error.message}).\\n\\n${this.reviewJsonSchemaHint(filePath, required)}\\n\\n` +\n `Then re-run: pnpm wp-finish-upsert-pr`,\n );\n }\n }\n}\n\n// Temporary migration delegators to ReviewJsonService — removed once consumers inject it.\nconst reviewJsonSvc = new ReviewJsonService();\n\n// webpieces-disable no-function-outside-class -- temporary back-compat delegator to ReviewJsonService; removed once consumers inject it\nexport function prDirFor(repoRoot: string, featureName: string): string {\n return reviewJsonSvc.prDirFor(repoRoot, featureName);\n}\n\n// webpieces-disable no-function-outside-class -- temporary back-compat delegator to ReviewJsonService; removed once consumers inject it\nexport function reviewJsonPath(repoRoot: string, featureName: string): string {\n return reviewJsonSvc.reviewJsonPath(repoRoot, featureName);\n}\n\n// webpieces-disable no-function-outside-class -- temporary back-compat delegator to ReviewJsonService; removed once consumers inject it\nexport function reviewJsonSchemaHint(filePath: string, required: readonly RequiredChecklist[] = []): string {\n return reviewJsonSvc.reviewJsonSchemaHint(filePath, required);\n}\n\n// webpieces-disable no-function-outside-class -- temporary back-compat delegator to ReviewJsonService; removed once consumers inject it\nexport function loadReviewJson(filePath: string, required: readonly RequiredChecklist[] = []): ReviewJson {\n return reviewJsonSvc.loadReviewJson(filePath, required);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"review-json.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/review-json.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { WEBPIECES_TMP_DIR, PR_REVIEW_DIR } from './constants';\nimport { InformAiError } from './inform-ai-error';\nimport { toError } from './to-error';\n\n// The verdict a reviewer SUBAGENT writes into `.webpieces/pr-review/<branch>/review-<id>.json`, one per\n// matched checklist. One file per checklist so N concurrent reviewer subagents never clobber a shared\n// file. It records the OUTCOME:\n// success:true → PASS\n// success:false + override non-empty → OVERRIDDEN (pass; the free-text justification reaches the PR)\n// success:false + no override → FAIL (refuse; `output` is printed verbatim)\n// `override` is deliberately free text, not a boolean — it forces the ship-anyway decision to be stated\n// in words and surfaces it on the dashboard, where a human sees it. Data-only (per CLAUDE.md).\nexport class ChecklistResult {\n id: string;\n success: boolean;\n output: string; // what the reviewer found; printed verbatim when the checklist fails\n override: string; // '' = no override; non-empty = ship-anyway justification (renders 🟡 overridden)\n\n constructor(id: string, success: boolean, output: string, override: string) {\n this.id = id;\n this.success = success;\n this.output = output;\n this.override = override;\n }\n}\n\n// What the pr-gate command computed from the diff: a checklist this branch MATCHED (its patterns hit the\n// diff, so its reviewer subagent must run). Drives review-<id>.json enforcement, provenance, the schema\n// hint, and the dashboard. Data-only.\nexport class RequiredChecklist {\n id: string; // = subagent name; keys review-<id>.json\n subagent: string; // reviewer agent that must run (agentType the harness stamps)\n doc: string; // REPO-RELATIVE guidance doc the reviewer reads ('' → it reads the manifest doc)\n matchedFiles: string[]; // the changed files that matched it (for the dashboard + hint)\n // Which of the checklist's OWN globs actually fired. Printed so a reviewer can judge how coarse the\n // match was — a precise `db/migrations/**` hit means something different from a blanket `**` — and the\n // template tells reviewers that matching IS deliberately coarse. [] = no patterns (matches every PR).\n matchedPatterns: string[];\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(id: string, subagent: string, doc: string, matchedFiles: string[], matchedPatterns: string[] = []) {\n this.id = id;\n this.subagent = subagent;\n this.doc = doc;\n this.matchedFiles = matchedFiles;\n this.matchedPatterns = matchedPatterns;\n }\n}\n\n/**\n * The per-PR facts every reviewer subagent needs GIVEN to it, alongside its own checklist: the exact base\n * sha the gate diffs against and the file holding the complete changed-file set. Both used to live only in\n * a doc the printed instruction told the AI to go read, one indirection away from the instruction to hand\n * them over — so the printed block could not stand on its own. Data-only; empty = omit those lines.\n */\nexport class ChecklistReviewContext {\n baseSha: string; // the 3-point merge-base sha; `git diff <baseSha> HEAD -- <file>`\n prContextPath: string; // path of pr-context.json — the AUTHORITATIVE full changed-file set\n\n constructor(baseSha = '', prContextPath = '') {\n this.baseSha = baseSha;\n this.prContextPath = prContextPath;\n }\n}\n\n// The AI-authored review for a PR. The AI writes review.json itself between `wp-start-upsert-pr` (which\n// prints the schema) and `wp-finish-upsert-pr` (which reads it); reviewer subagents write the per-checklist\n// review-<id>.json files. Data-only (per CLAUDE.md).\nexport class ReviewJson {\n title: string; // human PR title describing the change; used as the `gh pr` title (empty → caller falls back)\n riskScore: number; // 0–100, drives the risk bar\n riskLevel: string; // 'green' | 'yellow' | 'red'\n riskEmoji: string; // '🟢' | '🟡' | '🔴' — derived from riskLevel when omitted\n summary: string; // rendered in the dashboard Summary section\n violations: string[]; // pattern/architecture violations; length = the Pattern Violations count\n risks: string[];\n filesToReview: string[];\n results: ChecklistResult[]; // resolved per-checklist verdicts (from review-<id>.json); [] when none\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(\n title: string,\n riskScore: number,\n riskLevel: string,\n riskEmoji: string,\n summary: string,\n violations: string[],\n risks: string[],\n filesToReview: string[],\n results: ChecklistResult[] = [],\n ) {\n this.title = title;\n this.riskScore = riskScore;\n this.riskLevel = riskLevel;\n this.riskEmoji = riskEmoji;\n this.summary = summary;\n this.violations = violations;\n this.risks = risks;\n this.filesToReview = filesToReview;\n this.results = results;\n }\n}\n\n// A checklist's resolved outcome, shared by review.json enforcement and the dashboard so both agree.\nexport const CK_PASS = 'pass'; // review-<id>.json success:true\nexport const CK_OVERRIDDEN = 'overridden'; // review-<id>.json success:false + non-empty override → 🟡\nexport const CK_FAIL = 'fail'; // review-<id>.json success:false + no override → refuse\nexport const CK_MISSING = 'missing'; // no review-<id>.json written → refuse\n\nexport class ChecklistVerdict {\n id: string;\n status: string; // one of CK_PASS | CK_OVERRIDDEN | CK_FAIL | CK_MISSING\n detail: string; // reviewer output / override justification (for the dashboard + errors)\n\n constructor(id: string, status: string, detail: string) {\n this.id = id;\n this.status = status;\n this.detail = detail;\n }\n}\n\n// The PR's diff context, written by wp-start-upsert-pr into `.webpieces/pr-review/<branch>/pr-context.json`\n// so a reviewer subagent knows the exact 3-point base the gate used and the full changed-file set — then\n// reads any file's actual diff with `git diff <base> HEAD -- <file>`. This is what lets a checklist match\n// coarsely by path (in the manifest) while the subagent makes the fine, content-level judgment. Data-only.\nexport class PrContext {\n base: string; // the 3-point merge-base sha the gate diffs against\n head: string; // HEAD sha\n changedFiles: string[]; // every file changed base..head (NOT tsOnly — includes .sql/.gql/Dockerfile/…)\n\n constructor(base: string, head: string, changedFiles: string[]) {\n this.base = base;\n this.head = head;\n this.changedFiles = changedFiles;\n }\n}\n\nconst RISK_LEVELS = ['green', 'yellow', 'red'] as const;\nconst EMOJI_FOR_LEVEL: Record<string, string> = { green: '🟢', yellow: '🟡', red: '🔴' };\n\n/** Locates + loads/validates the AI-authored review.json. `@injectable(bindingScopeValues.Singleton)` so it's drawn in the design. */\n@injectable(bindingScopeValues.Singleton)\nexport class ReviewJsonService {\n // The per-feature PR working dir: `.webpieces/pr-review/<feature>`.\n prDirFor(repoRoot: string, featureName: string): string {\n return path.join(repoRoot, WEBPIECES_TMP_DIR, PR_REVIEW_DIR, featureName);\n }\n\n // Absolute path of the review.json for a feature — beside pr-body.md, keyed by branch name.\n reviewJsonPath(repoRoot: string, featureName: string): string {\n return path.join(this.prDirFor(repoRoot, featureName), 'review.json');\n }\n\n // Absolute path of the pr-context.json for a feature (the diff base/head + changed files).\n prContextPath(repoRoot: string, featureName: string): string {\n return path.join(this.prDirFor(repoRoot, featureName), 'pr-context.json');\n }\n\n // Persist the PR's diff context so reviewer subagents can read the changed-file set + the exact base\n // sha (then `git diff <base> HEAD -- <file>` for content). Returns the file path written.\n writePrContext(repoRoot: string, featureName: string, context: PrContext): string {\n const dir = this.prDirFor(repoRoot, featureName);\n fs.mkdirSync(dir, { recursive: true });\n const p = this.prContextPath(repoRoot, featureName);\n fs.writeFileSync(p, JSON.stringify(context, null, 2) + '\\n');\n return p;\n }\n\n /**\n * The review context for a feature, recovered from the pr-context.json wp-start-upsert-pr already wrote.\n * Lets wp-finish-upsert-pr's \"you still owe me review-<id>.json\" message inline the SAME self-sufficient\n * per-reviewer block start printed, instead of a checklist name and an indirection. Empty when the file\n * is absent or unreadable — the block then just omits those lines.\n */\n reviewContextFor(repoRoot: string, featureName: string): ChecklistReviewContext {\n const p = this.prContextPath(repoRoot, featureName);\n if (!fs.existsSync(p)) return new ChecklistReviewContext();\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: an unreadable context file degrades to fewer printed lines, never a crash\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n // webpieces-disable no-any-unknown -- parsed JSON is opaque until narrowed on the next line\n const raw = JSON.parse(fs.readFileSync(p, 'utf8')) as Record<string, unknown>;\n const base = typeof raw['base'] === 'string' ? (raw['base'] as string) : '';\n return new ChecklistReviewContext(base, p);\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return new ChecklistReviewContext('', p);\n }\n }\n\n // Copy-paste schema both commands print. `required` is the set of checklists the diff MATCHED; empty\n // ⇒ output identical to a repo with no checklists. Non-empty ⇒ appends per-checklist instructions\n // naming the reviewer subagent + doc + the review-<id>.json to write.\n reviewJsonSchemaHint(filePath: string): string {\n return (\n `Write your PR review to:\\n ${filePath}\\n\\n` +\n `with this exact JSON shape (riskEmoji optional — derived from riskLevel):\\n\\n` +\n `{\\n` +\n ` \"title\": \"concise PR title describing the change (imperative, no branch names)\",\\n` +\n ` \"riskScore\": 0, // integer 0–100 (higher = riskier)\\n` +\n ` \"riskLevel\": \"green | yellow | red\",\\n` +\n ` \"summary\": \"5–10 sentence review summary\",\\n` +\n ` \"violations\": [\"pattern/architecture violations you found (empty array if none)\"],\\n` +\n ` \"risks\": [\"notable risks (empty array if none)\"],\\n` +\n ` \"filesToReview\": [\"paths a human should look at (empty array if none)\"]\\n` +\n `}`\n );\n }\n\n // The per-checklist review file path that sits beside review.json: review-<id>.json.\n checklistResultPath(reviewJsonFilePath: string, checklistId: string): string {\n return path.join(path.dirname(reviewJsonFilePath), `review-${checklistId}.json`);\n }\n\n /**\n * Load + validate the AI-authored review.json. Throws InformAiError (with the schema) when missing,\n * unparseable, or structurally wrong. `required` is the set of checklists the diff matched: every one\n * must have a well-formed, passing (or overridden) review-<id>.json or a validation error is raised\n * alongside the usual ones so the AI gets ONE message.\n */\n // webpieces-disable max-lines-new-methods -- one cohesive load+validate pass over the review fields\n loadReviewJson(filePath: string, required: readonly RequiredChecklist[] = []): ReviewJson {\n if (!fs.existsSync(filePath)) {\n throw new InformAiError(\n `Required review.json not found.\\n\\n${this.reviewJsonSchemaHint(filePath)}\\n\\n` +\n `Then re-run: pnpm wp-finish-upsert-pr`,\n );\n }\n\n const raw = this.parseReviewJson(fs.readFileSync(filePath, 'utf8'), filePath);\n if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {\n throw new InformAiError(`review.json must be a JSON object.\\n\\n${this.reviewJsonSchemaHint(filePath)}`);\n }\n\n const errors: string[] = [];\n\n const riskScore = raw['riskScore'];\n if (typeof riskScore !== 'number' || !Number.isFinite(riskScore) || riskScore < 0 || riskScore > 100) {\n errors.push(`\"riskScore\" must be a number 0–100, got ${JSON.stringify(riskScore)}.`);\n }\n\n const riskLevel = raw['riskLevel'];\n if (typeof riskLevel !== 'string' || !RISK_LEVELS.includes(riskLevel as typeof RISK_LEVELS[number])) {\n errors.push(`\"riskLevel\" must be one of: ${RISK_LEVELS.join(', ')}.`);\n }\n\n const title = typeof raw['title'] === 'string' ? (raw['title'] as string).trim() : '';\n if (title === '') {\n errors.push('\"title\" must be a non-empty, imperative PR title describing the change (no branch names).');\n }\n\n const results = this.loadChecklistResults(filePath, required);\n for (const err of this.requiredChecklistErrors(required, results)) errors.push(err);\n\n if (errors.length > 0) {\n throw new InformAiError(\n `review.json has ${errors.length} error(s) — fix ALL, then re-run pnpm wp-finish-upsert-pr:\\n\\n` +\n errors.map((e: string): string => ` • ${e}`).join('\\n') +\n `\\n\\n${this.reviewJsonSchemaHint(filePath)}`,\n );\n }\n\n const level = riskLevel as string;\n const emoji = typeof raw['riskEmoji'] === 'string' && raw['riskEmoji'] !== ''\n ? (raw['riskEmoji'] as string)\n : (EMOJI_FOR_LEVEL[level] ?? '🟡');\n const summary = typeof raw['summary'] === 'string' ? (raw['summary'] as string) : '';\n\n return new ReviewJson(\n title,\n riskScore as number,\n level,\n emoji,\n summary,\n this.asStringArray(raw['violations']),\n this.asStringArray(raw['risks']),\n this.asStringArray(raw['filesToReview']),\n results,\n );\n }\n\n /**\n * The checklists that still OWE a verdict: no review-<id>.json at all, a malformed one, or one whose\n * verdict is an un-overridden FAIL. This is the set every message lists — a checklist already PASSed or\n * OVERRIDDEN on this branch is deliberately NOT re-listed, because re-instructing it invites a redundant\n * second run and reads as though the earlier verdict did not count.\n */\n pendingChecklists(required: readonly RequiredChecklist[], results: readonly ChecklistResult[]): RequiredChecklist[] {\n return required.filter((req: RequiredChecklist): boolean => {\n const status = this.resolveVerdict(req, results).status;\n return status !== CK_PASS && status !== CK_OVERRIDDEN;\n });\n }\n\n // Read the per-checklist verdict files `review-<id>.json` beside review.json — one per matched checklist.\n // A missing file is simply absent from the result (→ counts as MISSING for that checklist); a malformed\n // one is skipped (a stale review-<id>.json never wedges the branch).\n loadChecklistResults(reviewJsonFilePath: string, required: readonly RequiredChecklist[]): ChecklistResult[] {\n const results: ChecklistResult[] = [];\n for (const req of required) {\n const p = this.checklistResultPath(reviewJsonFilePath, req.id);\n if (!fs.existsSync(p)) continue;\n const parsed = this.parseChecklistResult(p, req.id);\n if (parsed) results.push(parsed);\n }\n return results;\n }\n\n // Resolve ONE checklist's verdict from its review-<id>.json. Central so review.json enforcement AND the\n // finish-command dashboard agree on the outcome.\n resolveVerdict(req: RequiredChecklist, results: readonly ChecklistResult[]): ChecklistVerdict {\n const result = results.find((r: ChecklistResult): boolean => r.id === req.id);\n if (!result) return new ChecklistVerdict(req.id, CK_MISSING, '');\n if (result.success) return new ChecklistVerdict(req.id, CK_PASS, result.output);\n if (result.override.trim() !== '') return new ChecklistVerdict(req.id, CK_OVERRIDDEN, result.override.trim());\n return new ChecklistVerdict(req.id, CK_FAIL, result.output);\n }\n\n // Every matched checklist whose verdict is FAIL (reviewed, found a problem, no override) or MISSING (no\n // review-<id>.json written) → one error each, printing the reviewer's `output` verbatim.\n private requiredChecklistErrors(required: readonly RequiredChecklist[], results: readonly ChecklistResult[]): string[] {\n const errors: string[] = [];\n for (const req of required) {\n const verdict = this.resolveVerdict(req, results);\n if (verdict.status === CK_FAIL) {\n errors.push(\n `Checklist \"${req.id}\" FAILED review. The reviewer (${req.subagent}) wrote:\\n ` +\n `${verdict.detail.split('\\n').join('\\n ')}\\n` +\n ` Fix it, then re-run; or set a non-empty \"override\" in ${this.checklistFileName(req.id)} to ship anyway with a stated justification.`,\n );\n } else if (verdict.status === CK_MISSING) {\n const doc = req.doc.trim() !== '' ? ` Read: ${req.doc}.` : '';\n errors.push(\n `Checklist \"${req.id}\" MATCHED this diff but has no verdict. Spawn the \"${req.subagent}\" subagent to review it, ` +\n `then write ${this.checklistFileName(req.id)} with {\"id\":\"${req.id}\",\"success\":true,\"output\":\"…\"}.${doc}`,\n );\n }\n }\n return errors;\n }\n\n private checklistFileName(checklistId: string): string {\n return `review-${checklistId}.json`;\n }\n\n // Parse one review-<id>.json into a ChecklistResult, or null when malformed. Tolerant: missing\n // `success` counts as false (fail-closed), `output`/`override` default to ''.\n // webpieces-disable no-any-unknown -- opaque parsed JSON, narrowed field-by-field\n private parseChecklistResult(filePath: string, id: string): ChecklistResult | null {\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: a malformed per-checklist file is skipped, not fatal\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n // webpieces-disable no-any-unknown -- parsed JSON is opaque until narrowed below\n const raw = JSON.parse(fs.readFileSync(filePath, 'utf8')) as Record<string, unknown>;\n if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) return null;\n const success = raw['success'] === true;\n const output = typeof raw['output'] === 'string' ? (raw['output'] as string) : '';\n const override = typeof raw['override'] === 'string' ? (raw['override'] as string) : '';\n return new ChecklistResult(id, success, output, override);\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return null;\n }\n }\n\n // webpieces-disable no-any-unknown -- opaque parsed JSON value, narrowed to string[] here\n private asStringArray(value: unknown): string[] {\n if (!Array.isArray(value)) return [];\n // webpieces-disable no-any-unknown -- element of an opaque JSON array, narrowed by the type guard\n return value.filter((v: unknown): v is string => typeof v === 'string');\n }\n\n // Parse opaque AI-authored JSON, converting a SyntaxError into a readable InformAiError.\n // webpieces-disable no-any-unknown -- returns the opaque parsed object; loadReviewJson narrows each field\n private parseReviewJson(raw: string, filePath: string): Record<string, unknown> {\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: convert JSON.parse SyntaxError to an InformAiError for the AI\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n // webpieces-disable no-any-unknown -- parsed JSON is opaque until narrowed by the caller\n return JSON.parse(raw) as Record<string, unknown>;\n } catch (err: unknown) {\n const error = toError(err);\n throw new InformAiError(\n `review.json is not valid JSON (${error.message}).\\n\\n${this.reviewJsonSchemaHint(filePath)}\\n\\n` +\n `Then re-run: pnpm wp-finish-upsert-pr`,\n );\n }\n }\n}\n\n// Temporary migration delegators to ReviewJsonService — removed once consumers inject it.\nconst reviewJsonSvc = new ReviewJsonService();\n\n// webpieces-disable no-function-outside-class -- temporary back-compat delegator to ReviewJsonService; removed once consumers inject it\nexport function prDirFor(repoRoot: string, featureName: string): string {\n return reviewJsonSvc.prDirFor(repoRoot, featureName);\n}\n\n// webpieces-disable no-function-outside-class -- temporary back-compat delegator to ReviewJsonService; removed once consumers inject it\nexport function reviewJsonPath(repoRoot: string, featureName: string): string {\n return reviewJsonSvc.reviewJsonPath(repoRoot, featureName);\n}\n\n// webpieces-disable no-function-outside-class -- temporary back-compat delegator to ReviewJsonService; removed once consumers inject it\nexport function reviewJsonSchemaHint(filePath: string): string {\n return reviewJsonSvc.reviewJsonSchemaHint(filePath);\n}\n\n// webpieces-disable no-function-outside-class -- temporary back-compat delegator to ReviewJsonService; removed once consumers inject it\nexport function loadReviewJson(filePath: string, required: readonly RequiredChecklist[] = []): ReviewJson {\n return reviewJsonSvc.loadReviewJson(filePath, required);\n}\n"]}
|
package/src/validate-config.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { validateChecklistsSection } from './pr-gate-section-validators';
|
|
2
|
+
export { validateChecklistsSection };
|
|
1
3
|
export declare function allRuleNames(): readonly string[];
|
|
2
4
|
export declare function validateWebpiecesConfig(rawRules: Record<string, Record<string, unknown>>, hasCustomRulesDir?: boolean): string[];
|
|
3
5
|
/**
|
|
@@ -8,7 +10,6 @@ export declare function validateWebpiecesConfig(rawRules: Record<string, Record<
|
|
|
8
10
|
* `repoRoot` (when known) lets the `checklists[].docs` existence check run.
|
|
9
11
|
*/
|
|
10
12
|
export declare function validatePrGateSection(section: unknown, repoRoot?: string): string[];
|
|
11
|
-
export declare function validateChecklistsSection(value: unknown, repoRoot?: string): string[];
|
|
12
13
|
/**
|
|
13
14
|
* Validate the REQUIRED top-level `excludePaths` block: two independent glob lists (`rules`,
|
|
14
15
|
* `guards`) that suppress hook enforcement per file path. Required so every client upgrading is
|
package/src/validate-config.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.validateChecklistsSection = void 0;
|
|
3
4
|
exports.allRuleNames = allRuleNames;
|
|
4
5
|
exports.validateWebpiecesConfig = validateWebpiecesConfig;
|
|
5
6
|
exports.validatePrGateSection = validatePrGateSection;
|
|
6
|
-
exports.validateChecklistsSection = validateChecklistsSection;
|
|
7
7
|
exports.validateExcludePaths = validateExcludePaths;
|
|
8
8
|
exports.validateMatchRulesSection = validateMatchRulesSection;
|
|
9
9
|
exports.validateSectionPlacement = validateSectionPlacement;
|
|
@@ -11,7 +11,8 @@ exports.validateCommandsSection = validateCommandsSection;
|
|
|
11
11
|
const sections_1 = require("./sections");
|
|
12
12
|
const rule_configs_1 = require("./rule-configs");
|
|
13
13
|
const main_sync_guard_configs_1 = require("./main-sync-guard-configs");
|
|
14
|
-
const
|
|
14
|
+
const pr_gate_section_validators_1 = require("./pr-gate-section-validators");
|
|
15
|
+
Object.defineProperty(exports, "validateChecklistsSection", { enumerable: true, get: function () { return pr_gate_section_validators_1.validateChecklistsSection; } });
|
|
15
16
|
const match_rules_config_1 = require("./match-rules-config");
|
|
16
17
|
const to_error_1 = require("./to-error");
|
|
17
18
|
const rule_configs_2 = require("./rule-configs");
|
|
@@ -223,9 +224,26 @@ function prGateExample() {
|
|
|
223
224
|
` "buildCommand": "<command CI runs to validate a PR, e.g. pnpm nx affected --target=ci --base=$(git merge-base origin/main HEAD)>",\n` +
|
|
224
225
|
` "gates": [\n` +
|
|
225
226
|
` { "name": "API Changed", "patterns": ["libraries/apis/**", "**/*Api.ts"], "warningColor": "yellow" }\n` +
|
|
227
|
+
` ],\n` +
|
|
228
|
+
` "checklists": [ // OPTIONAL — per-area review, one distinct reviewer subagent each\n` +
|
|
229
|
+
` { "subagent": "db-migration-reviewer", "doc": ".claude/review/db-migrations.md", "patterns": ["**/*.sql"] }\n` +
|
|
226
230
|
` ]\n` +
|
|
227
231
|
` }`);
|
|
228
232
|
}
|
|
233
|
+
// The `gates` array: dashboard-only warning flags. Extracted from validatePrGateSection to keep that
|
|
234
|
+
// method inside the length limit.
|
|
235
|
+
// webpieces-disable no-any-unknown -- `value` is the opaque consumer `gates` value until narrowed here
|
|
236
|
+
// webpieces-disable no-function-outside-class -- module-level config validator, matches the rest of this file
|
|
237
|
+
function validateGatesSection(value) {
|
|
238
|
+
if (!Array.isArray(value)) {
|
|
239
|
+
return [`[pr-gate] "gates" must be an array of { name, patterns, warningColor, disabled? }.`];
|
|
240
|
+
}
|
|
241
|
+
const errors = [];
|
|
242
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
243
|
+
errors.push(...validateGate(value[i], i));
|
|
244
|
+
}
|
|
245
|
+
return errors;
|
|
246
|
+
}
|
|
229
247
|
// webpieces-disable no-any-unknown -- one gate entry from opaque consumer JSON, validated field-by-field
|
|
230
248
|
function validateGate(gate, index) {
|
|
231
249
|
if (typeof gate !== 'object' || gate === null) {
|
|
@@ -291,21 +309,12 @@ function validatePrGateSection(section, repoRoot) {
|
|
|
291
309
|
errors.push(`[pr-gate] "mergeMode" = "${String(mm)}" is not valid. ${MERGE_MODE_HELP}`);
|
|
292
310
|
}
|
|
293
311
|
}
|
|
294
|
-
if ('gates' in s)
|
|
295
|
-
|
|
296
|
-
if (!Array.isArray(gates)) {
|
|
297
|
-
errors.push(`[pr-gate] "gates" must be an array of { name, patterns, warningColor, disabled? }.`);
|
|
298
|
-
}
|
|
299
|
-
else {
|
|
300
|
-
for (let i = 0; i < gates.length; i += 1) {
|
|
301
|
-
errors.push(...validateGate(gates[i], i));
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
}
|
|
312
|
+
if ('gates' in s)
|
|
313
|
+
errors.push(...validateGatesSection(s['gates']));
|
|
305
314
|
// Optional extension point: company review checklists. The config only points at ONE manifest doc —
|
|
306
315
|
// { "doc": "..." } — and the checklist SET lives in that doc. Absent ⇒ no checklists.
|
|
307
316
|
if ('checklists' in s)
|
|
308
|
-
errors.push(...validateChecklistsSection(s['checklists'], repoRoot));
|
|
317
|
+
errors.push(...(0, pr_gate_section_validators_1.validateChecklistsSection)(s['checklists'], repoRoot));
|
|
309
318
|
// Optional server-token salt. Absent ⇒ no token minted, CI enforcement is a no-op. Present ⇒ must be
|
|
310
319
|
// a non-empty string (an empty salt would mint a token anyone can forge from a known-empty secret).
|
|
311
320
|
if ('gateSalt' in s) {
|
|
@@ -314,31 +323,13 @@ function validatePrGateSection(section, repoRoot) {
|
|
|
314
323
|
errors.push(`[pr-gate] "gateSalt" must be a non-empty string — it is the shared secret the gate token is HMAC'd with. Omit the key entirely to disable server-side token enforcement.`);
|
|
315
324
|
}
|
|
316
325
|
}
|
|
326
|
+
errors.push(...(0, pr_gate_section_validators_1.validateNoGateSaltRationale)(s));
|
|
317
327
|
// Optional: publish reviewer output as a PR comment (defaults true). Must be a boolean when present.
|
|
318
328
|
if ('checklistComments' in s && typeof s['checklistComments'] !== 'boolean') {
|
|
319
329
|
errors.push(`[pr-gate] "checklistComments" must be a boolean (defaults to true; set false to keep the PR body-only).`);
|
|
320
330
|
}
|
|
321
331
|
return errors;
|
|
322
332
|
}
|
|
323
|
-
// The `checklists` section of a pr-gate config: { "doc": "<path to the manifest doc>" }. The manifest
|
|
324
|
-
// itself (the list of { subagent, doc?, patterns? }) lives in that doc's <!-- webpieces:checklists -->
|
|
325
|
-
// block and is validated by ChecklistManifestService. Exported so the isolated validate-checklist-docs
|
|
326
|
-
// target reuses it. `repoRoot` (when known) lets the doc + manifest existence checks run.
|
|
327
|
-
// webpieces-disable no-any-unknown -- `value` is opaque consumer JSON until narrowed below
|
|
328
|
-
// webpieces-disable no-function-outside-class -- module-level config validator, matches the rest of this file
|
|
329
|
-
function validateChecklistsSection(value, repoRoot) {
|
|
330
|
-
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
331
|
-
return [`[pr-gate] "checklists" must be an object { "doc": "<path to the review manifest doc>" }.`];
|
|
332
|
-
}
|
|
333
|
-
// webpieces-disable no-any-unknown -- narrowing the opaque checklists section
|
|
334
|
-
const doc = value['doc'];
|
|
335
|
-
if (typeof doc !== 'string' || doc.trim() === '') {
|
|
336
|
-
return [`[pr-gate] "checklists.doc" must be a non-empty string — the repo-relative markdown doc carrying the <!-- webpieces:checklists [...] --> manifest.`];
|
|
337
|
-
}
|
|
338
|
-
if (repoRoot === undefined)
|
|
339
|
-
return [];
|
|
340
|
-
return new checklist_manifest_1.ChecklistManifestService().validate(repoRoot, doc);
|
|
341
|
-
}
|
|
342
333
|
function excludePathsExample() {
|
|
343
334
|
return '"excludePaths": {\n' +
|
|
344
335
|
' "rules": ["repositories/**"],\n' +
|
|
@@ -519,6 +510,25 @@ function validateCommandsSection(commands, legacyPrGate, repoRoot) {
|
|
|
519
510
|
// pr-gate is required (set mode OFF to opt out). Prefer commands["pr-gate"]; fall back to the
|
|
520
511
|
// legacy top-level block so an un-migrated file still validates its gate config.
|
|
521
512
|
errors.push(...validatePrGateSection(c['pr-gate'] ?? legacyPrGate, repoRoot));
|
|
513
|
+
// Canonical guard-hint strings live under commands.guardHints; each value, when present, must be a
|
|
514
|
+
// non-empty command string. The two names bind to the two guards that surface them in fix hints.
|
|
515
|
+
const hints = c['guardHints'];
|
|
516
|
+
if (hints !== undefined) {
|
|
517
|
+
if (typeof hints !== 'object' || hints === null || Array.isArray(hints)) {
|
|
518
|
+
errors.push(`[commands] "guardHints" must be an object { "prCreationOrPush": "...", "mergeInProgress": "..." }.`);
|
|
519
|
+
}
|
|
520
|
+
else {
|
|
521
|
+
// webpieces-disable no-any-unknown -- narrowing the opaque guardHints object from consumer JSON
|
|
522
|
+
const h = hints;
|
|
523
|
+
for (const field of ['prCreationOrPush', 'mergeInProgress']) {
|
|
524
|
+
if (field in h && (typeof h[field] !== 'string' || h[field].trim() === '')) {
|
|
525
|
+
errors.push(`[commands] "guardHints.${field}" must be a non-empty string (the gated command to run).`);
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
// Legacy FLAT command strings (pre-guardHints). Still accepted for back-compat; a migrated file
|
|
531
|
+
// uses commands.guardHints instead.
|
|
522
532
|
for (const field of ['upsertPr', 'mergeComplete']) {
|
|
523
533
|
if (field in c && typeof c[field] !== 'string') {
|
|
524
534
|
errors.push(`[commands] "${field}" must be a string (the gated command to run).`);
|