@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.
- package/package.json +1 -1
- package/src/checklist-override.d.ts +106 -0
- package/src/checklist-override.js +207 -0
- package/src/checklist-override.js.map +1 -0
- package/src/index.d.ts +2 -1
- package/src/index.js +10 -3
- package/src/index.js.map +1 -1
- package/src/load-template.d.ts +8 -1
- package/src/load-template.js +22 -2
- package/src/load-template.js.map +1 -1
- package/src/review-json-data.d.ts +3 -2
- package/src/review-json-data.js +19 -11
- package/src/review-json-data.js.map +1 -1
- package/src/review-json.d.ts +34 -12
- package/src/review-json.js +91 -43
- package/src/review-json.js.map +1 -1
- package/src/stale-bin-sweep.d.ts +68 -0
- package/src/stale-bin-sweep.js +226 -0
- package/src/stale-bin-sweep.js.map +1 -0
- package/templates/webpieces.review-checklists.md +46 -6
package/src/load-template.js
CHANGED
|
@@ -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
|
-
|
|
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();
|
package/src/load-template.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"load-template.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/load-template.ts"],"names":[],"mappings":";;;
|
|
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:
|
|
18
|
+
override: ChecklistOverride | null;
|
|
18
19
|
problem: string;
|
|
19
|
-
constructor(id: string, status: string, output: string, override:
|
|
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
|
package/src/review-json-data.js
CHANGED
|
@@ -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'
|
|
24
|
-
// status:'yellow'
|
|
25
|
-
// status:'red' + override
|
|
26
|
-
// status:'red' + no override → FAIL (refuse; `output` is printed verbatim)
|
|
27
|
-
//
|
|
28
|
-
//
|
|
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
|
-
|
|
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
|
|
36
|
-
// complaint can be reported by BOTH wp-review-upsert-pr and
|
|
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'; //
|
|
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"]}
|
package/src/review-json.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
*
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
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 MAY — so 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
|
-
|
|
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
|
|
188
|
-
*
|
|
189
|
-
*
|
|
190
|
-
*
|
|
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
|
|
214
|
+
private verdictProblem;
|
|
193
215
|
private asStringArray;
|
|
194
216
|
private parseReviewJson;
|
|
195
217
|
}
|
package/src/review-json.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.ReviewJsonService = exports.PrContext = exports.ChecklistVerdict = exports.CK_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
|
-
|
|
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
|
-
*
|
|
366
|
-
*
|
|
367
|
-
*
|
|
368
|
-
*
|
|
369
|
-
*
|
|
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 MAY — so 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
|
-
|
|
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
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
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
|
-
|
|
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
|
-
|
|
422
|
-
|
|
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":"…"
|
|
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"
|
|
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
|
|
493
|
-
`${indent}Prefer "${review_json_data_1.VERDICT_YELLOW}" over red
|
|
494
|
-
`${indent}
|
|
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.
|
|
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
|
|
531
|
-
*
|
|
532
|
-
*
|
|
533
|
-
*
|
|
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
|
-
|
|
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();
|