@webpieces/rules-config 0.4.482 → 0.4.484

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,180 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.ChecklistManifestService = void 0;
4
- const tslib_1 = require("tslib");
5
- const fs = tslib_1.__importStar(require("fs"));
6
- const path = tslib_1.__importStar(require("path"));
7
- const inversify_1 = require("inversify");
8
- const checklist_config_1 = require("./checklist-config");
9
- const to_error_1 = require("./to-error");
10
- // The delimited JSON manifest embedded in the review doc — the LEGACY shape, kept working because it is
11
- // shipped. An HTML comment so it does NOT render in the markdown a human reads, but is trivially +
12
- // robustly parseable (JSON.parse — no YAML dependency, no hand-rolled frontmatter parser). Example, at the
13
- // top of `.claude/review/index.md`:
14
- //
15
- // <!-- webpieces:checklists
16
- // [ { "subagent": "morpheus-envvars-reviewer", "doc": "morpheus-envvars.md",
17
- // "patterns": ["**/.env*", "**/Dockerfile*"] } ]
18
- // -->
19
- //
20
- // New repos should put the SAME array directly in `pr-gate.checklists` in webpieces.config.json instead —
21
- // see ChecklistSource. A JSON array inside an HTML comment is reachable only by this regex: no JSON Schema
22
- // covers it, no editor completes it, no `jq` reads it.
23
- const MANIFEST_RE = /<!--\s*webpieces:checklists\s*([\s\S]*?)-->/;
24
- // Where a reviewer subagent's definition must live for Claude Code to be able to spawn it.
25
- const AGENTS_DIR = path.join('.claude', 'agents');
26
- /**
27
- * Loads + validates a repo's review checklists from EITHER shape (see {@link ChecklistSource}): the
28
- * `pr-gate.checklists` array in webpieces.config.json (primary) or the `<!-- webpieces:checklists -->`
29
- * manifest embedded in the doc that `pr-gate.checklists.doc` points at (legacy).
30
- *
31
- * `@injectable(bindingScopeValues.Singleton)` so it is injected by type + drawn in the DI design.
32
- */
33
- let ChecklistManifestService = class ChecklistManifestService {
34
- // Absolute path of the manifest doc under repoRoot.
35
- manifestDocPath(repoRoot, docRel) {
36
- return path.join(repoRoot, docRel);
37
- }
38
- /**
39
- * The repo's checklists, or [] when there are none / the manifest doc is missing / malformed. Callers
40
- * that need to REPORT those problems use validate(); this one is the tolerant runtime read.
41
- */
42
- load(repoRoot, source) {
43
- if (source.inline.length > 0)
44
- return this.usable(source.inline);
45
- if (source.doc.trim() === '')
46
- return [];
47
- const items = this.readItems(this.manifestDocPath(repoRoot, source.doc));
48
- if (items === null)
49
- return [];
50
- return this.usable(items.map((raw) => (0, checklist_config_1.toChecklist)(raw, path.posix.dirname(source.doc))));
51
- }
52
- // Human-readable errors for the repo's checklist config, or [] when valid. Never throws.
53
- validate(repoRoot, source) {
54
- if (source.inline.length > 0)
55
- return this.validateResolved(source.inline, repoRoot, source.describe());
56
- if (source.doc.trim() === '')
57
- return [];
58
- const docRel = source.doc;
59
- const docPath = this.manifestDocPath(repoRoot, docRel);
60
- if (!fs.existsSync(docPath)) {
61
- return [`[pr-gate] checklists.doc "${docRel}" does not exist — it must be a markdown doc carrying a <!-- webpieces:checklists [...] --> manifest.`];
62
- }
63
- const raw = this.extractManifest(fs.readFileSync(docPath, 'utf8'));
64
- if (raw === '') {
65
- return [`[pr-gate] "${docRel}" has no <!-- webpieces:checklists [...] --> block. Add a JSON array of { subagent, doc?, patterns? } inside that HTML comment.`];
66
- }
67
- const parsed = this.parse(raw);
68
- if (parsed === null) {
69
- return [`[pr-gate] the <!-- webpieces:checklists --> block in "${docRel}" is not a valid JSON array.`];
70
- }
71
- return this.validateItems(parsed, repoRoot, docRel);
72
- }
73
- // Validate raw manifest entries (legacy shape): narrow each to a ChecklistDefinition with its doc
74
- // resolved against the manifest doc's directory, then run the SAME checks the array form gets.
75
- validateItems(items, repoRoot, docRel) {
76
- const errors = [];
77
- const docBase = path.posix.dirname(docRel);
78
- items.forEach((item, i) => {
79
- const label = typeof item.subagent === 'string' && item.subagent !== '' ? `"${item.subagent}"` : `checklists[${i}]`;
80
- if (item.patterns !== undefined && !this.isStringArray(item.patterns)) {
81
- errors.push(`[pr-gate] ${docRel} ${label}.patterns must be a string[] of path globs (omit or [] to run on every PR).`);
82
- }
83
- });
84
- const defs = items.map((item) => (0, checklist_config_1.toChecklist)(item, docBase));
85
- return [...errors, ...this.validateResolved(defs, repoRoot, docRel)];
86
- }
87
- /**
88
- * The checks that are the SAME for both config shapes, run against already-narrowed definitions:
89
- * subagent present + distinct + actually resolves to a spawnable agent, and the guidance doc exists.
90
- */
91
- validateResolved(defs, repoRoot, sourceLabel) {
92
- const errors = [];
93
- const seen = new Set();
94
- // Only enforce the reviewer-agent file when this repo HAS an agents dir — a non-Claude-Code consumer
95
- // that drives the gate some other way must not be broken by a check for a directory it never has.
96
- const agentsDir = path.join(repoRoot, AGENTS_DIR);
97
- const checkAgents = fs.existsSync(agentsDir);
98
- defs.forEach((def, i) => {
99
- const label = def.subagent !== '' ? `"${def.subagent}"` : `checklists[${i}]`;
100
- if (def.subagent.trim() === '') {
101
- errors.push(`[pr-gate] ${sourceLabel} checklists[${i}].subagent must be a non-empty string (the reviewer agent name, matching .claude/agents/<subagent>.md).`);
102
- }
103
- else if (seen.has(def.subagent)) {
104
- errors.push(`[pr-gate] ${sourceLabel} duplicate subagent "${def.subagent}" — each checklist must use a DISTINCT reviewer subagent (that is how independent review is enforced).`);
105
- }
106
- else {
107
- seen.add(def.subagent);
108
- errors.push(...this.validateSubagentExists(def.subagent, label, checkAgents, agentsDir, sourceLabel));
109
- }
110
- if (def.doc !== '' && !fs.existsSync(path.join(repoRoot, def.doc))) {
111
- errors.push(`[pr-gate] ${sourceLabel} ${label}.doc "${def.doc}" does not exist (resolved repo-relative).`);
112
- }
113
- });
114
- return errors;
115
- }
116
- /**
117
- * The check this validator was missing: `subagent` is the ONE required field and the whole
118
- * distinct-reviewer guarantee rests on it, yet nothing confirmed it names a real agent. A typo used to
119
- * validate clean, then get printed to the coding agent as "spawn this" — and since wp-finish blocks on
120
- * `review-<that-typo>.json`, the path of least resistance became writing the reviewer's verdict itself,
121
- * which is the exact self-certification the distinct-subagent rule exists to prevent. Reject at load.
122
- */
123
- // eslint-disable-next-line @typescript-eslint/max-params
124
- validateSubagentExists(subagent, label, checkAgents, agentsDir, sourceLabel) {
125
- if (!checkAgents)
126
- return [];
127
- if (fs.existsSync(path.join(agentsDir, `${subagent}.md`)))
128
- return [];
129
- return [
130
- `[pr-gate] ${sourceLabel} ${label}.subagent names no reviewer — ${AGENTS_DIR}/${subagent}.md does not exist, ` +
131
- `so nothing can spawn it and wp-finish-upsert-pr would block forever on a review-${subagent}.json that no reviewer can write. ` +
132
- `Create that agent file or fix the name.`,
133
- ];
134
- }
135
- // Drop entries with no subagent — they have no id, so they can key neither review-<id>.json nor a
136
- // dashboard row. validate() reports them; the tolerant load() just skips them.
137
- usable(defs) {
138
- return defs.filter((d) => d.subagent !== '');
139
- }
140
- // Read the manifest doc and return its parsed items, or null on any problem (missing/no block/bad JSON).
141
- readItems(docPath) {
142
- if (!fs.existsSync(docPath))
143
- return null;
144
- const raw = this.extractManifest(fs.readFileSync(docPath, 'utf8'));
145
- if (raw === '')
146
- return null;
147
- return this.parse(raw);
148
- }
149
- extractManifest(content) {
150
- const m = MANIFEST_RE.exec(content);
151
- return m ? m[1].trim() : '';
152
- }
153
- // webpieces-disable no-any-unknown -- opaque parsed JSON, narrowed to an item array by the caller
154
- parse(json) {
155
- // webpieces-disable no-unmanaged-exceptions -- chokepoint: a malformed manifest is reported as one error / ignored
156
- // eslint-disable-next-line @webpieces/no-unmanaged-exceptions
157
- try {
158
- // webpieces-disable no-any-unknown -- parsed JSON is opaque until narrowed below
159
- const value = JSON.parse(json);
160
- if (!Array.isArray(value))
161
- return null;
162
- // webpieces-disable no-any-unknown -- each opaque entry is narrowed field-by-field downstream
163
- return value;
164
- }
165
- catch (err) {
166
- const error = (0, to_error_1.toError)(err);
167
- void error;
168
- return null;
169
- }
170
- }
171
- // webpieces-disable no-any-unknown -- opaque parsed JSON value, narrowed to string[] here
172
- isStringArray(value) {
173
- return Array.isArray(value) && value.every((v) => typeof v === 'string');
174
- }
175
- };
176
- exports.ChecklistManifestService = ChecklistManifestService;
177
- exports.ChecklistManifestService = ChecklistManifestService = tslib_1.__decorate([
178
- (0, inversify_1.injectable)(inversify_1.bindingScopeValues.Singleton)
179
- ], ChecklistManifestService);
180
- //# sourceMappingURL=checklist-manifest.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"checklist-manifest.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/checklist-manifest.ts"],"names":[],"mappings":"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","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { ChecklistDefinition, ChecklistSource, RawChecklistItem, toChecklist } from './checklist-config';\nimport { toError } from './to-error';\n\n// The delimited JSON manifest embedded in the review doc — the LEGACY shape, kept working because it is\n// shipped. An HTML comment so it does NOT render in the markdown a human reads, but is trivially +\n// robustly parseable (JSON.parse — no YAML dependency, no hand-rolled frontmatter parser). Example, at the\n// top of `.claude/review/index.md`:\n//\n// <!-- webpieces:checklists\n// [ { \"subagent\": \"morpheus-envvars-reviewer\", \"doc\": \"morpheus-envvars.md\",\n// \"patterns\": [\"**/.env*\", \"**/Dockerfile*\"] } ]\n// -->\n//\n// New repos should put the SAME array directly in `pr-gate.checklists` in webpieces.config.json instead —\n// see ChecklistSource. A JSON array inside an HTML comment is reachable only by this regex: no JSON Schema\n// covers it, no editor completes it, no `jq` reads it.\nconst MANIFEST_RE = /<!--\\s*webpieces:checklists\\s*([\\s\\S]*?)-->/;\n\n// Where a reviewer subagent's definition must live for Claude Code to be able to spawn it.\nconst AGENTS_DIR = path.join('.claude', 'agents');\n\n/**\n * Loads + validates a repo's review checklists from EITHER shape (see {@link ChecklistSource}): the\n * `pr-gate.checklists` array in webpieces.config.json (primary) or the `<!-- webpieces:checklists -->`\n * manifest embedded in the doc that `pr-gate.checklists.doc` points at (legacy).\n *\n * `@injectable(bindingScopeValues.Singleton)` so it is injected by type + drawn in the DI design.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class ChecklistManifestService {\n // Absolute path of the manifest doc under repoRoot.\n manifestDocPath(repoRoot: string, docRel: string): string {\n return path.join(repoRoot, docRel);\n }\n\n /**\n * The repo's checklists, or [] when there are none / the manifest doc is missing / malformed. Callers\n * that need to REPORT those problems use validate(); this one is the tolerant runtime read.\n */\n load(repoRoot: string, source: ChecklistSource): ChecklistDefinition[] {\n if (source.inline.length > 0) return this.usable(source.inline);\n if (source.doc.trim() === '') return [];\n const items = this.readItems(this.manifestDocPath(repoRoot, source.doc));\n if (items === null) return [];\n return this.usable(items.map((raw: RawChecklistItem): ChecklistDefinition => toChecklist(raw, path.posix.dirname(source.doc))));\n }\n\n // Human-readable errors for the repo's checklist config, or [] when valid. Never throws.\n validate(repoRoot: string, source: ChecklistSource): string[] {\n if (source.inline.length > 0) return this.validateResolved(source.inline, repoRoot, source.describe());\n if (source.doc.trim() === '') return [];\n const docRel = source.doc;\n const docPath = this.manifestDocPath(repoRoot, docRel);\n if (!fs.existsSync(docPath)) {\n return [`[pr-gate] checklists.doc \"${docRel}\" does not exist — it must be a markdown doc carrying a <!-- webpieces:checklists [...] --> manifest.`];\n }\n const raw = this.extractManifest(fs.readFileSync(docPath, 'utf8'));\n if (raw === '') {\n return [`[pr-gate] \"${docRel}\" has no <!-- webpieces:checklists [...] --> block. Add a JSON array of { subagent, doc?, patterns? } inside that HTML comment.`];\n }\n const parsed = this.parse(raw);\n if (parsed === null) {\n return [`[pr-gate] the <!-- webpieces:checklists --> block in \"${docRel}\" is not a valid JSON array.`];\n }\n return this.validateItems(parsed, repoRoot, docRel);\n }\n\n // Validate raw manifest entries (legacy shape): narrow each to a ChecklistDefinition with its doc\n // resolved against the manifest doc's directory, then run the SAME checks the array form gets.\n private validateItems(items: readonly RawChecklistItem[], repoRoot: string, docRel: string): string[] {\n const errors: string[] = [];\n const docBase = path.posix.dirname(docRel);\n items.forEach((item: RawChecklistItem, i: number): void => {\n const label = typeof item.subagent === 'string' && item.subagent !== '' ? `\"${item.subagent}\"` : `checklists[${i}]`;\n if (item.patterns !== undefined && !this.isStringArray(item.patterns)) {\n errors.push(`[pr-gate] ${docRel} ${label}.patterns must be a string[] of path globs (omit or [] to run on every PR).`);\n }\n });\n const defs = items.map((item: RawChecklistItem): ChecklistDefinition => toChecklist(item, docBase));\n return [...errors, ...this.validateResolved(defs, repoRoot, docRel)];\n }\n\n /**\n * The checks that are the SAME for both config shapes, run against already-narrowed definitions:\n * subagent present + distinct + actually resolves to a spawnable agent, and the guidance doc exists.\n */\n private validateResolved(defs: readonly ChecklistDefinition[], repoRoot: string, sourceLabel: string): string[] {\n const errors: string[] = [];\n const seen = new Set<string>();\n // Only enforce the reviewer-agent file when this repo HAS an agents dir — a non-Claude-Code consumer\n // that drives the gate some other way must not be broken by a check for a directory it never has.\n const agentsDir = path.join(repoRoot, AGENTS_DIR);\n const checkAgents = fs.existsSync(agentsDir);\n defs.forEach((def: ChecklistDefinition, i: number): void => {\n const label = def.subagent !== '' ? `\"${def.subagent}\"` : `checklists[${i}]`;\n if (def.subagent.trim() === '') {\n errors.push(`[pr-gate] ${sourceLabel} checklists[${i}].subagent must be a non-empty string (the reviewer agent name, matching .claude/agents/<subagent>.md).`);\n } else if (seen.has(def.subagent)) {\n errors.push(`[pr-gate] ${sourceLabel} duplicate subagent \"${def.subagent}\" — each checklist must use a DISTINCT reviewer subagent (that is how independent review is enforced).`);\n } else {\n seen.add(def.subagent);\n errors.push(...this.validateSubagentExists(def.subagent, label, checkAgents, agentsDir, sourceLabel));\n }\n if (def.doc !== '' && !fs.existsSync(path.join(repoRoot, def.doc))) {\n errors.push(`[pr-gate] ${sourceLabel} ${label}.doc \"${def.doc}\" does not exist (resolved repo-relative).`);\n }\n });\n return errors;\n }\n\n /**\n * The check this validator was missing: `subagent` is the ONE required field and the whole\n * distinct-reviewer guarantee rests on it, yet nothing confirmed it names a real agent. A typo used to\n * validate clean, then get printed to the coding agent as \"spawn this\" — and since wp-finish blocks on\n * `review-<that-typo>.json`, the path of least resistance became writing the reviewer's verdict itself,\n * which is the exact self-certification the distinct-subagent rule exists to prevent. Reject at load.\n */\n // eslint-disable-next-line @typescript-eslint/max-params\n private validateSubagentExists(subagent: string, label: string, checkAgents: boolean, agentsDir: string, sourceLabel: string): string[] {\n if (!checkAgents) return [];\n if (fs.existsSync(path.join(agentsDir, `${subagent}.md`))) return [];\n return [\n `[pr-gate] ${sourceLabel} ${label}.subagent names no reviewer — ${AGENTS_DIR}/${subagent}.md does not exist, ` +\n `so nothing can spawn it and wp-finish-upsert-pr would block forever on a review-${subagent}.json that no reviewer can write. ` +\n `Create that agent file or fix the name.`,\n ];\n }\n\n // Drop entries with no subagent — they have no id, so they can key neither review-<id>.json nor a\n // dashboard row. validate() reports them; the tolerant load() just skips them.\n private usable(defs: readonly ChecklistDefinition[]): ChecklistDefinition[] {\n return defs.filter((d: ChecklistDefinition): boolean => d.subagent !== '');\n }\n\n // Read the manifest doc and return its parsed items, or null on any problem (missing/no block/bad JSON).\n private readItems(docPath: string): RawChecklistItem[] | null {\n if (!fs.existsSync(docPath)) return null;\n const raw = this.extractManifest(fs.readFileSync(docPath, 'utf8'));\n if (raw === '') return null;\n return this.parse(raw);\n }\n\n private extractManifest(content: string): string {\n const m = MANIFEST_RE.exec(content);\n return m ? m[1].trim() : '';\n }\n\n // webpieces-disable no-any-unknown -- opaque parsed JSON, narrowed to an item array by the caller\n private parse(json: string): RawChecklistItem[] | null {\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: a malformed manifest is reported as one error / ignored\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n // webpieces-disable no-any-unknown -- parsed JSON is opaque until narrowed below\n const value = JSON.parse(json) as unknown;\n if (!Array.isArray(value)) return null;\n // webpieces-disable no-any-unknown -- each opaque entry is narrowed field-by-field downstream\n return value as RawChecklistItem[];\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return null;\n }\n }\n\n // webpieces-disable no-any-unknown -- opaque parsed JSON value, narrowed to string[] here\n private isStringArray(value: unknown): boolean {\n return Array.isArray(value) && value.every((v: unknown): boolean => typeof v === 'string');\n }\n}\n"]}