@webpieces/pr-gate 0.4.610 → 0.4.611

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webpieces/pr-gate",
3
- "version": "0.4.610",
3
+ "version": "0.4.611",
4
4
  "description": "Gated PR system: 3-point squash-merge, merge validation gate, and red/yellow/green PR dashboard. Standalone scripts, no Nx dependency required.",
5
5
  "type": "commonjs",
6
6
  "main": "./src/index.js",
@@ -15,7 +15,7 @@
15
15
  "directory": "packages/tooling/pr-gate"
16
16
  },
17
17
  "dependencies": {
18
- "@webpieces/rules-config": "0.4.610",
18
+ "@webpieces/rules-config": "0.4.611",
19
19
  "@inversifyjs/binding-decorators": "1.1.5",
20
20
  "inversify": "7.10.4",
21
21
  "reflect-metadata": "0.2.2"
@@ -1,6 +1,5 @@
1
1
  import { HomeConfigService } from '@webpieces/rules-config';
2
2
  import { BuildGateLog } from './build-gate-log';
3
- export declare const DEFAULT_BUILD_COMMAND = "pnpm nx affected --target=ci --base=$(git merge-base origin/main HEAD)";
4
3
  /**
5
4
  * The caller-supplied framing for the build gate (label, re-run command, failure headline, stage id). Kept
6
5
  * as a parameter object so runBuildGate stays agnostic of who invokes it. A class (not an object literal)
@@ -1,18 +1,17 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.BuildAffected = exports.BuildGateOptions = exports.DEFAULT_BUILD_COMMAND = void 0;
3
+ exports.BuildAffected = exports.BuildGateOptions = void 0;
4
4
  const tslib_1 = require("tslib");
5
5
  const child_process_1 = require("child_process");
6
6
  const rules_config_1 = require("@webpieces/rules-config");
7
7
  const inversify_1 = require("inversify");
8
8
  const build_gate_log_1 = require("./build-gate-log");
9
- // Single source of truth for the build gate. Only `wp-finish-upsert-pr` runs it (authoritatively,
10
- // before the one push) — `wp-start-upsert-pr` deliberately runs NO build gate, so `pr-gate.buildCommand`
11
- // is a finish-only knob. nx `affected` only rebuilds changed projects.
12
- // `--base=$(git merge-base origin/main HEAD)` (the fork point) instead of `--base=origin/main`:
13
- // origin/main rebuilds projects touched by OTHER people's merged PRs. The fork point scopes affected to
14
- // only YOUR branch's changes. The `$(...)` resolves because runBuildAffected runs with shell: true.
15
- exports.DEFAULT_BUILD_COMMAND = 'pnpm nx affected --target=ci --base=$(git merge-base origin/main HEAD)';
9
+ // Single source of truth for RUNNING the build gate. `wp-start-upsert-pr` runs NO build; stage ②
10
+ // (`wp-review-upsert-pr`) runs it authoritatively before any reviewer is spawned, and stage ③
11
+ // (`wp-finish-upsert-pr`) re-runs it only when HEAD has moved since. nx `affected` only rebuilds
12
+ // changed projects. The fallback command itself lives in @webpieces/rules-config as
13
+ // DEFAULT_BUILD_COMMAND — whole-repo-build-guard prints the same string, and one definition is what
14
+ // keeps the refusal message naming the build that actually runs.
16
15
  const SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n';
17
16
  /**
18
17
  * The caller-supplied framing for the build gate (label, re-run command, failure headline, stage id). Kept
@@ -59,7 +58,7 @@ let BuildAffected = class BuildAffected {
59
58
  */
60
59
  resolveBuildCommand(repoRoot) {
61
60
  const configured = (0, rules_config_1.loadAndValidate)(repoRoot).prGate.buildCommand;
62
- return configured !== undefined && configured.trim() !== '' ? configured : exports.DEFAULT_BUILD_COMMAND;
61
+ return configured !== undefined && configured.trim() !== '' ? configured : rules_config_1.DEFAULT_BUILD_COMMAND;
63
62
  }
64
63
  /**
65
64
  * Run the build gate. Returns the process exit code (0 = pass).
@@ -68,7 +67,7 @@ let BuildAffected = class BuildAffected {
68
67
  * `▶ Build gate: <cmd>` banner on top of that, so the command appeared twice in a row.
69
68
  */
70
69
  runBuildAffected(repoRoot, buildCommand) {
71
- const cmd = buildCommand !== undefined && buildCommand.trim() !== '' ? buildCommand : exports.DEFAULT_BUILD_COMMAND;
70
+ const cmd = buildCommand !== undefined && buildCommand.trim() !== '' ? buildCommand : rules_config_1.DEFAULT_BUILD_COMMAND;
72
71
  const result = (0, child_process_1.spawnSync)(cmd, { stdio: 'inherit', cwd: repoRoot, shell: true });
73
72
  return result.status ?? 1;
74
73
  }
@@ -1 +1 @@
1
- {"version":3,"file":"build-affected.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/build-affected.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawnSync } from 'child_process';\nimport { loadAndValidate, CliExitError, HomeConfigService } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { BuildGateLog } from './build-gate-log';\n\n// Single source of truth for the build gate. Only `wp-finish-upsert-pr` runs it (authoritatively,\n// before the one push) — `wp-start-upsert-pr` deliberately runs NO build gate, so `pr-gate.buildCommand`\n// is a finish-only knob. nx `affected` only rebuilds changed projects.\n// `--base=$(git merge-base origin/main HEAD)` (the fork point) instead of `--base=origin/main`:\n// origin/main rebuilds projects touched by OTHER people's merged PRs. The fork point scopes affected to\n// only YOUR branch's changes. The `$(...)` resolves because runBuildAffected runs with shell: true.\nexport const DEFAULT_BUILD_COMMAND = 'pnpm nx affected --target=ci --base=$(git merge-base origin/main HEAD)';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n/**\n * The caller-supplied framing for the build gate (label, re-run command, failure headline, stage id). Kept\n * as a parameter object so runBuildGate stays agnostic of who invokes it. A class (not an object literal)\n * per the codebase's data-structure convention.\n */\nexport class BuildGateOptions {\n label: string; // section header shown above the gate\n rerunCommand: string; // command the AI re-runs after fixing the build\n failureHeadline: string; // first line printed on failure\n // WHICH stage's gate this is — REVIEW_STAGE or FINISH_STAGE. Required, with no default: it is part of\n // the captured log's filename, and a default would silently make two stages share one log file.\n stage: string;\n\n constructor(label: string, rerunCommand: string, failureHeadline: string, stage: string) {\n this.label = label;\n this.rerunCommand = rerunCommand;\n this.failureHeadline = failureHeadline;\n this.stage = stage;\n }\n}\n\n/** Runs the authoritative nx-affected build gate for wp-review-upsert-pr and wp-finish-upsert-pr. */\n@injectable(bindingScopeValues.Singleton)\nexport class BuildAffected {\n constructor(\n private readonly homeConfig: HomeConfigService,\n private readonly buildLog: BuildGateLog,\n ) {}\n\n /**\n * EXPERIMENTAL, and OFF unless the OPTIONAL machine-local `~/.webpieces/config.json` turns it on.\n *\n * That file does not exist for essentially anyone, and its absence is not an error, a warning or a\n * behaviour change of any kind — `HomeConfigService.load` returns all-defaults silently. False here is\n * therefore the state every consumer is in, and false means runBuildGate executes exactly the code it\n * executed before this feature existed.\n */\n isCaptureEnabled(): boolean {\n return this.homeConfig.load().buildGateLogCapture;\n }\n\n /**\n * Resolve the exact build command this gate will run: the project's configured\n * PrGateConfig.buildCommand, or the default affected-ci command when none is set.\n */\n resolveBuildCommand(repoRoot: string): string {\n const configured = loadAndValidate(repoRoot).prGate.buildCommand;\n return configured !== undefined && configured.trim() !== '' ? configured : DEFAULT_BUILD_COMMAND;\n }\n\n /**\n * Run the build gate. Returns the process exit code (0 = pass).\n *\n * Prints NOTHING itself — `runBuildGate` announces the command in one line. This used to print its own\n * `▶ Build gate: <cmd>` banner on top of that, so the command appeared twice in a row.\n */\n runBuildAffected(repoRoot: string, buildCommand?: string): number {\n const cmd = buildCommand !== undefined && buildCommand.trim() !== '' ? buildCommand : DEFAULT_BUILD_COMMAND;\n const result = spawnSync(cmd, { stdio: 'inherit', cwd: repoRoot, shell: true });\n return result.status ?? 1;\n }\n\n /** Run the build gate using the project's configured command (PrGateConfig.buildCommand). */\n runConfiguredBuildGate(repoRoot: string): number {\n return this.runBuildAffected(repoRoot, loadAndValidate(repoRoot).prGate.buildCommand);\n }\n\n /**\n * Run the configured build gate with consistent framing, throwing CliExitError(buildCode) on\n * failure so the bin's main()/runMain owns the exit. Single source of truth: wp-start-upsert-pr and\n * wp-finish-upsert-pr both call THIS (only the BuildGateOptions differ).\n */\n runBuildGate(repoRoot: string, opts: BuildGateOptions): void {\n const buildCommand = this.resolveBuildCommand(repoRoot);\n // TWO lines on the happy path — the command, then the result. The old framing spent a banner and a\n // paragraph explaining how to reproduce a build that was about to pass anyway; that explanation is\n // only useful when the build FAILS, so it now lives solely on the failure path below.\n process.stdout.write(`\\n${opts.label}: ${buildCommand}\\n`);\n // '' means NOT capturing, which is the case for every user who has not created the OPTIONAL\n // `~/.webpieces/config.json`. Everything below then runs exactly the code it ran before this\n // feature existed — same spawn, same message, no extra file, no extra line of output.\n const logPath = this.isCaptureEnabled() ? this.buildLog.pathFor(repoRoot, opts.stage) : '';\n const buildCode = logPath === ''\n ? this.runConfiguredBuildGate(repoRoot)\n : this.buildLog.run(repoRoot, buildCommand, logPath);\n if (buildCode !== 0) throw new CliExitError(buildCode, this.failureText(opts, buildCommand, logPath));\n process.stdout.write('\\n✅ Build passed.\\n');\n }\n\n /**\n * On failure, WITHOUT capture: the pre-existing text, which tells the AI to re-run the build itself.\n * WITH capture: a deliberately tiny pointer at the log the gate already wrote — the whole point of the\n * feature is that the agent reads one file instead of rebuilding the repo and eating the transcript.\n */\n private failureText(opts: BuildGateOptions, buildCommand: string, logPath: string): string {\n if (logPath !== '') return this.buildLog.failureMessage(buildCommand, logPath);\n return `\\n❌ ${opts.failureHeadline}\\n\\n` +\n `Run THIS exact command to reproduce and fix all errors, then re-run ${opts.rerunCommand}:\\n\\n` +\n ` ${buildCommand}\\n`;\n }\n}\n"]}
1
+ {"version":3,"file":"build-affected.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/build-affected.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawnSync } from 'child_process';\nimport { loadAndValidate, CliExitError, HomeConfigService, DEFAULT_BUILD_COMMAND } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { BuildGateLog } from './build-gate-log';\n\n// Single source of truth for RUNNING the build gate. `wp-start-upsert-pr` runs NO build; stage ②\n// (`wp-review-upsert-pr`) runs it authoritatively before any reviewer is spawned, and stage ③\n// (`wp-finish-upsert-pr`) re-runs it only when HEAD has moved since. nx `affected` only rebuilds\n// changed projects. The fallback command itself lives in @webpieces/rules-config as\n// DEFAULT_BUILD_COMMAND — whole-repo-build-guard prints the same string, and one definition is what\n// keeps the refusal message naming the build that actually runs.\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n/**\n * The caller-supplied framing for the build gate (label, re-run command, failure headline, stage id). Kept\n * as a parameter object so runBuildGate stays agnostic of who invokes it. A class (not an object literal)\n * per the codebase's data-structure convention.\n */\nexport class BuildGateOptions {\n label: string; // section header shown above the gate\n rerunCommand: string; // command the AI re-runs after fixing the build\n failureHeadline: string; // first line printed on failure\n // WHICH stage's gate this is — REVIEW_STAGE or FINISH_STAGE. Required, with no default: it is part of\n // the captured log's filename, and a default would silently make two stages share one log file.\n stage: string;\n\n constructor(label: string, rerunCommand: string, failureHeadline: string, stage: string) {\n this.label = label;\n this.rerunCommand = rerunCommand;\n this.failureHeadline = failureHeadline;\n this.stage = stage;\n }\n}\n\n/** Runs the authoritative nx-affected build gate for wp-review-upsert-pr and wp-finish-upsert-pr. */\n@injectable(bindingScopeValues.Singleton)\nexport class BuildAffected {\n constructor(\n private readonly homeConfig: HomeConfigService,\n private readonly buildLog: BuildGateLog,\n ) {}\n\n /**\n * EXPERIMENTAL, and OFF unless the OPTIONAL machine-local `~/.webpieces/config.json` turns it on.\n *\n * That file does not exist for essentially anyone, and its absence is not an error, a warning or a\n * behaviour change of any kind — `HomeConfigService.load` returns all-defaults silently. False here is\n * therefore the state every consumer is in, and false means runBuildGate executes exactly the code it\n * executed before this feature existed.\n */\n isCaptureEnabled(): boolean {\n return this.homeConfig.load().buildGateLogCapture;\n }\n\n /**\n * Resolve the exact build command this gate will run: the project's configured\n * PrGateConfig.buildCommand, or the default affected-ci command when none is set.\n */\n resolveBuildCommand(repoRoot: string): string {\n const configured = loadAndValidate(repoRoot).prGate.buildCommand;\n return configured !== undefined && configured.trim() !== '' ? configured : DEFAULT_BUILD_COMMAND;\n }\n\n /**\n * Run the build gate. Returns the process exit code (0 = pass).\n *\n * Prints NOTHING itself — `runBuildGate` announces the command in one line. This used to print its own\n * `▶ Build gate: <cmd>` banner on top of that, so the command appeared twice in a row.\n */\n runBuildAffected(repoRoot: string, buildCommand?: string): number {\n const cmd = buildCommand !== undefined && buildCommand.trim() !== '' ? buildCommand : DEFAULT_BUILD_COMMAND;\n const result = spawnSync(cmd, { stdio: 'inherit', cwd: repoRoot, shell: true });\n return result.status ?? 1;\n }\n\n /** Run the build gate using the project's configured command (PrGateConfig.buildCommand). */\n runConfiguredBuildGate(repoRoot: string): number {\n return this.runBuildAffected(repoRoot, loadAndValidate(repoRoot).prGate.buildCommand);\n }\n\n /**\n * Run the configured build gate with consistent framing, throwing CliExitError(buildCode) on\n * failure so the bin's main()/runMain owns the exit. Single source of truth: wp-start-upsert-pr and\n * wp-finish-upsert-pr both call THIS (only the BuildGateOptions differ).\n */\n runBuildGate(repoRoot: string, opts: BuildGateOptions): void {\n const buildCommand = this.resolveBuildCommand(repoRoot);\n // TWO lines on the happy path — the command, then the result. The old framing spent a banner and a\n // paragraph explaining how to reproduce a build that was about to pass anyway; that explanation is\n // only useful when the build FAILS, so it now lives solely on the failure path below.\n process.stdout.write(`\\n${opts.label}: ${buildCommand}\\n`);\n // '' means NOT capturing, which is the case for every user who has not created the OPTIONAL\n // `~/.webpieces/config.json`. Everything below then runs exactly the code it ran before this\n // feature existed — same spawn, same message, no extra file, no extra line of output.\n const logPath = this.isCaptureEnabled() ? this.buildLog.pathFor(repoRoot, opts.stage) : '';\n const buildCode = logPath === ''\n ? this.runConfiguredBuildGate(repoRoot)\n : this.buildLog.run(repoRoot, buildCommand, logPath);\n if (buildCode !== 0) throw new CliExitError(buildCode, this.failureText(opts, buildCommand, logPath));\n process.stdout.write('\\n✅ Build passed.\\n');\n }\n\n /**\n * On failure, WITHOUT capture: the pre-existing text, which tells the AI to re-run the build itself.\n * WITH capture: a deliberately tiny pointer at the log the gate already wrote — the whole point of the\n * feature is that the agent reads one file instead of rebuilding the repo and eating the transcript.\n */\n private failureText(opts: BuildGateOptions, buildCommand: string, logPath: string): string {\n if (logPath !== '') return this.buildLog.failureMessage(buildCommand, logPath);\n return `\\n❌ ${opts.failureHeadline}\\n\\n` +\n `Run THIS exact command to reproduce and fix all errors, then re-run ${opts.rerunCommand}:\\n\\n` +\n ` ${buildCommand}\\n`;\n }\n}\n"]}
@@ -0,0 +1,26 @@
1
+ /**
2
+ * THIS repo's own `webpieces.config.json`, prepared for use inside a temp clone.
3
+ *
4
+ * Specs that exercise a command end-to-end need a config the REAL validator accepts, and using the
5
+ * repo's own file (rather than a hand-rolled minimal one) is what keeps them honest — a stub drifts out
6
+ * from under the validator on the next config change. Two adjustments are needed to make that file
7
+ * usable outside this checkout, and both are the same for every such spec, so they live here once:
8
+ *
9
+ * 1. `checklists` is dropped. Its `doc` paths are validated REPO-RELATIVE and point at
10
+ * `.claude/review/*.md`, which exist here and not in a temp clone.
11
+ * 2. Every locally-known rule with no entry is SEEDED. The repo's config is validated by the PUBLISHED
12
+ * validator and therefore lags local source by one release (CLAUDE.md, "Published vs local
13
+ * source"): a rule added in this working tree cannot get a config entry until its release ships,
14
+ * while the LOCAL validator a spec runs already demands one. Without this, adding any rule turns
15
+ * these specs red for exactly one release — a failure that says nothing about the code under test.
16
+ *
17
+ * A testkit, not product code: nothing in a published bin imports it (same role as ai-hook-rules'
18
+ * `shim-testkit.ts`).
19
+ */
20
+ export declare class RepoConfigFixture {
21
+ /** The repo's config as a mutable object, checklists dropped and missing rules seeded. */
22
+ load(): Record<string, unknown>;
23
+ /** Write `config` (usually a `load()` result a spec has edited) as the config of `dir`. */
24
+ writeTo(dir: string, config: Record<string, unknown>): void;
25
+ private seedMissingRules;
26
+ }
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RepoConfigFixture = 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 rules_config_1 = require("@webpieces/rules-config");
8
+ /**
9
+ * THIS repo's own `webpieces.config.json`, prepared for use inside a temp clone.
10
+ *
11
+ * Specs that exercise a command end-to-end need a config the REAL validator accepts, and using the
12
+ * repo's own file (rather than a hand-rolled minimal one) is what keeps them honest — a stub drifts out
13
+ * from under the validator on the next config change. Two adjustments are needed to make that file
14
+ * usable outside this checkout, and both are the same for every such spec, so they live here once:
15
+ *
16
+ * 1. `checklists` is dropped. Its `doc` paths are validated REPO-RELATIVE and point at
17
+ * `.claude/review/*.md`, which exist here and not in a temp clone.
18
+ * 2. Every locally-known rule with no entry is SEEDED. The repo's config is validated by the PUBLISHED
19
+ * validator and therefore lags local source by one release (CLAUDE.md, "Published vs local
20
+ * source"): a rule added in this working tree cannot get a config entry until its release ships,
21
+ * while the LOCAL validator a spec runs already demands one. Without this, adding any rule turns
22
+ * these specs red for exactly one release — a failure that says nothing about the code under test.
23
+ *
24
+ * A testkit, not product code: nothing in a published bin imports it (same role as ai-hook-rules'
25
+ * `shim-testkit.ts`).
26
+ */
27
+ class RepoConfigFixture {
28
+ /** The repo's config as a mutable object, checklists dropped and missing rules seeded. */
29
+ // webpieces-disable no-any-unknown -- the repo's own config document, opaque until a spec edits it
30
+ load() {
31
+ const source = path.join(__dirname, '..', '..', '..', '..', '..', '..', 'webpieces.config.json');
32
+ // webpieces-disable no-any-unknown -- parsed JSON, narrowed by the accessors below
33
+ const config = JSON.parse(fs.readFileSync(source, 'utf8'));
34
+ // webpieces-disable no-any-unknown -- narrowing one nested section
35
+ const commands = config['commands'];
36
+ delete commands['pr-gate']['checklists'];
37
+ delete commands['pr-gate']['checklistsWhy'];
38
+ this.seedMissingRules(config);
39
+ return config;
40
+ }
41
+ /** Write `config` (usually a `load()` result a spec has edited) as the config of `dir`. */
42
+ // webpieces-disable no-any-unknown -- see load()
43
+ writeTo(dir, config) {
44
+ fs.writeFileSync(path.join(dir, 'webpieces.config.json'), JSON.stringify(config, null, 4) + '\n');
45
+ }
46
+ // webpieces-disable no-any-unknown -- see load()
47
+ seedMissingRules(config) {
48
+ // webpieces-disable no-any-unknown -- narrowing the two rule sections
49
+ const rules = config['rules'];
50
+ // webpieces-disable no-any-unknown -- narrowing the two rule sections
51
+ const guards = config['hookGuards'];
52
+ for (const name of (0, rules_config_1.allRuleNames)()) {
53
+ const section = (0, rules_config_1.sectionForRule)(name) === 'hookGuards' ? guards : rules;
54
+ if (!(name in section))
55
+ section[name] = (0, rules_config_1.seedEntryForRule)(name);
56
+ }
57
+ }
58
+ }
59
+ exports.RepoConfigFixture = RepoConfigFixture;
60
+ //# sourceMappingURL=repo-config-testkit.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"repo-config-testkit.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/repo-config-testkit.ts"],"names":[],"mappings":";;;;AAAA,+CAAyB;AACzB,mDAA6B;AAE7B,0DAAyF;AAEzF;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAa,iBAAiB;IAC1B,0FAA0F;IAC1F,mGAAmG;IACnG,IAAI;QACA,MAAM,MAAM,GAAG,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,IAAI,EAAE,uBAAuB,CAAC,CAAC;QACjG,mFAAmF;QACnF,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC,YAAY,CAAC,MAAM,EAAE,MAAM,CAAC,CAA4B,CAAC;QACtF,mEAAmE;QACnE,MAAM,QAAQ,GAAG,MAAM,CAAC,UAAU,CAA4C,CAAC;QAC/E,OAAO,QAAQ,CAAC,SAAS,CAAC,CAAC,YAAY,CAAC,CAAC;QACzC,OAAO,QAAQ,CAAC,SAAS,CAAC,CAAC,eAAe,CAAC,CAAC;QAC5C,IAAI,CAAC,gBAAgB,CAAC,MAAM,CAAC,CAAC;QAC9B,OAAO,MAAM,CAAC;IAClB,CAAC;IAED,2FAA2F;IAC3F,iDAAiD;IACjD,OAAO,CAAC,GAAW,EAAE,MAA+B;QAChD,EAAE,CAAC,aAAa,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,uBAAuB,CAAC,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,GAAG,IAAI,CAAC,CAAC;IACtG,CAAC;IAED,iDAAiD;IACzC,gBAAgB,CAAC,MAA+B;QACpD,sEAAsE;QACtE,MAAM,KAAK,GAAG,MAAM,CAAC,OAAO,CAA4B,CAAC;QACzD,sEAAsE;QACtE,MAAM,MAAM,GAAG,MAAM,CAAC,YAAY,CAA4B,CAAC;QAC/D,KAAK,MAAM,IAAI,IAAI,IAAA,2BAAY,GAAE,EAAE,CAAC;YAChC,MAAM,OAAO,GAAG,IAAA,6BAAc,EAAC,IAAI,CAAC,KAAK,YAAY,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC;YACvE,IAAI,CAAC,CAAC,IAAI,IAAI,OAAO,CAAC;gBAAE,OAAO,CAAC,IAAI,CAAC,GAAG,IAAA,+BAAgB,EAAC,IAAI,CAAC,CAAC;QACnE,CAAC;IACL,CAAC;CACJ;AAhCD,8CAgCC","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\n\nimport { allRuleNames, sectionForRule, seedEntryForRule } from '@webpieces/rules-config';\n\n/**\n * THIS repo's own `webpieces.config.json`, prepared for use inside a temp clone.\n *\n * Specs that exercise a command end-to-end need a config the REAL validator accepts, and using the\n * repo's own file (rather than a hand-rolled minimal one) is what keeps them honest — a stub drifts out\n * from under the validator on the next config change. Two adjustments are needed to make that file\n * usable outside this checkout, and both are the same for every such spec, so they live here once:\n *\n * 1. `checklists` is dropped. Its `doc` paths are validated REPO-RELATIVE and point at\n * `.claude/review/*.md`, which exist here and not in a temp clone.\n * 2. Every locally-known rule with no entry is SEEDED. The repo's config is validated by the PUBLISHED\n * validator and therefore lags local source by one release (CLAUDE.md, \"Published vs local\n * source\"): a rule added in this working tree cannot get a config entry until its release ships,\n * while the LOCAL validator a spec runs already demands one. Without this, adding any rule turns\n * these specs red for exactly one release — a failure that says nothing about the code under test.\n *\n * A testkit, not product code: nothing in a published bin imports it (same role as ai-hook-rules'\n * `shim-testkit.ts`).\n */\nexport class RepoConfigFixture {\n /** The repo's config as a mutable object, checklists dropped and missing rules seeded. */\n // webpieces-disable no-any-unknown -- the repo's own config document, opaque until a spec edits it\n load(): Record<string, unknown> {\n const source = path.join(__dirname, '..', '..', '..', '..', '..', '..', 'webpieces.config.json');\n // webpieces-disable no-any-unknown -- parsed JSON, narrowed by the accessors below\n const config = JSON.parse(fs.readFileSync(source, 'utf8')) as Record<string, unknown>;\n // webpieces-disable no-any-unknown -- narrowing one nested section\n const commands = config['commands'] as Record<string, Record<string, unknown>>;\n delete commands['pr-gate']['checklists'];\n delete commands['pr-gate']['checklistsWhy'];\n this.seedMissingRules(config);\n return config;\n }\n\n /** Write `config` (usually a `load()` result a spec has edited) as the config of `dir`. */\n // webpieces-disable no-any-unknown -- see load()\n writeTo(dir: string, config: Record<string, unknown>): void {\n fs.writeFileSync(path.join(dir, 'webpieces.config.json'), JSON.stringify(config, null, 4) + '\\n');\n }\n\n // webpieces-disable no-any-unknown -- see load()\n private seedMissingRules(config: Record<string, unknown>): void {\n // webpieces-disable no-any-unknown -- narrowing the two rule sections\n const rules = config['rules'] as Record<string, unknown>;\n // webpieces-disable no-any-unknown -- narrowing the two rule sections\n const guards = config['hookGuards'] as Record<string, unknown>;\n for (const name of allRuleNames()) {\n const section = sectionForRule(name) === 'hookGuards' ? guards : rules;\n if (!(name in section)) section[name] = seedEntryForRule(name);\n }\n }\n}\n"]}