@webpieces/rules-config 0.4.565 → 0.4.566
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/cli-args.d.ts +21 -2
- package/src/cli-args.js +77 -10
- package/src/cli-args.js.map +1 -1
- package/src/constants.d.ts +1 -0
- package/src/constants.js +11 -1
- package/src/constants.js.map +1 -1
- package/src/index.d.ts +3 -3
- package/src/index.js +13 -5
- package/src/index.js.map +1 -1
- package/src/pr-gate-config.d.ts +40 -0
- package/src/pr-gate-config.js +67 -1
- package/src/pr-gate-config.js.map +1 -1
- package/src/pr-gate-section-validators.d.ts +10 -0
- package/src/pr-gate-section-validators.js +60 -0
- package/src/pr-gate-section-validators.js.map +1 -1
- package/src/sync-flow-guidance.d.ts +11 -0
- package/src/sync-flow-guidance.js +23 -1
- package/src/sync-flow-guidance.js.map +1 -1
- package/src/validate-config.js +3 -0
- package/src/validate-config.js.map +1 -1
- package/templates/webpieces.git-workflow.md +67 -0
package/src/pr-gate-config.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.PrGateConfig = exports.LandPrConfig = exports.ReviewContextEntry = exports.MERGE_MODES = exports.MERGE_MODE_NONE = exports.MERGE_MODE_AUTO = exports.GateDefinition = void 0;
|
|
3
|
+
exports.PrGateConfig = exports.DevDeployConfig = exports.DEFAULT_DEV_BRANCH = exports.DEFAULT_DEV_BRANCH_NAMESPACE = exports.LandPrConfig = exports.ReviewContextEntry = exports.MERGE_MODES = exports.MERGE_MODE_NONE = exports.MERGE_MODE_AUTO = exports.GateDefinition = void 0;
|
|
4
4
|
exports.defaultLandPrConfig = defaultLandPrConfig;
|
|
5
|
+
exports.defaultDevDeployConfig = defaultDevDeployConfig;
|
|
5
6
|
exports.defaultGates = defaultGates;
|
|
6
7
|
exports.defaultPrGateConfig = defaultPrGateConfig;
|
|
7
8
|
exports.buildPrGateConfig = buildPrGateConfig;
|
|
9
|
+
exports.buildDevDeployConfig = buildDevDeployConfig;
|
|
8
10
|
exports.buildLandPrConfig = buildLandPrConfig;
|
|
9
11
|
const branch_archiver_1 = require("./branch-archiver");
|
|
10
12
|
const checklist_config_1 = require("./checklist-config");
|
|
@@ -72,6 +74,48 @@ exports.LandPrConfig = LandPrConfig;
|
|
|
72
74
|
function defaultLandPrConfig() {
|
|
73
75
|
return new LandPrConfig(branch_archiver_1.BRANCH_RETENTION_ARCHIVE_TAG);
|
|
74
76
|
}
|
|
77
|
+
// The two literal ref names the dev-deploy flow derives everything else from. Defaults, not policy —
|
|
78
|
+
// `pr-gate.devDeploy` overrides both, because a consumer whose shared environment is called `staging`
|
|
79
|
+
// must not be forced onto webpieces' vocabulary.
|
|
80
|
+
exports.DEFAULT_DEV_BRANCH_NAMESPACE = 'dev-include';
|
|
81
|
+
exports.DEFAULT_DEV_BRANCH = 'dev';
|
|
82
|
+
/**
|
|
83
|
+
* `pr-gate.devDeploy` — where `wp-push-dev` publishes the throwaway copy of a feature branch, and which
|
|
84
|
+
* ref is the shared dev branch itself.
|
|
85
|
+
*
|
|
86
|
+
* WHY A NAMESPACE AT ALL, i.e. why the copy is not just the feature branch: the feature branch is the PR
|
|
87
|
+
* head, and landing that PR ships whatever is on it. The moment a conflict between two devs has to be
|
|
88
|
+
* resolved SOMEWHERE for the shared environment to build, that resolution needs a home that is not the PR
|
|
89
|
+
* branch — otherwise "test it in dev" silently ships another dev's unreviewed work to production. The
|
|
90
|
+
* `<branchNamespace>/<feature>` copy is that home, and it is disposable by construction.
|
|
91
|
+
*
|
|
92
|
+
* `devBranch` is REFUSED as a source branch (you never push the composed branch back into itself); it is
|
|
93
|
+
* written by the consumer's CI only, which recomputes it from `origin/main` on every run.
|
|
94
|
+
*/
|
|
95
|
+
class DevDeployConfig {
|
|
96
|
+
// Literal prefix on the feature branch name: `dev-include/dean/ONE-2275`. Git refs allow slashes, and
|
|
97
|
+
// nobody ever types this — the command derives it.
|
|
98
|
+
branchNamespace;
|
|
99
|
+
// The composed, CI-owned branch that actually deploys. Never a source, never pushed to by this flow.
|
|
100
|
+
devBranch;
|
|
101
|
+
constructor(branchNamespace = exports.DEFAULT_DEV_BRANCH_NAMESPACE, devBranch = exports.DEFAULT_DEV_BRANCH) {
|
|
102
|
+
this.branchNamespace = branchNamespace;
|
|
103
|
+
this.devBranch = devBranch;
|
|
104
|
+
}
|
|
105
|
+
/** `<branchNamespace>/<branch>` — the remote ref holding the disposable copy of `branch`. */
|
|
106
|
+
copyRefFor(branch) {
|
|
107
|
+
return `${this.branchNamespace}/${branch}`;
|
|
108
|
+
}
|
|
109
|
+
/** The `git ls-remote --heads origin <pattern>` pattern matching every live copy. */
|
|
110
|
+
copyRefGlob() {
|
|
111
|
+
return `${this.branchNamespace}/*`;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
exports.DevDeployConfig = DevDeployConfig;
|
|
115
|
+
// webpieces-disable no-function-outside-class -- module-level config default, matches defaultGates/defaultPrGateConfig in this file
|
|
116
|
+
function defaultDevDeployConfig() {
|
|
117
|
+
return new DevDeployConfig(exports.DEFAULT_DEV_BRANCH_NAMESPACE, exports.DEFAULT_DEV_BRANCH);
|
|
118
|
+
}
|
|
75
119
|
class PrGateConfig {
|
|
76
120
|
mode;
|
|
77
121
|
/**
|
|
@@ -163,6 +207,11 @@ class PrGateConfig {
|
|
|
163
207
|
* wedge every PR in the repo with no self-service way out.
|
|
164
208
|
*/
|
|
165
209
|
requireDiffEvidence = false;
|
|
210
|
+
/**
|
|
211
|
+
* Where `wp-push-dev` publishes the disposable copy. Omitted ⇒ `dev-include` / `dev`, which is what
|
|
212
|
+
* makes the whole flow work with NO config edit at all.
|
|
213
|
+
*/
|
|
214
|
+
devDeploy = defaultDevDeployConfig();
|
|
166
215
|
// eslint-disable-next-line @typescript-eslint/max-params
|
|
167
216
|
constructor(mode, buildCommand, gates, mergeMode, checklists = [], gateSalt = '', checklistComments = true) {
|
|
168
217
|
this.mode = mode;
|
|
@@ -229,8 +278,25 @@ function buildPrGateConfig(section) {
|
|
|
229
278
|
? raw.reviewContext.map((e) => new ReviewContextEntry(e.label ?? '', e.path ?? ''))
|
|
230
279
|
: defaults.reviewContext;
|
|
231
280
|
built.requireDiffEvidence = raw.requireDiffEvidence ?? defaults.requireDiffEvidence;
|
|
281
|
+
built.devDeploy = buildDevDeployConfig(raw.devDeploy);
|
|
232
282
|
return built;
|
|
233
283
|
}
|
|
284
|
+
/**
|
|
285
|
+
* Build the `pr-gate.devDeploy` block. Omitted (the state of every consumer config today) ⇒ the
|
|
286
|
+
* `dev-include` / `dev` defaults. An invalid value cannot reach here — validateDevDeploySection has
|
|
287
|
+
* already failed the load.
|
|
288
|
+
*/
|
|
289
|
+
// webpieces-disable no-function-outside-class -- module-level config transform, matches buildPrGateConfig above
|
|
290
|
+
function buildDevDeployConfig(raw) {
|
|
291
|
+
const defaults = defaultDevDeployConfig();
|
|
292
|
+
if (raw === undefined || raw === null || typeof raw !== 'object')
|
|
293
|
+
return defaults;
|
|
294
|
+
const namespace = typeof raw.branchNamespace === 'string' && raw.branchNamespace.trim() !== ''
|
|
295
|
+
? raw.branchNamespace.trim() : defaults.branchNamespace;
|
|
296
|
+
const devBranch = typeof raw.devBranch === 'string' && raw.devBranch.trim() !== ''
|
|
297
|
+
? raw.devBranch.trim() : defaults.devBranch;
|
|
298
|
+
return new DevDeployConfig(namespace, devBranch);
|
|
299
|
+
}
|
|
234
300
|
/**
|
|
235
301
|
* Build the `pr-gate.landPr` block. Omitted (the current state of every consumer's config) ⇒ the
|
|
236
302
|
* 'archive-tag' default, which is what makes this feature work with NO config edit at all. An invalid
|
|
@@ -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":["import { BRANCH_RETENTION_ARCHIVE_TAG, BRANCH_RETENTIONS } from './branch-archiver';\nimport { ChecklistDefinition, RawChecklistItem, toChecklist } from './checklist-config';\n\n// 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\n/**\n * `pr-gate.landPr` — what happens to the LOCAL branch once its PR has landed.\n *\n * A rich object rather than a bare string (the `commands.pr-gate` precedent) so the next knob about\n * landing has a home, and so the rationale can sit beside the setting as a `*Why` sibling: JSON has no\n * comments, and `<key>Why` is how this repo documents non-obvious config.\n */\n/**\n * One `pr-gate.reviewContext` entry: a human label and a repo-relative path a reviewer is handed instead of\n * having to find it. Data-only (per CLAUDE.md).\n */\nexport class ReviewContextEntry {\n label: string;\n path: string; // repo-relative in config; resolved to absolute when written into an instructions file\n\n constructor(label: string, entryPath: string) {\n this.label = label;\n this.path = entryPath;\n }\n}\n\nexport class LandPrConfig {\n // One of BRANCH_RETENTIONS. Defaults to 'archive-tag' — see BranchArchiver for why a tag beats\n // keeping the branch, storing a patch, or trusting the reflog.\n branchRetention: string;\n\n constructor(branchRetention: string = BRANCH_RETENTION_ARCHIVE_TAG) {\n this.branchRetention = branchRetention;\n }\n}\n\n// webpieces-disable no-function-outside-class -- module-level config default, matches defaultGates/defaultPrGateConfig in this file\nexport function defaultLandPrConfig(): LandPrConfig {\n return new LandPrConfig(BRANCH_RETENTION_ARCHIVE_TAG);\n}\n\nexport class PrGateConfig {\n mode: string;\n /**\n * The nx-affected build gate command, shared by the two commands that can run it.\n *\n * It is NOT finish-only any more, and the change is the point. It used to run once, in\n * wp-finish-upsert-pr, which meant reviewers were spawned against a branch nobody had built — they could\n * spend a full review on code that does not compile, or on an unresolved 3-point merge.\n *\n * Now `wp-review-upsert-pr` finalizes the merge and runs this gate BEFORE briefing any reviewer, and\n * records the passing HEAD sha in the stage receipt. `wp-finish-upsert-pr` re-runs it only when HEAD has\n * moved since — so the flow gained a gate where it mattered without paying for two full builds.\n *\n * Empty string => BuildAffected falls back to DEFAULT_BUILD_COMMAND.\n */\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 // This repo's review checklists, straight from the `pr-gate.checklists` ARRAY in webpieces.config.json —\n // the ONLY accepted shape (`patterns` is a path-glob dispatch table and `subagent` a name binding, so both\n // are config). [] = no checklists. The removed `{ doc }` manifest form is a hard config error; see\n // validateChecklistsSection.\n checklists: ChecklistDefinition[];\n // Whether wp-finish-upsert-pr publishes each reviewer's full `output` as ONE combined PR comment\n // (idempotently updated on every push). Defaults to true. Set false to keep the PR body-only.\n checklistComments: boolean;\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 * What `wp-land-pr` (and `wp-cleanup`, which reaps the same branches) does with the LOCAL branch\n * once its PR is in main. Field-with-default rather than another positional constructor param —\n * this constructor is already at the max-params limit, and every existing `new PrGateConfig(...)`\n * call site correctly wants the default.\n */\n landPr: LandPrConfig = defaultLandPrConfig();\n /**\n * Globs whose diffs are NOT extracted into `.webpieces/pr-review/<feature>/diff/` — regenerated noise a\n * reviewer should not spend context reading (lockfiles, generated graphs). Default [].\n *\n * They are still MATCHED against checklists and still listed in the manifest, with a stub naming the\n * command that gets the real diff. Removing them from the changed-file set would read as \"this file did\n * not change\", which is a different and false claim.\n *\n * Fields-with-defaults rather than constructor params: that constructor is already at max-params, and\n * every existing `new PrGateConfig(...)` correctly wants the defaults.\n */\n reviewDiffExclude: string[] = [];\n /**\n * Repo-specific places a reviewer would otherwise hunt for, as `{label, path}` — e.g.\n * `{\"label\":\"Cloud Tasks queue names\",\"path\":\"terraform/services/\"}`. Resolved to absolute paths in each\n * generated instructions file; a configured-but-missing path is printed as missing, never dropped.\n */\n reviewContext: ReviewContextEntry[] = [];\n /**\n * Installed packages to resolve and hand reviewers by absolute directory. This is the knob aimed at a\n * measured failure: one reviewer burned three separate greps into `node_modules/@webpieces` looking for\n * a scanner the tooling could have pointed at directly.\n */\n reviewContextPackages: string[] = [];\n /**\n * Promote \"this reviewer wrote a verdict without ever opening the diff\" from a warning to a refusal.\n * Default false, and it should stay false until a repo has watched the warning for a while: the signal\n * is derived from undocumented Claude Code transcript internals, so a format change would otherwise\n * wedge every PR in the repo with no self-service way out.\n */\n requireDiffEvidence = false;\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(mode: string, buildCommand: string, gates: GateDefinition[], mergeMode: string, checklists: ChecklistDefinition[] = [], gateSalt = '', checklistComments = true) {\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 this.checklistComments = checklistComments;\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 // An ARRAY, always. validateChecklistsSection rejects every other shape (including the removed { doc }).\n checklists?: RawChecklistItem[];\n gateSalt?: string;\n checklistComments?: boolean;\n landPr?: RawLandPr;\n reviewDiffExclude?: string[];\n reviewContext?: RawReviewContext[];\n reviewContextPackages?: string[];\n requireDiffEvidence?: boolean;\n}\n\ninterface RawLandPr {\n branchRetention?: string;\n}\n\ninterface RawReviewContext {\n label?: string;\n path?: 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. A non-array here\n // cannot reach us: validateChecklistsSection has already failed the load.\n const checklists = Array.isArray(raw.checklists)\n ? raw.checklists.map((item: RawChecklistItem): ChecklistDefinition => toChecklist(item))\n : 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 // Optional — omitted ⇒ true ⇒ reviewer output published as a PR comment.\n const checklistComments = raw.checklistComments ?? defaults.checklistComments;\n const built = new PrGateConfig(mode, buildCommand, gates, mergeMode, checklists, gateSalt, checklistComments);\n built.landPr = buildLandPrConfig(raw.landPr);\n // Review-context knobs. All optional and all defaulted, so a config that omits every one of them (which\n // is every consumer's config today) behaves exactly as it did before they existed.\n built.reviewDiffExclude = Array.isArray(raw.reviewDiffExclude) ? raw.reviewDiffExclude : defaults.reviewDiffExclude;\n built.reviewContextPackages = Array.isArray(raw.reviewContextPackages) ? raw.reviewContextPackages : defaults.reviewContextPackages;\n built.reviewContext = Array.isArray(raw.reviewContext)\n ? raw.reviewContext.map((e: RawReviewContext): ReviewContextEntry => new ReviewContextEntry(e.label ?? '', e.path ?? ''))\n : defaults.reviewContext;\n built.requireDiffEvidence = raw.requireDiffEvidence ?? defaults.requireDiffEvidence;\n return built;\n}\n\n/**\n * Build the `pr-gate.landPr` block. Omitted (the current state of every consumer's config) ⇒ the\n * 'archive-tag' default, which is what makes this feature work with NO config edit at all. An invalid\n * value cannot reach here — validatePrGateSection has already failed the load.\n */\n// webpieces-disable no-function-outside-class -- module-level config transform, matches buildPrGateConfig above\nexport function buildLandPrConfig(raw: RawLandPr | undefined): LandPrConfig {\n const defaults = defaultLandPrConfig();\n if (raw === undefined || raw === null || typeof raw !== 'object') return defaults;\n const retention = raw.branchRetention;\n if (typeof retention !== 'string' || !BRANCH_RETENTIONS.includes(retention)) return defaults;\n return new LandPrConfig(retention);\n}\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":["import { BRANCH_RETENTION_ARCHIVE_TAG, BRANCH_RETENTIONS } from './branch-archiver';\nimport { ChecklistDefinition, RawChecklistItem, toChecklist } from './checklist-config';\n\n// 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\n/**\n * `pr-gate.landPr` — what happens to the LOCAL branch once its PR has landed.\n *\n * A rich object rather than a bare string (the `commands.pr-gate` precedent) so the next knob about\n * landing has a home, and so the rationale can sit beside the setting as a `*Why` sibling: JSON has no\n * comments, and `<key>Why` is how this repo documents non-obvious config.\n */\n/**\n * One `pr-gate.reviewContext` entry: a human label and a repo-relative path a reviewer is handed instead of\n * having to find it. Data-only (per CLAUDE.md).\n */\nexport class ReviewContextEntry {\n label: string;\n path: string; // repo-relative in config; resolved to absolute when written into an instructions file\n\n constructor(label: string, entryPath: string) {\n this.label = label;\n this.path = entryPath;\n }\n}\n\nexport class LandPrConfig {\n // One of BRANCH_RETENTIONS. Defaults to 'archive-tag' — see BranchArchiver for why a tag beats\n // keeping the branch, storing a patch, or trusting the reflog.\n branchRetention: string;\n\n constructor(branchRetention: string = BRANCH_RETENTION_ARCHIVE_TAG) {\n this.branchRetention = branchRetention;\n }\n}\n\n// webpieces-disable no-function-outside-class -- module-level config default, matches defaultGates/defaultPrGateConfig in this file\nexport function defaultLandPrConfig(): LandPrConfig {\n return new LandPrConfig(BRANCH_RETENTION_ARCHIVE_TAG);\n}\n\n// The two literal ref names the dev-deploy flow derives everything else from. Defaults, not policy —\n// `pr-gate.devDeploy` overrides both, because a consumer whose shared environment is called `staging`\n// must not be forced onto webpieces' vocabulary.\nexport const DEFAULT_DEV_BRANCH_NAMESPACE = 'dev-include';\nexport const DEFAULT_DEV_BRANCH = 'dev';\n\n/**\n * `pr-gate.devDeploy` — where `wp-push-dev` publishes the throwaway copy of a feature branch, and which\n * ref is the shared dev branch itself.\n *\n * WHY A NAMESPACE AT ALL, i.e. why the copy is not just the feature branch: the feature branch is the PR\n * head, and landing that PR ships whatever is on it. The moment a conflict between two devs has to be\n * resolved SOMEWHERE for the shared environment to build, that resolution needs a home that is not the PR\n * branch — otherwise \"test it in dev\" silently ships another dev's unreviewed work to production. The\n * `<branchNamespace>/<feature>` copy is that home, and it is disposable by construction.\n *\n * `devBranch` is REFUSED as a source branch (you never push the composed branch back into itself); it is\n * written by the consumer's CI only, which recomputes it from `origin/main` on every run.\n */\nexport class DevDeployConfig {\n // Literal prefix on the feature branch name: `dev-include/dean/ONE-2275`. Git refs allow slashes, and\n // nobody ever types this — the command derives it.\n branchNamespace: string;\n // The composed, CI-owned branch that actually deploys. Never a source, never pushed to by this flow.\n devBranch: string;\n\n constructor(branchNamespace: string = DEFAULT_DEV_BRANCH_NAMESPACE, devBranch: string = DEFAULT_DEV_BRANCH) {\n this.branchNamespace = branchNamespace;\n this.devBranch = devBranch;\n }\n\n /** `<branchNamespace>/<branch>` — the remote ref holding the disposable copy of `branch`. */\n copyRefFor(branch: string): string {\n return `${this.branchNamespace}/${branch}`;\n }\n\n /** The `git ls-remote --heads origin <pattern>` pattern matching every live copy. */\n copyRefGlob(): string {\n return `${this.branchNamespace}/*`;\n }\n}\n\n// webpieces-disable no-function-outside-class -- module-level config default, matches defaultGates/defaultPrGateConfig in this file\nexport function defaultDevDeployConfig(): DevDeployConfig {\n return new DevDeployConfig(DEFAULT_DEV_BRANCH_NAMESPACE, DEFAULT_DEV_BRANCH);\n}\n\nexport class PrGateConfig {\n mode: string;\n /**\n * The nx-affected build gate command, shared by the two commands that can run it.\n *\n * It is NOT finish-only any more, and the change is the point. It used to run once, in\n * wp-finish-upsert-pr, which meant reviewers were spawned against a branch nobody had built — they could\n * spend a full review on code that does not compile, or on an unresolved 3-point merge.\n *\n * Now `wp-review-upsert-pr` finalizes the merge and runs this gate BEFORE briefing any reviewer, and\n * records the passing HEAD sha in the stage receipt. `wp-finish-upsert-pr` re-runs it only when HEAD has\n * moved since — so the flow gained a gate where it mattered without paying for two full builds.\n *\n * Empty string => BuildAffected falls back to DEFAULT_BUILD_COMMAND.\n */\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 // This repo's review checklists, straight from the `pr-gate.checklists` ARRAY in webpieces.config.json —\n // the ONLY accepted shape (`patterns` is a path-glob dispatch table and `subagent` a name binding, so both\n // are config). [] = no checklists. The removed `{ doc }` manifest form is a hard config error; see\n // validateChecklistsSection.\n checklists: ChecklistDefinition[];\n // Whether wp-finish-upsert-pr publishes each reviewer's full `output` as ONE combined PR comment\n // (idempotently updated on every push). Defaults to true. Set false to keep the PR body-only.\n checklistComments: boolean;\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 * What `wp-land-pr` (and `wp-cleanup`, which reaps the same branches) does with the LOCAL branch\n * once its PR is in main. Field-with-default rather than another positional constructor param —\n * this constructor is already at the max-params limit, and every existing `new PrGateConfig(...)`\n * call site correctly wants the default.\n */\n landPr: LandPrConfig = defaultLandPrConfig();\n /**\n * Globs whose diffs are NOT extracted into `.webpieces/pr-review/<feature>/diff/` — regenerated noise a\n * reviewer should not spend context reading (lockfiles, generated graphs). Default [].\n *\n * They are still MATCHED against checklists and still listed in the manifest, with a stub naming the\n * command that gets the real diff. Removing them from the changed-file set would read as \"this file did\n * not change\", which is a different and false claim.\n *\n * Fields-with-defaults rather than constructor params: that constructor is already at max-params, and\n * every existing `new PrGateConfig(...)` correctly wants the defaults.\n */\n reviewDiffExclude: string[] = [];\n /**\n * Repo-specific places a reviewer would otherwise hunt for, as `{label, path}` — e.g.\n * `{\"label\":\"Cloud Tasks queue names\",\"path\":\"terraform/services/\"}`. Resolved to absolute paths in each\n * generated instructions file; a configured-but-missing path is printed as missing, never dropped.\n */\n reviewContext: ReviewContextEntry[] = [];\n /**\n * Installed packages to resolve and hand reviewers by absolute directory. This is the knob aimed at a\n * measured failure: one reviewer burned three separate greps into `node_modules/@webpieces` looking for\n * a scanner the tooling could have pointed at directly.\n */\n reviewContextPackages: string[] = [];\n /**\n * Promote \"this reviewer wrote a verdict without ever opening the diff\" from a warning to a refusal.\n * Default false, and it should stay false until a repo has watched the warning for a while: the signal\n * is derived from undocumented Claude Code transcript internals, so a format change would otherwise\n * wedge every PR in the repo with no self-service way out.\n */\n requireDiffEvidence = false;\n /**\n * Where `wp-push-dev` publishes the disposable copy. Omitted ⇒ `dev-include` / `dev`, which is what\n * makes the whole flow work with NO config edit at all.\n */\n devDeploy: DevDeployConfig = defaultDevDeployConfig();\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(mode: string, buildCommand: string, gates: GateDefinition[], mergeMode: string, checklists: ChecklistDefinition[] = [], gateSalt = '', checklistComments = true) {\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 this.checklistComments = checklistComments;\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 // An ARRAY, always. validateChecklistsSection rejects every other shape (including the removed { doc }).\n checklists?: RawChecklistItem[];\n gateSalt?: string;\n checklistComments?: boolean;\n landPr?: RawLandPr;\n reviewDiffExclude?: string[];\n reviewContext?: RawReviewContext[];\n reviewContextPackages?: string[];\n requireDiffEvidence?: boolean;\n devDeploy?: RawDevDeploy;\n}\n\ninterface RawDevDeploy {\n branchNamespace?: string;\n devBranch?: string;\n}\n\ninterface RawLandPr {\n branchRetention?: string;\n}\n\ninterface RawReviewContext {\n label?: string;\n path?: 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. A non-array here\n // cannot reach us: validateChecklistsSection has already failed the load.\n const checklists = Array.isArray(raw.checklists)\n ? raw.checklists.map((item: RawChecklistItem): ChecklistDefinition => toChecklist(item))\n : 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 // Optional — omitted ⇒ true ⇒ reviewer output published as a PR comment.\n const checklistComments = raw.checklistComments ?? defaults.checklistComments;\n const built = new PrGateConfig(mode, buildCommand, gates, mergeMode, checklists, gateSalt, checklistComments);\n built.landPr = buildLandPrConfig(raw.landPr);\n // Review-context knobs. All optional and all defaulted, so a config that omits every one of them (which\n // is every consumer's config today) behaves exactly as it did before they existed.\n built.reviewDiffExclude = Array.isArray(raw.reviewDiffExclude) ? raw.reviewDiffExclude : defaults.reviewDiffExclude;\n built.reviewContextPackages = Array.isArray(raw.reviewContextPackages) ? raw.reviewContextPackages : defaults.reviewContextPackages;\n built.reviewContext = Array.isArray(raw.reviewContext)\n ? raw.reviewContext.map((e: RawReviewContext): ReviewContextEntry => new ReviewContextEntry(e.label ?? '', e.path ?? ''))\n : defaults.reviewContext;\n built.requireDiffEvidence = raw.requireDiffEvidence ?? defaults.requireDiffEvidence;\n built.devDeploy = buildDevDeployConfig(raw.devDeploy);\n return built;\n}\n\n/**\n * Build the `pr-gate.devDeploy` block. Omitted (the state of every consumer config today) ⇒ the\n * `dev-include` / `dev` defaults. An invalid value cannot reach here — validateDevDeploySection has\n * already failed the load.\n */\n// webpieces-disable no-function-outside-class -- module-level config transform, matches buildPrGateConfig above\nexport function buildDevDeployConfig(raw: RawDevDeploy | undefined): DevDeployConfig {\n const defaults = defaultDevDeployConfig();\n if (raw === undefined || raw === null || typeof raw !== 'object') return defaults;\n const namespace = typeof raw.branchNamespace === 'string' && raw.branchNamespace.trim() !== ''\n ? raw.branchNamespace.trim() : defaults.branchNamespace;\n const devBranch = typeof raw.devBranch === 'string' && raw.devBranch.trim() !== ''\n ? raw.devBranch.trim() : defaults.devBranch;\n return new DevDeployConfig(namespace, devBranch);\n}\n\n/**\n * Build the `pr-gate.landPr` block. Omitted (the current state of every consumer's config) ⇒ the\n * 'archive-tag' default, which is what makes this feature work with NO config edit at all. An invalid\n * value cannot reach here — validatePrGateSection has already failed the load.\n */\n// webpieces-disable no-function-outside-class -- module-level config transform, matches buildPrGateConfig above\nexport function buildLandPrConfig(raw: RawLandPr | undefined): LandPrConfig {\n const defaults = defaultLandPrConfig();\n if (raw === undefined || raw === null || typeof raw !== 'object') return defaults;\n const retention = raw.branchRetention;\n if (typeof retention !== 'string' || !BRANCH_RETENTIONS.includes(retention)) return defaults;\n return new LandPrConfig(retention);\n}\n\n"]}
|
|
@@ -24,3 +24,13 @@ export declare function validateNoGateSaltRationale(s: Record<string, unknown>):
|
|
|
24
24
|
*/
|
|
25
25
|
export declare function validateChecklistsSection(value: unknown, repoRoot?: string): string[];
|
|
26
26
|
export declare function validateLandPrSection(value: unknown): string[];
|
|
27
|
+
/**
|
|
28
|
+
* Validate the optional `pr-gate.devDeploy` block — where `wp-push-dev` publishes the disposable copy of
|
|
29
|
+
* a feature branch, and which ref is the shared dev branch itself. Absent ⇒ `dev-include` / `dev`.
|
|
30
|
+
*
|
|
31
|
+
* `devBranch` must NOT contain a slash and must not sit inside `branchNamespace`: the whole point of the
|
|
32
|
+
* namespace is that the composed dev branch is written by CI and the copies are written by developers, so
|
|
33
|
+
* a config where one contains the other makes `--list` enumerate the deploy branch and makes the
|
|
34
|
+
* "refused as a source branch" check ambiguous.
|
|
35
|
+
*/
|
|
36
|
+
export declare function validateDevDeploySection(value: unknown): string[];
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.validateNoGateSaltRationale = validateNoGateSaltRationale;
|
|
4
4
|
exports.validateChecklistsSection = validateChecklistsSection;
|
|
5
5
|
exports.validateLandPrSection = validateLandPrSection;
|
|
6
|
+
exports.validateDevDeploySection = validateDevDeploySection;
|
|
6
7
|
const branch_archiver_1 = require("./branch-archiver");
|
|
7
8
|
const checklist_validator_1 = require("./checklist-validator");
|
|
8
9
|
const checklist_config_1 = require("./checklist-config");
|
|
@@ -179,4 +180,63 @@ function validateLandPrSection(value) {
|
|
|
179
180
|
` "${branch_archiver_1.BRANCH_RETENTION_KEEP}" — do not delete. Branches then accumulate until branch-creation-guard trips.`,
|
|
180
181
|
];
|
|
181
182
|
}
|
|
183
|
+
// A git ref COMPONENT this flow is willing to build a ref name out of. Deliberately much narrower than
|
|
184
|
+
// git's own check-ref-format: these two values are concatenated into a ref that a command then
|
|
185
|
+
// force-pushes, so anything that could be read as a flag, a path escape, or a glob is rejected outright
|
|
186
|
+
// rather than trusted to `git push` argument order.
|
|
187
|
+
const REF_COMPONENT = /^[A-Za-z0-9][A-Za-z0-9._/-]*$/;
|
|
188
|
+
/**
|
|
189
|
+
* Validate the optional `pr-gate.devDeploy` block — where `wp-push-dev` publishes the disposable copy of
|
|
190
|
+
* a feature branch, and which ref is the shared dev branch itself. Absent ⇒ `dev-include` / `dev`.
|
|
191
|
+
*
|
|
192
|
+
* `devBranch` must NOT contain a slash and must not sit inside `branchNamespace`: the whole point of the
|
|
193
|
+
* namespace is that the composed dev branch is written by CI and the copies are written by developers, so
|
|
194
|
+
* a config where one contains the other makes `--list` enumerate the deploy branch and makes the
|
|
195
|
+
* "refused as a source branch" check ambiguous.
|
|
196
|
+
*/
|
|
197
|
+
// webpieces-disable no-any-unknown -- `value` is the opaque consumer devDeploy value until narrowed here
|
|
198
|
+
// webpieces-disable no-function-outside-class -- module-level config validator, matches the rest of this file
|
|
199
|
+
function validateDevDeploySection(value) {
|
|
200
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
201
|
+
return ['[pr-gate] "devDeploy" must be an object, e.g. { "branchNamespace": "dev-include", "devBranch": "dev" }.'];
|
|
202
|
+
}
|
|
203
|
+
// webpieces-disable no-any-unknown -- narrowing the opaque devDeploy object from consumer JSON
|
|
204
|
+
const s = value;
|
|
205
|
+
const errors = [];
|
|
206
|
+
errors.push(...validateRefComponent(s, 'branchNamespace', 'dev-include'));
|
|
207
|
+
errors.push(...validateRefComponent(s, 'devBranch', 'dev'));
|
|
208
|
+
if (errors.length > 0)
|
|
209
|
+
return errors;
|
|
210
|
+
const namespace = typeof s['branchNamespace'] === 'string' ? s['branchNamespace'].trim() : 'dev-include';
|
|
211
|
+
const devBranch = typeof s['devBranch'] === 'string' ? s['devBranch'].trim() : 'dev';
|
|
212
|
+
if (devBranch.includes('/')) {
|
|
213
|
+
errors.push(`[pr-gate] "devDeploy.devBranch" = "${devBranch}" must be a single ref name with no "/" — it is the ` +
|
|
214
|
+
`branch your CI composes and deploys, not a namespace.`);
|
|
215
|
+
}
|
|
216
|
+
if (devBranch === namespace || devBranch.startsWith(`${namespace}/`) || namespace.startsWith(`${devBranch}/`)) {
|
|
217
|
+
errors.push(`[pr-gate] "devDeploy.devBranch" ("${devBranch}") and "devDeploy.branchNamespace" ("${namespace}") must not ` +
|
|
218
|
+
`contain one another. The namespace holds the DISPOSABLE per-developer copies (written by wp-push-dev); ` +
|
|
219
|
+
`devBranch is the COMPOSED branch your CI rebuilds from origin/main. Overlapping them makes wp-push-dev ` +
|
|
220
|
+
`--list enumerate the deploy branch and makes "refused as a source branch" ambiguous.`);
|
|
221
|
+
}
|
|
222
|
+
return errors;
|
|
223
|
+
}
|
|
224
|
+
// One `devDeploy` string field: present ⇒ must be a non-empty, ref-safe string. Absent ⇒ the default.
|
|
225
|
+
// webpieces-disable no-any-unknown -- the already-narrowed opaque devDeploy object
|
|
226
|
+
// webpieces-disable no-function-outside-class -- module-level config validator, matches the rest of this file
|
|
227
|
+
function validateRefComponent(s, key, example) {
|
|
228
|
+
if (!(key in s))
|
|
229
|
+
return [];
|
|
230
|
+
const raw = s[key];
|
|
231
|
+
if (typeof raw !== 'string' || raw.trim() === '') {
|
|
232
|
+
return [`[pr-gate] "devDeploy.${key}" must be a non-empty string, e.g. "${example}". Omit the key for the "${example}" default.`];
|
|
233
|
+
}
|
|
234
|
+
if (!REF_COMPONENT.test(raw.trim())) {
|
|
235
|
+
return [
|
|
236
|
+
`[pr-gate] "devDeploy.${key}" = "${raw}" is not a usable git ref name. Use letters, digits, ".", "_", ` +
|
|
237
|
+
`"-" and "/" only, starting with a letter or digit (e.g. "${example}").`,
|
|
238
|
+
];
|
|
239
|
+
}
|
|
240
|
+
return [];
|
|
241
|
+
}
|
|
182
242
|
//# sourceMappingURL=pr-gate-section-validators.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pr-gate-section-validators.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/pr-gate-section-validators.ts"],"names":[],"mappings":";;AA6BA,kEASC;AAgCD,8DAIC;AA+FD,sDAkBC;AA3LD,uDAK2B;AAC3B,+DAA2D;AAC3D,yDAAwF;AAExF,yGAAyG;AACzG,sGAAsG;AACtG,oEAAoE;AAEpE,2GAA2G;AAC3G,wGAAwG;AACxG,MAAM,aAAa,GAAG,aAAa,CAAC;AAEpC;;;;;;;;;GASG;AACH,6GAA6G;AAC7G,8GAA8G;AAC9G,SAAgB,2BAA2B,CAAC,CAA0B;IAClE,IAAI,CAAC,CAAC,aAAa,IAAI,CAAC,CAAC;QAAE,OAAO,EAAE,CAAC;IACrC,OAAO;QACH,yBAAyB,aAAa,mEAAmE;YACzG,wGAAwG;YACxG,yGAAyG;YACzG,sGAAsG;YACtG,0EAA0E;KAC7E,CAAC;AACN,CAAC;AAED,MAAM,iBAAiB,GAAG,CACtB,YAAY;IACZ,uBAAuB;IACvB,gDAAgD;IAChD,qDAAqD;IACrD,yDAAyD;IACzD,8BAA8B;IAC9B,SAAS;IACT,2GAA2G;IAC3G,iGAAiG;IACjG,0GAA0G;IAC1G,yGAAyG;IACzG,YAAY,CACf,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,2FAA2F;AAC3F,8GAA8G;AAC9G,SAAgB,yBAAyB,CAAC,KAAc,EAAE,QAAiB;IACvE,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;QAAE,OAAO,sBAAsB,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;IACzE,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,IAAI,KAAK;QAAE,OAAO,CAAC,mBAAmB,CAAC,KAAK,CAAC,CAAC,CAAC;IACvG,OAAO,CAAC,mFAAmF,iBAAiB,EAAE,CAAC,CAAC;AACpH,CAAC;AAED;;;;GAIG;AACH,wGAAwG;AACxG,8GAA8G;AAC9G,SAAS,mBAAmB,CAAC,KAAa;IACtC,MAAM,GAAG,GAAI,KAAiC,CAAC,KAAK,CAAC,CAAC;IACtD,MAAM,MAAM,GAAG,OAAO,GAAG,KAAK,QAAQ,IAAI,GAAG,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,yBAAyB,CAAC;IAC9F,OAAO,CACH,mDAAmD,MAAM,qDAAqD;QAC9G,uFAAuF;QACvF,gBAAgB,MAAM,qFAAqF;QAC3G,2CAA2C,MAAM,0CAA0C;QAC3F,4FAA4F;QAC5F,WAAW,MAAM,mFAAmF;QACpG,qEAAqE,MAAM,sBAAsB;QACjG,KAAK,iBAAiB,EAAE,CAC3B,CAAC;AACN,CAAC;AAED;;;;;;;;;;;;GAYG;AACH,qGAAqG;AACrG,8GAA8G;AAC9G,SAAS,iBAAiB,CAAC,CAA0B,EAAE,CAAS;IAC5D,MAAM,IAAI,GAAG,OAAO,CAAC,CAAC,UAAU,CAAC,KAAK,QAAQ,IAAI,CAAC,CAAC,UAAU,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC;IAC7G,IAAI,CAAC,CAAC,UAAU,CAAC,KAAK,SAAS,EAAE,CAAC;QAC9B,OAAO;YACH,wBAAwB,CAAC,IAAI,IAAI,qEAAqE;gBACtG,sCAAsC;gBACtC,qGAAqG;gBACrG,sFAAsF;gBACtF,sGAAsG;gBACtG,iGAAiG;gBACjG,KAAK,iBAAiB,EAAE;SAC3B,CAAC;IACN,CAAC;IACD,IAAI,OAAO,CAAC,CAAC,UAAU,CAAC,KAAK,SAAS,EAAE,CAAC;QACrC,OAAO,CAAC,wBAAwB,CAAC,IAAI,IAAI,2FAA2F,CAAC,CAAC;IAC1I,CAAC;IACD,OAAO,EAAE,CAAC;AACd,CAAC;AAED,wGAAwG;AACxG,6GAA6G;AAC7G,kFAAkF;AAClF,6FAA6F;AAC7F,8GAA8G;AAC9G,SAAS,sBAAsB,CAAC,KAAyB,EAAE,QAAiB;IACxE,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,MAAM,KAAK,GAAuB,EAAE,CAAC;IACrC,8GAA8G;IAC9G,KAAK,CAAC,OAAO,CAAC,CAAC,KAAc,EAAE,CAAS,EAAQ,EAAE;QAC9C,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;YACtE,MAAM,CAAC,IAAI,CAAC,wBAAwB,CAAC,0DAA0D,CAAC,CAAC;YACjG,OAAO;QACX,CAAC;QACD,2EAA2E;QAC3E,MAAM,CAAC,GAAG,KAAgC,CAAC;QAC3C,IAAI,CAAC,CAAC,KAAK,CAAC,KAAK,SAAS,IAAI,OAAO,CAAC,CAAC,KAAK,CAAC,KAAK,QAAQ,EAAE,CAAC;YACzD,MAAM,CAAC,IAAI,CAAC,wBAAwB,CAAC,8HAA8H,CAAC,CAAC;QACzK,CAAC;QACD,yFAAyF;QACzF,IAAI,CAAC,CAAC,UAAU,CAAC,KAAK,SAAS,IAAI,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,CAAC,UAAU,CAAC,CAAC,KAAK,CAAC,CAAC,CAAU,EAAW,EAAE,CAAC,OAAO,CAAC,KAAK,QAAQ,CAAC,CAAC,EAAE,CAAC;YACxI,MAAM,CAAC,IAAI,CAAC,wBAAwB,CAAC,8EAA8E,CAAC,CAAC;QACzH,CAAC;QACD,MAAM,CAAC,IAAI,CAAC,GAAG,iBAAiB,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;QACxC,KAAK,CAAC,IAAI,CAAC,CAAqB,CAAC,CAAC;IACtC,CAAC,CAAC,CAAC;IACH,IAAI,QAAQ,KAAK,SAAS;QAAE,OAAO,MAAM,CAAC;IAC1C,MAAM,IAAI,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,IAAsB,EAAuB,EAAE,CAAC,IAAA,8BAAW,EAAC,IAAI,CAAC,CAAC,CAAC;IAC3F,OAAO,CAAC,GAAG,MAAM,EAAE,GAAG,IAAI,wCAAkB,EAAE,CAAC,QAAQ,CAAC,QAAQ,EAAE,IAAI,CAAC,CAAC,CAAC;AAC7E,CAAC;AAED,kGAAkG;AAClG,oGAAoG;AACpG,sGAAsG;AACtG,iGAAiG;AACjG,wGAAwG;AACxG,8GAA8G;AAC9G,SAAgB,qBAAqB,CAAC,KAAc;IAChD,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,KAAK,IAAI,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC,EAAE,CAAC;QACtE,OAAO,CAAC,oEAAoE,8CAA4B,MAAM,CAAC,CAAC;IACpH,CAAC;IACD,4FAA4F;IAC5F,MAAM,CAAC,GAAG,KAAgC,CAAC;IAC3C,IAAI,CAAC,CAAC,iBAAiB,IAAI,CAAC,CAAC;QAAE,OAAO,EAAE,CAAC;IACzC,MAAM,SAAS,GAAG,CAAC,CAAC,iBAAiB,CAAC,CAAC;IACvC,IAAI,OAAO,SAAS,KAAK,QAAQ,IAAI,mCAAiB,CAAC,QAAQ,CAAC,SAAS,CAAC;QAAE,OAAO,EAAE,CAAC;IACtF,OAAO;QACH,yCAAyC,MAAM,CAAC,SAAS,CAAC,kBAAkB;YAC5E,mBAAmB,mCAAiB,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK;YACpD,MAAM,8CAA4B,oFAAoF;YACtH,+FAA+F;YAC/F,oFAAoF;YACpF,MAAM,yCAAuB,8EAA8E;YAC3G,MAAM,uCAAqB,uFAAuF;KACrH,CAAC;AACN,CAAC","sourcesContent":["import {\n BRANCH_RETENTIONS,\n BRANCH_RETENTION_ARCHIVE_TAG,\n BRANCH_RETENTION_DELETE,\n BRANCH_RETENTION_KEEP,\n} from './branch-archiver';\nimport { ChecklistValidator } from './checklist-validator';\nimport { ChecklistDefinition, RawChecklistItem, toChecklist } from './checklist-config';\n\n// The two `pr-gate` sub-sections whose validation is bulky enough to own a file: the review `checklists`\n// and the one rationale key that is rejected outright. Split out of validate-config.ts only for size;\n// loadAndValidate still reaches both through validatePrGateSection.\n\n// The `*Why` convention (buildCommandWhy, mergeModeWhy, gatesWhy…) is free-form rationale a consumer keeps\n// beside a field, and pr-gate tolerates any of them — EXCEPT this one. See validateNoGateSaltRationale.\nconst GATE_SALT_WHY = 'gateSaltWhy';\n\n/**\n * Reject `gateSaltWhy` outright, and say why, so the next validate on upgrade FORCES its removal.\n *\n * webpieces.config.json is one of the first files a coding agent reads. A rationale note next to `gateSalt`\n * necessarily explains what the token protects, that the salt is committed, and therefore how to forge it —\n * i.e. it is a bypass how-to, sitting in the most-read file in the repo, defeating the only thing an\n * obscurity-grade mechanism has going for it. The rationale belongs in the webpieces source (pr-gate-config.ts\n * documents it in full for humans reading the tooling), never in consumer config. Every other `*Why` key\n * stays allowed; this is not a general ban on documenting your config.\n */\n// webpieces-disable no-any-unknown -- the already-narrowed opaque pr-gate section; only key PRESENCE is read\n// webpieces-disable no-function-outside-class -- module-level config validator, matches the rest of this file\nexport function validateNoGateSaltRationale(s: Record<string, unknown>): string[] {\n if (!(GATE_SALT_WHY in s)) return [];\n return [\n `[pr-gate] DELETE the \"${GATE_SALT_WHY}\" key from webpieces.config.json. It is a rationale note next to ` +\n `\"gateSalt\", which means it spells out what the gate token protects and that the salt is committed — a ` +\n `bypass how-to in the file a coding agent reads first. The mechanism is obscurity-grade; documenting it ` +\n `here removes the obscurity. Nothing else needs changing: the reasoning is already documented in the ` +\n `webpieces source (PrGateConfig.gateSalt) for humans reading the tooling.`,\n ];\n}\n\nconst CHECKLIST_EXAMPLE = (\n 'Example:\\n' +\n ' \"checklists\": [\\n' +\n ' { \"subagent\": \"db-migration-reviewer\",\\n' +\n ' \"doc\": \".claude/review/db-migrations.md\",\\n' +\n ' \"patterns\": [\"**/migrations/**\", \"**/*.sql\"],\\n' +\n ' \"required\": true }\\n' +\n ' ]\\n' +\n ' Each entry needs its OWN reviewer subagent (a .claude/agents/<subagent>.md) — that is how independent\\n' +\n ' review is enforced. \"doc\" is REPO-relative. Omit \"patterns\" (or use []) to run on every PR.\\n' +\n ' \"required\" is MANDATORY on every entry: true blocks the PR until the reviewer passes; false makes it\\n' +\n ' an OPTIONAL review the human is offered and may decline (but if they DO run it, a red verdict still\\n' +\n ' blocks).'\n);\n\n/**\n * The `checklists` section of a pr-gate config: an ARRAY of { subagent, doc?, patterns? }, and nothing else.\n *\n * The previous `{ \"doc\": \"...\" }` shape — which hid the same array in a `<!-- webpieces:checklists -->` HTML\n * comment inside a markdown doc — is REMOVED, not deprecated. It is rejected with the exact edit to make.\n * There is deliberately no back-compat branch: two accepted shapes means two code paths, two doc-resolution\n * rules and two sets of error messages to keep honest forever, while the migration itself is a mechanical\n * config edit that the coding agent reading this error applies in one pass. A hard failure naming the fix is\n * cheaper than permanent duality.\n *\n * Exported so the isolated validate-checklist-docs target reuses it. `repoRoot` (when known) lets the doc +\n * reviewer-agent existence checks run.\n */\n// webpieces-disable no-any-unknown -- `value` is opaque consumer JSON until narrowed below\n// webpieces-disable no-function-outside-class -- module-level config validator, matches the rest of this file\nexport function validateChecklistsSection(value: unknown, repoRoot?: string): string[] {\n if (Array.isArray(value)) return validateChecklistArray(value, repoRoot);\n if (typeof value === 'object' && value !== null && 'doc' in value) return [legacyManifestError(value)];\n return [`[pr-gate] \"checklists\" must be an ARRAY of { \"subagent\", \"doc\"?, \"patterns\"? }. ${CHECKLIST_EXAMPLE}`];\n}\n\n/**\n * The migration message for the removed `{ doc }` manifest shape. It names the doc the consumer pointed at,\n * because that is the file holding the array they must move, and spells out the one non-obvious part of the\n * move: entry `doc` paths used to resolve relative to that manifest doc and are now REPO-relative.\n */\n// webpieces-disable no-any-unknown -- narrowing the opaque checklists section to read the old `doc` key\n// webpieces-disable no-function-outside-class -- module-level config validator, matches the rest of this file\nfunction legacyManifestError(value: object): string {\n const doc = (value as Record<string, unknown>)['doc'];\n const docRel = typeof doc === 'string' && doc.trim() !== '' ? doc : '<your review index doc>';\n return (\n `[pr-gate] \"checklists\" is the REMOVED { \"doc\": \"${docRel}\" } shape. The checklist array no longer lives in\\n` +\n ` an HTML comment inside a markdown doc — put it directly in webpieces.config.json:\\n` +\n ` 1. Open \"${docRel}\" and copy the JSON array out of its <!-- webpieces:checklists [...] --> comment.\\n` +\n ` 2. Replace \"checklists\": { \"doc\": \"${docRel}\" } with \"checklists\": <that array>.\\n` +\n ` 3. Rewrite each entry's \"doc\" to be REPO-relative — they used to resolve relative to\\n` +\n ` \"${docRel}\", so a bare \"db-migrations.md\" becomes e.g. \".claude/review/db-migrations.md\".\\n` +\n ` 4. Delete the <!-- webpieces:checklists ... --> comment from \"${docRel}\"; keep the prose.\\n` +\n ` ${CHECKLIST_EXAMPLE}`\n );\n}\n\n/**\n * `required` is MANDATORY on every checklist entry — omitting it is an error, never a default.\n *\n * The error names the entry's own subagent, because that is what the consumer recognizes in a\n * twelve-entry array; `checklists[7]` alone means counting braces. It also states BOTH edits, because the\n * whole point of the key is that the answer differs per checklist and only the consumer knows which.\n *\n * Why a hard rejection instead of `?? true`: an accepted shape is never migrated. Defaulting to true\n * silently keeps the all-blocking behavior this key exists to relieve, and every consumer that would have\n * benefited stays on the old behavior forever without ever being told the dial exists. Defaulting to false\n * is worse — it would silently DOWNGRADE a live review gate on upgrade. Per CLAUDE.md the reader of this\n * message is a coding agent, so the migration is one mechanical pass.\n */\n// webpieces-disable no-any-unknown -- one opaque checklist entry, narrowed by the typeof guards here\n// webpieces-disable no-function-outside-class -- module-level config validator, matches the rest of this file\nfunction requiredKeyErrors(e: Record<string, unknown>, i: number): string[] {\n const name = typeof e['subagent'] === 'string' && e['subagent'].trim() !== '' ? ` (\"${e['subagent']}\")` : '';\n if (e['required'] === undefined) {\n return [\n `[pr-gate] checklists[${i}]${name} is missing \"required\". Every checklist must state one — there is\\n` +\n ` no default, in either direction.\\n` +\n ` \"required\": true → BLOCKING. wp-finish-upsert-pr refuses the PR until this reviewer passes.\\n` +\n ` This is what every checklist did before this key existed.\\n` +\n ` \"required\": false → OPTIONAL. When it matches the diff, wp-review-upsert-pr offers it and the\\n` +\n ` human may decline it. If they DO run it, a red verdict still blocks.\\n` +\n ` ${CHECKLIST_EXAMPLE}`,\n ];\n }\n if (typeof e['required'] !== 'boolean') {\n return [`[pr-gate] checklists[${i}]${name}.required must be a boolean (true = blocking, false = optional) — not a string or number.`];\n }\n return [];\n}\n\n// Structurally check each entry HERE — a bad `patterns` or a non-object entry is a config-file typo and\n// deserves a `checklists[i]` message — then hand the narrowed defs to ChecklistValidator for the checks only\n// the filesystem can answer (the guidance doc exists, the reviewer agent exists).\n// webpieces-disable no-any-unknown -- opaque consumer JSON entries, narrowed per-field below\n// webpieces-disable no-function-outside-class -- module-level config validator, matches the rest of this file\nfunction validateChecklistArray(value: readonly unknown[], repoRoot?: string): string[] {\n const errors: string[] = [];\n const items: RawChecklistItem[] = [];\n // webpieces-disable no-any-unknown -- each array entry is opaque consumer JSON, narrowed field-by-field below\n value.forEach((entry: unknown, i: number): void => {\n if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) {\n errors.push(`[pr-gate] checklists[${i}] must be an object { \"subagent\", \"doc\"?, \"patterns\"? }.`);\n return;\n }\n // webpieces-disable no-any-unknown -- narrowing one opaque checklist entry\n const e = entry as Record<string, unknown>;\n if (e['doc'] !== undefined && typeof e['doc'] !== 'string') {\n errors.push(`[pr-gate] checklists[${i}].doc must be a string — a REPO-relative path to the reviewer's guidance doc (omit it and the reviewer just reads the diff).`);\n }\n // webpieces-disable no-any-unknown -- opaque array element, narrowed by the typeof guard\n if (e['patterns'] !== undefined && !(Array.isArray(e['patterns']) && e['patterns'].every((p: unknown): boolean => typeof p === 'string'))) {\n errors.push(`[pr-gate] checklists[${i}].patterns must be a string[] of path globs (omit or [] to run on every PR).`);\n }\n errors.push(...requiredKeyErrors(e, i));\n items.push(e as RawChecklistItem);\n });\n if (repoRoot === undefined) return errors;\n const defs = items.map((item: RawChecklistItem): ChecklistDefinition => toChecklist(item));\n return [...errors, ...new ChecklistValidator().validate(repoRoot, defs)];\n}\n\n// The `landPr` block: what happens to the LOCAL branch once its PR is in main. Optional — omitted\n// means \"archive-tag\", which is deliberately the DEFAULT so a consumer gets the branch-accumulation\n// fix without editing config at all. `branchRetentionWhy` (and any other `*Why` sibling) is free-form\n// rationale prose and is tolerated, per the repo's convention for documenting comment-less JSON.\n// webpieces-disable no-any-unknown -- `value` is the opaque consumer `landPr` value until narrowed here\n// webpieces-disable no-function-outside-class -- module-level config validator, matches the rest of this file\nexport function validateLandPrSection(value: unknown): string[] {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n return [`[pr-gate] \"landPr\" must be an object, e.g. { \"branchRetention\": \"${BRANCH_RETENTION_ARCHIVE_TAG}\" }.`];\n }\n // webpieces-disable no-any-unknown -- narrowing the opaque landPr object from consumer JSON\n const s = value as Record<string, unknown>;\n if (!('branchRetention' in s)) return [];\n const retention = s['branchRetention'];\n if (typeof retention === 'string' && BRANCH_RETENTIONS.includes(retention)) return [];\n return [\n `[pr-gate] \"landPr.branchRetention\" = \"${String(retention)}\" is not valid. ` +\n `Must be one of: ${BRANCH_RETENTIONS.join(', ')}.\\n` +\n ` \"${BRANCH_RETENTION_ARCHIVE_TAG}\" — (default) tag the branch tip as archive/<date>/<branch>, THEN delete it. The\\n` +\n ` history stays byte-identical and restorable, but the branch stops counting\\n` +\n ` toward the branch cap and cannot be committed onto by accident.\\n` +\n ` \"${BRANCH_RETENTION_DELETE}\" — delete outright; recoverable only from the reflog, which expires.\\n` +\n ` \"${BRANCH_RETENTION_KEEP}\" — do not delete. Branches then accumulate until branch-creation-guard trips.`,\n ];\n}\n"]}
|
|
1
|
+
{"version":3,"file":"pr-gate-section-validators.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/pr-gate-section-validators.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n BRANCH_RETENTIONS,\n BRANCH_RETENTION_ARCHIVE_TAG,\n BRANCH_RETENTION_DELETE,\n BRANCH_RETENTION_KEEP,\n} from './branch-archiver';\nimport { ChecklistValidator } from './checklist-validator';\nimport { ChecklistDefinition, RawChecklistItem, toChecklist } from './checklist-config';\n\n// The two `pr-gate` sub-sections whose validation is bulky enough to own a file: the review `checklists`\n// and the one rationale key that is rejected outright. Split out of validate-config.ts only for size;\n// loadAndValidate still reaches both through validatePrGateSection.\n\n// The `*Why` convention (buildCommandWhy, mergeModeWhy, gatesWhy…) is free-form rationale a consumer keeps\n// beside a field, and pr-gate tolerates any of them — EXCEPT this one. See validateNoGateSaltRationale.\nconst GATE_SALT_WHY = 'gateSaltWhy';\n\n/**\n * Reject `gateSaltWhy` outright, and say why, so the next validate on upgrade FORCES its removal.\n *\n * webpieces.config.json is one of the first files a coding agent reads. A rationale note next to `gateSalt`\n * necessarily explains what the token protects, that the salt is committed, and therefore how to forge it —\n * i.e. it is a bypass how-to, sitting in the most-read file in the repo, defeating the only thing an\n * obscurity-grade mechanism has going for it. The rationale belongs in the webpieces source (pr-gate-config.ts\n * documents it in full for humans reading the tooling), never in consumer config. Every other `*Why` key\n * stays allowed; this is not a general ban on documenting your config.\n */\n// webpieces-disable no-any-unknown -- the already-narrowed opaque pr-gate section; only key PRESENCE is read\n// webpieces-disable no-function-outside-class -- module-level config validator, matches the rest of this file\nexport function validateNoGateSaltRationale(s: Record<string, unknown>): string[] {\n if (!(GATE_SALT_WHY in s)) return [];\n return [\n `[pr-gate] DELETE the \"${GATE_SALT_WHY}\" key from webpieces.config.json. It is a rationale note next to ` +\n `\"gateSalt\", which means it spells out what the gate token protects and that the salt is committed — a ` +\n `bypass how-to in the file a coding agent reads first. The mechanism is obscurity-grade; documenting it ` +\n `here removes the obscurity. Nothing else needs changing: the reasoning is already documented in the ` +\n `webpieces source (PrGateConfig.gateSalt) for humans reading the tooling.`,\n ];\n}\n\nconst CHECKLIST_EXAMPLE = (\n 'Example:\\n' +\n ' \"checklists\": [\\n' +\n ' { \"subagent\": \"db-migration-reviewer\",\\n' +\n ' \"doc\": \".claude/review/db-migrations.md\",\\n' +\n ' \"patterns\": [\"**/migrations/**\", \"**/*.sql\"],\\n' +\n ' \"required\": true }\\n' +\n ' ]\\n' +\n ' Each entry needs its OWN reviewer subagent (a .claude/agents/<subagent>.md) — that is how independent\\n' +\n ' review is enforced. \"doc\" is REPO-relative. Omit \"patterns\" (or use []) to run on every PR.\\n' +\n ' \"required\" is MANDATORY on every entry: true blocks the PR until the reviewer passes; false makes it\\n' +\n ' an OPTIONAL review the human is offered and may decline (but if they DO run it, a red verdict still\\n' +\n ' blocks).'\n);\n\n/**\n * The `checklists` section of a pr-gate config: an ARRAY of { subagent, doc?, patterns? }, and nothing else.\n *\n * The previous `{ \"doc\": \"...\" }` shape — which hid the same array in a `<!-- webpieces:checklists -->` HTML\n * comment inside a markdown doc — is REMOVED, not deprecated. It is rejected with the exact edit to make.\n * There is deliberately no back-compat branch: two accepted shapes means two code paths, two doc-resolution\n * rules and two sets of error messages to keep honest forever, while the migration itself is a mechanical\n * config edit that the coding agent reading this error applies in one pass. A hard failure naming the fix is\n * cheaper than permanent duality.\n *\n * Exported so the isolated validate-checklist-docs target reuses it. `repoRoot` (when known) lets the doc +\n * reviewer-agent existence checks run.\n */\n// webpieces-disable no-any-unknown -- `value` is opaque consumer JSON until narrowed below\n// webpieces-disable no-function-outside-class -- module-level config validator, matches the rest of this file\nexport function validateChecklistsSection(value: unknown, repoRoot?: string): string[] {\n if (Array.isArray(value)) return validateChecklistArray(value, repoRoot);\n if (typeof value === 'object' && value !== null && 'doc' in value) return [legacyManifestError(value)];\n return [`[pr-gate] \"checklists\" must be an ARRAY of { \"subagent\", \"doc\"?, \"patterns\"? }. ${CHECKLIST_EXAMPLE}`];\n}\n\n/**\n * The migration message for the removed `{ doc }` manifest shape. It names the doc the consumer pointed at,\n * because that is the file holding the array they must move, and spells out the one non-obvious part of the\n * move: entry `doc` paths used to resolve relative to that manifest doc and are now REPO-relative.\n */\n// webpieces-disable no-any-unknown -- narrowing the opaque checklists section to read the old `doc` key\n// webpieces-disable no-function-outside-class -- module-level config validator, matches the rest of this file\nfunction legacyManifestError(value: object): string {\n const doc = (value as Record<string, unknown>)['doc'];\n const docRel = typeof doc === 'string' && doc.trim() !== '' ? doc : '<your review index doc>';\n return (\n `[pr-gate] \"checklists\" is the REMOVED { \"doc\": \"${docRel}\" } shape. The checklist array no longer lives in\\n` +\n ` an HTML comment inside a markdown doc — put it directly in webpieces.config.json:\\n` +\n ` 1. Open \"${docRel}\" and copy the JSON array out of its <!-- webpieces:checklists [...] --> comment.\\n` +\n ` 2. Replace \"checklists\": { \"doc\": \"${docRel}\" } with \"checklists\": <that array>.\\n` +\n ` 3. Rewrite each entry's \"doc\" to be REPO-relative — they used to resolve relative to\\n` +\n ` \"${docRel}\", so a bare \"db-migrations.md\" becomes e.g. \".claude/review/db-migrations.md\".\\n` +\n ` 4. Delete the <!-- webpieces:checklists ... --> comment from \"${docRel}\"; keep the prose.\\n` +\n ` ${CHECKLIST_EXAMPLE}`\n );\n}\n\n/**\n * `required` is MANDATORY on every checklist entry — omitting it is an error, never a default.\n *\n * The error names the entry's own subagent, because that is what the consumer recognizes in a\n * twelve-entry array; `checklists[7]` alone means counting braces. It also states BOTH edits, because the\n * whole point of the key is that the answer differs per checklist and only the consumer knows which.\n *\n * Why a hard rejection instead of `?? true`: an accepted shape is never migrated. Defaulting to true\n * silently keeps the all-blocking behavior this key exists to relieve, and every consumer that would have\n * benefited stays on the old behavior forever without ever being told the dial exists. Defaulting to false\n * is worse — it would silently DOWNGRADE a live review gate on upgrade. Per CLAUDE.md the reader of this\n * message is a coding agent, so the migration is one mechanical pass.\n */\n// webpieces-disable no-any-unknown -- one opaque checklist entry, narrowed by the typeof guards here\n// webpieces-disable no-function-outside-class -- module-level config validator, matches the rest of this file\nfunction requiredKeyErrors(e: Record<string, unknown>, i: number): string[] {\n const name = typeof e['subagent'] === 'string' && e['subagent'].trim() !== '' ? ` (\"${e['subagent']}\")` : '';\n if (e['required'] === undefined) {\n return [\n `[pr-gate] checklists[${i}]${name} is missing \"required\". Every checklist must state one — there is\\n` +\n ` no default, in either direction.\\n` +\n ` \"required\": true → BLOCKING. wp-finish-upsert-pr refuses the PR until this reviewer passes.\\n` +\n ` This is what every checklist did before this key existed.\\n` +\n ` \"required\": false → OPTIONAL. When it matches the diff, wp-review-upsert-pr offers it and the\\n` +\n ` human may decline it. If they DO run it, a red verdict still blocks.\\n` +\n ` ${CHECKLIST_EXAMPLE}`,\n ];\n }\n if (typeof e['required'] !== 'boolean') {\n return [`[pr-gate] checklists[${i}]${name}.required must be a boolean (true = blocking, false = optional) — not a string or number.`];\n }\n return [];\n}\n\n// Structurally check each entry HERE — a bad `patterns` or a non-object entry is a config-file typo and\n// deserves a `checklists[i]` message — then hand the narrowed defs to ChecklistValidator for the checks only\n// the filesystem can answer (the guidance doc exists, the reviewer agent exists).\n// webpieces-disable no-any-unknown -- opaque consumer JSON entries, narrowed per-field below\n// webpieces-disable no-function-outside-class -- module-level config validator, matches the rest of this file\nfunction validateChecklistArray(value: readonly unknown[], repoRoot?: string): string[] {\n const errors: string[] = [];\n const items: RawChecklistItem[] = [];\n // webpieces-disable no-any-unknown -- each array entry is opaque consumer JSON, narrowed field-by-field below\n value.forEach((entry: unknown, i: number): void => {\n if (typeof entry !== 'object' || entry === null || Array.isArray(entry)) {\n errors.push(`[pr-gate] checklists[${i}] must be an object { \"subagent\", \"doc\"?, \"patterns\"? }.`);\n return;\n }\n // webpieces-disable no-any-unknown -- narrowing one opaque checklist entry\n const e = entry as Record<string, unknown>;\n if (e['doc'] !== undefined && typeof e['doc'] !== 'string') {\n errors.push(`[pr-gate] checklists[${i}].doc must be a string — a REPO-relative path to the reviewer's guidance doc (omit it and the reviewer just reads the diff).`);\n }\n // webpieces-disable no-any-unknown -- opaque array element, narrowed by the typeof guard\n if (e['patterns'] !== undefined && !(Array.isArray(e['patterns']) && e['patterns'].every((p: unknown): boolean => typeof p === 'string'))) {\n errors.push(`[pr-gate] checklists[${i}].patterns must be a string[] of path globs (omit or [] to run on every PR).`);\n }\n errors.push(...requiredKeyErrors(e, i));\n items.push(e as RawChecklistItem);\n });\n if (repoRoot === undefined) return errors;\n const defs = items.map((item: RawChecklistItem): ChecklistDefinition => toChecklist(item));\n return [...errors, ...new ChecklistValidator().validate(repoRoot, defs)];\n}\n\n// The `landPr` block: what happens to the LOCAL branch once its PR is in main. Optional — omitted\n// means \"archive-tag\", which is deliberately the DEFAULT so a consumer gets the branch-accumulation\n// fix without editing config at all. `branchRetentionWhy` (and any other `*Why` sibling) is free-form\n// rationale prose and is tolerated, per the repo's convention for documenting comment-less JSON.\n// webpieces-disable no-any-unknown -- `value` is the opaque consumer `landPr` value until narrowed here\n// webpieces-disable no-function-outside-class -- module-level config validator, matches the rest of this file\nexport function validateLandPrSection(value: unknown): string[] {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n return [`[pr-gate] \"landPr\" must be an object, e.g. { \"branchRetention\": \"${BRANCH_RETENTION_ARCHIVE_TAG}\" }.`];\n }\n // webpieces-disable no-any-unknown -- narrowing the opaque landPr object from consumer JSON\n const s = value as Record<string, unknown>;\n if (!('branchRetention' in s)) return [];\n const retention = s['branchRetention'];\n if (typeof retention === 'string' && BRANCH_RETENTIONS.includes(retention)) return [];\n return [\n `[pr-gate] \"landPr.branchRetention\" = \"${String(retention)}\" is not valid. ` +\n `Must be one of: ${BRANCH_RETENTIONS.join(', ')}.\\n` +\n ` \"${BRANCH_RETENTION_ARCHIVE_TAG}\" — (default) tag the branch tip as archive/<date>/<branch>, THEN delete it. The\\n` +\n ` history stays byte-identical and restorable, but the branch stops counting\\n` +\n ` toward the branch cap and cannot be committed onto by accident.\\n` +\n ` \"${BRANCH_RETENTION_DELETE}\" — delete outright; recoverable only from the reflog, which expires.\\n` +\n ` \"${BRANCH_RETENTION_KEEP}\" — do not delete. Branches then accumulate until branch-creation-guard trips.`,\n ];\n}\n\n// A git ref COMPONENT this flow is willing to build a ref name out of. Deliberately much narrower than\n// git's own check-ref-format: these two values are concatenated into a ref that a command then\n// force-pushes, so anything that could be read as a flag, a path escape, or a glob is rejected outright\n// rather than trusted to `git push` argument order.\nconst REF_COMPONENT = /^[A-Za-z0-9][A-Za-z0-9._/-]*$/;\n\n/**\n * Validate the optional `pr-gate.devDeploy` block — where `wp-push-dev` publishes the disposable copy of\n * a feature branch, and which ref is the shared dev branch itself. Absent ⇒ `dev-include` / `dev`.\n *\n * `devBranch` must NOT contain a slash and must not sit inside `branchNamespace`: the whole point of the\n * namespace is that the composed dev branch is written by CI and the copies are written by developers, so\n * a config where one contains the other makes `--list` enumerate the deploy branch and makes the\n * \"refused as a source branch\" check ambiguous.\n */\n// webpieces-disable no-any-unknown -- `value` is the opaque consumer devDeploy value until narrowed here\n// webpieces-disable no-function-outside-class -- module-level config validator, matches the rest of this file\nexport function validateDevDeploySection(value: unknown): string[] {\n if (typeof value !== 'object' || value === null || Array.isArray(value)) {\n return ['[pr-gate] \"devDeploy\" must be an object, e.g. { \"branchNamespace\": \"dev-include\", \"devBranch\": \"dev\" }.'];\n }\n // webpieces-disable no-any-unknown -- narrowing the opaque devDeploy object from consumer JSON\n const s = value as Record<string, unknown>;\n const errors: string[] = [];\n errors.push(...validateRefComponent(s, 'branchNamespace', 'dev-include'));\n errors.push(...validateRefComponent(s, 'devBranch', 'dev'));\n if (errors.length > 0) return errors;\n\n const namespace = typeof s['branchNamespace'] === 'string' ? s['branchNamespace'].trim() : 'dev-include';\n const devBranch = typeof s['devBranch'] === 'string' ? s['devBranch'].trim() : 'dev';\n if (devBranch.includes('/')) {\n errors.push(\n `[pr-gate] \"devDeploy.devBranch\" = \"${devBranch}\" must be a single ref name with no \"/\" — it is the ` +\n `branch your CI composes and deploys, not a namespace.`);\n }\n if (devBranch === namespace || devBranch.startsWith(`${namespace}/`) || namespace.startsWith(`${devBranch}/`)) {\n errors.push(\n `[pr-gate] \"devDeploy.devBranch\" (\"${devBranch}\") and \"devDeploy.branchNamespace\" (\"${namespace}\") must not ` +\n `contain one another. The namespace holds the DISPOSABLE per-developer copies (written by wp-push-dev); ` +\n `devBranch is the COMPOSED branch your CI rebuilds from origin/main. Overlapping them makes wp-push-dev ` +\n `--list enumerate the deploy branch and makes \"refused as a source branch\" ambiguous.`);\n }\n return errors;\n}\n\n// One `devDeploy` string field: present ⇒ must be a non-empty, ref-safe string. Absent ⇒ the default.\n// webpieces-disable no-any-unknown -- the already-narrowed opaque devDeploy object\n// webpieces-disable no-function-outside-class -- module-level config validator, matches the rest of this file\nfunction validateRefComponent(s: Record<string, unknown>, key: string, example: string): string[] {\n if (!(key in s)) return [];\n const raw = s[key];\n if (typeof raw !== 'string' || raw.trim() === '') {\n return [`[pr-gate] \"devDeploy.${key}\" must be a non-empty string, e.g. \"${example}\". Omit the key for the \"${example}\" default.`];\n }\n if (!REF_COMPONENT.test(raw.trim())) {\n return [\n `[pr-gate] \"devDeploy.${key}\" = \"${raw}\" is not a usable git ref name. Use letters, digits, \".\", \"_\", ` +\n `\"-\" and \"/\" only, starting with a letter or digit (e.g. \"${example}\").`,\n ];\n }\n return [];\n}\n"]}
|
|
@@ -2,6 +2,8 @@ export declare const WP_START_UPDATE = "pnpm wp-start-update";
|
|
|
2
2
|
export declare const WP_FINISH_UPDATE = "pnpm wp-finish-update";
|
|
3
3
|
export declare const WP_START_UPSERT_PR = "pnpm wp-start-upsert-pr";
|
|
4
4
|
export declare const WP_FINISH_UPSERT_PR = "pnpm wp-finish-upsert-pr";
|
|
5
|
+
export declare const WP_PUSH_DEV = "pnpm wp-push-dev";
|
|
6
|
+
export declare const WP_FINISH_PUSH_DEV = "pnpm wp-finish-push-dev";
|
|
5
7
|
/**
|
|
6
8
|
* Renders the canonical guidance blocks. Methods return string[] (not a joined string) so callers can
|
|
7
9
|
* indent//interleave them into their own message without re-wrapping.
|
|
@@ -16,6 +18,15 @@ export declare class SyncFlowGuidance {
|
|
|
16
18
|
updateOnlyFlow(): string[];
|
|
17
19
|
/** Flow B — a PR is open (or is about to be). */
|
|
18
20
|
prFlow(): string[];
|
|
21
|
+
/**
|
|
22
|
+
* Flow C — get this branch onto the shared dev server WITHOUT landing it on main.
|
|
23
|
+
*
|
|
24
|
+
* Deliberately printed as a THIRD destination rather than folded into flow B, because the two are
|
|
25
|
+
* opposites and an AI shown only the PR flow will open a PR when it was asked to deploy. Landing on
|
|
26
|
+
* main ships to production; this publishes a throwaway copy that the dev environment's CI composes
|
|
27
|
+
* and rebuilds. It never moves the feature branch and never opens a PR.
|
|
28
|
+
*/
|
|
29
|
+
devDeployFlow(): string[];
|
|
19
30
|
/** The half that keeps drifting: a start from one pair NEVER finishes with the other's finish. */
|
|
20
31
|
pairingRule(): string[];
|
|
21
32
|
/** Why an open PR removes the choice. Safe to print on its own alongside just the PR flow. */
|
|
@@ -7,12 +7,16 @@
|
|
|
7
7
|
// which flow it is in. Every guard/bin message now renders from HERE, so the two flows can only ever
|
|
8
8
|
// be described one way.
|
|
9
9
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
-
exports.SyncFlowGuidance = exports.WP_FINISH_UPSERT_PR = exports.WP_START_UPSERT_PR = exports.WP_FINISH_UPDATE = exports.WP_START_UPDATE = void 0;
|
|
10
|
+
exports.SyncFlowGuidance = exports.WP_FINISH_PUSH_DEV = exports.WP_PUSH_DEV = exports.WP_FINISH_UPSERT_PR = exports.WP_START_UPSERT_PR = exports.WP_FINISH_UPDATE = exports.WP_START_UPDATE = void 0;
|
|
11
11
|
// The four gated bins, as the AI must type them. Two flows, two halves each.
|
|
12
12
|
exports.WP_START_UPDATE = 'pnpm wp-start-update';
|
|
13
13
|
exports.WP_FINISH_UPDATE = 'pnpm wp-finish-update';
|
|
14
14
|
exports.WP_START_UPSERT_PR = 'pnpm wp-start-upsert-pr';
|
|
15
15
|
exports.WP_FINISH_UPSERT_PR = 'pnpm wp-finish-upsert-pr';
|
|
16
|
+
// The dev-deploy pair. NOT a way to land anything — it publishes a DISPOSABLE copy of the branch so a
|
|
17
|
+
// shared dev environment can build it, and never touches the feature branch or any PR.
|
|
18
|
+
exports.WP_PUSH_DEV = 'pnpm wp-push-dev';
|
|
19
|
+
exports.WP_FINISH_PUSH_DEV = 'pnpm wp-finish-push-dev';
|
|
16
20
|
/**
|
|
17
21
|
* Renders the canonical guidance blocks. Methods return string[] (not a joined string) so callers can
|
|
18
22
|
* indent//interleave them into their own message without re-wrapping.
|
|
@@ -54,6 +58,24 @@ class SyncFlowGuidance {
|
|
|
54
58
|
` 3. ${exports.WP_FINISH_UPSERT_PR} ← authoritative build gate, then create/update the PR`,
|
|
55
59
|
];
|
|
56
60
|
}
|
|
61
|
+
/**
|
|
62
|
+
* Flow C — get this branch onto the shared dev server WITHOUT landing it on main.
|
|
63
|
+
*
|
|
64
|
+
* Deliberately printed as a THIRD destination rather than folded into flow B, because the two are
|
|
65
|
+
* opposites and an AI shown only the PR flow will open a PR when it was asked to deploy. Landing on
|
|
66
|
+
* main ships to production; this publishes a throwaway copy that the dev environment's CI composes
|
|
67
|
+
* and rebuilds. It never moves the feature branch and never opens a PR.
|
|
68
|
+
*/
|
|
69
|
+
devDeployFlow() {
|
|
70
|
+
return [
|
|
71
|
+
' C. You just want it on the SHARED DEV SERVER — no PR, not landing on main:',
|
|
72
|
+
` 1. ${exports.WP_PUSH_DEV} ← publish a DISPOSABLE copy of this branch for dev CI`,
|
|
73
|
+
' 2. resolve conflicts in the files it lists (ONLY if it reported any)',
|
|
74
|
+
` 3. ${exports.WP_FINISH_PUSH_DEV} ← finalize (ONLY on the conflict path)`,
|
|
75
|
+
' Your feature branch is NEVER moved and never acquires another dev\'s commits — that is',
|
|
76
|
+
' the whole reason the copy exists.',
|
|
77
|
+
];
|
|
78
|
+
}
|
|
57
79
|
/** The half that keeps drifting: a start from one pair NEVER finishes with the other's finish. */
|
|
58
80
|
pairingRule() {
|
|
59
81
|
return [
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sync-flow-guidance.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/sync-flow-guidance.ts"],"names":[],"mappings":";AAAA,oFAAoF;AACpF,EAAE;AACF,qGAAqG;AACrG,qGAAqG;AACrG,qGAAqG;AACrG,qGAAqG;AACrG,wBAAwB;;;AAExB,6EAA6E;AAChE,QAAA,eAAe,GAAG,sBAAsB,CAAC;AACzC,QAAA,gBAAgB,GAAG,uBAAuB,CAAC;AAC3C,QAAA,kBAAkB,GAAG,yBAAyB,CAAC;AAC/C,QAAA,mBAAmB,GAAG,0BAA0B,CAAC;AAE9D;;;GAGG;AACH,MAAa,gBAAgB;IACzB;;;OAGG;IACH,KAAK;QACD,OAAO;YACH,4EAA4E;YAC5E,8CAA8C;YAC9C,EAAE;SACL;aACI,MAAM,CAAC,IAAI,CAAC,cAAc,EAAE,CAAC;aAC7B,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC;aACZ,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;aACrB,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC;aACZ,MAAM,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC;aAC1B,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC;aACZ,MAAM,CAAC,IAAI,CAAC,gBAAgB,EAAE,CAAC,CAAC;IACzC,CAAC;IAED,+BAA+B;IAC/B,cAAc;QACV,OAAO;YACH,qFAAqF;YACrF,WAAW,uBAAe,2DAA2D;YACrF,qFAAqF;YACrF,WAAW,wBAAgB,6CAA6C;SAC3E,CAAC;IACN,CAAC;IAED,iDAAiD;IACjD,MAAM;QACF,OAAO;YACH,qEAAqE;YACrE,WAAW,0BAAkB,oCAAoC;YACjE,qFAAqF;YACrF,WAAW,2BAAmB,yDAAyD;SAC1F,CAAC;IACN,CAAC;IAED,kGAAkG;IAClG,WAAW;QACP,OAAO;YACH,qFAAqF;YACrF,yCAAyC;YACzC,4CAA4C;SAC/C,CAAC;IACN,CAAC;IAED,8FAA8F;IAC9F,gBAAgB;QACZ,OAAO;YACH,6FAA6F;YAC7F,2FAA2F;YAC3F,8FAA8F;YAC9F,4FAA4F;YAC5F,4DAA4D;SAC/D,CAAC;IACN,CAAC;IAED;;;;OAIG;IACH,WAAW,CAAC,aAAqB;QAC7B,IAAI,aAAa,KAAK,kBAAkB;YAAE,OAAO,iBAAiB,CAAC;QACnE,IAAI,aAAa,KAAK,qBAAqB;YAAE,OAAO,oBAAoB,CAAC;QACzE,OAAO,aAAa,CAAC;IACzB,CAAC;IAED,oGAAoG;IACpG,gGAAgG;IAChG,0FAA0F;IAC1F,mGAAmG;IACnG,yFAAyF;IAEzF;;;;OAIG;IACH,cAAc;QACV,OAAO;YACH,4FAA4F;YAC5F,6FAA6F;YAC7F,0FAA0F;YAC1F,sFAAsF;YACtF,yFAAyF;YACzF,iGAAiG;YACjG,4FAA4F;YAC5F,2FAA2F;YAC3F,2FAA2F;YAC3F,kFAAkF;SACrF,CAAC;IACN,CAAC;CACJ;
|
|
1
|
+
{"version":3,"file":"sync-flow-guidance.js","sourceRoot":"","sources":["../../../../../packages/tooling/rules-config/src/sync-flow-guidance.ts"],"names":[],"mappings":";AAAA,oFAAoF;AACpF,EAAE;AACF,qGAAqG;AACrG,qGAAqG;AACrG,qGAAqG;AACrG,qGAAqG;AACrG,wBAAwB;;;AAExB,6EAA6E;AAChE,QAAA,eAAe,GAAG,sBAAsB,CAAC;AACzC,QAAA,gBAAgB,GAAG,uBAAuB,CAAC;AAC3C,QAAA,kBAAkB,GAAG,yBAAyB,CAAC;AAC/C,QAAA,mBAAmB,GAAG,0BAA0B,CAAC;AAE9D,sGAAsG;AACtG,uFAAuF;AAC1E,QAAA,WAAW,GAAG,kBAAkB,CAAC;AACjC,QAAA,kBAAkB,GAAG,yBAAyB,CAAC;AAE5D;;;GAGG;AACH,MAAa,gBAAgB;IACzB;;;OAGG;IACH,KAAK;QACD,OAAO;YACH,4EAA4E;YAC5E,8CAA8C;YAC9C,EAAE;SACL;aACI,MAAM,CAAC,IAAI,CAAC,cAAc,EAAE,CAAC;aAC7B,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC;aACZ,MAAM,CAAC,IAAI,CAAC,MAAM,EAAE,CAAC;aACrB,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC;aACZ,MAAM,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC;aAC1B,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC;aACZ,MAAM,CAAC,IAAI,CAAC,gBAAgB,EAAE,CAAC,CAAC;IACzC,CAAC;IAED,+BAA+B;IAC/B,cAAc;QACV,OAAO;YACH,qFAAqF;YACrF,WAAW,uBAAe,2DAA2D;YACrF,qFAAqF;YACrF,WAAW,wBAAgB,6CAA6C;SAC3E,CAAC;IACN,CAAC;IAED,iDAAiD;IACjD,MAAM;QACF,OAAO;YACH,qEAAqE;YACrE,WAAW,0BAAkB,oCAAoC;YACjE,qFAAqF;YACrF,WAAW,2BAAmB,yDAAyD;SAC1F,CAAC;IACN,CAAC;IAED;;;;;;;OAOG;IACH,aAAa;QACT,OAAO;YACH,8EAA8E;YAC9E,WAAW,mBAAW,mEAAmE;YACzF,2EAA2E;YAC3E,WAAW,0BAAkB,6CAA6C;YAC1E,6FAA6F;YAC7F,wCAAwC;SAC3C,CAAC;IACN,CAAC;IAED,kGAAkG;IAClG,WAAW;QACP,OAAO;YACH,qFAAqF;YACrF,yCAAyC;YACzC,4CAA4C;SAC/C,CAAC;IACN,CAAC;IAED,8FAA8F;IAC9F,gBAAgB;QACZ,OAAO;YACH,6FAA6F;YAC7F,2FAA2F;YAC3F,8FAA8F;YAC9F,4FAA4F;YAC5F,4DAA4D;SAC/D,CAAC;IACN,CAAC;IAED;;;;OAIG;IACH,WAAW,CAAC,aAAqB;QAC7B,IAAI,aAAa,KAAK,kBAAkB;YAAE,OAAO,iBAAiB,CAAC;QACnE,IAAI,aAAa,KAAK,qBAAqB;YAAE,OAAO,oBAAoB,CAAC;QACzE,OAAO,aAAa,CAAC;IACzB,CAAC;IAED,oGAAoG;IACpG,gGAAgG;IAChG,0FAA0F;IAC1F,mGAAmG;IACnG,yFAAyF;IAEzF;;;;OAIG;IACH,cAAc;QACV,OAAO;YACH,4FAA4F;YAC5F,6FAA6F;YAC7F,0FAA0F;YAC1F,sFAAsF;YACtF,yFAAyF;YACzF,iGAAiG;YACjG,4FAA4F;YAC5F,2FAA2F;YAC3F,2FAA2F;YAC3F,kFAAkF;SACrF,CAAC;IACN,CAAC;CACJ;AAnHD,4CAmHC","sourcesContent":["// SINGLE SOURCE OF TRUTH for \"how do I bring main into my branch\" as told to an AI.\n//\n// Why this file exists: the guards and the pr-gate bins each used to hand-write their own version of\n// this advice, and they drifted — one message told the AI to START with `wp-start-update` and FINISH\n// with `wp-finish-upsert-pr`, which is a pairing that does not exist. An AI reading that cannot tell\n// which flow it is in. Every guard/bin message now renders from HERE, so the two flows can only ever\n// be described one way.\n\n// The four gated bins, as the AI must type them. Two flows, two halves each.\nexport const WP_START_UPDATE = 'pnpm wp-start-update';\nexport const WP_FINISH_UPDATE = 'pnpm wp-finish-update';\nexport const WP_START_UPSERT_PR = 'pnpm wp-start-upsert-pr';\nexport const WP_FINISH_UPSERT_PR = 'pnpm wp-finish-upsert-pr';\n\n// The dev-deploy pair. NOT a way to land anything — it publishes a DISPOSABLE copy of the branch so a\n// shared dev environment can build it, and never touches the feature branch or any PR.\nexport const WP_PUSH_DEV = 'pnpm wp-push-dev';\nexport const WP_FINISH_PUSH_DEV = 'pnpm wp-finish-push-dev';\n\n/**\n * Renders the canonical guidance blocks. Methods return string[] (not a joined string) so callers can\n * indent//interleave them into their own message without re-wrapping.\n */\nexport class SyncFlowGuidance {\n /**\n * Both flows, ALWAYS paired. Use this when the caller does NOT know whether a PR is open — an AI\n * shown only one flow picks it even when the other one is the correct one.\n */\n flows(): string[] {\n return [\n 'There are exactly TWO flows. Which one you use is decided by ONE question:',\n 'is there already an OPEN PR for this branch?',\n '',\n ]\n .concat(this.updateOnlyFlow())\n .concat([''])\n .concat(this.prFlow())\n .concat([''])\n .concat(this.pairingRule())\n .concat([''])\n .concat(this.whyPrForcesFlowB());\n }\n\n /** Flow A — no PR open yet. */\n updateOnlyFlow(): string[] {\n return [\n ' A. NO PR yet — update-only flow (you are mid-work and just want main\\'s changes):',\n ` 1. ${WP_START_UPDATE} ← 3-point merge from main (auto-finalizes if clean)`,\n ' 2. /wp-merge ← resolve conflicts (ONLY if step 1 reported any)',\n ` 3. ${WP_FINISH_UPDATE} ← finalize (ONLY on the conflict path)`,\n ];\n }\n\n /** Flow B — a PR is open (or is about to be). */\n prFlow(): string[] {\n return [\n ' B. A PR IS ALREADY OPEN (or you are ready to post one) — PR flow:',\n ` 1. ${WP_START_UPSERT_PR} ← same 3-point merge, then push`,\n ' 2. /wp-merge ← resolve conflicts (ONLY if step 1 reported any)',\n ` 3. ${WP_FINISH_UPSERT_PR} ← authoritative build gate, then create/update the PR`,\n ];\n }\n\n /**\n * Flow C — get this branch onto the shared dev server WITHOUT landing it on main.\n *\n * Deliberately printed as a THIRD destination rather than folded into flow B, because the two are\n * opposites and an AI shown only the PR flow will open a PR when it was asked to deploy. Landing on\n * main ships to production; this publishes a throwaway copy that the dev environment's CI composes\n * and rebuilds. It never moves the feature branch and never opens a PR.\n */\n devDeployFlow(): string[] {\n return [\n ' C. You just want it on the SHARED DEV SERVER — no PR, not landing on main:',\n ` 1. ${WP_PUSH_DEV} ← publish a DISPOSABLE copy of this branch for dev CI`,\n ' 2. resolve conflicts in the files it lists (ONLY if it reported any)',\n ` 3. ${WP_FINISH_PUSH_DEV} ← finalize (ONLY on the conflict path)`,\n ' Your feature branch is NEVER moved and never acquires another dev\\'s commits — that is',\n ' the whole reason the copy exists.',\n ];\n }\n\n /** The half that keeps drifting: a start from one pair NEVER finishes with the other's finish. */\n pairingRule(): string[] {\n return [\n 'PAIRING IS NOT OPTIONAL — a start and a finish from different flows is not a thing:',\n ' wp-start-update → wp-finish-update',\n ' wp-start-upsert-pr → wp-finish-upsert-pr',\n ];\n }\n\n /** Why an open PR removes the choice. Safe to print on its own alongside just the PR flow. */\n whyPrForcesFlowB(): string[] {\n return [\n 'If a PR is open you MUST use the upsert-pr pair. The 3-point merge REWRITES this branch (it',\n 'squashes onto main and force-pushes a new generation), so the open PR\\'s history is blown',\n 'away and has to be re-pointed in the SAME run. The update-only pair never touches the PR, so',\n 'running it with a PR open would strand that PR on the OLD branch generation — which is why',\n 'wp-start-update refuses outright when it finds an open PR.',\n ];\n }\n\n /**\n * The start bin that PAIRS with a finish bin (bare names, no `pnpm` prefix) — so generated text can\n * name the command that actually produced it instead of guessing one of the two. Anything\n * unrecognized comes back unchanged rather than inventing a name.\n */\n pairedStart(finishCommand: string): string {\n if (finishCommand === 'wp-finish-update') return 'wp-start-update';\n if (finishCommand === 'wp-finish-upsert-pr') return 'wp-start-upsert-pr';\n return finishCommand;\n }\n\n // updateMainAdvice() / featureBranchSyncAdvice() were DELETED 2026-08-03. Their only caller was the\n // shim's fault-D deny, and both paragraphs were another guard's job: redirect-how-to-merge-main\n // fires on its own, with its own message, on exactly the commands they warned about — and\n // featureBranchSyncAdvice() named wp-start-update / wp-start-upsert-pr only to say \"not while this\n // block is up\", which is pure cost in a message whose one instruction is `pnpm install`.\n\n /**\n * The read-only alternatives, for when the AI only wanted to LOOK at how it stands vs main. This\n * exists because `git merge --ff-only origin/main` gets typed as if it were a query — it is not,\n * it mutates the branch whenever it succeeds, which is exactly the case you were probing for.\n */\n readOnlyChecks(): string[] {\n return [\n 'Only wanted to LOOK (am I behind main? would it fast-forward?) — none of the below mutate:',\n ' git fetch origin main ← refresh the remote ref (no merge)',\n ' git merge-base --is-ancestor origin/main HEAD ← exit 0 = already contains main',\n ' git rev-list --left-right --count origin/main...HEAD ← prints \"<behind> <ahead>\"',\n ' git log --oneline HEAD..origin/main ← what main has that you do not',\n ' git diff --stat origin/main...HEAD ← what you changed since the fork point',\n ' cat .webpieces/main-sync-status.json ← the tooling\\'s own answer, incl.',\n ' the files predicted to conflict',\n '`git merge --ff-only` is NOT a look — it MUTATES on success (that is the whole point of a',\n 'fast-forward), so it is blocked like every other merge. Never use it as a probe.',\n ];\n }\n}\n"]}
|
package/src/validate-config.js
CHANGED
|
@@ -304,6 +304,9 @@ function validatePrGateSection(section, repoRoot) {
|
|
|
304
304
|
// Optional: what happens to the LOCAL branch once its PR lands. Absent ⇒ "archive-tag".
|
|
305
305
|
if ('landPr' in s)
|
|
306
306
|
errors.push(...(0, pr_gate_section_validators_1.validateLandPrSection)(s['landPr']));
|
|
307
|
+
// Optional: where wp-push-dev publishes the disposable copy. Absent ⇒ "dev-include" / "dev".
|
|
308
|
+
if ('devDeploy' in s)
|
|
309
|
+
errors.push(...(0, pr_gate_section_validators_1.validateDevDeploySection)(s['devDeploy']));
|
|
307
310
|
errors.push(...(0, pr_gate_section_validators_1.validateNoGateSaltRationale)(s));
|
|
308
311
|
// Optional: publish reviewer output as a PR comment (defaults true). Must be a boolean when present.
|
|
309
312
|
if ('checklistComments' in s && typeof s['checklistComments'] !== 'boolean') {
|