@webpieces/rules-config 0.4.732 → 0.4.734

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.
@@ -12,6 +12,7 @@ const atomic_file_1 = require("./atomic-file");
12
12
  const instruct_ai_docs_1 = require("./instruct-ai-docs");
13
13
  const repo_root_1 = require("./repo-root");
14
14
  const state_dir_1 = require("./state-dir");
15
+ const stale_bin_sweep_1 = require("./stale-bin-sweep");
15
16
  const TEMPLATES_DIR = path.join(__dirname, '..', 'templates');
16
17
  // Sentinel for "use the resolved LOCAL instruct-ai dir". Kept as the parameter default so the handful
17
18
  // of callers that pass an explicit relative dir (they join it onto workspaceRoot themselves) are
@@ -25,10 +26,15 @@ let TemplateWriter = class TemplateWriter {
25
26
  dotDir;
26
27
  atomicFile;
27
28
  docs;
28
- constructor(dotDir = state_dir_1.dotWebpieces, atomicFile = new atomic_file_1.AtomicFile(), docs = new instruct_ai_docs_1.InstructAiDocSet()) {
29
+ staleBins;
30
+ constructor(dotDir = state_dir_1.dotWebpieces, atomicFile = new atomic_file_1.AtomicFile(), docs = new instruct_ai_docs_1.InstructAiDocSet(),
31
+ // The SHARED sweeper instance, never a fresh one: its "already swept this root" memo is per
32
+ // instance, and that memo is what keeps the report to once per `wp-*` command.
33
+ staleBins = stale_bin_sweep_1.staleBinSweeper) {
29
34
  this.dotDir = dotDir;
30
35
  this.atomicFile = atomicFile;
31
36
  this.docs = docs;
37
+ this.staleBins = staleBins;
32
38
  }
33
39
  loadTemplate(name) {
34
40
  return fs.readFileSync(path.join(TEMPLATES_DIR, name), 'utf-8');
@@ -63,6 +69,10 @@ let TemplateWriter = class TemplateWriter {
63
69
  * overwhelmingly common case (same package version ⇒ identical content) does not write at all.
64
70
  */
65
71
  writeTemplate(workspaceRoot, name, instructDir = DEFAULT_INSTRUCT_DIR) {
72
+ // THE SELF-HEAL. This is the pass every `wp-*` command takes, which makes it the only place a
73
+ // RELEASED sweep reaches every clone on every developer's machine — see stale-bin-sweep.ts. Silent
74
+ // when there is nothing to remove, and once per root per process.
75
+ this.sweepStaleBins(workspaceRoot);
66
76
  for (const doc of this.docs.closure(name, (docName) => this.loadTemplate(docName))) {
67
77
  const target = this.destination(workspaceRoot, doc.name, instructDir);
68
78
  // A doc stamped with live run state is SEEDED, never refreshed: `wp-finish-upsert-pr` runs
@@ -74,6 +84,15 @@ let TemplateWriter = class TemplateWriter {
74
84
  }
75
85
  return this.destination(workspaceRoot, name, instructDir);
76
86
  }
87
+ /**
88
+ * Remove this tree's dangling `wp-*` bin symlinks and SAY what went, on stdout beside everything else
89
+ * a `wp-*` command prints. Nothing removed ⇒ nothing printed, which is the overwhelmingly common case.
90
+ */
91
+ sweepStaleBins(workspaceRoot) {
92
+ const removed = this.staleBins.sweepOnce(workspaceRoot);
93
+ for (const line of this.staleBins.report(removed))
94
+ process.stdout.write(line + '\n');
95
+ }
77
96
  // LOCAL `.webpieces/instruct-ai/<name>` by default; an explicitly-passed relative dir is still
78
97
  // joined onto workspaceRoot exactly as before.
79
98
  destination(workspaceRoot, name, instructDir) {
@@ -88,7 +107,8 @@ exports.TemplateWriter = TemplateWriter = tslib_1.__decorate([
88
107
  (0, inversify_1.injectable)(inversify_1.bindingScopeValues.Singleton),
89
108
  tslib_1.__metadata("design:paramtypes", [state_dir_1.DotWebpieces,
90
109
  atomic_file_1.AtomicFile,
91
- instruct_ai_docs_1.InstructAiDocSet])
110
+ instruct_ai_docs_1.InstructAiDocSet,
111
+ stale_bin_sweep_1.StaleBinSweeper])
92
112
  ], TemplateWriter);
93
113
  // Temporary migration delegators — consumers migrate to injecting TemplateWriter over follow-up PRs.
94
114
  const templateWriterSvc = new TemplateWriter();
@@ -1 +1 @@
1
- {"version":3,"file":"load-template.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/load-template.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\nimport { injectable, bindingScopeValues } from 'inversify';\n\nimport { AtomicFile } from './atomic-file';\nimport { InstructAiDocSet } from './instruct-ai-docs';\nimport { INSTRUCT_AI_LEAF } from './repo-root';\nimport { DotWebpieces, dotWebpieces } from './state-dir';\n\nconst TEMPLATES_DIR = path.join(__dirname, '..', 'templates');\n// Sentinel for \"use the resolved LOCAL instruct-ai dir\". Kept as the parameter default so the handful\n// of callers that pass an explicit relative dir (they join it onto workspaceRoot themselves) are\n// unaffected; anything passing the default gets DotWebpieces.local() resolution.\nconst DEFAULT_INSTRUCT_DIR = '';\n\n/**\n * Writes the AI-facing instruct-ai template docs under `<workspaceRoot>/.webpieces/instruct-ai/`.\n * `@injectable(bindingScopeValues.Singleton)` so it can be injected and appear in the rules-config DI design.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class TemplateWriter {\n constructor(\n private readonly dotDir: DotWebpieces = dotWebpieces,\n private readonly atomicFile: AtomicFile = new AtomicFile(),\n private readonly docs: InstructAiDocSet = new InstructAiDocSet(),\n ) {}\n\n loadTemplate(name: string): string {\n return fs.readFileSync(path.join(TEMPLATES_DIR, name), 'utf-8');\n }\n\n /**\n * SEED `name` and everything it links to — writing only the ones that are not already on disk.\n *\n * Same closure as `writeTemplate`, for the same reason: a rule that drops\n * `webpieces.exceptions.md` next to its violation is delivering a doc a reader follows links out\n * of, and a seeded doc that later gains a sibling link would otherwise dangle exactly the way\n * git-workflow.md's link to the merge process did. The difference from `writeTemplate` is only\n * WHETHER an existing file is refreshed, never WHICH files are considered.\n */\n writeTemplateIfMissing(workspaceRoot: string, name: string, instructDir: string = DEFAULT_INSTRUCT_DIR): void {\n for (const doc of this.docs.closure(name, (docName: string): string => this.loadTemplate(docName))) {\n const target = this.destination(workspaceRoot, doc.name, instructDir);\n if (fs.existsSync(target)) continue;\n this.atomicFile.writeAtomic(target, doc.render(this.loadTemplate(doc.name), workspaceRoot));\n }\n }\n\n /**\n * Write `name` AND every instruct-ai doc it links to, ATOMICALLY and only where bytes changed.\n * Returns the absolute path of `name` itself.\n *\n * THE CLOSURE IS THE POINT. Callers name the ONE doc their command is about; the docs a reader is\n * sent on to arrive with it, because a doc whose links dangle is worse than no doc — it teaches the\n * reader that the paths in these files cannot be trusted. See instruct-ai-docs.ts for the incident.\n *\n * Every `wp-*` command regenerates these, and the AI is routinely told to open one by absolute\n * path. A plain truncating write means a reader can catch it empty; skip-if-unchanged means the\n * overwhelmingly common case (same package version ⇒ identical content) does not write at all.\n */\n writeTemplate(workspaceRoot: string, name: string, instructDir: string = DEFAULT_INSTRUCT_DIR): string {\n for (const doc of this.docs.closure(name, (docName: string): string => this.loadTemplate(docName))) {\n const target = this.destination(workspaceRoot, doc.name, instructDir);\n // A doc stamped with live run state is SEEDED, never refreshed: `wp-finish-upsert-pr` runs\n // while a conflicted merge is still open, and clobbering that handback with the reference\n // copy would delete the file list the agent is working from.\n if (doc.seedOnly() && fs.existsSync(target)) continue;\n this.atomicFile.writeIfChanged(target, doc.render(this.loadTemplate(doc.name), workspaceRoot));\n }\n return this.destination(workspaceRoot, name, instructDir);\n }\n\n // LOCAL `.webpieces/instruct-ai/<name>` by default; an explicitly-passed relative dir is still\n // joined onto workspaceRoot exactly as before.\n private destination(workspaceRoot: string, name: string, instructDir: string): string {\n if (instructDir === DEFAULT_INSTRUCT_DIR) {\n return this.dotDir.localFile(workspaceRoot, INSTRUCT_AI_LEAF, name);\n }\n return path.join(workspaceRoot, instructDir, name);\n }\n}\n\n// Temporary migration delegators — consumers migrate to injecting TemplateWriter over follow-up PRs.\nconst templateWriterSvc = new TemplateWriter();\n\nexport function loadTemplate(name: string): string {\n return templateWriterSvc.loadTemplate(name);\n}\n\nexport function writeTemplateIfMissing(\n workspaceRoot: string,\n name: string,\n instructDir: string = DEFAULT_INSTRUCT_DIR,\n): void {\n templateWriterSvc.writeTemplateIfMissing(workspaceRoot, name, instructDir);\n}\n\nexport function writeTemplate(\n workspaceRoot: string,\n name: string,\n instructDir: string = DEFAULT_INSTRUCT_DIR,\n): string {\n return templateWriterSvc.writeTemplate(workspaceRoot, name, instructDir);\n}\n"]}
1
+ {"version":3,"file":"load-template.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/load-template.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\nimport { injectable, bindingScopeValues } from 'inversify';\n\nimport { AtomicFile } from './atomic-file';\nimport { InstructAiDocSet } from './instruct-ai-docs';\nimport { INSTRUCT_AI_LEAF } from './repo-root';\nimport { DotWebpieces, dotWebpieces } from './state-dir';\nimport { StaleBinSweeper, staleBinSweeper } from './stale-bin-sweep';\n\nconst TEMPLATES_DIR = path.join(__dirname, '..', 'templates');\n// Sentinel for \"use the resolved LOCAL instruct-ai dir\". Kept as the parameter default so the handful\n// of callers that pass an explicit relative dir (they join it onto workspaceRoot themselves) are\n// unaffected; anything passing the default gets DotWebpieces.local() resolution.\nconst DEFAULT_INSTRUCT_DIR = '';\n\n/**\n * Writes the AI-facing instruct-ai template docs under `<workspaceRoot>/.webpieces/instruct-ai/`.\n * `@injectable(bindingScopeValues.Singleton)` so it can be injected and appear in the rules-config DI design.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class TemplateWriter {\n constructor(\n private readonly dotDir: DotWebpieces = dotWebpieces,\n private readonly atomicFile: AtomicFile = new AtomicFile(),\n private readonly docs: InstructAiDocSet = new InstructAiDocSet(),\n // The SHARED sweeper instance, never a fresh one: its \"already swept this root\" memo is per\n // instance, and that memo is what keeps the report to once per `wp-*` command.\n private readonly staleBins: StaleBinSweeper = staleBinSweeper,\n ) {}\n\n loadTemplate(name: string): string {\n return fs.readFileSync(path.join(TEMPLATES_DIR, name), 'utf-8');\n }\n\n /**\n * SEED `name` and everything it links to — writing only the ones that are not already on disk.\n *\n * Same closure as `writeTemplate`, for the same reason: a rule that drops\n * `webpieces.exceptions.md` next to its violation is delivering a doc a reader follows links out\n * of, and a seeded doc that later gains a sibling link would otherwise dangle exactly the way\n * git-workflow.md's link to the merge process did. The difference from `writeTemplate` is only\n * WHETHER an existing file is refreshed, never WHICH files are considered.\n */\n writeTemplateIfMissing(workspaceRoot: string, name: string, instructDir: string = DEFAULT_INSTRUCT_DIR): void {\n for (const doc of this.docs.closure(name, (docName: string): string => this.loadTemplate(docName))) {\n const target = this.destination(workspaceRoot, doc.name, instructDir);\n if (fs.existsSync(target)) continue;\n this.atomicFile.writeAtomic(target, doc.render(this.loadTemplate(doc.name), workspaceRoot));\n }\n }\n\n /**\n * Write `name` AND every instruct-ai doc it links to, ATOMICALLY and only where bytes changed.\n * Returns the absolute path of `name` itself.\n *\n * THE CLOSURE IS THE POINT. Callers name the ONE doc their command is about; the docs a reader is\n * sent on to arrive with it, because a doc whose links dangle is worse than no doc — it teaches the\n * reader that the paths in these files cannot be trusted. See instruct-ai-docs.ts for the incident.\n *\n * Every `wp-*` command regenerates these, and the AI is routinely told to open one by absolute\n * path. A plain truncating write means a reader can catch it empty; skip-if-unchanged means the\n * overwhelmingly common case (same package version ⇒ identical content) does not write at all.\n */\n writeTemplate(workspaceRoot: string, name: string, instructDir: string = DEFAULT_INSTRUCT_DIR): string {\n // THE SELF-HEAL. This is the pass every `wp-*` command takes, which makes it the only place a\n // RELEASED sweep reaches every clone on every developer's machine — see stale-bin-sweep.ts. Silent\n // when there is nothing to remove, and once per root per process.\n this.sweepStaleBins(workspaceRoot);\n for (const doc of this.docs.closure(name, (docName: string): string => this.loadTemplate(docName))) {\n const target = this.destination(workspaceRoot, doc.name, instructDir);\n // A doc stamped with live run state is SEEDED, never refreshed: `wp-finish-upsert-pr` runs\n // while a conflicted merge is still open, and clobbering that handback with the reference\n // copy would delete the file list the agent is working from.\n if (doc.seedOnly() && fs.existsSync(target)) continue;\n this.atomicFile.writeIfChanged(target, doc.render(this.loadTemplate(doc.name), workspaceRoot));\n }\n return this.destination(workspaceRoot, name, instructDir);\n }\n\n /**\n * Remove this tree's dangling `wp-*` bin symlinks and SAY what went, on stdout beside everything else\n * a `wp-*` command prints. Nothing removed ⇒ nothing printed, which is the overwhelmingly common case.\n */\n private sweepStaleBins(workspaceRoot: string): void {\n const removed = this.staleBins.sweepOnce(workspaceRoot);\n for (const line of this.staleBins.report(removed)) process.stdout.write(line + '\\n');\n }\n\n // LOCAL `.webpieces/instruct-ai/<name>` by default; an explicitly-passed relative dir is still\n // joined onto workspaceRoot exactly as before.\n private destination(workspaceRoot: string, name: string, instructDir: string): string {\n if (instructDir === DEFAULT_INSTRUCT_DIR) {\n return this.dotDir.localFile(workspaceRoot, INSTRUCT_AI_LEAF, name);\n }\n return path.join(workspaceRoot, instructDir, name);\n }\n}\n\n// Temporary migration delegators — consumers migrate to injecting TemplateWriter over follow-up PRs.\nconst templateWriterSvc = new TemplateWriter();\n\nexport function loadTemplate(name: string): string {\n return templateWriterSvc.loadTemplate(name);\n}\n\nexport function writeTemplateIfMissing(\n workspaceRoot: string,\n name: string,\n instructDir: string = DEFAULT_INSTRUCT_DIR,\n): void {\n templateWriterSvc.writeTemplateIfMissing(workspaceRoot, name, instructDir);\n}\n\nexport function writeTemplate(\n workspaceRoot: string,\n name: string,\n instructDir: string = DEFAULT_INSTRUCT_DIR,\n): string {\n return templateWriterSvc.writeTemplate(workspaceRoot, name, instructDir);\n}\n"]}
@@ -6,6 +6,7 @@
6
6
  * kind (data vs behaviour): review-json.ts re-exports every name below, so `from './review-json'` and
7
7
  * `from '@webpieces/rules-config'` keep resolving exactly as before and no consumer changes.
8
8
  */
9
+ import { ChecklistOverride } from './checklist-override';
9
10
  export declare const VERDICT_GREEN = "green";
10
11
  export declare const VERDICT_YELLOW = "yellow";
11
12
  export declare const VERDICT_RED = "red";
@@ -14,9 +15,9 @@ export declare class ChecklistResult {
14
15
  id: string;
15
16
  status: string;
16
17
  output: string;
17
- override: string;
18
+ override: ChecklistOverride | null;
18
19
  problem: string;
19
- constructor(id: string, status: string, output: string, override: string, problem?: string);
20
+ constructor(id: string, status: string, output: string, override: ChecklistOverride | null, problem?: string);
20
21
  }
21
22
  /**
22
23
  * What the pr-gate command computed from the diff: a checklist this branch MATCHED (its patterns hit the
@@ -20,21 +20,29 @@ exports.VERDICT_STATUSES = [exports.VERDICT_GREEN, exports.VERDICT_YELLOW, expor
20
20
  // The verdict a reviewer SUBAGENT writes into `.webpieces/pr-review/<featureSlug>/review-<id>.json`, one per
21
21
  // matched checklist. One file per checklist so N concurrent reviewer subagents never clobber a shared
22
22
  // file. It records the OUTCOME:
23
- // status:'green' → PASS
24
- // status:'yellow' → WARN (passes; the concern is published on the PR, nothing is blocked)
25
- // status:'red' + override non-empty → OVERRIDDEN (pass; the free-text justification reaches the PR)
26
- // status:'red' + no override → FAIL (refuse; `output` is printed verbatim)
27
- // `override` is deliberately free text, not a boolean — it forces the ship-anyway decision to be stated
28
- // in words and surfaces it on the dashboard, where a human sees it. Data-only (per CLAUDE.md).
23
+ // status:'green' → PASS
24
+ // status:'yellow' → WARN (passes; the concern is published on the PR, nothing is blocked)
25
+ // status:'red' + an override-<id>.json → OVERRIDDEN (pass; the human's stated reason reaches the PR)
26
+ // status:'red' + no override file → FAIL (refuse; `output` is printed verbatim)
27
+ //
28
+ // THE `override` FIELD IS GONE FROM THIS FILE, and there is no compatibility mode. The ship-anyway
29
+ // justification used to live here as free text, which made the ONE participant who hears the human — the
30
+ // coordinating agent — the one participant that could not record it, because editing a reviewer's verdict
31
+ // file is (correctly) refused by the harness. It now lives in its own `override-<id>.json`; see
32
+ // ChecklistOverride. A verdict file still carrying an `override` key is reported through `problem` with the
33
+ // destination named, exactly as the removed `success` field is. Data-only (per CLAUDE.md).
29
34
  class ChecklistResult {
30
35
  id;
31
36
  status; // one of VERDICT_STATUSES; anything else is reported via `problem`
32
37
  output; // what the reviewer found; printed verbatim when the checklist fails
33
- override; // '' = no override; non-empty = ship-anyway justification (renders 🟠 overridden)
38
+ // The HUMAN's authorization loaded from `override-<id>.json` beside this verdict, or null when there is
39
+ // none. Loaded alongside the verdict so `resolveVerdict` needs no second read of disk and every command
40
+ // resolves the same outcome from the same bytes.
41
+ override;
34
42
  // '' = a well-formed verdict. Non-empty = the file exists and parses but its verdict cannot be READ
35
- // (most often: it still uses the removed `success` field). Carried as data rather than thrown so the
36
- // complaint can be reported by BOTH wp-review-upsert-pr and wp-finish-upsert-pr in identical words, and so a
37
- // legacy file is never silently mistaken for a missing one.
43
+ // (most often: it still uses the removed `success` field, or the moved `override` field). Carried as
44
+ // data rather than thrown so the complaint can be reported by BOTH wp-review-upsert-pr and
45
+ // wp-finish-upsert-pr in identical words, and so a legacy file is never silently mistaken for a missing one.
38
46
  problem;
39
47
  // eslint-disable-next-line @typescript-eslint/max-params
40
48
  constructor(id, status, output, override, problem = '') {
@@ -141,7 +149,7 @@ exports.ReviewJson = ReviewJson;
141
149
  // PASS, WARN and OVERRIDDEN all ship; FAIL, MISSING and BAD_FORMAT all refuse the PR.
142
150
  exports.CK_PASS = 'pass'; // review-<id>.json status:'green'
143
151
  exports.CK_WARN = 'warn'; // review-<id>.json status:'yellow' → 🟡 passes WITH concerns
144
- exports.CK_OVERRIDDEN = 'overridden'; // review-<id>.json status:'red' + non-empty override → 🟠
152
+ exports.CK_OVERRIDDEN = 'overridden'; // status:'red' + a human's override-<id>.json → 🟠
145
153
  exports.CK_FAIL = 'fail'; // review-<id>.json status:'red' + no override → refuse
146
154
  exports.CK_MISSING = 'missing'; // no review-<id>.json written → refuse
147
155
  exports.CK_BAD_FORMAT = 'bad-format'; // written, but its verdict is unreadable (e.g. legacy `success`)
@@ -1 +1 @@
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/<featureSlug>/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/<featureSlug>/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\nimport { ChecklistOverride } from './checklist-override';\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/<featureSlug>/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' + an override-<id>.json → OVERRIDDEN (pass; the human's stated reason reaches the PR)\n// status:'red' + no override file → FAIL (refuse; `output` is printed verbatim)\n//\n// THE `override` FIELD IS GONE FROM THIS FILE, and there is no compatibility mode. The ship-anyway\n// justification used to live here as free text, which made the ONE participant who hears the human — the\n// coordinating agent — the one participant that could not record it, because editing a reviewer's verdict\n// file is (correctly) refused by the harness. It now lives in its own `override-<id>.json`; see\n// ChecklistOverride. A verdict file still carrying an `override` key is reported through `problem` with the\n// destination named, exactly as the removed `success` field is. 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 // The HUMAN's authorization loaded from `override-<id>.json` beside this verdict, or null when there is\n // none. Loaded alongside the verdict so `resolveVerdict` needs no second read of disk and every command\n // resolves the same outcome from the same bytes.\n override: ChecklistOverride | null;\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, or the moved `override` field). Carried as\n // data rather than thrown so the complaint can be reported by BOTH wp-review-upsert-pr and\n // wp-finish-upsert-pr in identical words, and so a 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: ChecklistOverride | null, 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'; // status:'red' + a human's override-<id>.json → 🟠\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/<featureSlug>/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,10 +1,13 @@
1
1
  import { DotWebpieces } from './state-dir';
2
+ import { ChecklistOverride, ChecklistOverrideService, checklistOverrideService } from './checklist-override';
2
3
  import { VERDICT_GREEN, VERDICT_YELLOW, VERDICT_RED, VERDICT_STATUSES, ChecklistResult, RequiredChecklist, ChecklistReviewContext, ReviewJson, CK_PASS, CK_WARN, CK_OVERRIDDEN, CK_FAIL, CK_MISSING, CK_BAD_FORMAT, ChecklistVerdict, PrContext } from './review-json-data';
4
+ export { ChecklistOverride, ChecklistOverrideService, checklistOverrideService };
3
5
  export { VERDICT_GREEN, VERDICT_YELLOW, VERDICT_RED, VERDICT_STATUSES, ChecklistResult, RequiredChecklist, ChecklistReviewContext, ReviewJson, CK_PASS, CK_WARN, CK_OVERRIDDEN, CK_FAIL, CK_MISSING, CK_BAD_FORMAT, ChecklistVerdict, PrContext, };
4
6
  /** Locates + loads/validates the AI-authored review.json. `@injectable(bindingScopeValues.Singleton)` so it's drawn in the design. */
5
7
  export declare class ReviewJsonService {
6
8
  private readonly dotDir;
7
- constructor(dotDir?: DotWebpieces);
9
+ private readonly overrides;
10
+ constructor(dotDir?: DotWebpieces, overrides?: ChecklistOverrideService);
8
11
  prDirFor(repoRoot: string, featureName: string): string;
9
12
  reviewJsonPath(repoRoot: string, featureName: string): string;
10
13
  prContextPath(repoRoot: string, featureName: string): string;
@@ -140,13 +143,26 @@ export declare class ReviewJsonService {
140
143
  * It always quotes the reviewer's own `output` verbatim: the finding is the whole point, and an error
141
144
  * that names a checklist without saying what it objected to gives the reader nothing to fix.
142
145
  *
143
- * `archivedPath` non-empty the verdict has just been RETIRED (moved) to that path, so the message must
144
- * change in two ways. It says where the record went otherwise the move reads as data loss — and,
145
- * critically, it must NOT tell the reader to "set override in review-<id>.json", because that file no
146
- * longer exists. The escape hatch is therefore worded as writing a FRESH verdict file (the body can be
147
- * copied back out of the archive) with a human-authored override.
146
+ * IT NAMES THE WRITER, which is the half that was missing. The old text said WHAT to write ("set a
147
+ * non-empty override") and WHERE, but never WHO MAYso the only reachable move was an agent editing a
148
+ * reviewer's verdict file in place, which the harness denies, which is how a human ended up hand-editing
149
+ * JSON. The ship-anyway route is now a SEPARATE file the coordinating agent may write, and the command
150
+ * that writes it is printed ready to run. See {@link ChecklistOverrideService.writerRule}.
151
+ *
152
+ * `archivedPath` non-empty ⇒ the verdict has just been RETIRED (moved) to that path, so the message says
153
+ * where the record went — otherwise the move reads as data loss — and that a FRESH verdict is required.
154
+ * The AUTHORIZATION is unaffected by that move: `override-<id>.json` is a different file, it survives the
155
+ * retirement, and a human who already decided to accept this checklist is never asked again.
156
+ */
157
+ refusalError(req: RequiredChecklist, verdict: ChecklistVerdict, reviewJsonFilePath: string, archivedPath?: string): string;
158
+ /**
159
+ * The ship-anyway paragraph: who may authorize, and the exact command that records it.
160
+ *
161
+ * Its own method because every refusal surface must say the identical thing about who may write an
162
+ * override. A second copy of this paragraph is precisely how the previous one drifted into naming a
163
+ * command that had since been deleted.
148
164
  */
149
- refusalError(req: RequiredChecklist, verdict: ChecklistVerdict, archivedPath?: string): string;
165
+ private overrideRoute;
150
166
  loadChecklistResults(reviewJsonFilePath: string, required: readonly RequiredChecklist[]): ChecklistResult[];
151
167
  resolveVerdict(req: RequiredChecklist, results: readonly ChecklistResult[]): ChecklistVerdict;
152
168
  /**
@@ -184,12 +200,18 @@ export declare class ReviewJsonService {
184
200
  */
185
201
  private parseChecklistResult;
186
202
  /**
187
- * '' when `status` is one of the three colors. Otherwise the complaint to show the AI verbatim. The
188
- * legacy-`success` case gets its OWN message: `success` was removed outright (no compatibility mode),
189
- * and a reviewer told only "status must be green|yellow|red" cannot tell whether it wrote the wrong
190
- * value or is using a field that no longer exists.
203
+ * '' when the verdict can be READ. Otherwise the complaint to show the AI verbatim.
204
+ *
205
+ * The MOVED `override` field is checked FIRST, ahead of `status`, because it is the more specific fact: a
206
+ * file carrying it was written against a schema that no longer exists, and a reader told only "status
207
+ * must be green|yellow|red" would fix the wrong thing. It is rejected even when EMPTY — an accepted shape
208
+ * is never migrated, and `"override": ""` sitting in a reviewer's file is the copy that teaches the next
209
+ * reviewer the field still exists.
210
+ *
211
+ * The legacy-`success` case keeps its OWN message for the same reason: `success` was removed outright
212
+ * (no compatibility mode), and a reviewer cannot tell a wrong value from a field that no longer exists.
191
213
  */
192
- private statusProblem;
214
+ private verdictProblem;
193
215
  private asStringArray;
194
216
  private parseReviewJson;
195
217
  }
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ReviewJsonService = exports.PrContext = exports.ChecklistVerdict = exports.CK_BAD_FORMAT = exports.CK_MISSING = exports.CK_FAIL = exports.CK_OVERRIDDEN = exports.CK_WARN = exports.CK_PASS = exports.ReviewJson = exports.ChecklistReviewContext = exports.RequiredChecklist = exports.ChecklistResult = exports.VERDICT_STATUSES = exports.VERDICT_RED = exports.VERDICT_YELLOW = exports.VERDICT_GREEN = void 0;
3
+ exports.ReviewJsonService = exports.PrContext = exports.ChecklistVerdict = exports.CK_BAD_FORMAT = exports.CK_MISSING = exports.CK_FAIL = exports.CK_OVERRIDDEN = exports.CK_WARN = exports.CK_PASS = exports.ReviewJson = exports.ChecklistReviewContext = exports.RequiredChecklist = exports.ChecklistResult = exports.VERDICT_STATUSES = exports.VERDICT_RED = exports.VERDICT_YELLOW = exports.VERDICT_GREEN = exports.checklistOverrideService = exports.ChecklistOverrideService = exports.ChecklistOverride = void 0;
4
4
  exports.prDirFor = prDirFor;
5
5
  exports.reviewJsonPath = reviewJsonPath;
6
6
  exports.reviewJsonSchemaHint = reviewJsonSchemaHint;
@@ -12,6 +12,10 @@ const constants_1 = require("./constants");
12
12
  const state_dir_1 = require("./state-dir");
13
13
  const inform_ai_error_1 = require("./inform-ai-error");
14
14
  const to_error_1 = require("./to-error");
15
+ const checklist_override_1 = require("./checklist-override");
16
+ Object.defineProperty(exports, "ChecklistOverride", { enumerable: true, get: function () { return checklist_override_1.ChecklistOverride; } });
17
+ Object.defineProperty(exports, "ChecklistOverrideService", { enumerable: true, get: function () { return checklist_override_1.ChecklistOverrideService; } });
18
+ Object.defineProperty(exports, "checklistOverrideService", { enumerable: true, get: function () { return checklist_override_1.checklistOverrideService; } });
15
19
  const review_json_data_1 = require("./review-json-data");
16
20
  Object.defineProperty(exports, "VERDICT_GREEN", { enumerable: true, get: function () { return review_json_data_1.VERDICT_GREEN; } });
17
21
  Object.defineProperty(exports, "VERDICT_YELLOW", { enumerable: true, get: function () { return review_json_data_1.VERDICT_YELLOW; } });
@@ -53,8 +57,10 @@ const CHECKLIST_ARCHIVE_NOTE = 'ARCHIVE — this is a checklist verdict from a P
53
57
  /** Locates + loads/validates the AI-authored review.json. `@injectable(bindingScopeValues.Singleton)` so it's drawn in the design. */
54
58
  let ReviewJsonService = class ReviewJsonService {
55
59
  dotDir;
56
- constructor(dotDir = state_dir_1.dotWebpieces) {
60
+ overrides;
61
+ constructor(dotDir = state_dir_1.dotWebpieces, overrides = checklist_override_1.checklistOverrideService) {
57
62
  this.dotDir = dotDir;
63
+ this.overrides = overrides;
58
64
  }
59
65
  // The per-feature PR working dir: `<worktree>/.webpieces/pr-review/<feature>`. AI-WRITABLE scope,
60
66
  // not local() — an agent AUTHORS review.json here, and each reviewer subagent authors its own
@@ -295,7 +301,7 @@ let ReviewJsonService = class ReviewJsonService {
295
301
  errors.push('"title" must be a non-empty, imperative PR title describing the change (no branch names).');
296
302
  }
297
303
  const results = this.loadChecklistResults(filePath, required);
298
- for (const err of this.requiredChecklistErrors(required, results))
304
+ for (const err of this.requiredChecklistErrors(required, results, filePath))
299
305
  errors.push(err);
300
306
  if (errors.length > 0) {
301
307
  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` +
@@ -362,27 +368,43 @@ let ReviewJsonService = class ReviewJsonService {
362
368
  * It always quotes the reviewer's own `output` verbatim: the finding is the whole point, and an error
363
369
  * that names a checklist without saying what it objected to gives the reader nothing to fix.
364
370
  *
365
- * `archivedPath` non-empty the verdict has just been RETIRED (moved) to that path, so the message must
366
- * change in two ways. It says where the record went otherwise the move reads as data loss — and,
367
- * critically, it must NOT tell the reader to "set override in review-<id>.json", because that file no
368
- * longer exists. The escape hatch is therefore worded as writing a FRESH verdict file (the body can be
369
- * copied back out of the archive) with a human-authored override.
371
+ * IT NAMES THE WRITER, which is the half that was missing. The old text said WHAT to write ("set a
372
+ * non-empty override") and WHERE, but never WHO MAYso the only reachable move was an agent editing a
373
+ * reviewer's verdict file in place, which the harness denies, which is how a human ended up hand-editing
374
+ * JSON. The ship-anyway route is now a SEPARATE file the coordinating agent may write, and the command
375
+ * that writes it is printed ready to run. See {@link ChecklistOverrideService.writerRule}.
376
+ *
377
+ * `archivedPath` non-empty ⇒ the verdict has just been RETIRED (moved) to that path, so the message says
378
+ * where the record went — otherwise the move reads as data loss — and that a FRESH verdict is required.
379
+ * The AUTHORIZATION is unaffected by that move: `override-<id>.json` is a different file, it survives the
380
+ * retirement, and a human who already decided to accept this checklist is never asked again.
370
381
  */
371
- refusalError(req, verdict, archivedPath = '') {
382
+ // eslint-disable-next-line @typescript-eslint/max-params
383
+ refusalError(req, verdict, reviewJsonFilePath, archivedPath = '') {
372
384
  const finding = `${verdict.detail.split('\n').join('\n ')}\n`;
373
385
  const head = `Checklist "${req.id}" FAILED review (status:"${review_json_data_1.VERDICT_RED}"). The reviewer (${req.subagent}) wrote:\n ` + finding;
374
- if (archivedPath === '') {
375
- return head +
376
- ` Fix it, then re-run; or set a non-empty "override" in ${this.checklistFileName(req.id)} to ship anyway with a stated justification.`;
377
- }
378
- // Re-spawning is the LAST thing said, and only after the finding, because an instruction to spawn a
379
- // subagent is the one line an AI acts on first see refusedChecklists for what that cost.
380
- return head +
381
- ` That verdict has been RETIRED to ${archivedPath} (audit only — it is not a live verdict).\n` +
382
- ` A FRESH ${this.checklistFileName(req.id)} is now required. Fix the finding first, then have the ` +
383
- `"${req.subagent}" subagent review again and write a new verdict.\n` +
384
- ` To ship anyway, a HUMAN must decide it: write a fresh ${this.checklistFileName(req.id)} (you may copy the ` +
385
- `body back from the archive) carrying a non-empty, human-authored "override" justification.`;
386
+ const retired = archivedPath === ''
387
+ ? ' Fix it, then re-run.\n'
388
+ // Re-spawning is said only after the finding, because an instruction to spawn a subagent is the
389
+ // one line an AI acts on first — see refusedChecklists for what that cost.
390
+ : ` That verdict has been RETIRED to ${archivedPath} (audit only it is not a live verdict).\n` +
391
+ ` A FRESH ${this.checklistFileName(req.id)} is now required. Fix the finding first, then have the ` +
392
+ `"${req.subagent}" subagent review again and write a new verdict.\n`;
393
+ return head + retired + this.overrideRoute(req, reviewJsonFilePath);
394
+ }
395
+ /**
396
+ * The ship-anyway paragraph: who may authorize, and the exact command that records it.
397
+ *
398
+ * Its own method because every refusal surface must say the identical thing about who may write an
399
+ * override. A second copy of this paragraph is precisely how the previous one drifted into naming a
400
+ * command that had since been deleted.
401
+ */
402
+ overrideRoute(req, reviewJsonFilePath) {
403
+ return ` To SHIP ANYWAY a human must decide it, and the decision is recorded in its own file — `
404
+ + `${this.overrides.overrideFileName(req.id)}, never inside the reviewer's verdict.\n`
405
+ + ` ${this.overrides.writerRule()}\n`
406
+ + ' Run exactly this, replacing only the "reason" with what the human actually said:\n\n'
407
+ + `${this.overrides.writeCommand(reviewJsonFilePath, req.id)}\n`;
386
408
  }
387
409
  // Read the per-checklist verdict files `review-<id>.json` beside review.json — one per matched checklist.
388
410
  // A missing file is simply absent from the result (→ counts as MISSING for that checklist); a malformed
@@ -399,7 +421,9 @@ let ReviewJsonService = class ReviewJsonService {
399
421
  const p = this.checklistResultPath(reviewJsonFilePath, req.id);
400
422
  if (!fs.existsSync(p))
401
423
  continue;
402
- const parsed = this.parseChecklistResult(p, req.id);
424
+ // The human's authorization is read from its OWN file beside the verdict, in the same pass, so
425
+ // resolveVerdict never touches disk and every command resolves one outcome from one read.
426
+ const parsed = this.parseChecklistResult(p, req.id, this.overrides.load(reviewJsonFilePath, req.id));
403
427
  if (parsed)
404
428
  results.push(parsed);
405
429
  }
@@ -418,8 +442,14 @@ let ReviewJsonService = class ReviewJsonService {
418
442
  return new review_json_data_1.ChecklistVerdict(req.id, review_json_data_1.CK_PASS, result.output);
419
443
  if (result.status === review_json_data_1.VERDICT_YELLOW)
420
444
  return new review_json_data_1.ChecklistVerdict(req.id, review_json_data_1.CK_WARN, result.output);
421
- if (result.override.trim() !== '')
422
- return new review_json_data_1.ChecklistVerdict(req.id, review_json_data_1.CK_OVERRIDDEN, result.override.trim());
445
+ const override = result.override;
446
+ // A malformed authorization is reported as a FORMAT problem, never treated as one: an override with
447
+ // no stated reason authorizes nothing, and silently ignoring it would tell the reader their decision
448
+ // was not recorded without ever saying why.
449
+ if (override !== null && override.problem !== '')
450
+ return new review_json_data_1.ChecklistVerdict(req.id, review_json_data_1.CK_BAD_FORMAT, override.problem);
451
+ if (override !== null)
452
+ return new review_json_data_1.ChecklistVerdict(req.id, review_json_data_1.CK_OVERRIDDEN, this.overrides.detail(override));
423
453
  return new review_json_data_1.ChecklistVerdict(req.id, review_json_data_1.CK_FAIL, result.output);
424
454
  }
425
455
  /**
@@ -440,7 +470,7 @@ let ReviewJsonService = class ReviewJsonService {
440
470
  }
441
471
  // Every matched checklist whose verdict is FAIL (reviewed, found a problem, no override) or MISSING (no
442
472
  // review-<id>.json written) → one error each, printing the reviewer's `output` verbatim.
443
- requiredChecklistErrors(required, results) {
473
+ requiredChecklistErrors(required, results, filePath) {
444
474
  // Format complaints come from the ONE renderer, so wp-review-upsert-pr and wp-finish word them identically.
445
475
  const errors = this.checklistFormatErrors(required, results);
446
476
  for (const req of required) {
@@ -450,7 +480,7 @@ let ReviewJsonService = class ReviewJsonService {
450
480
  if (verdict.status === review_json_data_1.CK_FAIL) {
451
481
  // Through the ONE renderer, so this path and the command layer's refusal say the same thing.
452
482
  // No archive path here: this is validation, not the act of retiring the verdict.
453
- errors.push(this.refusalError(req, verdict));
483
+ errors.push(this.refusalError(req, verdict, filePath));
454
484
  }
455
485
  else if (verdict.status === review_json_data_1.CK_MISSING) {
456
486
  // An OPTIONAL checklist with no verdict was legitimately not run — the human was offered it
@@ -462,7 +492,7 @@ let ReviewJsonService = class ReviewJsonService {
462
492
  const doc = req.doc.trim() !== '' ? ` Read: ${req.doc}.` : '';
463
493
  errors.push(`Checklist "${req.id}" MATCHED this diff but has no verdict. Spawn the "${req.subagent}" subagent to review it, ` +
464
494
  `then write ${this.checklistFileName(req.id)} with ` +
465
- `{"id":"${req.id}","status":"${review_json_data_1.VERDICT_GREEN}","output":"…","override":""}.${doc}`);
495
+ `{"id":"${req.id}","status":"${review_json_data_1.VERDICT_GREEN}","output":"…"}.${doc}`);
466
496
  }
467
497
  }
468
498
  return errors;
@@ -486,12 +516,17 @@ let ReviewJsonService = class ReviewJsonService {
486
516
  verdictSchemaFor(id, verdictPath = '', indent = ' ') {
487
517
  const lines = [
488
518
  `${indent}{ "id": "${id}", "status": "${review_json_data_1.VERDICT_GREEN} | ${review_json_data_1.VERDICT_YELLOW} | ${review_json_data_1.VERDICT_RED}", ` +
489
- `"output": "what you checked / found", "override": "" }`,
519
+ `"output": "what you checked / found" }`,
490
520
  `${indent} ${review_json_data_1.VERDICT_GREEN} → passes, nothing to flag`,
491
521
  `${indent} ${review_json_data_1.VERDICT_YELLOW} → passes WITH CONCERNS; nothing is blocked and the concern is published on the PR`,
492
- `${indent} ${review_json_data_1.VERDICT_RED} → REFUSES the PR (set a non-empty "override" to ship anyway with a stated justification)`,
493
- `${indent}Prefer "${review_json_data_1.VERDICT_YELLOW}" over red-plus-override when the change is acceptable but worth a human's`,
494
- `${indent}attention an override reads as a deliberately-accepted defect, a yellow reads as a note.`,
522
+ `${indent} ${review_json_data_1.VERDICT_RED} → REFUSES the PR; your "output" is printed verbatim`,
523
+ `${indent}Prefer "${review_json_data_1.VERDICT_YELLOW}" over red when the change is acceptable but worth a human's attention —`,
524
+ `${indent}a red a human then authorizes reads as a deliberately-accepted defect, a yellow reads as a note.`,
525
+ // The one sentence that stops a reviewer doing what a reviewer did once: telling the human to run
526
+ // a command, on its own authority, to get past its own finding.
527
+ `${indent}THERE IS NO "override" FIELD HERE, and you NEVER write one. A reviewer does not authorize`,
528
+ `${indent}shipping past its own finding: if this needs a human's decision, SAY SO in "output" and STOP.`,
529
+ `${indent}The coordinating agent is the one with the human, and records that decision in override-${id}.json.`,
495
530
  ];
496
531
  if (verdictPath !== '')
497
532
  lines.push(`${indent}File: ${verdictPath}`);
@@ -507,7 +542,7 @@ let ReviewJsonService = class ReviewJsonService {
507
542
  * format" into "never wrote a verdict" and send the AI off to re-run a reviewer that already ran.
508
543
  */
509
544
  // webpieces-disable no-any-unknown -- opaque parsed JSON, narrowed field-by-field
510
- parseChecklistResult(filePath, id) {
545
+ parseChecklistResult(filePath, id, override) {
511
546
  // webpieces-disable no-unmanaged-exceptions -- chokepoint: an unparseable per-checklist file is skipped, not fatal
512
547
  // eslint-disable-next-line @webpieces/no-unmanaged-exceptions
513
548
  try {
@@ -516,9 +551,8 @@ let ReviewJsonService = class ReviewJsonService {
516
551
  if (typeof raw !== 'object' || raw === null || Array.isArray(raw))
517
552
  return null;
518
553
  const output = typeof raw['output'] === 'string' ? raw['output'] : '';
519
- const override = typeof raw['override'] === 'string' ? raw['override'] : '';
520
554
  const status = typeof raw['status'] === 'string' ? raw['status'].trim().toLowerCase() : '';
521
- return new review_json_data_1.ChecklistResult(id, status, output, override, this.statusProblem(filePath, id, status, raw));
555
+ return new review_json_data_1.ChecklistResult(id, status, output, override, this.verdictProblem(filePath, id, status, raw));
522
556
  }
523
557
  catch (err) {
524
558
  const error = (0, to_error_1.toError)(err);
@@ -527,19 +561,32 @@ let ReviewJsonService = class ReviewJsonService {
527
561
  }
528
562
  }
529
563
  /**
530
- * '' when `status` is one of the three colors. Otherwise the complaint to show the AI verbatim. The
531
- * legacy-`success` case gets its OWN message: `success` was removed outright (no compatibility mode),
532
- * and a reviewer told only "status must be green|yellow|red" cannot tell whether it wrote the wrong
533
- * value or is using a field that no longer exists.
564
+ * '' when the verdict can be READ. Otherwise the complaint to show the AI verbatim.
565
+ *
566
+ * The MOVED `override` field is checked FIRST, ahead of `status`, because it is the more specific fact: a
567
+ * file carrying it was written against a schema that no longer exists, and a reader told only "status
568
+ * must be green|yellow|red" would fix the wrong thing. It is rejected even when EMPTY — an accepted shape
569
+ * is never migrated, and `"override": ""` sitting in a reviewer's file is the copy that teaches the next
570
+ * reviewer the field still exists.
571
+ *
572
+ * The legacy-`success` case keeps its OWN message for the same reason: `success` was removed outright
573
+ * (no compatibility mode), and a reviewer cannot tell a wrong value from a field that no longer exists.
534
574
  */
535
575
  // webpieces-disable no-any-unknown -- opaque parsed JSON; only tested for key presence here
536
- statusProblem(filePath, id, status, raw) {
537
- // webpieces-disable no-any-unknown -- comparing against the readonly literal tuple of valid colors
538
- if (review_json_data_1.VERDICT_STATUSES.includes(status))
539
- return '';
576
+ verdictProblem(filePath, id, status, raw) {
540
577
  // The ONE renderer — see verdictSchemaFor. A second copy here is what let the old `success` shape
541
578
  // survive in print after it was removed from the parser.
542
579
  const shape = this.verdictSchemaFor(id, filePath);
580
+ if ('override' in raw) {
581
+ return `Checklist "${id}" wrote its verdict with the MOVED "override" field. A ship-anyway `
582
+ + 'authorization is no longer part of a reviewer\'s verdict: it MOVED to its own file, '
583
+ + `${this.overrides.overrideFileName(id)}, which only the coordinating agent writes and only on a `
584
+ + 'human\'s in-session instruction. There is no compatibility mode — DELETE the "override" key from '
585
+ + `${filePath}. Rewrite the file as:\n${shape}`;
586
+ }
587
+ // webpieces-disable no-any-unknown -- comparing against the readonly literal tuple of valid colors
588
+ if (review_json_data_1.VERDICT_STATUSES.includes(status))
589
+ return '';
543
590
  if ('success' in raw) {
544
591
  return `Checklist "${id}" wrote its verdict with the REMOVED "success" field. It is now a tri-state ` +
545
592
  `"status" — there is no compatibility mode. Rewrite the file as:\n${shape}`;
@@ -573,7 +620,8 @@ let ReviewJsonService = class ReviewJsonService {
573
620
  exports.ReviewJsonService = ReviewJsonService;
574
621
  exports.ReviewJsonService = ReviewJsonService = tslib_1.__decorate([
575
622
  (0, inversify_1.injectable)(inversify_1.bindingScopeValues.Singleton),
576
- tslib_1.__metadata("design:paramtypes", [state_dir_1.DotWebpieces])
623
+ tslib_1.__metadata("design:paramtypes", [state_dir_1.DotWebpieces,
624
+ checklist_override_1.ChecklistOverrideService])
577
625
  ], ReviewJsonService);
578
626
  // Temporary migration delegators to ReviewJsonService — removed once consumers inject it.
579
627
  const reviewJsonSvc = new ReviewJsonService();