@webpieces/rules-config 0.4.564 → 0.4.566

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.
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.validateNoGateSaltRationale = validateNoGateSaltRationale;
4
4
  exports.validateChecklistsSection = validateChecklistsSection;
5
5
  exports.validateLandPrSection = validateLandPrSection;
6
+ exports.validateDevDeploySection = validateDevDeploySection;
6
7
  const branch_archiver_1 = require("./branch-archiver");
7
8
  const checklist_validator_1 = require("./checklist-validator");
8
9
  const checklist_config_1 = require("./checklist-config");
@@ -39,10 +40,14 @@ const CHECKLIST_EXAMPLE = ('Example:\n' +
39
40
  ' "checklists": [\n' +
40
41
  ' { "subagent": "db-migration-reviewer",\n' +
41
42
  ' "doc": ".claude/review/db-migrations.md",\n' +
42
- ' "patterns": ["**/migrations/**", "**/*.sql"] }\n' +
43
+ ' "patterns": ["**/migrations/**", "**/*.sql"],\n' +
44
+ ' "required": true }\n' +
43
45
  ' ]\n' +
44
46
  ' Each entry needs its OWN reviewer subagent (a .claude/agents/<subagent>.md) — that is how independent\n' +
45
- ' review is enforced. "doc" is REPO-relative. Omit "patterns" (or use []) to run on every PR.');
47
+ ' review is enforced. "doc" is REPO-relative. Omit "patterns" (or use []) to run on every PR.\n' +
48
+ ' "required" is MANDATORY on every entry: true blocks the PR until the reviewer passes; false makes it\n' +
49
+ ' an OPTIONAL review the human is offered and may decline (but if they DO run it, a red verdict still\n' +
50
+ ' blocks).');
46
51
  /**
47
52
  * The `checklists` section of a pr-gate config: an ARRAY of { subagent, doc?, patterns? }, and nothing else.
48
53
  *
@@ -84,6 +89,39 @@ function legacyManifestError(value) {
84
89
  ` 4. Delete the <!-- webpieces:checklists ... --> comment from "${docRel}"; keep the prose.\n` +
85
90
  ` ${CHECKLIST_EXAMPLE}`);
86
91
  }
92
+ /**
93
+ * `required` is MANDATORY on every checklist entry — omitting it is an error, never a default.
94
+ *
95
+ * The error names the entry's own subagent, because that is what the consumer recognizes in a
96
+ * twelve-entry array; `checklists[7]` alone means counting braces. It also states BOTH edits, because the
97
+ * whole point of the key is that the answer differs per checklist and only the consumer knows which.
98
+ *
99
+ * Why a hard rejection instead of `?? true`: an accepted shape is never migrated. Defaulting to true
100
+ * silently keeps the all-blocking behavior this key exists to relieve, and every consumer that would have
101
+ * benefited stays on the old behavior forever without ever being told the dial exists. Defaulting to false
102
+ * is worse — it would silently DOWNGRADE a live review gate on upgrade. Per CLAUDE.md the reader of this
103
+ * message is a coding agent, so the migration is one mechanical pass.
104
+ */
105
+ // webpieces-disable no-any-unknown -- one opaque checklist entry, narrowed by the typeof guards here
106
+ // webpieces-disable no-function-outside-class -- module-level config validator, matches the rest of this file
107
+ function requiredKeyErrors(e, i) {
108
+ const name = typeof e['subagent'] === 'string' && e['subagent'].trim() !== '' ? ` ("${e['subagent']}")` : '';
109
+ if (e['required'] === undefined) {
110
+ return [
111
+ `[pr-gate] checklists[${i}]${name} is missing "required". Every checklist must state one — there is\n` +
112
+ ` no default, in either direction.\n` +
113
+ ` "required": true → BLOCKING. wp-finish-upsert-pr refuses the PR until this reviewer passes.\n` +
114
+ ` This is what every checklist did before this key existed.\n` +
115
+ ` "required": false → OPTIONAL. When it matches the diff, wp-review-upsert-pr offers it and the\n` +
116
+ ` human may decline it. If they DO run it, a red verdict still blocks.\n` +
117
+ ` ${CHECKLIST_EXAMPLE}`,
118
+ ];
119
+ }
120
+ if (typeof e['required'] !== 'boolean') {
121
+ return [`[pr-gate] checklists[${i}]${name}.required must be a boolean (true = blocking, false = optional) — not a string or number.`];
122
+ }
123
+ return [];
124
+ }
87
125
  // Structurally check each entry HERE — a bad `patterns` or a non-object entry is a config-file typo and
88
126
  // deserves a `checklists[i]` message — then hand the narrowed defs to ChecklistValidator for the checks only
89
127
  // the filesystem can answer (the guidance doc exists, the reviewer agent exists).
@@ -107,6 +145,7 @@ function validateChecklistArray(value, repoRoot) {
107
145
  if (e['patterns'] !== undefined && !(Array.isArray(e['patterns']) && e['patterns'].every((p) => typeof p === 'string'))) {
108
146
  errors.push(`[pr-gate] checklists[${i}].patterns must be a string[] of path globs (omit or [] to run on every PR).`);
109
147
  }
148
+ errors.push(...requiredKeyErrors(e, i));
110
149
  items.push(e);
111
150
  });
112
151
  if (repoRoot === undefined)
@@ -141,4 +180,63 @@ function validateLandPrSection(value) {
141
180
  ` "${branch_archiver_1.BRANCH_RETENTION_KEEP}" — do not delete. Branches then accumulate until branch-creation-guard trips.`,
142
181
  ];
143
182
  }
183
+ // A git ref COMPONENT this flow is willing to build a ref name out of. Deliberately much narrower than
184
+ // git's own check-ref-format: these two values are concatenated into a ref that a command then
185
+ // force-pushes, so anything that could be read as a flag, a path escape, or a glob is rejected outright
186
+ // rather than trusted to `git push` argument order.
187
+ const REF_COMPONENT = /^[A-Za-z0-9][A-Za-z0-9._/-]*$/;
188
+ /**
189
+ * Validate the optional `pr-gate.devDeploy` block — where `wp-push-dev` publishes the disposable copy of
190
+ * a feature branch, and which ref is the shared dev branch itself. Absent ⇒ `dev-include` / `dev`.
191
+ *
192
+ * `devBranch` must NOT contain a slash and must not sit inside `branchNamespace`: the whole point of the
193
+ * namespace is that the composed dev branch is written by CI and the copies are written by developers, so
194
+ * a config where one contains the other makes `--list` enumerate the deploy branch and makes the
195
+ * "refused as a source branch" check ambiguous.
196
+ */
197
+ // webpieces-disable no-any-unknown -- `value` is the opaque consumer devDeploy value until narrowed here
198
+ // webpieces-disable no-function-outside-class -- module-level config validator, matches the rest of this file
199
+ function validateDevDeploySection(value) {
200
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
201
+ return ['[pr-gate] "devDeploy" must be an object, e.g. { "branchNamespace": "dev-include", "devBranch": "dev" }.'];
202
+ }
203
+ // webpieces-disable no-any-unknown -- narrowing the opaque devDeploy object from consumer JSON
204
+ const s = value;
205
+ const errors = [];
206
+ errors.push(...validateRefComponent(s, 'branchNamespace', 'dev-include'));
207
+ errors.push(...validateRefComponent(s, 'devBranch', 'dev'));
208
+ if (errors.length > 0)
209
+ return errors;
210
+ const namespace = typeof s['branchNamespace'] === 'string' ? s['branchNamespace'].trim() : 'dev-include';
211
+ const devBranch = typeof s['devBranch'] === 'string' ? s['devBranch'].trim() : 'dev';
212
+ if (devBranch.includes('/')) {
213
+ errors.push(`[pr-gate] "devDeploy.devBranch" = "${devBranch}" must be a single ref name with no "/" — it is the ` +
214
+ `branch your CI composes and deploys, not a namespace.`);
215
+ }
216
+ if (devBranch === namespace || devBranch.startsWith(`${namespace}/`) || namespace.startsWith(`${devBranch}/`)) {
217
+ errors.push(`[pr-gate] "devDeploy.devBranch" ("${devBranch}") and "devDeploy.branchNamespace" ("${namespace}") must not ` +
218
+ `contain one another. The namespace holds the DISPOSABLE per-developer copies (written by wp-push-dev); ` +
219
+ `devBranch is the COMPOSED branch your CI rebuilds from origin/main. Overlapping them makes wp-push-dev ` +
220
+ `--list enumerate the deploy branch and makes "refused as a source branch" ambiguous.`);
221
+ }
222
+ return errors;
223
+ }
224
+ // One `devDeploy` string field: present ⇒ must be a non-empty, ref-safe string. Absent ⇒ the default.
225
+ // webpieces-disable no-any-unknown -- the already-narrowed opaque devDeploy object
226
+ // webpieces-disable no-function-outside-class -- module-level config validator, matches the rest of this file
227
+ function validateRefComponent(s, key, example) {
228
+ if (!(key in s))
229
+ return [];
230
+ const raw = s[key];
231
+ if (typeof raw !== 'string' || raw.trim() === '') {
232
+ return [`[pr-gate] "devDeploy.${key}" must be a non-empty string, e.g. "${example}". Omit the key for the "${example}" default.`];
233
+ }
234
+ if (!REF_COMPONENT.test(raw.trim())) {
235
+ return [
236
+ `[pr-gate] "devDeploy.${key}" = "${raw}" is not a usable git ref name. Use letters, digits, ".", "_", ` +
237
+ `"-" and "/" only, starting with a letter or digit (e.g. "${example}").`,
238
+ ];
239
+ }
240
+ return [];
241
+ }
144
242
  //# sourceMappingURL=pr-gate-section-validators.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"pr-gate-section-validators.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/pr-gate-section-validators.ts"],"names":[],"mappings":";;AA6BA,kEASC;AA4BD,8DAIC;AA4DD,sDAkBC;AApJD,uDAK2B;AAC3B,+DAA2D;AAC3D,yDAAwF;AAExF,yGAAyG;AACzG,sGAAsG;AACtG,oEAAoE;AAEpE,2GAA2G;AAC3G,wGAAwG;AACxG,MAAM,aAAa,GAAG,aAAa,CAAC;AAEpC;;;;;;;;;GASG;AACH,6GAA6G;AAC7G,8GAA8G;AAC9G,SAAgB,2BAA2B,CAAC,CAA0B;IAClE,IAAI,CAAC,CAAC,aAAa,IAAI,CAAC,CAAC;QAAE,OAAO,EAAE,CAAC;IACrC,OAAO;QACH,yBAAyB,aAAa,mEAAmE;YACzG,wGAAwG;YACxG,yGAAyG;YACzG,sGAAsG;YACtG,0EAA0E;KAC7E,CAAC;AACN,CAAC;AAED,MAAM,iBAAiB,GAAG,CACtB,YAAY;IACZ,uBAAuB;IACvB,gDAAgD;IAChD,qDAAqD;IACrD,0DAA0D;IAC1D,SAAS;IACT,2GAA2G;IAC3G,+FAA+F,CAClG,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,2FAA2F;AAC3F,8GAA8G;AAC9G,SAAgB,yBAAyB,CAAC,KAAc,EAAE,QAAiB;IACvE,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,sBAAsB,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;IACzE,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,IAAI,KAAK;QAAE,OAAO,CAAC,mBAAmB,CAAC,KAAK,CAAC,CAAC,CAAC;IACvG,OAAO,CAAC,mFAAmF,iBAAiB,EAAE,CAAC,CAAC;AACpH,CAAC;AAED;;;;GAIG;AACH,wGAAwG;AACxG,8GAA8G;AAC9G,SAAS,mBAAmB,CAAC,KAAa;IACtC,MAAM,GAAG,GAAI,KAAiC,CAAC,KAAK,CAAC,CAAC;IACtD,MAAM,MAAM,GAAG,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,yBAAyB,CAAC;IAC9F,OAAO,CACH,mDAAmD,MAAM,qDAAqD;QAC9G,uFAAuF;QACvF,gBAAgB,MAAM,qFAAqF;QAC3G,2CAA2C,MAAM,0CAA0C;QAC3F,4FAA4F;QAC5F,WAAW,MAAM,mFAAmF;QACpG,qEAAqE,MAAM,sBAAsB;QACjG,KAAK,iBAAiB,EAAE,CAC3B,CAAC;AACN,CAAC;AAED,wGAAwG;AACxG,6GAA6G;AAC7G,kFAAkF;AAClF,6FAA6F;AAC7F,8GAA8G;AAC9G,SAAS,sBAAsB,CAAC,KAAyB,EAAE,QAAiB;IACxE,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,MAAM,KAAK,GAAuB,EAAE,CAAC;IACrC,8GAA8G;IAC9G,KAAK,CAAC,OAAO,CAAC,CAAC,KAAc,EAAE,CAAS,EAAQ,EAAE;QAC9C,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;YACtE,MAAM,CAAC,IAAI,CAAC,wBAAwB,CAAC,0DAA0D,CAAC,CAAC;YACjG,OAAO;QACX,CAAC;QACD,2EAA2E;QAC3E,MAAM,CAAC,GAAG,KAAgC,CAAC;QAC3C,IAAI,CAAC,CAAC,KAAK,CAAC,KAAK,SAAS,IAAI,OAAO,CAAC,CAAC,KAAK,CAAC,KAAK,QAAQ,EAAE,CAAC;YACzD,MAAM,CAAC,IAAI,CAAC,wBAAwB,CAAC,8HAA8H,CAAC,CAAC;QACzK,CAAC;QACD,yFAAyF;QACzF,IAAI,CAAC,CAAC,UAAU,CAAC,KAAK,SAAS,IAAI,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,CAAC,UAAU,CAAC,CAAC,KAAK,CAAC,CAAC,CAAU,EAAW,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,EAAE,CAAC;YACxI,MAAM,CAAC,IAAI,CAAC,wBAAwB,CAAC,8EAA8E,CAAC,CAAC;QACzH,CAAC;QACD,KAAK,CAAC,IAAI,CAAC,CAAqB,CAAC,CAAC;IACtC,CAAC,CAAC,CAAC;IACH,IAAI,QAAQ,KAAK,SAAS;QAAE,OAAO,MAAM,CAAC;IAC1C,MAAM,IAAI,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,IAAsB,EAAuB,EAAE,CAAC,IAAA,8BAAW,EAAC,IAAI,CAAC,CAAC,CAAC;IAC3F,OAAO,CAAC,GAAG,MAAM,EAAE,GAAG,IAAI,wCAAkB,EAAE,CAAC,QAAQ,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC,CAAC;AAC7E,CAAC;AAED,kGAAkG;AAClG,oGAAoG;AACpG,sGAAsG;AACtG,iGAAiG;AACjG,wGAAwG;AACxG,8GAA8G;AAC9G,SAAgB,qBAAqB,CAAC,KAAc;IAChD,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACtE,OAAO,CAAC,oEAAoE,8CAA4B,MAAM,CAAC,CAAC;IACpH,CAAC;IACD,4FAA4F;IAC5F,MAAM,CAAC,GAAG,KAAgC,CAAC;IAC3C,IAAI,CAAC,CAAC,iBAAiB,IAAI,CAAC,CAAC;QAAE,OAAO,EAAE,CAAC;IACzC,MAAM,SAAS,GAAG,CAAC,CAAC,iBAAiB,CAAC,CAAC;IACvC,IAAI,OAAO,SAAS,KAAK,QAAQ,IAAI,mCAAiB,CAAC,QAAQ,CAAC,SAAS,CAAC;QAAE,OAAO,EAAE,CAAC;IACtF,OAAO;QACH,yCAAyC,MAAM,CAAC,SAAS,CAAC,kBAAkB;YAC5E,mBAAmB,mCAAiB,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK;YACpD,MAAM,8CAA4B,oFAAoF;YACtH,+FAA+F;YAC/F,oFAAoF;YACpF,MAAM,yCAAuB,8EAA8E;YAC3G,MAAM,uCAAqB,uFAAuF;KACrH,CAAC;AACN,CAAC","sourcesContent":["import {\n BRANCH_RETENTIONS,\n BRANCH_RETENTION_ARCHIVE_TAG,\n BRANCH_RETENTION_DELETE,\n BRANCH_RETENTION_KEEP,\n} from './branch-archiver';\nimport { ChecklistValidator } from './checklist-validator';\nimport { ChecklistDefinition, 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// and the one rationale key that is rejected outright. Split out of validate-config.ts only for size;\n// 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\nconst CHECKLIST_EXAMPLE = (\n 'Example:\\n' +\n ' \"checklists\": [\\n' +\n ' { \"subagent\": \"db-migration-reviewer\",\\n' +\n ' \"doc\": \".claude/review/db-migrations.md\",\\n' +\n ' \"patterns\": [\"**/migrations/**\", \"**/*.sql\"] }\\n' +\n ' ]\\n' +\n ' Each entry needs its OWN reviewer subagent (a .claude/agents/<subagent>.md) — that is how independent\\n' +\n ' review is enforced. \"doc\" is REPO-relative. Omit \"patterns\" (or use []) to run on every PR.'\n);\n\n/**\n * The `checklists` section of a pr-gate config: an ARRAY of { subagent, doc?, patterns? }, and nothing else.\n *\n * The previous `{ \"doc\": \"...\" }` shape — which hid the same array in a `<!-- webpieces:checklists -->` HTML\n * comment inside a markdown doc — is REMOVED, not deprecated. It is rejected with the exact edit to make.\n * There is deliberately no back-compat branch: two accepted shapes means two code paths, two doc-resolution\n * rules and two sets of error messages to keep honest forever, while the migration itself is a mechanical\n * config edit that the coding agent reading this error applies in one pass. A hard failure naming the fix is\n * cheaper than permanent duality.\n *\n * Exported so the isolated validate-checklist-docs target reuses it. `repoRoot` (when known) lets the doc +\n * 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 && 'doc' in value) return [legacyManifestError(value)];\n return [`[pr-gate] \"checklists\" must be an ARRAY of { \"subagent\", \"doc\"?, \"patterns\"? }. ${CHECKLIST_EXAMPLE}`];\n}\n\n/**\n * The migration message for the removed `{ doc }` manifest shape. It names the doc the consumer pointed at,\n * because that is the file holding the array they must move, and spells out the one non-obvious part of the\n * move: entry `doc` paths used to resolve relative to that manifest doc and are now REPO-relative.\n */\n// webpieces-disable no-any-unknown -- narrowing the opaque checklists section to read the old `doc` key\n// webpieces-disable no-function-outside-class -- module-level config validator, matches the rest of this file\nfunction legacyManifestError(value: object): string {\n const doc = (value as Record<string, unknown>)['doc'];\n const docRel = typeof doc === 'string' && doc.trim() !== '' ? doc : '<your review index doc>';\n return (\n `[pr-gate] \"checklists\" is the REMOVED { \"doc\": \"${docRel}\" } shape. The checklist array no longer lives in\\n` +\n ` an HTML comment inside a markdown doc — put it directly in webpieces.config.json:\\n` +\n ` 1. Open \"${docRel}\" and copy the JSON array out of its <!-- webpieces:checklists [...] --> comment.\\n` +\n ` 2. Replace \"checklists\": { \"doc\": \"${docRel}\" } with \"checklists\": <that array>.\\n` +\n ` 3. Rewrite each entry's \"doc\" to be REPO-relative — they used to resolve relative to\\n` +\n ` \"${docRel}\", so a bare \"db-migrations.md\" becomes e.g. \".claude/review/db-migrations.md\".\\n` +\n ` 4. Delete the <!-- webpieces:checklists ... --> comment from \"${docRel}\"; keep the prose.\\n` +\n ` ${CHECKLIST_EXAMPLE}`\n );\n}\n\n// Structurally check each entry HERE — a bad `patterns` or a non-object entry is a config-file typo and\n// deserves a `checklists[i]` message — then hand the narrowed defs to ChecklistValidator for the checks only\n// the filesystem can answer (the guidance doc exists, the reviewer agent exists).\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 ChecklistValidator().validate(repoRoot, defs)];\n}\n\n// The `landPr` block: what happens to the LOCAL branch once its PR is in main. Optional — omitted\n// means \"archive-tag\", which is deliberately the DEFAULT so a consumer gets the branch-accumulation\n// fix without editing config at all. `branchRetentionWhy` (and any other `*Why` sibling) is free-form\n// rationale prose and is tolerated, per the repo's convention for documenting comment-less JSON.\n// webpieces-disable no-any-unknown -- `value` is the opaque consumer `landPr` value until narrowed here\n// webpieces-disable no-function-outside-class -- module-level config validator, matches the rest of this file\nexport function validateLandPrSection(value: unknown): string[] {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n return [`[pr-gate] \"landPr\" must be an object, e.g. { \"branchRetention\": \"${BRANCH_RETENTION_ARCHIVE_TAG}\" }.`];\n }\n // webpieces-disable no-any-unknown -- narrowing the opaque landPr object from consumer JSON\n const s = value as Record<string, unknown>;\n if (!('branchRetention' in s)) return [];\n const retention = s['branchRetention'];\n if (typeof retention === 'string' && BRANCH_RETENTIONS.includes(retention)) return [];\n return [\n `[pr-gate] \"landPr.branchRetention\" = \"${String(retention)}\" is not valid. ` +\n `Must be one of: ${BRANCH_RETENTIONS.join(', ')}.\\n` +\n ` \"${BRANCH_RETENTION_ARCHIVE_TAG}\" — (default) tag the branch tip as archive/<date>/<branch>, THEN delete it. The\\n` +\n ` history stays byte-identical and restorable, but the branch stops counting\\n` +\n ` toward the branch cap and cannot be committed onto by accident.\\n` +\n ` \"${BRANCH_RETENTION_DELETE}\" — delete outright; recoverable only from the reflog, which expires.\\n` +\n ` \"${BRANCH_RETENTION_KEEP}\" — do not delete. Branches then accumulate until branch-creation-guard trips.`,\n ];\n}\n"]}
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 {\n BRANCH_RETENTIONS,\n BRANCH_RETENTION_ARCHIVE_TAG,\n BRANCH_RETENTION_DELETE,\n BRANCH_RETENTION_KEEP,\n} from './branch-archiver';\nimport { ChecklistValidator } from './checklist-validator';\nimport { ChecklistDefinition, 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// and the one rationale key that is rejected outright. Split out of validate-config.ts only for size;\n// 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\nconst CHECKLIST_EXAMPLE = (\n 'Example:\\n' +\n ' \"checklists\": [\\n' +\n ' { \"subagent\": \"db-migration-reviewer\",\\n' +\n ' \"doc\": \".claude/review/db-migrations.md\",\\n' +\n ' \"patterns\": [\"**/migrations/**\", \"**/*.sql\"],\\n' +\n ' \"required\": true }\\n' +\n ' ]\\n' +\n ' Each entry needs its OWN reviewer subagent (a .claude/agents/<subagent>.md) — that is how independent\\n' +\n ' review is enforced. \"doc\" is REPO-relative. Omit \"patterns\" (or use []) to run on every PR.\\n' +\n ' \"required\" is MANDATORY on every entry: true blocks the PR until the reviewer passes; false makes it\\n' +\n ' an OPTIONAL review the human is offered and may decline (but if they DO run it, a red verdict still\\n' +\n ' blocks).'\n);\n\n/**\n * The `checklists` section of a pr-gate config: an ARRAY of { subagent, doc?, patterns? }, and nothing else.\n *\n * The previous `{ \"doc\": \"...\" }` shape — which hid the same array in a `<!-- webpieces:checklists -->` HTML\n * comment inside a markdown doc — is REMOVED, not deprecated. It is rejected with the exact edit to make.\n * There is deliberately no back-compat branch: two accepted shapes means two code paths, two doc-resolution\n * rules and two sets of error messages to keep honest forever, while the migration itself is a mechanical\n * config edit that the coding agent reading this error applies in one pass. A hard failure naming the fix is\n * cheaper than permanent duality.\n *\n * Exported so the isolated validate-checklist-docs target reuses it. `repoRoot` (when known) lets the doc +\n * 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 && 'doc' in value) return [legacyManifestError(value)];\n return [`[pr-gate] \"checklists\" must be an ARRAY of { \"subagent\", \"doc\"?, \"patterns\"? }. ${CHECKLIST_EXAMPLE}`];\n}\n\n/**\n * The migration message for the removed `{ doc }` manifest shape. It names the doc the consumer pointed at,\n * because that is the file holding the array they must move, and spells out the one non-obvious part of the\n * move: entry `doc` paths used to resolve relative to that manifest doc and are now REPO-relative.\n */\n// webpieces-disable no-any-unknown -- narrowing the opaque checklists section to read the old `doc` key\n// webpieces-disable no-function-outside-class -- module-level config validator, matches the rest of this file\nfunction legacyManifestError(value: object): string {\n const doc = (value as Record<string, unknown>)['doc'];\n const docRel = typeof doc === 'string' && doc.trim() !== '' ? doc : '<your review index doc>';\n return (\n `[pr-gate] \"checklists\" is the REMOVED { \"doc\": \"${docRel}\" } shape. The checklist array no longer lives in\\n` +\n ` an HTML comment inside a markdown doc — put it directly in webpieces.config.json:\\n` +\n ` 1. Open \"${docRel}\" and copy the JSON array out of its <!-- webpieces:checklists [...] --> comment.\\n` +\n ` 2. Replace \"checklists\": { \"doc\": \"${docRel}\" } with \"checklists\": <that array>.\\n` +\n ` 3. Rewrite each entry's \"doc\" to be REPO-relative — they used to resolve relative to\\n` +\n ` \"${docRel}\", so a bare \"db-migrations.md\" becomes e.g. \".claude/review/db-migrations.md\".\\n` +\n ` 4. Delete the <!-- webpieces:checklists ... --> comment from \"${docRel}\"; keep the prose.\\n` +\n ` ${CHECKLIST_EXAMPLE}`\n );\n}\n\n/**\n * `required` is MANDATORY on every checklist entry — omitting it is an error, never a default.\n *\n * The error names the entry's own subagent, because that is what the consumer recognizes in a\n * twelve-entry array; `checklists[7]` alone means counting braces. It also states BOTH edits, because the\n * whole point of the key is that the answer differs per checklist and only the consumer knows which.\n *\n * Why a hard rejection instead of `?? true`: an accepted shape is never migrated. Defaulting to true\n * silently keeps the all-blocking behavior this key exists to relieve, and every consumer that would have\n * benefited stays on the old behavior forever without ever being told the dial exists. Defaulting to false\n * is worse — it would silently DOWNGRADE a live review gate on upgrade. Per CLAUDE.md the reader of this\n * message is a coding agent, so the migration is one mechanical pass.\n */\n// webpieces-disable no-any-unknown -- one opaque checklist entry, narrowed by the typeof guards here\n// webpieces-disable no-function-outside-class -- module-level config validator, matches the rest of this file\nfunction requiredKeyErrors(e: Record<string, unknown>, i: number): string[] {\n const name = typeof e['subagent'] === 'string' && e['subagent'].trim() !== '' ? ` (\"${e['subagent']}\")` : '';\n if (e['required'] === undefined) {\n return [\n `[pr-gate] checklists[${i}]${name} is missing \"required\". Every checklist must state one — there is\\n` +\n ` no default, in either direction.\\n` +\n ` \"required\": true → BLOCKING. wp-finish-upsert-pr refuses the PR until this reviewer passes.\\n` +\n ` This is what every checklist did before this key existed.\\n` +\n ` \"required\": false → OPTIONAL. When it matches the diff, wp-review-upsert-pr offers it and the\\n` +\n ` human may decline it. If they DO run it, a red verdict still blocks.\\n` +\n ` ${CHECKLIST_EXAMPLE}`,\n ];\n }\n if (typeof e['required'] !== 'boolean') {\n return [`[pr-gate] checklists[${i}]${name}.required must be a boolean (true = blocking, false = optional) — not a string or number.`];\n }\n return [];\n}\n\n// Structurally check each entry HERE — a bad `patterns` or a non-object entry is a config-file typo and\n// deserves a `checklists[i]` message — then hand the narrowed defs to ChecklistValidator for the checks only\n// the filesystem can answer (the guidance doc exists, the reviewer agent exists).\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 errors.push(...requiredKeyErrors(e, i));\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 ChecklistValidator().validate(repoRoot, defs)];\n}\n\n// The `landPr` block: what happens to the LOCAL branch once its PR is in main. Optional — omitted\n// means \"archive-tag\", which is deliberately the DEFAULT so a consumer gets the branch-accumulation\n// fix without editing config at all. `branchRetentionWhy` (and any other `*Why` sibling) is free-form\n// rationale prose and is tolerated, per the repo's convention for documenting comment-less JSON.\n// webpieces-disable no-any-unknown -- `value` is the opaque consumer `landPr` value until narrowed here\n// webpieces-disable no-function-outside-class -- module-level config validator, matches the rest of this file\nexport function validateLandPrSection(value: unknown): string[] {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n return [`[pr-gate] \"landPr\" must be an object, e.g. { \"branchRetention\": \"${BRANCH_RETENTION_ARCHIVE_TAG}\" }.`];\n }\n // webpieces-disable no-any-unknown -- narrowing the opaque landPr object from consumer JSON\n const s = value as Record<string, unknown>;\n if (!('branchRetention' in s)) return [];\n const retention = s['branchRetention'];\n if (typeof retention === 'string' && BRANCH_RETENTIONS.includes(retention)) return [];\n return [\n `[pr-gate] \"landPr.branchRetention\" = \"${String(retention)}\" is not valid. ` +\n `Must be one of: ${BRANCH_RETENTIONS.join(', ')}.\\n` +\n ` \"${BRANCH_RETENTION_ARCHIVE_TAG}\" — (default) tag the branch tip as archive/<date>/<branch>, THEN delete it. The\\n` +\n ` history stays byte-identical and restorable, but the branch stops counting\\n` +\n ` toward the branch cap and cannot be committed onto by accident.\\n` +\n ` \"${BRANCH_RETENTION_DELETE}\" — delete outright; recoverable only from the reflog, which expires.\\n` +\n ` \"${BRANCH_RETENTION_KEEP}\" — do not delete. Branches then accumulate until branch-creation-guard trips.`,\n ];\n}\n\n// A git ref COMPONENT this flow is willing to build a ref name out of. Deliberately much narrower than\n// git's own check-ref-format: these two values are concatenated into a ref that a command then\n// force-pushes, so anything that could be read as a flag, a path escape, or a glob is rejected outright\n// rather than trusted to `git push` argument order.\nconst REF_COMPONENT = /^[A-Za-z0-9][A-Za-z0-9._/-]*$/;\n\n/**\n * Validate the optional `pr-gate.devDeploy` block — where `wp-push-dev` publishes the disposable copy of\n * a feature branch, and which ref is the shared dev branch itself. Absent ⇒ `dev-include` / `dev`.\n *\n * `devBranch` must NOT contain a slash and must not sit inside `branchNamespace`: the whole point of the\n * namespace is that the composed dev branch is written by CI and the copies are written by developers, so\n * a config where one contains the other makes `--list` enumerate the deploy branch and makes the\n * \"refused as a source branch\" check ambiguous.\n */\n// webpieces-disable no-any-unknown -- `value` is the opaque consumer devDeploy value until narrowed here\n// webpieces-disable no-function-outside-class -- module-level config validator, matches the rest of this file\nexport function validateDevDeploySection(value: unknown): string[] {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n return ['[pr-gate] \"devDeploy\" must be an object, e.g. { \"branchNamespace\": \"dev-include\", \"devBranch\": \"dev\" }.'];\n }\n // webpieces-disable no-any-unknown -- narrowing the opaque devDeploy object from consumer JSON\n const s = value as Record<string, unknown>;\n const errors: string[] = [];\n errors.push(...validateRefComponent(s, 'branchNamespace', 'dev-include'));\n errors.push(...validateRefComponent(s, 'devBranch', 'dev'));\n if (errors.length > 0) return errors;\n\n const namespace = typeof s['branchNamespace'] === 'string' ? s['branchNamespace'].trim() : 'dev-include';\n const devBranch = typeof s['devBranch'] === 'string' ? s['devBranch'].trim() : 'dev';\n if (devBranch.includes('/')) {\n errors.push(\n `[pr-gate] \"devDeploy.devBranch\" = \"${devBranch}\" must be a single ref name with no \"/\" — it is the ` +\n `branch your CI composes and deploys, not a namespace.`);\n }\n if (devBranch === namespace || devBranch.startsWith(`${namespace}/`) || namespace.startsWith(`${devBranch}/`)) {\n errors.push(\n `[pr-gate] \"devDeploy.devBranch\" (\"${devBranch}\") and \"devDeploy.branchNamespace\" (\"${namespace}\") must not ` +\n `contain one another. The namespace holds the DISPOSABLE per-developer copies (written by wp-push-dev); ` +\n `devBranch is the COMPOSED branch your CI rebuilds from origin/main. Overlapping them makes wp-push-dev ` +\n `--list enumerate the deploy branch and makes \"refused as a source branch\" ambiguous.`);\n }\n return errors;\n}\n\n// One `devDeploy` string field: present ⇒ must be a non-empty, ref-safe string. Absent ⇒ the default.\n// webpieces-disable no-any-unknown -- the already-narrowed opaque devDeploy object\n// webpieces-disable no-function-outside-class -- module-level config validator, matches the rest of this file\nfunction validateRefComponent(s: Record<string, unknown>, key: string, example: string): string[] {\n if (!(key in s)) return [];\n const raw = s[key];\n if (typeof raw !== 'string' || raw.trim() === '') {\n return [`[pr-gate] \"devDeploy.${key}\" must be a non-empty string, e.g. \"${example}\". Omit the key for the \"${example}\" default.`];\n }\n if (!REF_COMPONENT.test(raw.trim())) {\n return [\n `[pr-gate] \"devDeploy.${key}\" = \"${raw}\" is not a usable git ref name. Use letters, digits, \".\", \"_\", ` +\n `\"-\" and \"/\" only, starting with a letter or digit (e.g. \"${example}\").`,\n ];\n }\n return [];\n}\n"]}
@@ -18,13 +18,23 @@ export declare class ChecklistResult {
18
18
  problem: string;
19
19
  constructor(id: string, status: string, output: string, override: string, problem?: string);
20
20
  }
21
+ /**
22
+ * What the pr-gate command computed from the diff: a checklist this branch MATCHED (its patterns hit the
23
+ * diff, so its reviewer subagent is in scope). Drives review-<id>.json enforcement, provenance, the schema
24
+ * hint, and the dashboard. Data-only.
25
+ *
26
+ * NAME NOTE: "Required" here means MATCHED, not mandatory — it predates `required` by a long way and is
27
+ * the shared shape across review-json, the detector, the briefing builder, provenance and the dashboard.
28
+ * Whether the reviewer must actually run is the {@link RequiredChecklist.required} field below.
29
+ */
21
30
  export declare class RequiredChecklist {
22
31
  id: string;
23
32
  subagent: string;
24
33
  doc: string;
25
34
  matchedFiles: string[];
26
35
  matchedPatterns: string[];
27
- constructor(id: string, subagent: string, doc: string, matchedFiles: string[], matchedPatterns?: string[]);
36
+ required: boolean;
37
+ constructor(id: string, subagent: string, doc: string, matchedFiles: string[], matchedPatterns?: string[], required?: boolean);
28
38
  }
29
39
  /**
30
40
  * The per-PR facts every reviewer subagent needs GIVEN to it, alongside its own checklist: the exact base
@@ -46,9 +46,15 @@ class ChecklistResult {
46
46
  }
47
47
  }
48
48
  exports.ChecklistResult = ChecklistResult;
49
- // What the pr-gate command computed from the diff: a checklist this branch MATCHED (its patterns hit the
50
- // diff, so its reviewer subagent must run). Drives review-<id>.json enforcement, provenance, the schema
51
- // hint, and the dashboard. Data-only.
49
+ /**
50
+ * What the pr-gate command computed from the diff: a checklist this branch MATCHED (its patterns hit the
51
+ * diff, so its reviewer subagent is in scope). Drives review-<id>.json enforcement, provenance, the schema
52
+ * hint, and the dashboard. Data-only.
53
+ *
54
+ * NAME NOTE: "Required" here means MATCHED, not mandatory — it predates `required` by a long way and is
55
+ * the shared shape across review-json, the detector, the briefing builder, provenance and the dashboard.
56
+ * Whether the reviewer must actually run is the {@link RequiredChecklist.required} field below.
57
+ */
52
58
  class RequiredChecklist {
53
59
  id; // = subagent name; keys review-<id>.json
54
60
  subagent; // reviewer agent that must run (agentType the harness stamps)
@@ -58,13 +64,21 @@ class RequiredChecklist {
58
64
  // match was — a precise `db/migrations/**` hit means something different from a blanket `**` — and the
59
65
  // template tells reviewers that matching IS deliberately coarse. [] = no patterns (matches every PR).
60
66
  matchedPatterns;
67
+ // Straight from the checklist's config `required`. true = blocking; false = the human is offered it and
68
+ // may decline. Carried on the MATCH rather than looked up from config downstream so the set that gates
69
+ // and the set that is reported cannot disagree about which of the two a checklist is.
70
+ required;
61
71
  // eslint-disable-next-line @typescript-eslint/max-params
62
- constructor(id, subagent, doc, matchedFiles, matchedPatterns = []) {
72
+ constructor(id, subagent, doc, matchedFiles, matchedPatterns = [],
73
+ // Defaulted to the BLOCKING value so any construction that forgets it fails closed — a test or a
74
+ // future call site that silently produced an optional checklist would be a hole in the gate.
75
+ required = true) {
63
76
  this.id = id;
64
77
  this.subagent = subagent;
65
78
  this.doc = doc;
66
79
  this.matchedFiles = matchedFiles;
67
80
  this.matchedPatterns = matchedPatterns;
81
+ this.required = required;
68
82
  }
69
83
  }
70
84
  exports.RequiredChecklist = RequiredChecklist;
@@ -1 +1 @@
1
- {"version":3,"file":"review-json-data.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/review-json-data.ts"],"names":[],"mappings":";AAAA;;;;;;;GAOG;;;AAEH,qGAAqG;AACrG,yGAAyG;AACzG,0GAA0G;AAC1G,4EAA4E;AAC/D,QAAA,aAAa,GAAG,OAAO,CAAC;AACxB,QAAA,cAAc,GAAG,QAAQ,CAAC;AAC1B,QAAA,WAAW,GAAG,KAAK,CAAC;AACpB,QAAA,gBAAgB,GAAG,CAAC,qBAAa,EAAE,sBAAc,EAAE,mBAAW,CAAU,CAAC;AAEtF,wGAAwG;AACxG,sGAAsG;AACtG,gCAAgC;AAChC,2CAA2C;AAC3C,4GAA4G;AAC5G,sGAAsG;AACtG,kFAAkF;AAClF,wGAAwG;AACxG,+FAA+F;AAC/F,MAAa,eAAe;IACxB,EAAE,CAAS;IACX,MAAM,CAAS,CAAI,mEAAmE;IACtF,MAAM,CAAS,CAAI,qEAAqE;IACxF,QAAQ,CAAS,CAAE,kFAAkF;IACrG,oGAAoG;IACpG,qGAAqG;IACrG,6GAA6G;IAC7G,4DAA4D;IAC5D,OAAO,CAAS;IAEhB,yDAAyD;IACzD,YAAY,EAAU,EAAE,MAAc,EAAE,MAAc,EAAE,QAAgB,EAAE,OAAO,GAAG,EAAE;QAClF,IAAI,CAAC,EAAE,GAAG,EAAE,CAAC;QACb,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IAC3B,CAAC;CACJ;AAnBD,0CAmBC;AAED,yGAAyG;AACzG,wGAAwG;AACxG,sCAAsC;AACtC,MAAa,iBAAiB;IAC1B,EAAE,CAAS,CAAa,yCAAyC;IACjE,QAAQ,CAAS,CAAO,8DAA8D;IACtF,GAAG,CAAS,CAAY,8EAA8E;IACtG,YAAY,CAAW,CAAC,+DAA+D;IACvF,oGAAoG;IACpG,uGAAuG;IACvG,sGAAsG;IACtG,eAAe,CAAW;IAE1B,yDAAyD;IACzD,YAAY,EAAU,EAAE,QAAgB,EAAE,GAAW,EAAE,YAAsB,EAAE,kBAA4B,EAAE;QACzG,IAAI,CAAC,EAAE,GAAG,EAAE,CAAC;QACb,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAC;QACzB,IAAI,CAAC,GAAG,GAAG,GAAG,CAAC;QACf,IAAI,CAAC,YAAY,GAAG,YAAY,CAAC;QACjC,IAAI,CAAC,eAAe,GAAG,eAAe,CAAC;IAC3C,CAAC;CACJ;AAlBD,8CAkBC;AAED;;;;;GAKG;AACH,MAAa,sBAAsB;IAC/B,OAAO,CAAS,CAAQ,6BAA6B;IACrD,aAAa,CAAS,CAAE,oEAAoE;IAC5F;;;;;OAKG;IACH,eAAe,CAAS;IACxB,OAAO,CAAS,CAAQ,mFAAmF;IAC3G,KAAK,CAAU,CAAS,oFAAoF;IAE5G,yDAAyD;IACzD,YAAY,OAAO,GAAG,EAAE,EAAE,aAAa,GAAG,EAAE,EAAE,eAAe,GAAG,EAAE,EAAE,OAAO,GAAG,EAAE,EAAE,KAAK,GAAG,KAAK;QAC3F,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,aAAa,GAAG,aAAa,CAAC;QACnC,IAAI,CAAC,eAAe,GAAG,eAAe,CAAC;QACvC,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;IACvB,CAAC;CACJ;AArBD,wDAqBC;AAED,wGAAwG;AACxG,4GAA4G;AAC5G,qDAAqD;AACrD,MAAa,UAAU;IACnB,KAAK,CAAS,CAAC,8FAA8F;IAC7G,SAAS,CAAS,CAAC,6BAA6B;IAChD,SAAS,CAAS,CAAC,6BAA6B;IAChD,SAAS,CAAS,CAAC,2DAA2D;IAC9E,OAAO,CAAS,CAAC,4CAA4C;IAC7D,UAAU,CAAW,CAAC,yEAAyE;IAC/F,KAAK,CAAW;IAChB,aAAa,CAAW;IACxB,OAAO,CAAoB,CAAC,wEAAwE;IAEpG,yDAAyD;IACzD,YACI,KAAa,EACb,SAAiB,EACjB,SAAiB,EACjB,SAAiB,EACjB,OAAe,EACf,UAAoB,EACpB,KAAe,EACf,aAAuB,EACvB,UAA6B,EAAE;QAE/B,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;QAC3B,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;QAC3B,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;QAC3B,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;QAC7B,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,IAAI,CAAC,aAAa,GAAG,aAAa,CAAC;QACnC,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;IAC3B,CAAC;CACJ;AAjCD,gCAiCC;AAED,qGAAqG;AACrG,sFAAsF;AACzE,QAAA,OAAO,GAAG,MAAM,CAAC,CAAe,kCAAkC;AAClE,QAAA,OAAO,GAAG,MAAM,CAAC,CAAe,6DAA6D;AAC7F,QAAA,aAAa,GAAG,YAAY,CAAC,CAAG,0DAA0D;AAC1F,QAAA,OAAO,GAAG,MAAM,CAAC,CAAe,uDAAuD;AACvF,QAAA,UAAU,GAAG,SAAS,CAAC,CAAS,uCAAuC;AACvE,QAAA,aAAa,GAAG,YAAY,CAAC,CAAG,iEAAiE;AAE9G,MAAa,gBAAgB;IACzB,EAAE,CAAS;IACX,MAAM,CAAS,CAAC,kFAAkF;IAClG,MAAM,CAAS,CAAC,mFAAmF;IAEnG,YAAY,EAAU,EAAE,MAAc,EAAE,MAAc;QAClD,IAAI,CAAC,EAAE,GAAG,EAAE,CAAC;QACb,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;QACrB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACzB,CAAC;CACJ;AAVD,4CAUC;AAED,4GAA4G;AAC5G,yGAAyG;AACzG,0GAA0G;AAC1G,yGAAyG;AACzG,MAAa,SAAS;IAClB,IAAI,CAAS,CAAU,oDAAoD;IAC3E;;;;OAIG;IACH,IAAI,CAAS;IACb,YAAY,CAAW,CAAC,iFAAiF;IACzG,KAAK,CAAU,CAAS,4DAA4D;IACpF,UAAU,CAAW,CAAG,sEAAsE;IAC9F,WAAW,CAAS,CAAI,iFAAiF;IACzG,OAAO,CAAS,CAAQ,mFAAmF;IAC3G,WAAW,CAAS,CAAI,+EAA+E;IACvG;;;;;;;;OAQG;IACH,YAAY,CAAS;IAErB,yDAAyD;IACzD,YACI,IAAY,EAAE,IAAY,EAAE,YAAsB,EAClD,KAAK,GAAG,KAAK,EAAE,aAAuB,EAAE,EAAE,WAAW,GAAG,EAAE,EAAE,OAAO,GAAG,EAAE,EAAE,WAAW,GAAG,EAAE,EAC1F,YAAY,GAAG,EAAE;QAEjB,IAAI,CAAC,YAAY,GAAG,YAAY,CAAC;QACjC,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,YAAY,GAAG,YAAY,CAAC;QACjC,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;QACnB,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;QAC7B,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC;QAC/B,IAAI,CAAC,OAAO,GAAG,OAAO,CAAC;QACvB,IAAI,CAAC,WAAW,GAAG,WAAW,CAAC;IACnC,CAAC;CACJ;AAzCD,8BAyCC","sourcesContent":["/**\n * The DATA-ONLY classes and status constants of the PR review system — the verdict a reviewer writes, the\n * checklist that demanded it, the resolved outcome, and the diff context handed to reviewers.\n *\n * Split out of review-json.ts, which holds the SERVICE that reads and writes them. The split is purely by\n * kind (data vs behaviour): review-json.ts re-exports every name below, so `from './review-json'` and\n * `from '@webpieces/rules-config'` keep resolving exactly as before and no consumer changes.\n */\n\n// The three colors a reviewer subagent may report in `review-<id>.json`. A TRI-state, not a boolean,\n// because the boolean it replaced gave a reviewer no way to say \"this passes, but a human should look at\n// X\" — the only way to raise a concern was to FAIL the PR and then override your own failure, which reads\n// on the dashboard as a deliberately-accepted defect rather than as a note.\nexport const VERDICT_GREEN = 'green';\nexport const VERDICT_YELLOW = 'yellow';\nexport const VERDICT_RED = 'red';\nexport const VERDICT_STATUSES = [VERDICT_GREEN, VERDICT_YELLOW, VERDICT_RED] as const;\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// status:'green' → PASS\n// status:'yellow' → WARN (passes; the concern is published on the PR, nothing is blocked)\n// status:'red' + override non-empty → OVERRIDDEN (pass; the free-text justification reaches the PR)\n// status:'red' + 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 status: string; // one of VERDICT_STATUSES; anything else is reported via `problem`\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 // '' = a well-formed verdict. Non-empty = the file exists and parses but its verdict cannot be READ\n // (most often: it still uses the removed `success` field). Carried as data rather than thrown so the\n // complaint can be reported by BOTH wp-review-upsert-pr and wp-finish-upsert-pr in identical words, and so a\n // legacy file is never silently mistaken for a missing one.\n problem: string;\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(id: string, status: string, output: string, override: string, problem = '') {\n this.id = id;\n this.status = status;\n this.output = output;\n this.override = override;\n this.problem = problem;\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 just reads the diff)\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\n prContextPath: string; // path of pr-context.json — the AUTHORITATIVE full changed-file set\n /**\n * The exact command that reproduces ONE file's diff, with a `-- <file>` tail — NOT assembled by the\n * caller. This used to be hardcoded as `git diff <baseSha> HEAD -- <file>`, which returns NOTHING on a\n * dirty tree because the changed-file set is computed base→working-tree. See DiffBasis, which derives\n * this string from the same range the file set came from.\n */\n fileDiffCommand: string;\n diffDir: string; // dir of the MATERIALIZED diff (diff/ALL.diff + diff/files/…); '' when not written\n dirty: boolean; // true ⇒ the range includes uncommitted + untracked work, and must be said out loud\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(baseSha = '', prContextPath = '', fileDiffCommand = '', diffDir = '', dirty = false) {\n this.baseSha = baseSha;\n this.prContextPath = prContextPath;\n this.fileDiffCommand = fileDiffCommand;\n this.diffDir = diffDir;\n this.dirty = dirty;\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.\n// PASS, WARN and OVERRIDDEN all ship; FAIL, MISSING and BAD_FORMAT all refuse the PR.\nexport const CK_PASS = 'pass'; // review-<id>.json status:'green'\nexport const CK_WARN = 'warn'; // review-<id>.json status:'yellow' → 🟡 passes WITH concerns\nexport const CK_OVERRIDDEN = 'overridden'; // review-<id>.json status:'red' + non-empty override → 🟠\nexport const CK_FAIL = 'fail'; // review-<id>.json status:'red' + no override → refuse\nexport const CK_MISSING = 'missing'; // no review-<id>.json written → refuse\nexport const CK_BAD_FORMAT = 'bad-format'; // written, but its verdict is unreadable (e.g. legacy `success`)\n\nexport class ChecklistVerdict {\n id: string;\n status: string; // one of CK_PASS | CK_WARN | CK_OVERRIDDEN | CK_FAIL | CK_MISSING | CK_BAD_FORMAT\n detail: string; // reviewer output / override justification / format complaint (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 config) 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 /**\n * The real HEAD sha. This was once the literal string 'HEAD', which is not a fact — it cannot be\n * compared later to detect that the tree moved under a review, and it reads as a range that was never\n * actually diffed. Its only reader (reviewContextFor) takes `base`, so recording the sha is free.\n */\n head: string;\n changedFiles: string[]; // every file changed in the range (NOT tsOnly — includes .sql/.gql/Dockerfile/…)\n dirty: boolean; // true ⇒ changedFiles includes uncommitted + untracked work\n dirtyFiles: string[]; // exactly which paths are uncommitted/untracked — why `dirty` is true\n diffCommand: string; // the command that reproduces the WHOLE diff (see DiffBasis; correct when dirty)\n diffDir: string; // dir holding the materialized per-file diffs + ALL.diff; '' when not materialized\n generatedAt: string; // ISO timestamp, so a stale context is detectable rather than silently trusted\n /**\n * Main's head as this clone last saw it — the THIRD hash point, matching the trio the 3-point merge\n * records in `merge-info/<branch>/updatemain-hashes.json`. `base`/`head` above are points A and B\n * under the review side's older names.\n *\n * The review side used to record only A and B, so nothing could answer \"did main move while this was\n * under review?\" — the question you most want answered when a review looks stale. '' when origin/main\n * is unresolvable. Purely informational; nothing gates on it.\n */\n hashMainHead: string;\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(\n base: string, head: string, changedFiles: string[],\n dirty = false, dirtyFiles: string[] = [], diffCommand = '', diffDir = '', generatedAt = '',\n hashMainHead = '',\n ) {\n this.hashMainHead = hashMainHead;\n this.base = base;\n this.head = head;\n this.changedFiles = changedFiles;\n this.dirty = dirty;\n this.dirtyFiles = dirtyFiles;\n this.diffCommand = diffCommand;\n this.diffDir = diffDir;\n this.generatedAt = generatedAt;\n }\n}\n"]}
1
+ {"version":3,"file":"review-json-data.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/review-json-data.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The DATA-ONLY classes and status constants of the PR review system — the verdict a reviewer writes, the\n * checklist that demanded it, the resolved outcome, and the diff context handed to reviewers.\n *\n * Split out of review-json.ts, which holds the SERVICE that reads and writes them. The split is purely by\n * kind (data vs behaviour): review-json.ts re-exports every name below, so `from './review-json'` and\n * `from '@webpieces/rules-config'` keep resolving exactly as before and no consumer changes.\n */\n\n// The three colors a reviewer subagent may report in `review-<id>.json`. A TRI-state, not a boolean,\n// because the boolean it replaced gave a reviewer no way to say \"this passes, but a human should look at\n// X\" — the only way to raise a concern was to FAIL the PR and then override your own failure, which reads\n// on the dashboard as a deliberately-accepted defect rather than as a note.\nexport const VERDICT_GREEN = 'green';\nexport const VERDICT_YELLOW = 'yellow';\nexport const VERDICT_RED = 'red';\nexport const VERDICT_STATUSES = [VERDICT_GREEN, VERDICT_YELLOW, VERDICT_RED] as const;\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// status:'green' → PASS\n// status:'yellow' → WARN (passes; the concern is published on the PR, nothing is blocked)\n// status:'red' + override non-empty → OVERRIDDEN (pass; the free-text justification reaches the PR)\n// status:'red' + 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 status: string; // one of VERDICT_STATUSES; anything else is reported via `problem`\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 // '' = a well-formed verdict. Non-empty = the file exists and parses but its verdict cannot be READ\n // (most often: it still uses the removed `success` field). Carried as data rather than thrown so the\n // complaint can be reported by BOTH wp-review-upsert-pr and wp-finish-upsert-pr in identical words, and so a\n // legacy file is never silently mistaken for a missing one.\n problem: string;\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(id: string, status: string, output: string, override: string, problem = '') {\n this.id = id;\n this.status = status;\n this.output = output;\n this.override = override;\n this.problem = problem;\n }\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 is in scope). Drives review-<id>.json enforcement, provenance, the schema\n * hint, and the dashboard. Data-only.\n *\n * NAME NOTE: \"Required\" here means MATCHED, not mandatory — it predates `required` by a long way and is\n * the shared shape across review-json, the detector, the briefing builder, provenance and the dashboard.\n * Whether the reviewer must actually run is the {@link RequiredChecklist.required} field below.\n */\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 just reads the diff)\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 // Straight from the checklist's config `required`. true = blocking; false = the human is offered it and\n // may decline. Carried on the MATCH rather than looked up from config downstream so the set that gates\n // and the set that is reported cannot disagree about which of the two a checklist is.\n required: boolean;\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(\n id: string, subagent: string, doc: string, matchedFiles: string[], matchedPatterns: string[] = [],\n // Defaulted to the BLOCKING value so any construction that forgets it fails closed — a test or a\n // future call site that silently produced an optional checklist would be a hole in the gate.\n required = true,\n ) {\n this.id = id;\n this.subagent = subagent;\n this.doc = doc;\n this.matchedFiles = matchedFiles;\n this.matchedPatterns = matchedPatterns;\n this.required = required;\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\n prContextPath: string; // path of pr-context.json — the AUTHORITATIVE full changed-file set\n /**\n * The exact command that reproduces ONE file's diff, with a `-- <file>` tail — NOT assembled by the\n * caller. This used to be hardcoded as `git diff <baseSha> HEAD -- <file>`, which returns NOTHING on a\n * dirty tree because the changed-file set is computed base→working-tree. See DiffBasis, which derives\n * this string from the same range the file set came from.\n */\n fileDiffCommand: string;\n diffDir: string; // dir of the MATERIALIZED diff (diff/ALL.diff + diff/files/…); '' when not written\n dirty: boolean; // true ⇒ the range includes uncommitted + untracked work, and must be said out loud\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(baseSha = '', prContextPath = '', fileDiffCommand = '', diffDir = '', dirty = false) {\n this.baseSha = baseSha;\n this.prContextPath = prContextPath;\n this.fileDiffCommand = fileDiffCommand;\n this.diffDir = diffDir;\n this.dirty = dirty;\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.\n// PASS, WARN and OVERRIDDEN all ship; FAIL, MISSING and BAD_FORMAT all refuse the PR.\nexport const CK_PASS = 'pass'; // review-<id>.json status:'green'\nexport const CK_WARN = 'warn'; // review-<id>.json status:'yellow' → 🟡 passes WITH concerns\nexport const CK_OVERRIDDEN = 'overridden'; // review-<id>.json status:'red' + non-empty override → 🟠\nexport const CK_FAIL = 'fail'; // review-<id>.json status:'red' + no override → refuse\nexport const CK_MISSING = 'missing'; // no review-<id>.json written → refuse\nexport const CK_BAD_FORMAT = 'bad-format'; // written, but its verdict is unreadable (e.g. legacy `success`)\n\nexport class ChecklistVerdict {\n id: string;\n status: string; // one of CK_PASS | CK_WARN | CK_OVERRIDDEN | CK_FAIL | CK_MISSING | CK_BAD_FORMAT\n detail: string; // reviewer output / override justification / format complaint (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 config) 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 /**\n * The real HEAD sha. This was once the literal string 'HEAD', which is not a fact — it cannot be\n * compared later to detect that the tree moved under a review, and it reads as a range that was never\n * actually diffed. Its only reader (reviewContextFor) takes `base`, so recording the sha is free.\n */\n head: string;\n changedFiles: string[]; // every file changed in the range (NOT tsOnly — includes .sql/.gql/Dockerfile/…)\n dirty: boolean; // true ⇒ changedFiles includes uncommitted + untracked work\n dirtyFiles: string[]; // exactly which paths are uncommitted/untracked — why `dirty` is true\n diffCommand: string; // the command that reproduces the WHOLE diff (see DiffBasis; correct when dirty)\n diffDir: string; // dir holding the materialized per-file diffs + ALL.diff; '' when not materialized\n generatedAt: string; // ISO timestamp, so a stale context is detectable rather than silently trusted\n /**\n * Main's head as this clone last saw it — the THIRD hash point, matching the trio the 3-point merge\n * records in `merge-info/<branch>/updatemain-hashes.json`. `base`/`head` above are points A and B\n * under the review side's older names.\n *\n * The review side used to record only A and B, so nothing could answer \"did main move while this was\n * under review?\" — the question you most want answered when a review looks stale. '' when origin/main\n * is unresolvable. Purely informational; nothing gates on it.\n */\n hashMainHead: string;\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(\n base: string, head: string, changedFiles: string[],\n dirty = false, dirtyFiles: string[] = [], diffCommand = '', diffDir = '', generatedAt = '',\n hashMainHead = '',\n ) {\n this.hashMainHead = hashMainHead;\n this.base = base;\n this.head = head;\n this.changedFiles = changedFiles;\n this.dirty = dirty;\n this.dirtyFiles = dirtyFiles;\n this.diffCommand = diffCommand;\n this.diffDir = diffDir;\n this.generatedAt = generatedAt;\n }\n}\n"]}
@@ -106,6 +106,19 @@ export declare class ReviewJsonService {
106
106
  * second run and reads as though the earlier verdict did not count.
107
107
  */
108
108
  pendingChecklists(required: readonly RequiredChecklist[], results: readonly ChecklistResult[]): RequiredChecklist[];
109
+ /**
110
+ * The OPTIONAL checklists (`required: false`) that matched the diff but have no verdict file at all.
111
+ *
112
+ * This is the ONE set that separates "nobody ran it" from "it failed", and the distinction is the whole
113
+ * feature: an optional checklist with no verdict was legitimately not run — declined by the human, or
114
+ * skipped via `--no-optional` — so it must NOT block. An optional checklist with a RED verdict is not in
115
+ * here (it resolves to CK_FAIL) and blocks exactly like a required one: choosing to run a reviewer and
116
+ * then ignoring its answer would make the whole thing theater.
117
+ *
118
+ * A strict subset of {@link pendingChecklists}, computed here rather than at each call site so the
119
+ * command that gates and the dashboard that reports cannot disagree about which checklists were skipped.
120
+ */
121
+ optionalWithoutVerdict(required: readonly RequiredChecklist[], results: readonly ChecklistResult[]): RequiredChecklist[];
109
122
  /**
110
123
  * The checklists that REFUSED: a reviewer ran, judged the change, and said no (CK_FAIL — status red with
111
124
  * no override). A strict subset of {@link pendingChecklists}, split out because it demands a completely
@@ -322,6 +322,21 @@ let ReviewJsonService = class ReviewJsonService {
322
322
  return status !== review_json_data_1.CK_PASS && status !== review_json_data_1.CK_WARN && status !== review_json_data_1.CK_OVERRIDDEN;
323
323
  });
324
324
  }
325
+ /**
326
+ * The OPTIONAL checklists (`required: false`) that matched the diff but have no verdict file at all.
327
+ *
328
+ * This is the ONE set that separates "nobody ran it" from "it failed", and the distinction is the whole
329
+ * feature: an optional checklist with no verdict was legitimately not run — declined by the human, or
330
+ * skipped via `--no-optional` — so it must NOT block. An optional checklist with a RED verdict is not in
331
+ * here (it resolves to CK_FAIL) and blocks exactly like a required one: choosing to run a reviewer and
332
+ * then ignoring its answer would make the whole thing theater.
333
+ *
334
+ * A strict subset of {@link pendingChecklists}, computed here rather than at each call site so the
335
+ * command that gates and the dashboard that reports cannot disagree about which checklists were skipped.
336
+ */
337
+ optionalWithoutVerdict(required, results) {
338
+ return required.filter((req) => !req.required && this.resolveVerdict(req, results).status === review_json_data_1.CK_MISSING);
339
+ }
325
340
  /**
326
341
  * The checklists that REFUSED: a reviewer ran, judged the change, and said no (CK_FAIL — status red with
327
342
  * no override). A strict subset of {@link pendingChecklists}, split out because it demands a completely
@@ -436,6 +451,12 @@ let ReviewJsonService = class ReviewJsonService {
436
451
  errors.push(this.refusalError(req, verdict));
437
452
  }
438
453
  else if (verdict.status === review_json_data_1.CK_MISSING) {
454
+ // An OPTIONAL checklist with no verdict was legitimately not run — the human was offered it
455
+ // and declined (or `--no-optional` skipped the offer). Demanding it here would make
456
+ // `required: false` mean nothing. Note the CK_FAIL branch above deliberately has no such
457
+ // exemption: once an optional reviewer RUNS, its refusal counts.
458
+ if (!req.required)
459
+ continue;
439
460
  const doc = req.doc.trim() !== '' ? ` Read: ${req.doc}.` : '';
440
461
  errors.push(`Checklist "${req.id}" MATCHED this diff but has no verdict. Spawn the "${req.subagent}" subagent to review it, ` +
441
462
  `then write ${this.checklistFileName(req.id)} with ` +