@webpieces/rules-config 0.4.457 → 0.4.458

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.
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ReviewJsonService = exports.ReviewJson = void 0;
3
+ exports.ReviewJsonService = exports.ReviewJson = exports.RequiredChecklist = exports.ChecklistAck = void 0;
4
4
  exports.prDirFor = prDirFor;
5
5
  exports.reviewJsonPath = reviewJsonPath;
6
6
  exports.reviewJsonSchemaHint = reviewJsonSchemaHint;
@@ -10,8 +10,43 @@ const fs = tslib_1.__importStar(require("fs"));
10
10
  const path = tslib_1.__importStar(require("path"));
11
11
  const inversify_1 = require("inversify");
12
12
  const constants_1 = require("./constants");
13
+ const checklist_config_1 = require("./checklist-config");
13
14
  const inform_ai_error_1 = require("./inform-ai-error");
14
15
  const to_error_1 = require("./to-error");
16
+ // One entry the AI writes into review.json's `checklists[]`: "I read the doc for <id> and walked it".
17
+ // acknowledged: true is the AI attesting it did the walk — a SURFACING/AUDIT signal, not authorization.
18
+ class ChecklistAck {
19
+ id;
20
+ acknowledged;
21
+ notes; // per-item findings the AI chose to record (optional; [] when none)
22
+ constructor(id, acknowledged, notes) {
23
+ this.id = id;
24
+ this.acknowledged = acknowledged;
25
+ this.notes = notes;
26
+ }
27
+ }
28
+ exports.ChecklistAck = ChecklistAck;
29
+ // What the CALLER (the pr-gate command) computed from the diff: the checklists this branch triggered.
30
+ // Drives BOTH review.json validation (BLOCK must be acknowledged) AND the printed schema hint (so the
31
+ // AI is told, at the moment it writes review.json, exactly which docs to read). Data-only.
32
+ class RequiredChecklist {
33
+ id;
34
+ title;
35
+ severity; // 'BLOCK' | 'WARN'
36
+ docs;
37
+ blockMessage;
38
+ matchedFiles; // the changed files that triggered it (for the dashboard + hint)
39
+ // eslint-disable-next-line @typescript-eslint/max-params
40
+ constructor(id, title, severity, docs, blockMessage, matchedFiles) {
41
+ this.id = id;
42
+ this.title = title;
43
+ this.severity = severity;
44
+ this.docs = docs;
45
+ this.blockMessage = blockMessage;
46
+ this.matchedFiles = matchedFiles;
47
+ }
48
+ }
49
+ exports.RequiredChecklist = RequiredChecklist;
15
50
  // The AI-authored review for a PR. The AI writes this file itself between `wp-start-upsert-pr` (which
16
51
  // prints the schema) and `wp-finish-upsert-pr` (which reads it). Data-only (per CLAUDE.md).
17
52
  class ReviewJson {
@@ -23,7 +58,9 @@ class ReviewJson {
23
58
  violations; // pattern/architecture violations; length = the Pattern Violations count
24
59
  risks;
25
60
  filesToReview;
26
- constructor(title, riskScore, riskLevel, riskEmoji, summary, violations, risks, filesToReview) {
61
+ checklists; // consumer-checklist acknowledgments; [] when no checklists were required
62
+ // eslint-disable-next-line @typescript-eslint/max-params
63
+ constructor(title, riskScore, riskLevel, riskEmoji, summary, violations, risks, filesToReview, checklists = []) {
27
64
  this.title = title;
28
65
  this.riskScore = riskScore;
29
66
  this.riskLevel = riskLevel;
@@ -32,6 +69,7 @@ class ReviewJson {
32
69
  this.violations = violations;
33
70
  this.risks = risks;
34
71
  this.filesToReview = filesToReview;
72
+ this.checklists = checklists;
35
73
  }
36
74
  }
37
75
  exports.ReviewJson = ReviewJson;
@@ -47,8 +85,15 @@ let ReviewJsonService = class ReviewJsonService {
47
85
  reviewJsonPath(repoRoot, featureName) {
48
86
  return path.join(this.prDirFor(repoRoot, featureName), 'review.json');
49
87
  }
50
- // Copy-paste schema both commands print (write it / fix it).
51
- reviewJsonSchemaHint(filePath) {
88
+ // Copy-paste schema both commands print (write it / fix it). `required` is the set of consumer
89
+ // checklists the diff triggered; when it is empty the output is byte-identical to before this
90
+ // feature existed (non-adopting repos see no change). When non-empty it grows a `checklists` line
91
+ // in the JSON shape PLUS an instruction block naming the docs to read — diff-derived instructions
92
+ // injected at exactly the moment the AI writes review.json.
93
+ reviewJsonSchemaHint(filePath, required = []) {
94
+ const checklistLine = required.length > 0
95
+ ? `,\n "checklists": [{ "id": "<id from the list below>", "acknowledged": true, "notes": ["what you checked"] }]\n`
96
+ : `\n`;
52
97
  return (`Write your PR review to:\n ${filePath}\n\n` +
53
98
  `with this exact JSON shape (riskEmoji optional — derived from riskLevel):\n\n` +
54
99
  `{\n` +
@@ -58,22 +103,52 @@ let ReviewJsonService = class ReviewJsonService {
58
103
  ` "summary": "5–10 sentence review summary",\n` +
59
104
  ` "violations": ["pattern/architecture violations you found (empty array if none)"],\n` +
60
105
  ` "risks": ["notable risks (empty array if none)"],\n` +
61
- ` "filesToReview": ["paths a human should look at (empty array if none)"]\n` +
62
- `}`);
106
+ ` "filesToReview": ["paths a human should look at (empty array if none)"]` +
107
+ checklistLine +
108
+ `}` +
109
+ this.requiredChecklistHint(required));
110
+ }
111
+ // The diff-triggered instruction block, appended ONLY when the branch triggered a checklist. This
112
+ // is the consumer's review process, re-injected: read doc Y because the diff touched X. BLOCK
113
+ // entries must be acknowledged in `checklists[]` or wp-finish-upsert-pr refuses to open the PR.
114
+ requiredChecklistHint(required) {
115
+ if (required.length === 0)
116
+ return '';
117
+ const lines = ['', '', 'This branch triggered company review checklist(s). BEFORE writing review.json, READ each'];
118
+ lines.push('doc, walk its items against your diff, then add a `checklists[]` entry acknowledging it:');
119
+ lines.push('');
120
+ for (const req of required) {
121
+ const gate = req.severity === checklist_config_1.CHECKLIST_BLOCK
122
+ ? 'BLOCK — the PR will NOT open until you acknowledge it'
123
+ : 'WARN — acknowledge if it applies (never blocks)';
124
+ lines.push(` • [${req.id}] ${req.title} (${gate})`);
125
+ lines.push(` docs to read: ${req.docs.join(', ')}`);
126
+ if (req.blockMessage.trim() !== '')
127
+ lines.push(` ${req.blockMessage.trim()}`);
128
+ if (req.matchedFiles.length > 0)
129
+ lines.push(` triggered by: ${req.matchedFiles.slice(0, 5).join(', ')}`);
130
+ }
131
+ return lines.join('\n');
63
132
  }
64
133
  /**
65
134
  * Load + validate the AI-authored review.json. Throws InformAiError (with the schema) when missing,
66
135
  * unparseable, or structurally wrong. Returns a fully-populated ReviewJson on success.
136
+ *
137
+ * `required` is the set of consumer checklists the diff triggered (empty for non-adopting repos, in
138
+ * which case this behaves byte-identically to before the feature). Every BLOCK entry must appear in
139
+ * review.json's `checklists[]` with `acknowledged: true`, or a validation error is raised alongside
140
+ * the usual ones so the AI gets ONE message. WARN entries are never validated; unknown ids in
141
+ * `checklists[]` are ignored (forward-compat).
67
142
  */
68
143
  // webpieces-disable max-lines-new-methods -- one cohesive load+validate pass over the review fields
69
- loadReviewJson(filePath) {
144
+ loadReviewJson(filePath, required = []) {
70
145
  if (!fs.existsSync(filePath)) {
71
- throw new inform_ai_error_1.InformAiError(`Required review.json not found.\n\n${this.reviewJsonSchemaHint(filePath)}\n\n` +
146
+ throw new inform_ai_error_1.InformAiError(`Required review.json not found.\n\n${this.reviewJsonSchemaHint(filePath, required)}\n\n` +
72
147
  `Then re-run: pnpm wp-finish-upsert-pr`);
73
148
  }
74
149
  const raw = this.parseReviewJson(fs.readFileSync(filePath, 'utf8'), filePath);
75
150
  if (typeof raw !== 'object' || raw === null || Array.isArray(raw)) {
76
- throw new inform_ai_error_1.InformAiError(`review.json must be a JSON object.\n\n${this.reviewJsonSchemaHint(filePath)}`);
151
+ throw new inform_ai_error_1.InformAiError(`review.json must be a JSON object.\n\n${this.reviewJsonSchemaHint(filePath, required)}`);
77
152
  }
78
153
  const errors = [];
79
154
  const riskScore = raw['riskScore'];
@@ -88,17 +163,57 @@ let ReviewJsonService = class ReviewJsonService {
88
163
  if (title === '') {
89
164
  errors.push('"title" must be a non-empty, imperative PR title describing the change (no branch names).');
90
165
  }
166
+ const acks = this.parseChecklistAcks(raw['checklists']);
167
+ for (const err of this.requiredChecklistErrors(required, acks))
168
+ errors.push(err);
91
169
  if (errors.length > 0) {
92
170
  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` +
93
171
  errors.map((e) => ` • ${e}`).join('\n') +
94
- `\n\n${this.reviewJsonSchemaHint(filePath)}`);
172
+ `\n\n${this.reviewJsonSchemaHint(filePath, required)}`);
95
173
  }
96
174
  const level = riskLevel;
97
175
  const emoji = typeof raw['riskEmoji'] === 'string' && raw['riskEmoji'] !== ''
98
176
  ? raw['riskEmoji']
99
177
  : (EMOJI_FOR_LEVEL[level] ?? '🟡');
100
178
  const summary = typeof raw['summary'] === 'string' ? raw['summary'] : '';
101
- return new ReviewJson(title, riskScore, level, emoji, summary, this.asStringArray(raw['violations']), this.asStringArray(raw['risks']), this.asStringArray(raw['filesToReview']));
179
+ return new ReviewJson(title, riskScore, level, emoji, summary, this.asStringArray(raw['violations']), this.asStringArray(raw['risks']), this.asStringArray(raw['filesToReview']), acks);
180
+ }
181
+ // Parse the AI-authored `checklists[]` into typed ChecklistAck[]. Tolerant of a missing/garbage
182
+ // field (→ []) and of non-object entries (skipped) — malformed acks simply fail to satisfy a BLOCK
183
+ // requirement rather than crashing the load.
184
+ // webpieces-disable no-any-unknown -- opaque parsed JSON value, narrowed here
185
+ parseChecklistAcks(value) {
186
+ if (!Array.isArray(value))
187
+ return [];
188
+ const acks = [];
189
+ for (const entry of value) {
190
+ if (typeof entry !== 'object' || entry === null || Array.isArray(entry))
191
+ continue;
192
+ // webpieces-disable no-any-unknown -- one opaque ack entry, narrowed field-by-field
193
+ const e = entry;
194
+ const id = typeof e['id'] === 'string' ? e['id'] : '';
195
+ if (id === '')
196
+ continue;
197
+ acks.push(new ChecklistAck(id, e['acknowledged'] === true, this.asStringArray(e['notes'])));
198
+ }
199
+ return acks;
200
+ }
201
+ // BLOCK requirements that are not acknowledged → one error each, using the consumer's blockMessage
202
+ // verbatim so the consumer owns the wording and webpieces owns the mechanism.
203
+ requiredChecklistErrors(required, acks) {
204
+ const errors = [];
205
+ for (const req of required) {
206
+ if (req.severity !== checklist_config_1.CHECKLIST_BLOCK)
207
+ continue;
208
+ const ack = acks.find((a) => a.id === req.id);
209
+ if (!ack || !ack.acknowledged) {
210
+ const docs = req.docs.length > 0 ? ` Read: ${req.docs.join(', ')}.` : '';
211
+ const msg = req.blockMessage.trim() !== '' ? `${req.blockMessage.trim()} ` : '';
212
+ errors.push(`Checklist "${req.id}" (${req.title}) is REQUIRED for this diff but not acknowledged. ${msg}` +
213
+ `Add {"id":"${req.id}","acknowledged":true,"notes":[...]} to "checklists" once you have walked it.${docs}`);
214
+ }
215
+ }
216
+ return errors;
102
217
  }
103
218
  // webpieces-disable no-any-unknown -- opaque parsed JSON value, narrowed to string[] here
104
219
  asStringArray(value) {
@@ -138,11 +253,11 @@ function reviewJsonPath(repoRoot, featureName) {
138
253
  return reviewJsonSvc.reviewJsonPath(repoRoot, featureName);
139
254
  }
140
255
  // webpieces-disable no-function-outside-class -- temporary back-compat delegator to ReviewJsonService; removed once consumers inject it
141
- function reviewJsonSchemaHint(filePath) {
142
- return reviewJsonSvc.reviewJsonSchemaHint(filePath);
256
+ function reviewJsonSchemaHint(filePath, required = []) {
257
+ return reviewJsonSvc.reviewJsonSchemaHint(filePath, required);
143
258
  }
144
259
  // webpieces-disable no-function-outside-class -- temporary back-compat delegator to ReviewJsonService; removed once consumers inject it
145
- function loadReviewJson(filePath) {
146
- return reviewJsonSvc.loadReviewJson(filePath);
260
+ function loadReviewJson(filePath, required = []) {
261
+ return reviewJsonSvc.loadReviewJson(filePath, required);
147
262
  }
148
263
  //# sourceMappingURL=review-json.js.map
@@ -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 AI-authored review for a PR. The AI writes this file itself between `wp-start-upsert-pr` (which\n// prints the schema) and `wp-finish-upsert-pr` (which reads it). 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\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 ) {\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 }\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 // Copy-paste schema both commands print (write it / fix it).\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 /**\n * Load + validate the AI-authored review.json. Throws InformAiError (with the schema) when missing,\n * unparseable, or structurally wrong. Returns a fully-populated ReviewJson on success.\n */\n // webpieces-disable max-lines-new-methods -- one cohesive load+validate pass over the review fields\n loadReviewJson(filePath: string): 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 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 );\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): ReviewJson {\n return reviewJsonSvc.loadReviewJson(filePath);\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 { CHECKLIST_BLOCK } from './checklist-config';\nimport { InformAiError } from './inform-ai-error';\nimport { toError } from './to-error';\n\n// One entry the AI writes into review.json's `checklists[]`: \"I read the doc for <id> and walked it\".\n// acknowledged: true is the AI attesting it did the walk — a SURFACING/AUDIT signal, not authorization.\nexport class ChecklistAck {\n id: string;\n acknowledged: boolean;\n notes: string[]; // per-item findings the AI chose to record (optional; [] when none)\n\n constructor(id: string, acknowledged: boolean, notes: string[]) {\n this.id = id;\n this.acknowledged = acknowledged;\n this.notes = notes;\n }\n}\n\n// What the CALLER (the pr-gate command) computed from the diff: the checklists this branch triggered.\n// Drives BOTH review.json validation (BLOCK must be acknowledged) AND the printed schema hint (so the\n// AI is told, at the moment it writes review.json, exactly which docs to read). Data-only.\nexport class RequiredChecklist {\n id: string;\n title: string;\n severity: string; // 'BLOCK' | 'WARN'\n docs: string[];\n blockMessage: string;\n matchedFiles: string[]; // the changed files that triggered it (for the dashboard + hint)\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(id: string, title: string, severity: string, docs: string[], blockMessage: string, matchedFiles: string[]) {\n this.id = id;\n this.title = title;\n this.severity = severity;\n this.docs = docs;\n this.blockMessage = blockMessage;\n this.matchedFiles = matchedFiles;\n }\n}\n\n// The AI-authored review for a PR. The AI writes this file itself between `wp-start-upsert-pr` (which\n// prints the schema) and `wp-finish-upsert-pr` (which reads it). 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 checklists: ChecklistAck[]; // consumer-checklist acknowledgments; [] when no checklists were required\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 checklists: ChecklistAck[] = [],\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.checklists = checklists;\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 // Copy-paste schema both commands print (write it / fix it). `required` is the set of consumer\n // checklists the diff triggered; when it is empty the output is byte-identical to before this\n // feature existed (non-adopting repos see no change). When non-empty it grows a `checklists` line\n // in the JSON shape PLUS an instruction block naming the docs to read — diff-derived instructions\n // injected at exactly the moment the AI writes review.json.\n reviewJsonSchemaHint(filePath: string, required: readonly RequiredChecklist[] = []): string {\n const checklistLine = required.length > 0\n ? `,\\n \"checklists\": [{ \"id\": \"<id from the list below>\", \"acknowledged\": true, \"notes\": [\"what you checked\"] }]\\n`\n : `\\n`;\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 checklistLine +\n `}` +\n this.requiredChecklistHint(required)\n );\n }\n\n // The diff-triggered instruction block, appended ONLY when the branch triggered a checklist. This\n // is the consumer's review process, re-injected: read doc Y because the diff touched X. BLOCK\n // entries must be acknowledged in `checklists[]` or wp-finish-upsert-pr refuses to open the PR.\n private requiredChecklistHint(required: readonly RequiredChecklist[]): string {\n if (required.length === 0) return '';\n const lines: string[] = ['', '', 'This branch triggered company review checklist(s). BEFORE writing review.json, READ each'];\n lines.push('doc, walk its items against your diff, then add a `checklists[]` entry acknowledging it:');\n lines.push('');\n for (const req of required) {\n const gate = req.severity === CHECKLIST_BLOCK\n ? 'BLOCK — the PR will NOT open until you acknowledge it'\n : 'WARN — acknowledge if it applies (never blocks)';\n lines.push(` • [${req.id}] ${req.title} (${gate})`);\n lines.push(` docs to read: ${req.docs.join(', ')}`);\n if (req.blockMessage.trim() !== '') lines.push(` ${req.blockMessage.trim()}`);\n if (req.matchedFiles.length > 0) lines.push(` triggered by: ${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. Returns a fully-populated ReviewJson on success.\n *\n * `required` is the set of consumer checklists the diff triggered (empty for non-adopting repos, in\n * which case this behaves byte-identically to before the feature). Every BLOCK entry must appear in\n * review.json's `checklists[]` with `acknowledged: true`, or a validation error is raised alongside\n * the usual ones so the AI gets ONE message. WARN entries are never validated; unknown ids in\n * `checklists[]` are ignored (forward-compat).\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);\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 acks = this.parseChecklistAcks(raw['checklists']);\n for (const err of this.requiredChecklistErrors(required, acks)) 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 acks,\n );\n }\n\n // Parse the AI-authored `checklists[]` into typed ChecklistAck[]. Tolerant of a missing/garbage\n // field (→ []) and of non-object entries (skipped) — malformed acks simply fail to satisfy a BLOCK\n // requirement rather than crashing the load.\n // webpieces-disable no-any-unknown -- opaque parsed JSON value, narrowed here\n private parseChecklistAcks(value: unknown): ChecklistAck[] {\n if (!Array.isArray(value)) return [];\n const acks: ChecklistAck[] = [];\n for (const entry of value) {\n if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) continue;\n // webpieces-disable no-any-unknown -- one opaque ack entry, narrowed field-by-field\n const e = entry as Record<string, unknown>;\n const id = typeof e['id'] === 'string' ? (e['id'] as string) : '';\n if (id === '') continue;\n acks.push(new ChecklistAck(id, e['acknowledged'] === true, this.asStringArray(e['notes'])));\n }\n return acks;\n }\n\n // BLOCK requirements that are not acknowledged → one error each, using the consumer's blockMessage\n // verbatim so the consumer owns the wording and webpieces owns the mechanism.\n private requiredChecklistErrors(required: readonly RequiredChecklist[], acks: readonly ChecklistAck[]): string[] {\n const errors: string[] = [];\n for (const req of required) {\n if (req.severity !== CHECKLIST_BLOCK) continue;\n const ack = acks.find((a: ChecklistAck): boolean => a.id === req.id);\n if (!ack || !ack.acknowledged) {\n const docs = req.docs.length > 0 ? ` Read: ${req.docs.join(', ')}.` : '';\n const msg = req.blockMessage.trim() !== '' ? `${req.blockMessage.trim()} ` : '';\n errors.push(\n `Checklist \"${req.id}\" (${req.title}) is REQUIRED for this diff but not acknowledged. ${msg}` +\n `Add {\"id\":\"${req.id}\",\"acknowledged\":true,\"notes\":[...]} to \"checklists\" once you have walked it.${docs}`,\n );\n }\n }\n return errors;\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, 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"]}
@@ -4,9 +4,10 @@ export declare function validateWebpiecesConfig(rawRules: Record<string, Record<
4
4
  * Validate the top-level `pr-gate` section. It is REQUIRED (a client that opts out sets mode "OFF").
5
5
  * `buildCommand` is required unless mode is "OFF". Returns human-readable, copy-paste-friendly errors
6
6
  * — never throws. The pr-gate block lives outside the FieldDef-driven `rules` schema because its
7
- * nested `gates` array can't be expressed there, so it gets its own structural validation here.
7
+ * nested `gates`/`checklists` arrays can't be expressed there, so they get structural validation here.
8
+ * `repoRoot` (when known) lets the `checklists[].docs` existence check run.
8
9
  */
9
- export declare function validatePrGateSection(section: unknown): string[];
10
+ export declare function validatePrGateSection(section: unknown, repoRoot?: string): string[];
10
11
  /**
11
12
  * Validate the REQUIRED top-level `excludePaths` block: two independent glob lists (`rules`,
12
13
  * `guards`) that suppress hook enforcement per file path. Required so every client upgrading is
@@ -35,4 +36,4 @@ export declare function validateSectionPlacement(rulesSection: Record<string, Re
35
36
  * optional command-string fields. Also surfaces a migration error if a DEPRECATED top-level `pr-gate`
36
37
  * block is still present, telling the consumer to move it under `commands`.
37
38
  */
38
- export declare function validateCommandsSection(commands: unknown, legacyPrGate: unknown): string[];
39
+ export declare function validateCommandsSection(commands: unknown, legacyPrGate: unknown, repoRoot?: string): string[];
@@ -7,8 +7,12 @@ exports.validateExcludePaths = validateExcludePaths;
7
7
  exports.validateMatchRulesSection = validateMatchRulesSection;
8
8
  exports.validateSectionPlacement = validateSectionPlacement;
9
9
  exports.validateCommandsSection = validateCommandsSection;
10
+ const tslib_1 = require("tslib");
11
+ const fs = tslib_1.__importStar(require("fs"));
12
+ const path = tslib_1.__importStar(require("path"));
10
13
  const sections_1 = require("./sections");
11
14
  const rule_configs_1 = require("./rule-configs");
15
+ const checklist_config_1 = require("./checklist-config");
12
16
  const match_rules_config_1 = require("./match-rules-config");
13
17
  const to_error_1 = require("./to-error");
14
18
  const rule_configs_2 = require("./rule-configs");
@@ -235,14 +239,64 @@ function validateGate(gate, index) {
235
239
  errors.push(`[pr-gate] gates[${index}].disabled must be a boolean (example/inactive gate kept in the file).`);
236
240
  return errors;
237
241
  }
242
+ // One `checklists[]` entry, validated field-by-field (mirrors validateGate + validateMatchRule). When
243
+ // `repoRoot` is supplied, each doc path must EXIST — a checklist pointing at a deleted doc is otherwise
244
+ // a silent no-op, which is exactly the failure mode this whole feature exists to prevent.
245
+ // webpieces-disable no-any-unknown -- one checklist entry from opaque consumer JSON, validated field-by-field
246
+ // webpieces-disable no-function-outside-class -- module-level config validator, matches validateGate/validateMatchRule
247
+ function validateChecklist(entry, index, repoRoot) {
248
+ if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) {
249
+ return [`[pr-gate] checklists[${index}] must be an object { id, title, patterns, contentPatterns, docs, severity, blockMessage, disabled? }.`];
250
+ }
251
+ // webpieces-disable no-any-unknown -- narrowing one opaque checklist entry from consumer JSON
252
+ const c = entry;
253
+ const label = typeof c['id'] === 'string' && c['id'] !== '' ? `"${c['id']}"` : `checklists[${index}]`;
254
+ const errors = [];
255
+ if (typeof c['id'] !== 'string' || c['id'].trim() === '')
256
+ errors.push(`[pr-gate] checklists[${index}].id must be a non-empty string (it is the key echoed in review.json).`);
257
+ if (typeof c['title'] !== 'string' || c['title'].trim() === '')
258
+ errors.push(`[pr-gate] ${label}.title must be a non-empty string (the dashboard label).`);
259
+ if (c['patterns'] !== undefined && !isStringArray(c['patterns']))
260
+ errors.push(`[pr-gate] ${label}.patterns must be a string[] of path globs (omit or [] to match any changed file).`);
261
+ if (c['contentPatterns'] !== undefined && !isStringArray(c['contentPatterns'])) {
262
+ errors.push(`[pr-gate] ${label}.contentPatterns must be a string[] of regexes (omit or [] for a path-only trigger).`);
263
+ }
264
+ else if (isStringArray(c['contentPatterns'])) {
265
+ c['contentPatterns'].forEach((p, pi) => {
266
+ const rxErr = regexError(p);
267
+ if (rxErr)
268
+ errors.push(`[pr-gate] ${label}.contentPatterns[${pi}] is not a valid regex: ${rxErr}`);
269
+ });
270
+ }
271
+ if (!isStringArray(c['docs']) || c['docs'].length === 0) {
272
+ errors.push(`[pr-gate] ${label}.docs must be a non-empty string[] of repo-relative doc paths the AI must read.`);
273
+ }
274
+ else if (repoRoot !== undefined) {
275
+ c['docs'].forEach((doc) => {
276
+ if (!fs.existsSync(path.join(repoRoot, doc)))
277
+ errors.push(`[pr-gate] ${label}.docs references "${doc}", which does not exist — a checklist pointing at a missing doc silently never fires.`);
278
+ });
279
+ }
280
+ if (typeof c['severity'] !== 'string' || !checklist_config_1.CHECKLIST_SEVERITIES.includes(c['severity']))
281
+ errors.push(`[pr-gate] ${label}.severity must be one of: ${checklist_config_1.CHECKLIST_SEVERITIES.join(', ')}.`);
282
+ if (c['severity'] === checklist_config_1.CHECKLIST_BLOCK && (typeof c['blockMessage'] !== 'string' || c['blockMessage'].trim() === ''))
283
+ errors.push(`[pr-gate] ${label}.blockMessage is required for a ${checklist_config_1.CHECKLIST_BLOCK} checklist — it is the wording shown when the PR is refused.`);
284
+ if (c['blockMessage'] !== undefined && typeof c['blockMessage'] !== 'string')
285
+ errors.push(`[pr-gate] ${label}.blockMessage must be a string.`);
286
+ if (c['disabled'] !== undefined && typeof c['disabled'] !== 'boolean')
287
+ errors.push(`[pr-gate] ${label}.disabled must be a boolean (example/inactive checklist kept in the file).`);
288
+ return errors;
289
+ }
238
290
  /**
239
291
  * Validate the top-level `pr-gate` section. It is REQUIRED (a client that opts out sets mode "OFF").
240
292
  * `buildCommand` is required unless mode is "OFF". Returns human-readable, copy-paste-friendly errors
241
293
  * — never throws. The pr-gate block lives outside the FieldDef-driven `rules` schema because its
242
- * nested `gates` array can't be expressed there, so it gets its own structural validation here.
294
+ * nested `gates`/`checklists` arrays can't be expressed there, so they get structural validation here.
295
+ * `repoRoot` (when known) lets the `checklists[].docs` existence check run.
243
296
  */
244
297
  // webpieces-disable no-any-unknown -- `section` is opaque consumer JSON until narrowed below
245
- function validatePrGateSection(section) {
298
+ // webpieces-disable no-function-outside-class -- module-level config validator, matches the rest of this file
299
+ function validatePrGateSection(section, repoRoot) {
246
300
  if (section === undefined || section === null) {
247
301
  return [
248
302
  `[pr-gate] Not configured in webpieces.config.json. Add this block under the "commands" ` +
@@ -289,6 +343,31 @@ function validatePrGateSection(section) {
289
343
  }
290
344
  }
291
345
  }
346
+ // Optional extension point: diff-triggered company review checklists. Absent ⇒ no validation and no
347
+ // behavior change. Present ⇒ every entry validated field-by-field, ids unique across the array.
348
+ if ('checklists' in s)
349
+ errors.push(...validateChecklists(s['checklists'], repoRoot));
350
+ return errors;
351
+ }
352
+ // The `checklists` array of a pr-gate section: each entry validated field-by-field, ids unique.
353
+ // webpieces-disable no-any-unknown -- `value` is opaque consumer JSON until narrowed below
354
+ // webpieces-disable no-function-outside-class -- module-level config validator, matches the rest of this file
355
+ function validateChecklists(value, repoRoot) {
356
+ if (!Array.isArray(value)) {
357
+ return [`[pr-gate] "checklists" must be an array of { id, title, patterns, contentPatterns, docs, severity, blockMessage, disabled? }.`];
358
+ }
359
+ const errors = [];
360
+ const seenIds = new Set();
361
+ for (let i = 0; i < value.length; i += 1) {
362
+ errors.push(...validateChecklist(value[i], i, repoRoot));
363
+ // webpieces-disable no-any-unknown -- reading the id off an opaque entry only to dedupe
364
+ const id = value[i]?.['id'];
365
+ if (typeof id === 'string' && id !== '') {
366
+ if (seenIds.has(id))
367
+ errors.push(`[pr-gate] duplicate checklist id "${id}" — each checklists[].id must be unique.`);
368
+ seenIds.add(id);
369
+ }
370
+ }
292
371
  return errors;
293
372
  }
294
373
  function excludePathsExample() {
@@ -455,7 +534,8 @@ function validateSectionPlacement(rulesSection, hookGuardsSection) {
455
534
  * block is still present, telling the consumer to move it under `commands`.
456
535
  */
457
536
  // webpieces-disable no-any-unknown -- `commands`/`legacyPrGate` are opaque consumer JSON
458
- function validateCommandsSection(commands, legacyPrGate) {
537
+ // webpieces-disable no-function-outside-class -- module-level config validator, matches the rest of this file
538
+ function validateCommandsSection(commands, legacyPrGate, repoRoot) {
459
539
  const errors = [];
460
540
  if (legacyPrGate !== undefined) {
461
541
  errors.push(`[pr-gate] The top-level "pr-gate" block is deprecated. Move it under the "commands" ` +
@@ -469,7 +549,7 @@ function validateCommandsSection(commands, legacyPrGate) {
469
549
  const c = (commands ?? {});
470
550
  // pr-gate is required (set mode OFF to opt out). Prefer commands["pr-gate"]; fall back to the
471
551
  // legacy top-level block so an un-migrated file still validates its gate config.
472
- errors.push(...validatePrGateSection(c['pr-gate'] ?? legacyPrGate));
552
+ errors.push(...validatePrGateSection(c['pr-gate'] ?? legacyPrGate, repoRoot));
473
553
  for (const field of ['upsertPr', 'mergeComplete']) {
474
554
  if (field in c && typeof c[field] !== 'string') {
475
555
  errors.push(`[commands] "${field}" must be a string (the gated command to run).`);