@webpieces/rules-config 0.4.472 → 0.4.474
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/checklist-config.d.ts +7 -21
- package/src/checklist-config.js +21 -45
- package/src/checklist-config.js.map +1 -1
- package/src/checklist-docs-validator.d.ts +5 -6
- package/src/checklist-docs-validator.js +7 -8
- package/src/checklist-docs-validator.js.map +1 -1
- package/src/checklist-manifest.d.ts +17 -0
- package/src/checklist-manifest.js +120 -0
- package/src/checklist-manifest.js.map +1 -0
- package/src/index.d.ts +5 -4
- package/src/index.js +8 -10
- package/src/index.js.map +1 -1
- package/src/pr-gate-config.d.ts +2 -3
- package/src/pr-gate-config.js +10 -11
- package/src/pr-gate-config.js.map +1 -1
- package/src/review-json.d.ts +16 -24
- package/src/review-json.js +94 -134
- package/src/review-json.js.map +1 -1
- package/src/subagent-provenance.d.ts +2 -0
- package/src/subagent-provenance.js +41 -8
- package/src/subagent-provenance.js.map +1 -1
- package/src/validate-config.d.ts +1 -1
- package/src/validate-config.js +21 -77
- package/src/validate-config.js.map +1 -1
- package/templates/webpieces.review-checklists.md +65 -0
package/src/pr-gate-config.js
CHANGED
|
@@ -8,7 +8,6 @@ exports.PrGateConfig = exports.MERGE_MODES = exports.MERGE_MODE_NONE = exports.M
|
|
|
8
8
|
exports.defaultGates = defaultGates;
|
|
9
9
|
exports.defaultPrGateConfig = defaultPrGateConfig;
|
|
10
10
|
exports.buildPrGateConfig = buildPrGateConfig;
|
|
11
|
-
const checklist_config_1 = require("./checklist-config");
|
|
12
11
|
class GateDefinition {
|
|
13
12
|
name;
|
|
14
13
|
patterns;
|
|
@@ -55,9 +54,9 @@ class PrGateConfig {
|
|
|
55
54
|
* commits land as the internal "Squash merge of <branch>" subject.
|
|
56
55
|
*/
|
|
57
56
|
mergeMode;
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
|
|
57
|
+
// Repo-relative path of the ONE doc carrying the checklist manifest (a <!-- webpieces:checklists [...] -->
|
|
58
|
+
// JSON block). '' = no checklists. The checklist SET lives in that doc (content), never here (config).
|
|
59
|
+
checklistDoc;
|
|
61
60
|
/**
|
|
62
61
|
* Shared secret used to mint the server-verifiable gate token. `wp-finish-upsert-pr` writes
|
|
63
62
|
* `HMAC(gateSalt, HEAD_sha)` as a hidden marker into the PR body (and REFUSES to mint it unless
|
|
@@ -72,12 +71,12 @@ class PrGateConfig {
|
|
|
72
71
|
*/
|
|
73
72
|
gateSalt;
|
|
74
73
|
// eslint-disable-next-line @typescript-eslint/max-params
|
|
75
|
-
constructor(mode, buildCommand, gates, mergeMode,
|
|
74
|
+
constructor(mode, buildCommand, gates, mergeMode, checklistDoc = '', gateSalt = '') {
|
|
76
75
|
this.mode = mode;
|
|
77
76
|
this.buildCommand = buildCommand;
|
|
78
77
|
this.gates = gates;
|
|
79
78
|
this.mergeMode = mergeMode;
|
|
80
|
-
this.
|
|
79
|
+
this.checklistDoc = checklistDoc;
|
|
81
80
|
this.gateSalt = gateSalt;
|
|
82
81
|
}
|
|
83
82
|
}
|
|
@@ -93,8 +92,8 @@ function defaultGates() {
|
|
|
93
92
|
];
|
|
94
93
|
}
|
|
95
94
|
function defaultPrGateConfig() {
|
|
96
|
-
// No default
|
|
97
|
-
return new PrGateConfig('ON', '', defaultGates(), exports.MERGE_MODE_AUTO,
|
|
95
|
+
// No default checklist doc — the extension point is opt-in; the default monorepo ships none.
|
|
96
|
+
return new PrGateConfig('ON', '', defaultGates(), exports.MERGE_MODE_AUTO, '');
|
|
98
97
|
}
|
|
99
98
|
function toGate(raw) {
|
|
100
99
|
return new GateDefinition(raw.name ?? '', raw.patterns ?? [], raw.warningColor ?? 'yellow', raw.disabled ?? false);
|
|
@@ -117,10 +116,10 @@ function buildPrGateConfig(section) {
|
|
|
117
116
|
// REQUIRED — validatePrGateSection rejects an omitted/unknown value, so this fallback only ever
|
|
118
117
|
// applies to the no-config-file path that defaultPrGateConfig() serves.
|
|
119
118
|
const mergeMode = raw.mergeMode ?? defaults.mergeMode;
|
|
120
|
-
// Optional extension point — omitted ⇒
|
|
121
|
-
const
|
|
119
|
+
// Optional extension point — omitted ⇒ '' ⇒ no checklists computed anywhere downstream.
|
|
120
|
+
const checklistDoc = raw.checklists?.doc ?? defaults.checklistDoc;
|
|
122
121
|
// Optional — omitted ⇒ '' ⇒ no gate token minted and CI enforcement is a no-op (back-compat).
|
|
123
122
|
const gateSalt = raw.gateSalt ?? defaults.gateSalt;
|
|
124
|
-
return new PrGateConfig(mode, buildCommand, gates, mergeMode,
|
|
123
|
+
return new PrGateConfig(mode, buildCommand, gates, mergeMode, checklistDoc, gateSalt);
|
|
125
124
|
}
|
|
126
125
|
//# sourceMappingURL=pr-gate-config.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pr-gate-config.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/pr-gate-config.ts"],"names":[],"mappings":"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","sourcesContent":["// PrGateConfig is the \"special section\" for the pr-gate dashboard. It does NOT live in the\n// validated `rules` map (the FieldDef schema can't express nested object arrays), but as a\n// top-level `pr-gate` key in webpieces.config.json. It is built and validated by\n// loadAndValidate (load-config.ts); this module holds only the data classes + defaults + toGate.\n\nimport { ChecklistDefinition, RawChecklist, toChecklist } from './checklist-config';\n\nexport class GateDefinition {\n name: string;\n patterns: string[];\n // The warning color shown on the dashboard WHEN this gate's patterns match a changed file. Green\n // is implicit (shown when nothing matched), so it is never configured. warningColor is purely\n // visual — even 'red' never fails/blocks the PR (only the build gate can). 'yellow' = caution,\n // 'red' = louder \"look here\" flag (e.g. DB schema / migration changes). REQUIRED on every gate.\n warningColor: string; // 'yellow' | 'red'\n // Example/inactive gate: parsed and kept in the file (JSON has no comments) but skipped at\n // compute/render time. Other projects flip this to false and tune patterns/warningColor.\n disabled: boolean;\n\n constructor(name: string, patterns: string[], warningColor: string, disabled = false) {\n this.name = name;\n this.patterns = patterns;\n this.warningColor = warningColor;\n this.disabled = disabled;\n }\n}\n\n// How wp-finish-upsert-pr should try to LAND the PR once it has posted it. GitHub's auto-merge queue\n// is a REPO-level setting (`allow_auto_merge`) that many orgs turn OFF as a policy control, and it\n// CANNOT be forced from the client: `gh pr merge --auto` calls the enablePullRequestAutoMerge GraphQL\n// mutation, which hard-errors with \"Auto merge is not allowed for this repository\" when the repo says\n// no. So the only lever a config knob has is WHICH PATHS WE ATTEMPT — never whether the queue exists.\nexport const MERGE_MODE_AUTO = 'AUTO';\nexport const MERGE_MODE_NONE = 'NONE';\nexport const MERGE_MODES = [MERGE_MODE_AUTO, MERGE_MODE_NONE];\n\nexport class PrGateConfig {\n mode: string;\n buildCommand: string;\n gates: GateDefinition[];\n /**\n * REQUIRED — every repo must state its policy; there is deliberately no default, because the two\n * answers are a real policy decision and guessing it either merges when a team did not want that,\n * or silently stops landing PRs on a team that relied on it.\n *\n * AUTO — wp-finish-upsert-pr LANDS the PR: squash-merge it right away when it is mergeable, else\n * enable GitHub auto-merge so it lands when the checks pass. Both carry an explicit --subject /\n * --body-file, which is the ONLY way main's history gets the PR title plus the compact\n * risk/flags body — no repo setting can produce that. Requires allow_auto_merge on the repo.\n * NONE — wp-finish-upsert-pr only opens/updates the PR and stops; a human merges. NOTE the cost:\n * a UI merge cannot use the compact body, so main's commit falls back to the repo's\n * squash_merge_commit_title/message settings. Set squash_merge_commit_title=PR_TITLE there, or\n * commits land as the internal \"Squash merge of <branch>\" subject.\n */\n mergeMode: string;\n // Diff-triggered company review checklists (the extension point). Defaults to [] — an absent\n // `checklists` key produces byte-identical behavior to before this field existed.\n checklists: ChecklistDefinition[];\n /**\n * Shared secret used to mint the server-verifiable gate token. `wp-finish-upsert-pr` writes\n * `HMAC(gateSalt, HEAD_sha)` as a hidden marker into the PR body (and REFUSES to mint it unless\n * every BLOCK checklist passed), so a valid token IS proof the local gate ran and passed. A CI\n * check (`wp-check-pr` + the scaffolded workflow) recomputes it from the PR head sha and this salt.\n *\n * Optional, defaults to '' — empty means \"no token minted, no CI enforcement\" (byte-identical to\n * before this field existed). This is COMMITTED, obscurity-grade: it stops unhooked teammates who\n * push + open a PR in the web UI, but is readable in-repo and therefore forgeable by a determined\n * reader. It is deliberately NOT cryptographically sound; nothing local can stop a filesystem-reading\n * agent. See RESPONSE-pr-gate-ci-enforcement / the design memo for the full tradeoff.\n */\n gateSalt: string;\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(mode: string, buildCommand: string, gates: GateDefinition[], mergeMode: string, checklists: ChecklistDefinition[] = [], gateSalt = '') {\n this.mode = mode;\n this.buildCommand = buildCommand;\n this.gates = gates;\n this.mergeMode = mergeMode;\n this.checklists = checklists;\n this.gateSalt = gateSalt;\n }\n}\n\n// Default infra gates — path-pattern based, tuned for this monorepo. Clients override the\n// whole list via the `pr-gate.gates` array in webpieces.config.json.\nexport function defaultGates(): GateDefinition[] {\n return [\n new GateDefinition('API Changed', ['libraries/apis/**', '**/*Api.ts'], 'yellow'),\n new GateDefinition('Config Files Changed', ['**/package.json', '**/tsconfig*.json', 'nx.json', '**/*.config.*'], 'yellow'),\n new GateDefinition('Dependency Graph Changed', ['architecture/dependencies.json'], 'yellow'),\n new GateDefinition('Claude / Rules Changed', ['**/CLAUDE.md', '**/claude.*.md', '.claude/**', 'webpieces.config.json'], 'yellow'),\n ];\n}\n\nexport function defaultPrGateConfig(): PrGateConfig {\n // No default checklists — the extension point is opt-in; the default monorepo ships none.\n return new PrGateConfig('ON', '', defaultGates(), MERGE_MODE_AUTO, []);\n}\n\ninterface RawGate {\n name?: string;\n patterns?: string[];\n warningColor?: string;\n disabled?: boolean;\n}\n\ninterface RawPrGateSection {\n mode?: string;\n buildCommand?: string;\n gates?: RawGate[];\n mergeMode?: string;\n checklists?: RawChecklist[];\n gateSalt?: string;\n}\n\nfunction toGate(raw: RawGate): GateDefinition {\n return new GateDefinition(raw.name ?? '', raw.patterns ?? [], raw.warningColor ?? 'yellow', raw.disabled ?? false);\n}\n\n/**\n * Build a PrGateConfig from the already-parsed top-level `pr-gate` section, falling back to defaults\n * for any field the consumer omits. Pure transform — the file read + structural validation happen in\n * loadAndValidate (load-config.ts) so every consumer goes through one validated path. Pass undefined\n * (no `pr-gate` key / no config file) to get full defaults.\n */\n// webpieces-disable no-any-unknown -- `section` is opaque consumer JSON until narrowed here\nexport function buildPrGateConfig(section: unknown): PrGateConfig {\n const defaults = defaultPrGateConfig();\n if (section === undefined || section === null || typeof section !== 'object') return defaults;\n\n const raw = section as RawPrGateSection;\n const mode = raw.mode ?? defaults.mode;\n const buildCommand = raw.buildCommand ?? defaults.buildCommand;\n const gates = raw.gates !== undefined ? raw.gates.map(toGate) : defaults.gates;\n // REQUIRED — validatePrGateSection rejects an omitted/unknown value, so this fallback only ever\n // applies to the no-config-file path that defaultPrGateConfig() serves.\n const mergeMode = raw.mergeMode ?? defaults.mergeMode;\n // Optional extension point — omitted ⇒ [] ⇒ no checklists computed anywhere downstream.\n const checklists = raw.checklists !== undefined ? raw.checklists.map(toChecklist) : defaults.checklists;\n // Optional — omitted ⇒ '' ⇒ no gate token minted and CI enforcement is a no-op (back-compat).\n const gateSalt = raw.gateSalt ?? defaults.gateSalt;\n return new PrGateConfig(mode, buildCommand, gates, mergeMode, checklists, gateSalt);\n}\n"]}
|
|
1
|
+
{"version":3,"file":"pr-gate-config.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/pr-gate-config.ts"],"names":[],"mappings":"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","sourcesContent":["// PrGateConfig is the \"special section\" for the pr-gate dashboard. It does NOT live in the\n// validated `rules` map (the FieldDef schema can't express nested object arrays), but as a\n// top-level `pr-gate` key in webpieces.config.json. It is built and validated by\n// loadAndValidate (load-config.ts); this module holds only the data classes + defaults + toGate.\n\nexport class GateDefinition {\n name: string;\n patterns: string[];\n // The warning color shown on the dashboard WHEN this gate's patterns match a changed file. Green\n // is implicit (shown when nothing matched), so it is never configured. warningColor is purely\n // visual — even 'red' never fails/blocks the PR (only the build gate can). 'yellow' = caution,\n // 'red' = louder \"look here\" flag (e.g. DB schema / migration changes). REQUIRED on every gate.\n warningColor: string; // 'yellow' | 'red'\n // Example/inactive gate: parsed and kept in the file (JSON has no comments) but skipped at\n // compute/render time. Other projects flip this to false and tune patterns/warningColor.\n disabled: boolean;\n\n constructor(name: string, patterns: string[], warningColor: string, disabled = false) {\n this.name = name;\n this.patterns = patterns;\n this.warningColor = warningColor;\n this.disabled = disabled;\n }\n}\n\n// How wp-finish-upsert-pr should try to LAND the PR once it has posted it. GitHub's auto-merge queue\n// is a REPO-level setting (`allow_auto_merge`) that many orgs turn OFF as a policy control, and it\n// CANNOT be forced from the client: `gh pr merge --auto` calls the enablePullRequestAutoMerge GraphQL\n// mutation, which hard-errors with \"Auto merge is not allowed for this repository\" when the repo says\n// no. So the only lever a config knob has is WHICH PATHS WE ATTEMPT — never whether the queue exists.\nexport const MERGE_MODE_AUTO = 'AUTO';\nexport const MERGE_MODE_NONE = 'NONE';\nexport const MERGE_MODES = [MERGE_MODE_AUTO, MERGE_MODE_NONE];\n\nexport class PrGateConfig {\n mode: string;\n buildCommand: string;\n gates: GateDefinition[];\n /**\n * REQUIRED — every repo must state its policy; there is deliberately no default, because the two\n * answers are a real policy decision and guessing it either merges when a team did not want that,\n * or silently stops landing PRs on a team that relied on it.\n *\n * AUTO — wp-finish-upsert-pr LANDS the PR: squash-merge it right away when it is mergeable, else\n * enable GitHub auto-merge so it lands when the checks pass. Both carry an explicit --subject /\n * --body-file, which is the ONLY way main's history gets the PR title plus the compact\n * risk/flags body — no repo setting can produce that. Requires allow_auto_merge on the repo.\n * NONE — wp-finish-upsert-pr only opens/updates the PR and stops; a human merges. NOTE the cost:\n * a UI merge cannot use the compact body, so main's commit falls back to the repo's\n * squash_merge_commit_title/message settings. Set squash_merge_commit_title=PR_TITLE there, or\n * commits land as the internal \"Squash merge of <branch>\" subject.\n */\n mergeMode: string;\n // Repo-relative path of the ONE doc carrying the checklist manifest (a <!-- webpieces:checklists [...] -->\n // JSON block). '' = no checklists. The checklist SET lives in that doc (content), never here (config).\n checklistDoc: string;\n /**\n * Shared secret used to mint the server-verifiable gate token. `wp-finish-upsert-pr` writes\n * `HMAC(gateSalt, HEAD_sha)` as a hidden marker into the PR body (and REFUSES to mint it unless\n * every BLOCK checklist passed), so a valid token IS proof the local gate ran and passed. A CI\n * check (`wp-check-pr` + the scaffolded workflow) recomputes it from the PR head sha and this salt.\n *\n * Optional, defaults to '' — empty means \"no token minted, no CI enforcement\" (byte-identical to\n * before this field existed). This is COMMITTED, obscurity-grade: it stops unhooked teammates who\n * push + open a PR in the web UI, but is readable in-repo and therefore forgeable by a determined\n * reader. It is deliberately NOT cryptographically sound; nothing local can stop a filesystem-reading\n * agent. See RESPONSE-pr-gate-ci-enforcement / the design memo for the full tradeoff.\n */\n gateSalt: string;\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(mode: string, buildCommand: string, gates: GateDefinition[], mergeMode: string, checklistDoc = '', gateSalt = '') {\n this.mode = mode;\n this.buildCommand = buildCommand;\n this.gates = gates;\n this.mergeMode = mergeMode;\n this.checklistDoc = checklistDoc;\n this.gateSalt = gateSalt;\n }\n}\n\n// Default infra gates — path-pattern based, tuned for this monorepo. Clients override the\n// whole list via the `pr-gate.gates` array in webpieces.config.json.\nexport function defaultGates(): GateDefinition[] {\n return [\n new GateDefinition('API Changed', ['libraries/apis/**', '**/*Api.ts'], 'yellow'),\n new GateDefinition('Config Files Changed', ['**/package.json', '**/tsconfig*.json', 'nx.json', '**/*.config.*'], 'yellow'),\n new GateDefinition('Dependency Graph Changed', ['architecture/dependencies.json'], 'yellow'),\n new GateDefinition('Claude / Rules Changed', ['**/CLAUDE.md', '**/claude.*.md', '.claude/**', 'webpieces.config.json'], 'yellow'),\n ];\n}\n\nexport function defaultPrGateConfig(): PrGateConfig {\n // No default checklist doc — the extension point is opt-in; the default monorepo ships none.\n return new PrGateConfig('ON', '', defaultGates(), MERGE_MODE_AUTO, '');\n}\n\ninterface RawGate {\n name?: string;\n patterns?: string[];\n warningColor?: string;\n disabled?: boolean;\n}\n\ninterface RawPrGateSection {\n mode?: string;\n buildCommand?: string;\n gates?: RawGate[];\n mergeMode?: string;\n checklists?: { doc?: string };\n gateSalt?: string;\n}\n\nfunction toGate(raw: RawGate): GateDefinition {\n return new GateDefinition(raw.name ?? '', raw.patterns ?? [], raw.warningColor ?? 'yellow', raw.disabled ?? false);\n}\n\n/**\n * Build a PrGateConfig from the already-parsed top-level `pr-gate` section, falling back to defaults\n * for any field the consumer omits. Pure transform — the file read + structural validation happen in\n * loadAndValidate (load-config.ts) so every consumer goes through one validated path. Pass undefined\n * (no `pr-gate` key / no config file) to get full defaults.\n */\n// webpieces-disable no-any-unknown -- `section` is opaque consumer JSON until narrowed here\nexport function buildPrGateConfig(section: unknown): PrGateConfig {\n const defaults = defaultPrGateConfig();\n if (section === undefined || section === null || typeof section !== 'object') return defaults;\n\n const raw = section as RawPrGateSection;\n const mode = raw.mode ?? defaults.mode;\n const buildCommand = raw.buildCommand ?? defaults.buildCommand;\n const gates = raw.gates !== undefined ? raw.gates.map(toGate) : defaults.gates;\n // REQUIRED — validatePrGateSection rejects an omitted/unknown value, so this fallback only ever\n // applies to the no-config-file path that defaultPrGateConfig() serves.\n const mergeMode = raw.mergeMode ?? defaults.mergeMode;\n // Optional extension point — omitted ⇒ '' ⇒ no checklists computed anywhere downstream.\n const checklistDoc = raw.checklists?.doc ?? defaults.checklistDoc;\n // Optional — omitted ⇒ '' ⇒ no gate token minted and CI enforcement is a no-op (back-compat).\n const gateSalt = raw.gateSalt ?? defaults.gateSalt;\n return new PrGateConfig(mode, buildCommand, gates, mergeMode, checklistDoc, gateSalt);\n}\n"]}
|
package/src/review-json.d.ts
CHANGED
|
@@ -1,9 +1,3 @@
|
|
|
1
|
-
export declare class ChecklistAck {
|
|
2
|
-
id: string;
|
|
3
|
-
acknowledged: boolean;
|
|
4
|
-
notes: string[];
|
|
5
|
-
constructor(id: string, acknowledged: boolean, notes: string[]);
|
|
6
|
-
}
|
|
7
1
|
export declare class ChecklistResult {
|
|
8
2
|
id: string;
|
|
9
3
|
success: boolean;
|
|
@@ -13,13 +7,10 @@ export declare class ChecklistResult {
|
|
|
13
7
|
}
|
|
14
8
|
export declare class RequiredChecklist {
|
|
15
9
|
id: string;
|
|
16
|
-
title: string;
|
|
17
|
-
severity: string;
|
|
18
|
-
docs: string[];
|
|
19
|
-
blockMessage: string;
|
|
20
|
-
matchedFiles: string[];
|
|
21
10
|
subagent: string;
|
|
22
|
-
|
|
11
|
+
doc: string;
|
|
12
|
+
matchedFiles: string[];
|
|
13
|
+
constructor(id: string, subagent: string, doc: string, matchedFiles: string[]);
|
|
23
14
|
}
|
|
24
15
|
export declare class ReviewJson {
|
|
25
16
|
title: string;
|
|
@@ -30,42 +21,43 @@ export declare class ReviewJson {
|
|
|
30
21
|
violations: string[];
|
|
31
22
|
risks: string[];
|
|
32
23
|
filesToReview: string[];
|
|
33
|
-
checklists: ChecklistAck[];
|
|
34
24
|
results: ChecklistResult[];
|
|
35
|
-
constructor(title: string, riskScore: number, riskLevel: string, riskEmoji: string, summary: string, violations: string[], risks: string[], filesToReview: string[],
|
|
25
|
+
constructor(title: string, riskScore: number, riskLevel: string, riskEmoji: string, summary: string, violations: string[], risks: string[], filesToReview: string[], results?: ChecklistResult[]);
|
|
36
26
|
}
|
|
37
27
|
export declare const CK_PASS = "pass";
|
|
38
28
|
export declare const CK_OVERRIDDEN = "overridden";
|
|
39
29
|
export declare const CK_FAIL = "fail";
|
|
40
30
|
export declare const CK_MISSING = "missing";
|
|
41
|
-
export declare const CK_ACKED = "acknowledged";
|
|
42
31
|
export declare class ChecklistVerdict {
|
|
43
32
|
id: string;
|
|
44
33
|
status: string;
|
|
45
34
|
detail: string;
|
|
46
35
|
constructor(id: string, status: string, detail: string);
|
|
47
36
|
}
|
|
37
|
+
export declare class PrContext {
|
|
38
|
+
base: string;
|
|
39
|
+
head: string;
|
|
40
|
+
changedFiles: string[];
|
|
41
|
+
constructor(base: string, head: string, changedFiles: string[]);
|
|
42
|
+
}
|
|
48
43
|
/** Locates + loads/validates the AI-authored review.json. `@injectable(bindingScopeValues.Singleton)` so it's drawn in the design. */
|
|
49
44
|
export declare class ReviewJsonService {
|
|
50
45
|
prDirFor(repoRoot: string, featureName: string): string;
|
|
51
46
|
reviewJsonPath(repoRoot: string, featureName: string): string;
|
|
47
|
+
prContextPath(repoRoot: string, featureName: string): string;
|
|
48
|
+
writePrContext(repoRoot: string, featureName: string, context: PrContext): string;
|
|
52
49
|
reviewJsonSchemaHint(filePath: string, required?: readonly RequiredChecklist[]): string;
|
|
53
50
|
checklistResultPath(reviewJsonFilePath: string, checklistId: string): string;
|
|
54
51
|
private requiredChecklistHint;
|
|
55
52
|
/**
|
|
56
53
|
* Load + validate the AI-authored review.json. Throws InformAiError (with the schema) when missing,
|
|
57
|
-
* unparseable, or structurally wrong.
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
* which case this behaves byte-identically to before the feature). Every BLOCK entry must appear in
|
|
61
|
-
* review.json's `checklists[]` with `acknowledged: true`, or a validation error is raised alongside
|
|
62
|
-
* the usual ones so the AI gets ONE message. WARN entries are never validated; unknown ids in
|
|
63
|
-
* `checklists[]` are ignored (forward-compat).
|
|
54
|
+
* unparseable, or structurally wrong. `required` is the set of checklists the diff matched: every one
|
|
55
|
+
* must have a well-formed, passing (or overridden) review-<id>.json or a validation error is raised
|
|
56
|
+
* alongside the usual ones so the AI gets ONE message.
|
|
64
57
|
*/
|
|
65
58
|
loadReviewJson(filePath: string, required?: readonly RequiredChecklist[]): ReviewJson;
|
|
66
|
-
private parseChecklistAcks;
|
|
67
59
|
loadChecklistResults(reviewJsonFilePath: string, required: readonly RequiredChecklist[]): ChecklistResult[];
|
|
68
|
-
resolveVerdict(req: RequiredChecklist,
|
|
60
|
+
resolveVerdict(req: RequiredChecklist, results: readonly ChecklistResult[]): ChecklistVerdict;
|
|
69
61
|
private requiredChecklistErrors;
|
|
70
62
|
private checklistFileName;
|
|
71
63
|
private parseChecklistResult;
|
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.
|
|
3
|
+
exports.ReviewJsonService = exports.PrContext = exports.ChecklistVerdict = exports.CK_MISSING = exports.CK_FAIL = exports.CK_OVERRIDDEN = exports.CK_PASS = exports.ReviewJson = exports.RequiredChecklist = exports.ChecklistResult = void 0;
|
|
4
4
|
exports.prDirFor = prDirFor;
|
|
5
5
|
exports.reviewJsonPath = reviewJsonPath;
|
|
6
6
|
exports.reviewJsonSchemaHint = reviewJsonSchemaHint;
|
|
@@ -10,34 +10,20 @@ const fs = tslib_1.__importStar(require("fs"));
|
|
|
10
10
|
const path = tslib_1.__importStar(require("path"));
|
|
11
11
|
const inversify_1 = require("inversify");
|
|
12
12
|
const constants_1 = require("./constants");
|
|
13
|
-
const checklist_config_1 = require("./checklist-config");
|
|
14
13
|
const inform_ai_error_1 = require("./inform-ai-error");
|
|
15
14
|
const to_error_1 = require("./to-error");
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
id;
|
|
20
|
-
acknowledged;
|
|
21
|
-
notes; // per-item findings the AI chose to record (optional; [] when none)
|
|
22
|
-
constructor(id, acknowledged, notes) {
|
|
23
|
-
this.id = id;
|
|
24
|
-
this.acknowledged = acknowledged;
|
|
25
|
-
this.notes = notes;
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
exports.ChecklistAck = ChecklistAck;
|
|
29
|
-
// The verdict a reviewer wrote into a PER-CHECKLIST file `.webpieces/pr-review/<branch>/review-<id>.json`,
|
|
30
|
-
// one per triggered checklist. Replaces the single shared `checklists[]` array so concurrent reviewer
|
|
31
|
-
// subagents never clobber one file. Unlike ChecklistAck's bare boolean, this records the OUTCOME:
|
|
15
|
+
// The verdict a reviewer SUBAGENT writes into `.webpieces/pr-review/<branch>/review-<id>.json`, one per
|
|
16
|
+
// matched checklist. One file per checklist so N concurrent reviewer subagents never clobber a shared
|
|
17
|
+
// file. It records the OUTCOME:
|
|
32
18
|
// success:true → PASS
|
|
33
19
|
// success:false + override non-empty → OVERRIDDEN (pass; the free-text justification reaches the PR)
|
|
34
20
|
// success:false + no override → FAIL (refuse; `output` is printed verbatim)
|
|
35
|
-
// `override` is deliberately free text, not a boolean — it forces the
|
|
36
|
-
// surfaces it on the dashboard, where a human sees it. Data-only (per CLAUDE.md).
|
|
21
|
+
// `override` is deliberately free text, not a boolean — it forces the ship-anyway decision to be stated
|
|
22
|
+
// in words and surfaces it on the dashboard, where a human sees it. Data-only (per CLAUDE.md).
|
|
37
23
|
class ChecklistResult {
|
|
38
24
|
id;
|
|
39
25
|
success;
|
|
40
|
-
output; // what the reviewer found; printed verbatim when
|
|
26
|
+
output; // what the reviewer found; printed verbatim when the checklist fails
|
|
41
27
|
override; // '' = no override; non-empty = ship-anyway justification (renders 🟡 overridden)
|
|
42
28
|
constructor(id, success, output, override) {
|
|
43
29
|
this.id = id;
|
|
@@ -47,31 +33,25 @@ class ChecklistResult {
|
|
|
47
33
|
}
|
|
48
34
|
}
|
|
49
35
|
exports.ChecklistResult = ChecklistResult;
|
|
50
|
-
// What the
|
|
51
|
-
//
|
|
52
|
-
//
|
|
36
|
+
// What the pr-gate command computed from the diff: a checklist this branch MATCHED (its patterns hit the
|
|
37
|
+
// diff, so its reviewer subagent must run). Drives review-<id>.json enforcement, provenance, the schema
|
|
38
|
+
// hint, and the dashboard. Data-only.
|
|
53
39
|
class RequiredChecklist {
|
|
54
|
-
id;
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
matchedFiles; // the changed files that triggered it (for the dashboard + hint)
|
|
60
|
-
subagent; // expected reviewer agentType ('' = no provenance requirement); enforced by finish
|
|
61
|
-
// eslint-disable-next-line @typescript-eslint/max-params
|
|
62
|
-
constructor(id, title, severity, docs, blockMessage, matchedFiles, subagent = '') {
|
|
40
|
+
id; // = subagent name; keys review-<id>.json
|
|
41
|
+
subagent; // reviewer agent that must run (agentType the harness stamps)
|
|
42
|
+
doc; // guidance doc the reviewer reads ('' → it reads the manifest doc)
|
|
43
|
+
matchedFiles; // the changed files that matched it (for the dashboard + hint)
|
|
44
|
+
constructor(id, subagent, doc, matchedFiles) {
|
|
63
45
|
this.id = id;
|
|
64
|
-
this.title = title;
|
|
65
|
-
this.severity = severity;
|
|
66
|
-
this.docs = docs;
|
|
67
|
-
this.blockMessage = blockMessage;
|
|
68
|
-
this.matchedFiles = matchedFiles;
|
|
69
46
|
this.subagent = subagent;
|
|
47
|
+
this.doc = doc;
|
|
48
|
+
this.matchedFiles = matchedFiles;
|
|
70
49
|
}
|
|
71
50
|
}
|
|
72
51
|
exports.RequiredChecklist = RequiredChecklist;
|
|
73
|
-
// The AI-authored review for a PR. The AI writes
|
|
74
|
-
// prints the schema) and `wp-finish-upsert-pr` (which reads it)
|
|
52
|
+
// The AI-authored review for a PR. The AI writes review.json itself between `wp-start-upsert-pr` (which
|
|
53
|
+
// prints the schema) and `wp-finish-upsert-pr` (which reads it); reviewer subagents write the per-checklist
|
|
54
|
+
// review-<id>.json files. Data-only (per CLAUDE.md).
|
|
75
55
|
class ReviewJson {
|
|
76
56
|
title; // human PR title describing the change; used as the `gh pr` title (empty → caller falls back)
|
|
77
57
|
riskScore; // 0–100, drives the risk bar
|
|
@@ -81,10 +61,9 @@ class ReviewJson {
|
|
|
81
61
|
violations; // pattern/architecture violations; length = the Pattern Violations count
|
|
82
62
|
risks;
|
|
83
63
|
filesToReview;
|
|
84
|
-
checklists; // legacy inline acknowledgments (back-compat); [] when none written
|
|
85
64
|
results; // resolved per-checklist verdicts (from review-<id>.json); [] when none
|
|
86
65
|
// eslint-disable-next-line @typescript-eslint/max-params
|
|
87
|
-
constructor(title, riskScore, riskLevel, riskEmoji, summary, violations, risks, filesToReview,
|
|
66
|
+
constructor(title, riskScore, riskLevel, riskEmoji, summary, violations, risks, filesToReview, results = []) {
|
|
88
67
|
this.title = title;
|
|
89
68
|
this.riskScore = riskScore;
|
|
90
69
|
this.riskLevel = riskLevel;
|
|
@@ -93,7 +72,6 @@ class ReviewJson {
|
|
|
93
72
|
this.violations = violations;
|
|
94
73
|
this.risks = risks;
|
|
95
74
|
this.filesToReview = filesToReview;
|
|
96
|
-
this.checklists = checklists;
|
|
97
75
|
this.results = results;
|
|
98
76
|
}
|
|
99
77
|
}
|
|
@@ -102,12 +80,11 @@ exports.ReviewJson = ReviewJson;
|
|
|
102
80
|
exports.CK_PASS = 'pass'; // review-<id>.json success:true
|
|
103
81
|
exports.CK_OVERRIDDEN = 'overridden'; // review-<id>.json success:false + non-empty override → 🟡
|
|
104
82
|
exports.CK_FAIL = 'fail'; // review-<id>.json success:false + no override → refuse
|
|
105
|
-
exports.CK_MISSING = 'missing'; // no
|
|
106
|
-
exports.CK_ACKED = 'acknowledged'; // legacy inline checklists[] ack satisfied (back-compat)
|
|
83
|
+
exports.CK_MISSING = 'missing'; // no review-<id>.json written → refuse
|
|
107
84
|
class ChecklistVerdict {
|
|
108
85
|
id;
|
|
109
|
-
status; // one of CK_PASS | CK_OVERRIDDEN | CK_FAIL | CK_MISSING
|
|
110
|
-
detail; // reviewer output / override justification
|
|
86
|
+
status; // one of CK_PASS | CK_OVERRIDDEN | CK_FAIL | CK_MISSING
|
|
87
|
+
detail; // reviewer output / override justification (for the dashboard + errors)
|
|
111
88
|
constructor(id, status, detail) {
|
|
112
89
|
this.id = id;
|
|
113
90
|
this.status = status;
|
|
@@ -115,6 +92,21 @@ class ChecklistVerdict {
|
|
|
115
92
|
}
|
|
116
93
|
}
|
|
117
94
|
exports.ChecklistVerdict = ChecklistVerdict;
|
|
95
|
+
// The PR's diff context, written by wp-start-upsert-pr into `.webpieces/pr-review/<branch>/pr-context.json`
|
|
96
|
+
// so a reviewer subagent knows the exact 3-point base the gate used and the full changed-file set — then
|
|
97
|
+
// reads any file's actual diff with `git diff <base> HEAD -- <file>`. This is what lets a checklist match
|
|
98
|
+
// coarsely by path (in the manifest) while the subagent makes the fine, content-level judgment. Data-only.
|
|
99
|
+
class PrContext {
|
|
100
|
+
base; // the 3-point merge-base sha the gate diffs against
|
|
101
|
+
head; // HEAD sha
|
|
102
|
+
changedFiles; // every file changed base..head (NOT tsOnly — includes .sql/.gql/Dockerfile/…)
|
|
103
|
+
constructor(base, head, changedFiles) {
|
|
104
|
+
this.base = base;
|
|
105
|
+
this.head = head;
|
|
106
|
+
this.changedFiles = changedFiles;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
exports.PrContext = PrContext;
|
|
118
110
|
const RISK_LEVELS = ['green', 'yellow', 'red'];
|
|
119
111
|
const EMOJI_FOR_LEVEL = { green: '🟢', yellow: '🟡', red: '🔴' };
|
|
120
112
|
/** Locates + loads/validates the AI-authored review.json. `@injectable(bindingScopeValues.Singleton)` so it's drawn in the design. */
|
|
@@ -127,11 +119,22 @@ let ReviewJsonService = class ReviewJsonService {
|
|
|
127
119
|
reviewJsonPath(repoRoot, featureName) {
|
|
128
120
|
return path.join(this.prDirFor(repoRoot, featureName), 'review.json');
|
|
129
121
|
}
|
|
130
|
-
//
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
//
|
|
122
|
+
// Absolute path of the pr-context.json for a feature (the diff base/head + changed files).
|
|
123
|
+
prContextPath(repoRoot, featureName) {
|
|
124
|
+
return path.join(this.prDirFor(repoRoot, featureName), 'pr-context.json');
|
|
125
|
+
}
|
|
126
|
+
// Persist the PR's diff context so reviewer subagents can read the changed-file set + the exact base
|
|
127
|
+
// sha (then `git diff <base> HEAD -- <file>` for content). Returns the file path written.
|
|
128
|
+
writePrContext(repoRoot, featureName, context) {
|
|
129
|
+
const dir = this.prDirFor(repoRoot, featureName);
|
|
130
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
131
|
+
const p = this.prContextPath(repoRoot, featureName);
|
|
132
|
+
fs.writeFileSync(p, JSON.stringify(context, null, 2) + '\n');
|
|
133
|
+
return p;
|
|
134
|
+
}
|
|
135
|
+
// Copy-paste schema both commands print. `required` is the set of checklists the diff MATCHED; empty
|
|
136
|
+
// ⇒ output identical to a repo with no checklists. Non-empty ⇒ appends per-checklist instructions
|
|
137
|
+
// naming the reviewer subagent + doc + the review-<id>.json to write.
|
|
135
138
|
reviewJsonSchemaHint(filePath, required = []) {
|
|
136
139
|
return (`Write your PR review to:\n ${filePath}\n\n` +
|
|
137
140
|
`with this exact JSON shape (riskEmoji optional — derived from riskLevel):\n\n` +
|
|
@@ -150,44 +153,33 @@ let ReviewJsonService = class ReviewJsonService {
|
|
|
150
153
|
checklistResultPath(reviewJsonFilePath, checklistId) {
|
|
151
154
|
return path.join(path.dirname(reviewJsonFilePath), `review-${checklistId}.json`);
|
|
152
155
|
}
|
|
153
|
-
// The
|
|
154
|
-
//
|
|
155
|
-
// file per checklist — `review-<id>.json` — so concurrent reviewer subagents never clobber a shared
|
|
156
|
-
// file. A BLOCK checklist with success:false and no override refuses to open the PR.
|
|
156
|
+
// The matched-checklist instruction block, appended ONLY when the diff matched a checklist. Each matched
|
|
157
|
+
// checklist must be reviewed by its OWN subagent (a distinct one), which writes review-<id>.json.
|
|
157
158
|
requiredChecklistHint(reviewJsonFilePath, required) {
|
|
158
159
|
if (required.length === 0)
|
|
159
160
|
return '';
|
|
160
|
-
const lines = ['', '', 'This
|
|
161
|
-
lines.push('
|
|
162
|
-
lines.push('
|
|
161
|
+
const lines = ['', '', 'This diff MATCHED company review checklist(s). Spawn EACH named subagent as a SEPARATE'];
|
|
162
|
+
lines.push('subagent (a different one per checklist — the coding agent may NOT self-certify). Each reads its');
|
|
163
|
+
lines.push('doc + the diff and writes its own review-<id>.json with this shape:');
|
|
163
164
|
lines.push('');
|
|
164
|
-
lines.push(' { "success": true, "output": "what you checked /
|
|
165
|
+
lines.push(' { "id": "<id>", "success": true, "output": "what you checked / found", "override": "" }');
|
|
166
|
+
lines.push(' (success:false with no non-empty "override" refuses the PR; a non-empty override ships it 🟡.)');
|
|
165
167
|
lines.push('');
|
|
166
168
|
for (const req of required) {
|
|
167
|
-
|
|
168
|
-
? 'BLOCK — success:false with no "override" will NOT open the PR'
|
|
169
|
-
: 'WARN — recorded on the dashboard; never blocks';
|
|
170
|
-
lines.push(` • [${req.id}] ${req.title} (${gate})`);
|
|
169
|
+
lines.push(` • [${req.id}] reviewer subagent: ${req.subagent}`);
|
|
171
170
|
lines.push(` write: ${this.checklistResultPath(reviewJsonFilePath, req.id)}`);
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
lines.push(` must be reviewed by the "${req.subagent.trim()}" subagent (its independent run is verified from the harness).`);
|
|
175
|
-
if (req.blockMessage.trim() !== '')
|
|
176
|
-
lines.push(` ${req.blockMessage.trim()}`);
|
|
171
|
+
if (req.doc.trim() !== '')
|
|
172
|
+
lines.push(` doc to read: ${req.doc}`);
|
|
177
173
|
if (req.matchedFiles.length > 0)
|
|
178
|
-
lines.push(`
|
|
174
|
+
lines.push(` matched: ${req.matchedFiles.slice(0, 5).join(', ')}`);
|
|
179
175
|
}
|
|
180
176
|
return lines.join('\n');
|
|
181
177
|
}
|
|
182
178
|
/**
|
|
183
179
|
* Load + validate the AI-authored review.json. Throws InformAiError (with the schema) when missing,
|
|
184
|
-
* unparseable, or structurally wrong.
|
|
185
|
-
*
|
|
186
|
-
*
|
|
187
|
-
* which case this behaves byte-identically to before the feature). Every BLOCK entry must appear in
|
|
188
|
-
* review.json's `checklists[]` with `acknowledged: true`, or a validation error is raised alongside
|
|
189
|
-
* the usual ones so the AI gets ONE message. WARN entries are never validated; unknown ids in
|
|
190
|
-
* `checklists[]` are ignored (forward-compat).
|
|
180
|
+
* unparseable, or structurally wrong. `required` is the set of checklists the diff matched: every one
|
|
181
|
+
* must have a well-formed, passing (or overridden) review-<id>.json or a validation error is raised
|
|
182
|
+
* alongside the usual ones so the AI gets ONE message.
|
|
191
183
|
*/
|
|
192
184
|
// webpieces-disable max-lines-new-methods -- one cohesive load+validate pass over the review fields
|
|
193
185
|
loadReviewJson(filePath, required = []) {
|
|
@@ -212,9 +204,8 @@ let ReviewJsonService = class ReviewJsonService {
|
|
|
212
204
|
if (title === '') {
|
|
213
205
|
errors.push('"title" must be a non-empty, imperative PR title describing the change (no branch names).');
|
|
214
206
|
}
|
|
215
|
-
const acks = this.parseChecklistAcks(raw['checklists']);
|
|
216
207
|
const results = this.loadChecklistResults(filePath, required);
|
|
217
|
-
for (const err of this.requiredChecklistErrors(required,
|
|
208
|
+
for (const err of this.requiredChecklistErrors(required, results))
|
|
218
209
|
errors.push(err);
|
|
219
210
|
if (errors.length > 0) {
|
|
220
211
|
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` +
|
|
@@ -226,32 +217,11 @@ let ReviewJsonService = class ReviewJsonService {
|
|
|
226
217
|
? raw['riskEmoji']
|
|
227
218
|
: (EMOJI_FOR_LEVEL[level] ?? '🟡');
|
|
228
219
|
const summary = typeof raw['summary'] === 'string' ? raw['summary'] : '';
|
|
229
|
-
return new ReviewJson(title, riskScore, level, emoji, summary, this.asStringArray(raw['violations']), this.asStringArray(raw['risks']), this.asStringArray(raw['filesToReview']),
|
|
230
|
-
}
|
|
231
|
-
// Parse the AI-authored `checklists[]` into typed ChecklistAck[]. Tolerant of a missing/garbage
|
|
232
|
-
// field (→ []) and of non-object entries (skipped) — malformed acks simply fail to satisfy a BLOCK
|
|
233
|
-
// requirement rather than crashing the load.
|
|
234
|
-
// webpieces-disable no-any-unknown -- opaque parsed JSON value, narrowed here
|
|
235
|
-
parseChecklistAcks(value) {
|
|
236
|
-
if (!Array.isArray(value))
|
|
237
|
-
return [];
|
|
238
|
-
const acks = [];
|
|
239
|
-
for (const entry of value) {
|
|
240
|
-
if (typeof entry !== 'object' || entry === null || Array.isArray(entry))
|
|
241
|
-
continue;
|
|
242
|
-
// webpieces-disable no-any-unknown -- one opaque ack entry, narrowed field-by-field
|
|
243
|
-
const e = entry;
|
|
244
|
-
const id = typeof e['id'] === 'string' ? e['id'] : '';
|
|
245
|
-
if (id === '')
|
|
246
|
-
continue;
|
|
247
|
-
acks.push(new ChecklistAck(id, e['acknowledged'] === true, this.asStringArray(e['notes'])));
|
|
248
|
-
}
|
|
249
|
-
return acks;
|
|
220
|
+
return new ReviewJson(title, riskScore, level, emoji, summary, this.asStringArray(raw['violations']), this.asStringArray(raw['risks']), this.asStringArray(raw['filesToReview']), results);
|
|
250
221
|
}
|
|
251
|
-
// Read the per-checklist verdict files `review-<id>.json`
|
|
252
|
-
//
|
|
253
|
-
//
|
|
254
|
-
// skipped (an unknown/stale review-<id>.json never wedges the branch).
|
|
222
|
+
// Read the per-checklist verdict files `review-<id>.json` beside review.json — one per matched checklist.
|
|
223
|
+
// A missing file is simply absent from the result (→ counts as MISSING for that checklist); a malformed
|
|
224
|
+
// one is skipped (a stale review-<id>.json never wedges the branch).
|
|
255
225
|
loadChecklistResults(reviewJsonFilePath, required) {
|
|
256
226
|
const results = [];
|
|
257
227
|
for (const req of required) {
|
|
@@ -264,42 +234,33 @@ let ReviewJsonService = class ReviewJsonService {
|
|
|
264
234
|
}
|
|
265
235
|
return results;
|
|
266
236
|
}
|
|
267
|
-
// Resolve ONE checklist's verdict from its
|
|
268
|
-
//
|
|
269
|
-
resolveVerdict(req,
|
|
237
|
+
// Resolve ONE checklist's verdict from its review-<id>.json. Central so review.json enforcement AND the
|
|
238
|
+
// finish-command dashboard agree on the outcome.
|
|
239
|
+
resolveVerdict(req, results) {
|
|
270
240
|
const result = results.find((r) => r.id === req.id);
|
|
271
|
-
if (result)
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
return new ChecklistVerdict(req.id, exports.
|
|
277
|
-
|
|
278
|
-
const ack = acks.find((a) => a.id === req.id);
|
|
279
|
-
if (ack && ack.acknowledged)
|
|
280
|
-
return new ChecklistVerdict(req.id, exports.CK_ACKED, ack.notes.join('; '));
|
|
281
|
-
return new ChecklistVerdict(req.id, exports.CK_MISSING, '');
|
|
241
|
+
if (!result)
|
|
242
|
+
return new ChecklistVerdict(req.id, exports.CK_MISSING, '');
|
|
243
|
+
if (result.success)
|
|
244
|
+
return new ChecklistVerdict(req.id, exports.CK_PASS, result.output);
|
|
245
|
+
if (result.override.trim() !== '')
|
|
246
|
+
return new ChecklistVerdict(req.id, exports.CK_OVERRIDDEN, result.override.trim());
|
|
247
|
+
return new ChecklistVerdict(req.id, exports.CK_FAIL, result.output);
|
|
282
248
|
}
|
|
283
|
-
//
|
|
284
|
-
//
|
|
285
|
-
|
|
286
|
-
requiredChecklistErrors(required, acks, results) {
|
|
249
|
+
// Every matched checklist whose verdict is FAIL (reviewed, found a problem, no override) or MISSING (no
|
|
250
|
+
// review-<id>.json written) → one error each, printing the reviewer's `output` verbatim.
|
|
251
|
+
requiredChecklistErrors(required, results) {
|
|
287
252
|
const errors = [];
|
|
288
253
|
for (const req of required) {
|
|
289
|
-
|
|
290
|
-
continue;
|
|
291
|
-
const verdict = this.resolveVerdict(req, acks, results);
|
|
254
|
+
const verdict = this.resolveVerdict(req, results);
|
|
292
255
|
if (verdict.status === exports.CK_FAIL) {
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
`The reviewer wrote:\n ${verdict.detail.split('\n').join('\n ')}\n` +
|
|
256
|
+
errors.push(`Checklist "${req.id}" FAILED review. The reviewer (${req.subagent}) wrote:\n ` +
|
|
257
|
+
`${verdict.detail.split('\n').join('\n ')}\n` +
|
|
296
258
|
` Fix it, then re-run; or set a non-empty "override" in ${this.checklistFileName(req.id)} to ship anyway with a stated justification.`);
|
|
297
259
|
}
|
|
298
260
|
else if (verdict.status === exports.CK_MISSING) {
|
|
299
|
-
const
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
`Write ${this.checklistFileName(req.id)} with {"success":true,"output":"…"} once you have walked it.${docs}`);
|
|
261
|
+
const doc = req.doc.trim() !== '' ? ` Read: ${req.doc}.` : '';
|
|
262
|
+
errors.push(`Checklist "${req.id}" MATCHED this diff but has no verdict. Spawn the "${req.subagent}" subagent to review it, ` +
|
|
263
|
+
`then write ${this.checklistFileName(req.id)} with {"id":"${req.id}","success":true,"output":"…"}.${doc}`);
|
|
303
264
|
}
|
|
304
265
|
}
|
|
305
266
|
return errors;
|
|
@@ -337,8 +298,7 @@ let ReviewJsonService = class ReviewJsonService {
|
|
|
337
298
|
return value.filter((v) => typeof v === 'string');
|
|
338
299
|
}
|
|
339
300
|
// Parse opaque AI-authored JSON, converting a SyntaxError into a readable InformAiError. `required`
|
|
340
|
-
// is threaded through so a JSON syntax error still prints the checklist instructions the AI needs
|
|
341
|
-
// exactly when it most needs them — rather than the bare schema.
|
|
301
|
+
// is threaded through so a JSON syntax error still prints the checklist instructions the AI needs.
|
|
342
302
|
// webpieces-disable no-any-unknown -- returns the opaque parsed object; loadReviewJson narrows each field
|
|
343
303
|
parseReviewJson(raw, filePath, required) {
|
|
344
304
|
// webpieces-disable no-unmanaged-exceptions -- chokepoint: convert JSON.parse SyntaxError to an InformAiError for the AI
|