@webpieces/pr-gate 0.4.613 → 0.4.614
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 +2 -2
- package/src/scripts/workflow/build-gate-log.js +5 -0
- package/src/scripts/workflow/build-gate-log.js.map +1 -1
- package/src/scripts/workflow/repo-config-testkit.d.ts +5 -0
- package/src/scripts/workflow/repo-config-testkit.js +19 -0
- package/src/scripts/workflow/repo-config-testkit.js.map +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webpieces/pr-gate",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.614",
|
|
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.
|
|
18
|
+
"@webpieces/rules-config": "0.4.614",
|
|
19
19
|
"@inversifyjs/binding-decorators": "1.1.5",
|
|
20
20
|
"inversify": "7.10.4",
|
|
21
21
|
"reflect-metadata": "0.2.2"
|
|
@@ -11,6 +11,11 @@ const inversify_1 = require("inversify");
|
|
|
11
11
|
// file runs for a user who has not created that OPTIONAL file — see `HomeConfigService`
|
|
12
12
|
// (`@webpieces/rules-config`), which returns all-defaults silently when it is absent.
|
|
13
13
|
//
|
|
14
|
+
// That file has a second, REQUIRED key once it exists — `experimental.whole-repo-build-guard` — so the
|
|
15
|
+
// smallest document enabling THIS feature is:
|
|
16
|
+
// { "experimental": { "whole-repo-build-guard": false, "buildGateLogCapture": true } }
|
|
17
|
+
// `HomeConfigService` owns that rule and prints the same edit when the key is missing.
|
|
18
|
+
//
|
|
14
19
|
// ─── Why ───────────────────────────────────────────────────────────────────────────────────────────────
|
|
15
20
|
// The build gate already builds everything. When it fails, the pre-existing message says "run THIS exact
|
|
16
21
|
// command to reproduce", and an AI agent obeys it — so the repo is built a SECOND time purely to see the
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"build-gate-log.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/build-gate-log.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawnSync } from 'child_process';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { dotWebpieces } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\n\n// EXPERIMENTAL (opt-in via `~/.webpieces/config.json` → experimental.buildGateLogCapture). Nothing in this\n// file runs for a user who has not created that OPTIONAL file — see `HomeConfigService`\n// (`@webpieces/rules-config`), which returns all-defaults silently when it is absent.\n//\n// ─── Why ───────────────────────────────────────────────────────────────────────────────────────────────\n// The build gate already builds everything. When it fails, the pre-existing message says \"run THIS exact\n// command to reproduce\", and an AI agent obeys it — so the repo is built a SECOND time purely to see the\n// errors that scrolled past the first time. This captures the first build's output instead.\n//\n// ─── Why a shell `tee` and not spawnSync's pipes ───────────────────────────────────────────────────────\n// spawnSync BUFFERS a piped stream and hands it over only after the child exits, so the human would watch\n// a silent terminal for the length of a full build. The output has to be split at the OS level, which is\n// what `tee` is. The gate keeps `stdio: 'inherit'`, so the terminal is byte-identical to today.\n//\n// ─── Why an exit-status side file and not `pipefail` ───────────────────────────────────────────────────\n// In a pipeline the shell reports the LAST command's status, i.e. tee's, which is 0 whether the build\n// passed or failed. `set -o pipefail` and `${PIPESTATUS[0]}` are bash/zsh, and spawnSync's `shell: true`\n// gives `/bin/sh` — dash on Debian. Writing `$?` to a side file inside the group is plain POSIX and works\n// under every one of them. The redirect keeps it out of the pipe, so it never lands in the log.\nconst STATUS_SUFFIX = '.status';\n\n/**\n * Which stage's gate is being captured. The value is part of the log FILENAME, so review and finish never\n * write the same file even at the same commit on the same branch.\n */\nexport const REVIEW_STAGE = 'review';\nexport const FINISH_STAGE = 'finish';\n\n/**\n * Captures the build gate's full output to `.webpieces/logs/`, and renders the small pointer the AI is\n * handed instead of a rebuild instruction.\n *\n * ─── Why the filename cannot collide ───────────────────────────────────────────────────────────────────\n * The path is `dotWebpieces.logsFile(repoRoot, 'build-gate-<stage>-<branch>-<shortSha>.log')`, and each\n * component rules out one class of concurrent writer:\n * • the DIRECTORY is `dotWebpieces.local()`-scoped — `<primary>/.webpieces/worktrees/<git worktree\n * name>/logs/` in a linked worktree. Every worktree therefore has its own log directory already, which\n * is what makes \"N agents in N worktrees\" safe by construction rather than by naming.\n * • `<stage>` separates review from finish, the two gates that CAN run against one commit.\n * • `<branch>` — a branch can be checked out in at most one worktree (git enforces it), so within one\n * log directory the branch is effectively constant; it is in the name so a human reading the directory\n * can tell whose log is whose, and so switching branches never appends to a stale file.\n * • `<shortSha>` — a re-run at a NEW commit gets a new file, so the log a failure message points at is\n * always the build for the code that failed. A re-run at the SAME commit deliberately TRUNCATES: it is\n * the same build of the same tree, and the fresh one is the one worth reading.\n * The residual case is two agents running the same stage, at the same commit, in the SAME worktree,\n * simultaneously. That is already unsupported — both would be driving one git index and one merge state —\n * and it is the only case this scheme does not separate.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class BuildGateLog {\n /** Absolute path of the log for `stage` at the current HEAD, creating the log directory. */\n pathFor(repoRoot: string, stage: string): string {\n const file = dotWebpieces.logsFile(repoRoot, this.fileNameFor(repoRoot, stage));\n fs.mkdirSync(path.dirname(file), { recursive: true });\n return file;\n }\n\n /** The same path WITHOUT creating anything, and '' when no such log exists. Used by finish's skip path. */\n existingLogFor(repoRoot: string, stage: string): string {\n const file = dotWebpieces.logsFile(repoRoot, this.fileNameFor(repoRoot, stage));\n return fs.existsSync(file) ? file : '';\n }\n\n /** `build-gate-<stage>-<branch>-<shortSha>.log` — see the class docstring for why this cannot collide. */\n fileNameFor(repoRoot: string, stage: string): string {\n const branch = this.slug(this.git(repoRoot, ['rev-parse', '--abbrev-ref', 'HEAD']));\n const sha = this.slug(this.git(repoRoot, ['rev-parse', '--short', 'HEAD']));\n return `build-gate-${this.slug(stage)}-${branch === '' ? 'nobranch' : branch}-${sha === '' ? 'nosha' : sha}.log`;\n }\n\n /**\n * Run `buildCommand` with its combined stdout+stderr streaming to the terminal AND appended in full to\n * `logPath`. Returns the BUILD's exit code (not tee's). Nothing is truncated.\n */\n run(repoRoot: string, buildCommand: string, logPath: string): number {\n const statusFile = `${logPath}${STATUS_SUFFIX}`;\n fs.rmSync(statusFile, { force: true });\n const result = spawnSync(this.wrap(buildCommand, logPath, statusFile),\n { stdio: 'inherit', cwd: repoRoot, shell: true });\n const fromFile = this.readStatus(statusFile);\n fs.rmSync(statusFile, { force: true });\n if (fromFile !== null) return fromFile;\n // The side file never appeared ⇒ the shell died before the echo. The PIPELINE's status is tee's,\n // which is 0, and reporting 0 there would call a dead build green — so fail CLOSED instead.\n return result.status !== null && result.status !== 0 ? result.status : 1;\n }\n\n /**\n * The ENTIRE message the AI gets on a captured failure. Deliberately tiny: the whole point is that the\n * agent reads ONE file rather than carrying a build transcript in its context.\n *\n * The last sentence is not filler. If the log holds no visible failure then something upstream is wrong\n * (a runner that died without printing, a truncated pipe), and the worst possible response is an agent\n * guessing or rebuilding — so it is told to surface the contradiction to the human and stop.\n */\n failureMessage(buildCommand: string, logPath: string): string {\n return `\\n❌ The CI build failed. We ran\\n\\n` +\n ` ${buildCommand} > ${logPath}\\n\\n` +\n `and it failed, so read that file for the failures. Do NOT re-run the build to see them.\\n` +\n `If you do not see failures in that log, report that to the user and stop.\\n`;\n }\n\n /**\n * `{ ( <build> ) ; echo $? > <status> ; } 2>&1 | tee <log>` — written across LINES so a build command\n * ending in a `#` comment cannot swallow what follows it.\n *\n * The inner `( … )` is load-bearing, not decoration. The left side of a pipeline is already a subshell,\n * so a build command containing a plain `exit 7` (or any script that calls `exit`) would terminate that\n * subshell OUTRIGHT — `echo $?` never runs, the side file never appears, and the only status left is\n * tee's 0. The nested subshell absorbs the `exit`, so `$?` is the build's code every time.\n */\n private wrap(buildCommand: string, logPath: string, statusFile: string): string {\n return `{\\n(\\n${buildCommand}\\n)\\necho $? > ${this.quote(statusFile)}\\n} 2>&1 | tee ${this.quote(logPath)}`;\n }\n\n // The build's own exit code, or null when the side file never appeared (the shell died before the echo).\n private readStatus(statusFile: string): number | null {\n if (!fs.existsSync(statusFile)) return null;\n const parsed = Number.parseInt(fs.readFileSync(statusFile, 'utf8').trim(), 10);\n return Number.isNaN(parsed) ? null : parsed;\n }\n\n // POSIX single-quoting: everything is literal inside '…', and a literal ' is spelled '\\''.\n private quote(value: string): string {\n return `'${value.split(\"'\").join(`'\\\\''`)}'`;\n }\n\n // Anything that is not a filename-safe character becomes '-', so `dean/feat` cannot create directories.\n private slug(value: string): string {\n return value.replace(/[^A-Za-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '');\n }\n\n // One local git read. Fails SOFT to '' — a missing branch/sha degrades the FILENAME, and degrading a\n // filename may never be the reason a build gate does not run.\n private git(repoRoot: string, args: string[]): string {\n const result = spawnSync('git', ['-C', repoRoot, ...args], { encoding: 'utf8' });\n if (result.status !== 0) return '';\n return (result.stdout ?? '').trim();\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"build-gate-log.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/build-gate-log.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawnSync } from 'child_process';\nimport * as fs from 'fs';\nimport * as path from 'path';\nimport { dotWebpieces } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\n\n// EXPERIMENTAL (opt-in via `~/.webpieces/config.json` → experimental.buildGateLogCapture). Nothing in this\n// file runs for a user who has not created that OPTIONAL file — see `HomeConfigService`\n// (`@webpieces/rules-config`), which returns all-defaults silently when it is absent.\n//\n// That file has a second, REQUIRED key once it exists — `experimental.whole-repo-build-guard` — so the\n// smallest document enabling THIS feature is:\n// { \"experimental\": { \"whole-repo-build-guard\": false, \"buildGateLogCapture\": true } }\n// `HomeConfigService` owns that rule and prints the same edit when the key is missing.\n//\n// ─── Why ───────────────────────────────────────────────────────────────────────────────────────────────\n// The build gate already builds everything. When it fails, the pre-existing message says \"run THIS exact\n// command to reproduce\", and an AI agent obeys it — so the repo is built a SECOND time purely to see the\n// errors that scrolled past the first time. This captures the first build's output instead.\n//\n// ─── Why a shell `tee` and not spawnSync's pipes ───────────────────────────────────────────────────────\n// spawnSync BUFFERS a piped stream and hands it over only after the child exits, so the human would watch\n// a silent terminal for the length of a full build. The output has to be split at the OS level, which is\n// what `tee` is. The gate keeps `stdio: 'inherit'`, so the terminal is byte-identical to today.\n//\n// ─── Why an exit-status side file and not `pipefail` ───────────────────────────────────────────────────\n// In a pipeline the shell reports the LAST command's status, i.e. tee's, which is 0 whether the build\n// passed or failed. `set -o pipefail` and `${PIPESTATUS[0]}` are bash/zsh, and spawnSync's `shell: true`\n// gives `/bin/sh` — dash on Debian. Writing `$?` to a side file inside the group is plain POSIX and works\n// under every one of them. The redirect keeps it out of the pipe, so it never lands in the log.\nconst STATUS_SUFFIX = '.status';\n\n/**\n * Which stage's gate is being captured. The value is part of the log FILENAME, so review and finish never\n * write the same file even at the same commit on the same branch.\n */\nexport const REVIEW_STAGE = 'review';\nexport const FINISH_STAGE = 'finish';\n\n/**\n * Captures the build gate's full output to `.webpieces/logs/`, and renders the small pointer the AI is\n * handed instead of a rebuild instruction.\n *\n * ─── Why the filename cannot collide ───────────────────────────────────────────────────────────────────\n * The path is `dotWebpieces.logsFile(repoRoot, 'build-gate-<stage>-<branch>-<shortSha>.log')`, and each\n * component rules out one class of concurrent writer:\n * • the DIRECTORY is `dotWebpieces.local()`-scoped — `<primary>/.webpieces/worktrees/<git worktree\n * name>/logs/` in a linked worktree. Every worktree therefore has its own log directory already, which\n * is what makes \"N agents in N worktrees\" safe by construction rather than by naming.\n * • `<stage>` separates review from finish, the two gates that CAN run against one commit.\n * • `<branch>` — a branch can be checked out in at most one worktree (git enforces it), so within one\n * log directory the branch is effectively constant; it is in the name so a human reading the directory\n * can tell whose log is whose, and so switching branches never appends to a stale file.\n * • `<shortSha>` — a re-run at a NEW commit gets a new file, so the log a failure message points at is\n * always the build for the code that failed. A re-run at the SAME commit deliberately TRUNCATES: it is\n * the same build of the same tree, and the fresh one is the one worth reading.\n * The residual case is two agents running the same stage, at the same commit, in the SAME worktree,\n * simultaneously. That is already unsupported — both would be driving one git index and one merge state —\n * and it is the only case this scheme does not separate.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class BuildGateLog {\n /** Absolute path of the log for `stage` at the current HEAD, creating the log directory. */\n pathFor(repoRoot: string, stage: string): string {\n const file = dotWebpieces.logsFile(repoRoot, this.fileNameFor(repoRoot, stage));\n fs.mkdirSync(path.dirname(file), { recursive: true });\n return file;\n }\n\n /** The same path WITHOUT creating anything, and '' when no such log exists. Used by finish's skip path. */\n existingLogFor(repoRoot: string, stage: string): string {\n const file = dotWebpieces.logsFile(repoRoot, this.fileNameFor(repoRoot, stage));\n return fs.existsSync(file) ? file : '';\n }\n\n /** `build-gate-<stage>-<branch>-<shortSha>.log` — see the class docstring for why this cannot collide. */\n fileNameFor(repoRoot: string, stage: string): string {\n const branch = this.slug(this.git(repoRoot, ['rev-parse', '--abbrev-ref', 'HEAD']));\n const sha = this.slug(this.git(repoRoot, ['rev-parse', '--short', 'HEAD']));\n return `build-gate-${this.slug(stage)}-${branch === '' ? 'nobranch' : branch}-${sha === '' ? 'nosha' : sha}.log`;\n }\n\n /**\n * Run `buildCommand` with its combined stdout+stderr streaming to the terminal AND appended in full to\n * `logPath`. Returns the BUILD's exit code (not tee's). Nothing is truncated.\n */\n run(repoRoot: string, buildCommand: string, logPath: string): number {\n const statusFile = `${logPath}${STATUS_SUFFIX}`;\n fs.rmSync(statusFile, { force: true });\n const result = spawnSync(this.wrap(buildCommand, logPath, statusFile),\n { stdio: 'inherit', cwd: repoRoot, shell: true });\n const fromFile = this.readStatus(statusFile);\n fs.rmSync(statusFile, { force: true });\n if (fromFile !== null) return fromFile;\n // The side file never appeared ⇒ the shell died before the echo. The PIPELINE's status is tee's,\n // which is 0, and reporting 0 there would call a dead build green — so fail CLOSED instead.\n return result.status !== null && result.status !== 0 ? result.status : 1;\n }\n\n /**\n * The ENTIRE message the AI gets on a captured failure. Deliberately tiny: the whole point is that the\n * agent reads ONE file rather than carrying a build transcript in its context.\n *\n * The last sentence is not filler. If the log holds no visible failure then something upstream is wrong\n * (a runner that died without printing, a truncated pipe), and the worst possible response is an agent\n * guessing or rebuilding — so it is told to surface the contradiction to the human and stop.\n */\n failureMessage(buildCommand: string, logPath: string): string {\n return `\\n❌ The CI build failed. We ran\\n\\n` +\n ` ${buildCommand} > ${logPath}\\n\\n` +\n `and it failed, so read that file for the failures. Do NOT re-run the build to see them.\\n` +\n `If you do not see failures in that log, report that to the user and stop.\\n`;\n }\n\n /**\n * `{ ( <build> ) ; echo $? > <status> ; } 2>&1 | tee <log>` — written across LINES so a build command\n * ending in a `#` comment cannot swallow what follows it.\n *\n * The inner `( … )` is load-bearing, not decoration. The left side of a pipeline is already a subshell,\n * so a build command containing a plain `exit 7` (or any script that calls `exit`) would terminate that\n * subshell OUTRIGHT — `echo $?` never runs, the side file never appears, and the only status left is\n * tee's 0. The nested subshell absorbs the `exit`, so `$?` is the build's code every time.\n */\n private wrap(buildCommand: string, logPath: string, statusFile: string): string {\n return `{\\n(\\n${buildCommand}\\n)\\necho $? > ${this.quote(statusFile)}\\n} 2>&1 | tee ${this.quote(logPath)}`;\n }\n\n // The build's own exit code, or null when the side file never appeared (the shell died before the echo).\n private readStatus(statusFile: string): number | null {\n if (!fs.existsSync(statusFile)) return null;\n const parsed = Number.parseInt(fs.readFileSync(statusFile, 'utf8').trim(), 10);\n return Number.isNaN(parsed) ? null : parsed;\n }\n\n // POSIX single-quoting: everything is literal inside '…', and a literal ' is spelled '\\''.\n private quote(value: string): string {\n return `'${value.split(\"'\").join(`'\\\\''`)}'`;\n }\n\n // Anything that is not a filename-safe character becomes '-', so `dean/feat` cannot create directories.\n private slug(value: string): string {\n return value.replace(/[^A-Za-z0-9._-]+/g, '-').replace(/^-+|-+$/g, '');\n }\n\n // One local git read. Fails SOFT to '' — a missing branch/sha degrades the FILENAME, and degrading a\n // filename may never be the reason a build gate does not run.\n private git(repoRoot: string, args: string[]): string {\n const result = spawnSync('git', ['-C', repoRoot, ...args], { encoding: 'utf8' });\n if (result.status !== 0) return '';\n return (result.stdout ?? '').trim();\n }\n}\n"]}
|
|
@@ -13,6 +13,10 @@
|
|
|
13
13
|
* source"): a rule added in this working tree cannot get a config entry until its release ships,
|
|
14
14
|
* while the LOCAL validator a spec runs already demands one. Without this, adding any rule turns
|
|
15
15
|
* these specs red for exactly one release — a failure that says nothing about the code under test.
|
|
16
|
+
* 3. Every RETIRED rule entry is dropped — the same one-release lag pointing the other way. When a rule
|
|
17
|
+
* is retired in this working tree, the repo's config must KEEP its entry until the release carrying
|
|
18
|
+
* the retirement ships (deleting it early would fail the published validator and block every Bash
|
|
19
|
+
* call), while the LOCAL validator already REJECTS it. Same trade as (2), same fix.
|
|
16
20
|
*
|
|
17
21
|
* A testkit, not product code: nothing in a published bin imports it (same role as ai-hook-rules'
|
|
18
22
|
* `shim-testkit.ts`).
|
|
@@ -20,6 +24,7 @@
|
|
|
20
24
|
export declare class RepoConfigFixture {
|
|
21
25
|
/** The repo's config as a mutable object, checklists dropped and missing rules seeded. */
|
|
22
26
|
load(): Record<string, unknown>;
|
|
27
|
+
private dropRetiredRules;
|
|
23
28
|
/** Write `config` (usually a `load()` result a spec has edited) as the config of `dir`. */
|
|
24
29
|
writeTo(dir: string, config: Record<string, unknown>): void;
|
|
25
30
|
private seedMissingRules;
|
|
@@ -20,6 +20,10 @@ const rules_config_1 = require("@webpieces/rules-config");
|
|
|
20
20
|
* source"): a rule added in this working tree cannot get a config entry until its release ships,
|
|
21
21
|
* while the LOCAL validator a spec runs already demands one. Without this, adding any rule turns
|
|
22
22
|
* these specs red for exactly one release — a failure that says nothing about the code under test.
|
|
23
|
+
* 3. Every RETIRED rule entry is dropped — the same one-release lag pointing the other way. When a rule
|
|
24
|
+
* is retired in this working tree, the repo's config must KEEP its entry until the release carrying
|
|
25
|
+
* the retirement ships (deleting it early would fail the published validator and block every Bash
|
|
26
|
+
* call), while the LOCAL validator already REJECTS it. Same trade as (2), same fix.
|
|
23
27
|
*
|
|
24
28
|
* A testkit, not product code: nothing in a published bin imports it (same role as ai-hook-rules'
|
|
25
29
|
* `shim-testkit.ts`).
|
|
@@ -35,9 +39,24 @@ class RepoConfigFixture {
|
|
|
35
39
|
const commands = config['commands'];
|
|
36
40
|
delete commands['pr-gate']['checklists'];
|
|
37
41
|
delete commands['pr-gate']['checklistsWhy'];
|
|
42
|
+
this.dropRetiredRules(config);
|
|
38
43
|
this.seedMissingRules(config);
|
|
39
44
|
return config;
|
|
40
45
|
}
|
|
46
|
+
// Drop every rule/guard name this build knows to be RETIRED, from whichever section holds it.
|
|
47
|
+
// webpieces-disable no-any-unknown -- see load()
|
|
48
|
+
dropRetiredRules(config) {
|
|
49
|
+
for (const section of ['rules', 'hookGuards']) {
|
|
50
|
+
// webpieces-disable no-any-unknown -- narrowing one rule section
|
|
51
|
+
const entries = config[section];
|
|
52
|
+
if (entries === undefined)
|
|
53
|
+
continue;
|
|
54
|
+
for (const name of Object.keys(entries)) {
|
|
55
|
+
if ((0, rules_config_1.retiredRuleFor)(name) !== null)
|
|
56
|
+
delete entries[name];
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
41
60
|
/** Write `config` (usually a `load()` result a spec has edited) as the config of `dir`. */
|
|
42
61
|
// webpieces-disable no-any-unknown -- see load()
|
|
43
62
|
writeTo(dir, config) {
|
|
@@ -1 +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,
|
|
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,0DAAyG;AAEzG;;;;;;;;;;;;;;;;;;;;;;GAsBG;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,IAAI,CAAC,gBAAgB,CAAC,MAAM,CAAC,CAAC;QAC9B,OAAO,MAAM,CAAC;IAClB,CAAC;IAED,8FAA8F;IAC9F,iDAAiD;IACzC,gBAAgB,CAAC,MAA+B;QACpD,KAAK,MAAM,OAAO,IAAI,CAAC,OAAO,EAAE,YAAY,CAAC,EAAE,CAAC;YAC5C,iEAAiE;YACjE,MAAM,OAAO,GAAG,MAAM,CAAC,OAAO,CAAwC,CAAC;YACvE,IAAI,OAAO,KAAK,SAAS;gBAAE,SAAS;YACpC,KAAK,MAAM,IAAI,IAAI,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,EAAE,CAAC;gBACtC,IAAI,IAAA,6BAAc,EAAC,IAAI,CAAC,KAAK,IAAI;oBAAE,OAAO,OAAO,CAAC,IAAI,CAAC,CAAC;YAC5D,CAAC;QACL,CAAC;IACL,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;AA9CD,8CA8CC","sourcesContent":["import * as fs from 'fs';\nimport * as path from 'path';\n\nimport { allRuleNames, retiredRuleFor, 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 * 3. Every RETIRED rule entry is dropped — the same one-release lag pointing the other way. When a rule\n * is retired in this working tree, the repo's config must KEEP its entry until the release carrying\n * the retirement ships (deleting it early would fail the published validator and block every Bash\n * call), while the LOCAL validator already REJECTS it. Same trade as (2), same fix.\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.dropRetiredRules(config);\n this.seedMissingRules(config);\n return config;\n }\n\n // Drop every rule/guard name this build knows to be RETIRED, from whichever section holds it.\n // webpieces-disable no-any-unknown -- see load()\n private dropRetiredRules(config: Record<string, unknown>): void {\n for (const section of ['rules', 'hookGuards']) {\n // webpieces-disable no-any-unknown -- narrowing one rule section\n const entries = config[section] as Record<string, unknown> | undefined;\n if (entries === undefined) continue;\n for (const name of Object.keys(entries)) {\n if (retiredRuleFor(name) !== null) delete entries[name];\n }\n }\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"]}
|