@webpieces/pr-gate 0.4.697 → 0.4.699
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/dashboard/dashboard.d.ts +25 -0
- package/src/dashboard/dashboard.js +28 -1
- package/src/dashboard/dashboard.js.map +1 -1
- package/src/scripts/commands/land-pr-command.d.ts +16 -2
- package/src/scripts/commands/land-pr-command.js +27 -8
- package/src/scripts/commands/land-pr-command.js.map +1 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@webpieces/pr-gate",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.699",
|
|
4
4
|
"description": "Gated PR system: 3-point squash-merge, merge validation gate, and red/yellow/green PR dashboard. Standalone scripts, no Nx dependency required.",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"main": "./src/index.js",
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"directory": "packages/tooling/pr-gate"
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@webpieces/rules-config": "0.4.
|
|
18
|
+
"@webpieces/rules-config": "0.4.699",
|
|
19
19
|
"@inversifyjs/binding-decorators": "1.1.5",
|
|
20
20
|
"inversify": "7.10.4",
|
|
21
21
|
"reflect-metadata": "0.2.2"
|
|
@@ -103,6 +103,31 @@ export declare class Dashboard {
|
|
|
103
103
|
* CI-critical surface that two consumer repos verify on every PR, to save one line of history.
|
|
104
104
|
*/
|
|
105
105
|
renderPrBody(input: DashboardInput, prUrl: string): string;
|
|
106
|
+
/**
|
|
107
|
+
* Makes THIS renderer own the invariant `wp-land-pr` checks: the compact body carries no markdown
|
|
108
|
+
* heading and no table pipe.
|
|
109
|
+
*
|
|
110
|
+
* It is applied to the WHOLE joined string rather than to each author-supplied field, on purpose.
|
|
111
|
+
* Every field this body interpolates — the review summary, an override justification, a checklist
|
|
112
|
+
* title, a disabled rule name, `commands.pr-gate.buildCommand` — is text this package did not write,
|
|
113
|
+
* and a `|` reaches all of them for entirely ordinary non-markdown reasons: a TypeScript union
|
|
114
|
+
* (`'open' | 'paused'`), a regex alternation, a quoted shell pipeline, an OR in prose. Sanitizing at
|
|
115
|
+
* the single exit point means a field added later is covered without anyone remembering to wrap it,
|
|
116
|
+
* and none of this renderer's own literals contain either marker, so nothing of ours changes.
|
|
117
|
+
*
|
|
118
|
+
* Before this, `wp-finish-upsert-pr` would happily POST such a body and report success, and then
|
|
119
|
+
* `wp-land-pr` refused those exact bytes — two commands in one flow disagreeing about what the
|
|
120
|
+
* renderer produces, with a refusal that blamed an old release and prescribed re-running finish,
|
|
121
|
+
* which re-rendered the identical pipe from the unchanged `review.json`. An infinite loop costing a
|
|
122
|
+
* CI cycle per turn.
|
|
123
|
+
*
|
|
124
|
+
* `¦` (BROKEN BAR) rather than `\|`: this string's home is `git log` in a terminal, where a
|
|
125
|
+
* backslash is literal punctuation and a markdown escape means nothing. The substitution is visible
|
|
126
|
+
* and reads correctly; the full, unaltered text is one click away in the 1st comment, which this
|
|
127
|
+
* body's last bullet always points at. `##` collapses to `#` for the same reason — a heading in a
|
|
128
|
+
* summary is markdown a plain-text history cannot carry either.
|
|
129
|
+
*/
|
|
130
|
+
private gitLogSafe;
|
|
106
131
|
/**
|
|
107
132
|
* The footer, naming the build command from `commands.pr-gate.buildCommand` VERBATIM.
|
|
108
133
|
*
|
|
@@ -265,7 +265,34 @@ let Dashboard = class Dashboard {
|
|
|
265
265
|
}
|
|
266
266
|
lines.push('');
|
|
267
267
|
lines.push(this.prBodyFooter(input.buildCommand));
|
|
268
|
-
return lines.join('\n');
|
|
268
|
+
return this.gitLogSafe(lines.join('\n'));
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Makes THIS renderer own the invariant `wp-land-pr` checks: the compact body carries no markdown
|
|
272
|
+
* heading and no table pipe.
|
|
273
|
+
*
|
|
274
|
+
* It is applied to the WHOLE joined string rather than to each author-supplied field, on purpose.
|
|
275
|
+
* Every field this body interpolates — the review summary, an override justification, a checklist
|
|
276
|
+
* title, a disabled rule name, `commands.pr-gate.buildCommand` — is text this package did not write,
|
|
277
|
+
* and a `|` reaches all of them for entirely ordinary non-markdown reasons: a TypeScript union
|
|
278
|
+
* (`'open' | 'paused'`), a regex alternation, a quoted shell pipeline, an OR in prose. Sanitizing at
|
|
279
|
+
* the single exit point means a field added later is covered without anyone remembering to wrap it,
|
|
280
|
+
* and none of this renderer's own literals contain either marker, so nothing of ours changes.
|
|
281
|
+
*
|
|
282
|
+
* Before this, `wp-finish-upsert-pr` would happily POST such a body and report success, and then
|
|
283
|
+
* `wp-land-pr` refused those exact bytes — two commands in one flow disagreeing about what the
|
|
284
|
+
* renderer produces, with a refusal that blamed an old release and prescribed re-running finish,
|
|
285
|
+
* which re-rendered the identical pipe from the unchanged `review.json`. An infinite loop costing a
|
|
286
|
+
* CI cycle per turn.
|
|
287
|
+
*
|
|
288
|
+
* `¦` (BROKEN BAR) rather than `\|`: this string's home is `git log` in a terminal, where a
|
|
289
|
+
* backslash is literal punctuation and a markdown escape means nothing. The substitution is visible
|
|
290
|
+
* and reads correctly; the full, unaltered text is one click away in the 1st comment, which this
|
|
291
|
+
* body's last bullet always points at. `##` collapses to `#` for the same reason — a heading in a
|
|
292
|
+
* summary is markdown a plain-text history cannot carry either.
|
|
293
|
+
*/
|
|
294
|
+
gitLogSafe(body) {
|
|
295
|
+
return body.replace(/\|/g, '\u00a6').replace(/#{2,}/g, '#');
|
|
269
296
|
}
|
|
270
297
|
/**
|
|
271
298
|
* The footer, naming the build command from `commands.pr-gate.buildCommand` VERBATIM.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"dashboard.js","sourceRoot":"","sources":["../../../../../../packages/tooling/pr-gate/src/dashboard/dashboard.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n GateDefinition,\n WEBPIECES_DISABLE,\n RULE_NAMES,\n ReviewJson,\n CK_PASS,\n CK_WARN,\n CK_OVERRIDDEN,\n CK_FAIL,\n CK_MISSING,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\n\n/**\n * Hidden marker on the FULL-DASHBOARD comment — the \"1st comment\" the PR description points at — so\n * finish PATCHes its own comment on every push instead of appending a new one, exactly like the checklist\n * comment (see ChecklistCommentRenderer, which owns the 2nd).\n *\n * This comment did not exist before the PR description became the git-log body: the dashboard WAS the\n * description. Moving it here is what keeps a risk table out of every squash commit on main.\n */\nexport const DETAIL_COMMENT_MARKER = '<!-- webpieces-pr-detail v1 -->';\n\n/**\n * Footer on the full-dashboard comment. Names the tooling by REPO URL so a reader landing on a\n * generated comment in any consumer repo can find what generated it.\n *\n * It deliberately no longer asserts \"build ran via nx affected — not self-attested\": that sentence was\n * hard-coded and therefore wrong on every repo whose `buildCommand` is not nx. The build command is now\n * named for real, from config, in the PR-body footer — see {@link Dashboard.renderPrBody}.\n */\nconst DETAIL_COMMENT_FOOTER = 'Generated by https://github.com/deanhiller/webpieces-ts wp-finish-upsert-pr';\n\n/**\n * The last bullet of the PR description, and therefore of every squash commit body.\n *\n * Positional (\"1st\"/\"2nd\") on purpose — it is what a reader of `git log` can actually act on, far more\n * useful than naming an HTML marker they cannot see. On a PR opened by an OLDER release the checklist\n * comment already exists, so the dashboard comment lands second and this reads one off for that PR's\n * remaining life; it is cosmetic and self-corrects on the next PR.\n */\nconst DETAIL_POINTER = '(Full dashboard in 1st comment, reviewer checklist in 2nd — kept out of git log)';\n\nexport class GateResult {\n name: string;\n warningColor: string; // 'yellow' | 'red' — the color shown WHEN files matched (green is implicit)\n matchedFiles: string[];\n\n constructor(name: string, warningColor: string, matchedFiles: string[]) {\n this.name = name;\n this.warningColor = warningColor;\n this.matchedFiles = matchedFiles;\n }\n}\n\n// One row for a consumer review checklist the branch triggered. `status` is the resolved verdict (one of\n// CK_PASS | CK_WARN | CK_OVERRIDDEN | CK_FAIL | CK_MISSING | CK_BAD_FORMAT); `detail` is the reviewer\n// output / override justification. A row is always PASS/WARN/OVERRIDDEN by the time it renders — a failed,\n// missing or unreadable verdict throws before the dashboard is built. Rendered into the PR body so the\n// verdict reaches the server — the PR body is the artifact of the local flow that leaves the checkout\n// (alongside the HMAC gate token that proves the flow ran).\nexport class ChecklistRow {\n title: string; // the checklist id (= reviewer subagent name)\n status: string; // CK_* verdict\n detail: string; // reviewer output / override justification (surfaced for OVERRIDDEN)\n\n constructor(title: string, status: string, detail = '') {\n this.title = title;\n this.status = status;\n this.detail = detail;\n }\n}\n\n/**\n * One severity bucket of the rolled-up **Checklists** dashboard row: its emoji, the words that describe it,\n * and which checklists landed in it. Data-only.\n *\n * A bucket knows whether it should NAME its members: the non-green ones do (a reader has to know WHICH\n * reviewer to go read), the passed bucket does not (a list of what went fine is noise on a one-line row).\n */\nclass RollupBucket {\n label: string; // 'blocking' | 'overridden' | 'with concerns' | 'passed'\n emoji: string;\n named: boolean; // list the checklist ids on the row, or report a bare count\n titles: string[];\n\n constructor(label: string, emoji: string, named: boolean) {\n this.label = label;\n this.emoji = emoji;\n this.named = named;\n this.titles = [];\n }\n}\n\nexport class DisableCounts {\n webpiecesCount: number;\n eslintCount: number;\n webpiecesRules: string[];\n\n constructor(webpiecesCount: number, eslintCount: number, webpiecesRules: string[]) {\n this.webpiecesCount = webpiecesCount;\n this.eslintCount = eslintCount;\n this.webpiecesRules = webpiecesRules;\n }\n}\n\nexport class DashboardInput {\n title: string;\n gateResults: GateResult[];\n disables: DisableCounts;\n buildPassed: boolean;\n forkPoint: string;\n featureHead: string;\n mainHead: string;\n review: ReviewJson; // AI-authored risk/violations/summary (from review.json)\n checklists: ChecklistRow[]; // consumer checklists this branch triggered; [] for non-adopting repos\n /**\n * `commands.pr-gate.buildCommand` VERBATIM, named in the PR-body footer so `git log` records WHICH\n * command vouched for the commit. Read from config rather than hard-coded because the footer used to\n * assert \"build ran via nx affected\" on every repo, including those whose buildCommand is not nx.\n *\n * REQUIRED, with no default, and `checklists` lost its `= []` for the same reason. A defaulted\n * `buildCommand: string = ''` let every pre-existing 9-argument construction keep compiling while\n * silently rendering a footer that claims nothing — an absence that quietly means \"no build was\n * named\", which is the widening-by-omission the compatibility policy calls out. Making it required\n * means every caller states what vouched for the commit, and the empty string stays available for a\n * repo that genuinely has no build command, but only when someone writes it down.\n */\n buildCommand: string;\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(\n title: string,\n gateResults: GateResult[],\n disables: DisableCounts,\n buildPassed: boolean,\n forkPoint: string,\n featureHead: string,\n mainHead: string,\n review: ReviewJson,\n checklists: ChecklistRow[],\n buildCommand: string,\n ) {\n this.title = title;\n this.gateResults = gateResults;\n this.disables = disables;\n this.buildPassed = buildPassed;\n this.forkPoint = forkPoint;\n this.featureHead = featureHead;\n this.mainHead = mainHead;\n this.review = review;\n this.checklists = checklists;\n this.buildCommand = buildCommand;\n }\n}\n\n/** Renders the PR-gate dashboard markdown (gates × changed files, disables, risk, 3-point hashes). */\n@injectable(bindingScopeValues.Singleton)\nexport class Dashboard {\n // Disabled gates are in-file examples (JSON has no comments) — skip them entirely.\n computeGateResults(gates: GateDefinition[], changedFiles: string[]): GateResult[] {\n return gates\n .filter((gate: GateDefinition): boolean => !gate.disabled)\n .map((gate: GateDefinition): GateResult => {\n const matched = changedFiles.filter((file: string): boolean =>\n this.matchesAny(gate.patterns, file),\n );\n return new GateResult(gate.name, gate.warningColor, matched);\n });\n }\n\n // Count disables ADDED in this PR by scanning added (`+`) lines of the diff patch. Rule-aware:\n // reports which webpieces rules were disabled, using the canonical RULE_NAMES vocabulary.\n countAddedDisables(patch: string): DisableCounts {\n let webpiecesCount = 0;\n let eslintCount = 0;\n const rules = new Set<string>();\n const allRuleTokens = Object.keys(RULE_NAMES).map(\n (key: string): string => (RULE_NAMES as Record<string, string>)[key],\n );\n\n for (const line of patch.split('\\n')) {\n if (!line.startsWith('+') || line.startsWith('+++')) continue;\n if (line.includes(WEBPIECES_DISABLE)) {\n webpiecesCount += 1;\n for (const token of allRuleTokens) {\n if (line.includes(token)) rules.add(token);\n }\n }\n if (line.includes('eslint-disable')) eslintCount += 1;\n }\n return new DisableCounts(webpiecesCount, eslintCount, Array.from(rules).sort());\n }\n\n /**\n * The FULL dashboard — every row (green included), the whole summary, the 3-point hash points.\n *\n * This is the **1st PR comment**, not the PR description. It used to be the description, and that is\n * exactly what put it into main's history: GitHub's `squash_merge_commit_message: PR_BODY` copies the\n * description verbatim into the squash commit, so every `git log` entry carried the risk table, the\n * hash points and the gate token. The description now holds {@link renderPrBody}'s compact form, and\n * everything long-form lives here where a reader can open it and `git log` never sees it.\n *\n * Machine-facing content belongs HERE too — this comment carries the HMAC gate token. That is the\n * whole rule that keeps the description clean: an HTML comment is invisible in rendered markdown but\n * perfectly visible in `git log`, so nothing hidden may live in the description.\n */\n renderDetailComment(input: DashboardInput): string {\n const lines: string[] = [];\n lines.push('## 🚦 PR Gate Dashboard');\n lines.push('');\n for (const line of this.riskLines(input.review)) lines.push(line);\n lines.push(`**Build (nx affected):** ${input.buildPassed ? '🟢 Passed' : '🔴 Failed'}`);\n for (const result of input.gateResults) lines.push(this.gateLine(result));\n lines.push(this.disableLine(input.disables));\n const eslintEmoji =\n input.disables.eslintCount === 0 ? '🟢 No' : `🟡 ${input.disables.eslintCount} line(s)`;\n lines.push(`**ESLint Disables Added:** ${eslintEmoji}`);\n // ONE rolled-up row for ALL triggered consumer checklists — a worst-of colour and a count, sitting\n // with the other status rows. It used to be one row PER checklist, each inlining that checklist's\n // whole verdict/override paragraph: on a six-checklist PR that was the same ~90-word override\n // repeated six times, burying the signal rows above it — and every word of it was already in the\n // checklist COMMENT, per reviewer, in full. ALWAYS emitted, including the zero case: \"no reviewer\n // looked at this PR\" is a fact a reader must be told, and an absent row silently reads as a green\n // all-clear (see checklistRollupLine).\n lines.push(this.checklistRollupLine(input.checklists));\n lines.push('');\n if (input.review.summary.trim() !== '') {\n lines.push('### Summary');\n lines.push(input.review.summary.trim());\n lines.push('');\n }\n lines.push('### 🔍 3-Point Hash Points');\n lines.push(`- Fork point (A): \\`${input.forkPoint.slice(0, 12)}\\``);\n lines.push(`- Feature HEAD (B): \\`${input.featureHead.slice(0, 12)}\\``);\n lines.push(`- Main HEAD (C): \\`${input.mainHead.slice(0, 12)}\\``);\n lines.push('');\n lines.push(`<sub>🤖 ${DETAIL_COMMENT_FOOTER}</sub>`);\n return lines.join('\\n');\n }\n\n /**\n * The PR DESCRIPTION — which is also, byte for byte, the squash-commit body that lands in main.\n *\n * ─── Why one string serves both ─────────────────────────────────────────────────────────────────\n * There used to be two: a long dashboard in the description and this compact form passed to\n * `gh pr merge --body-file`. That made the good `git log` reachable ONLY through an explicit\n * `--body-file` merge, because every other route (the GitHub Merge button, a bare `gh pr merge`)\n * takes its body from the repo's `squash_merge_commit_message`, and on `PR_BODY` that copied the\n * whole dashboard into history. Two repos ran that way for months.\n *\n * Making the DESCRIPTION the compact form inverts it: `PR_BODY` now yields exactly the right commit,\n * so the UI button, a bare `gh pr merge`, `wp-land-pr` and finish's own auto-merge all converge on\n * identical bytes. It rests on two GitHub repo settings — `squash_merge_commit_title: PR_TITLE` and\n * `squash_merge_commit_message: PR_BODY` — which are NOT a consumer's job: they are server-side, so no\n * config key can express them, and SquashSettingsEnforcer pins them on every run. That convergence is\n * the point, and `pr-body-is-merge-body.spec.ts` pins it.\n *\n * ─── The shape, and why each part earns its line ────────────────────────────────────────────────\n * The URL leads, BARE — no caption. It used to carry `(for git log)`, which was aimed at a reader on\n * the PR page (where the link is redundant, being the page you are already on) and was meant to stop\n * them deleting it. But this string is also the squash-commit body, so that caption landed verbatim in\n * `main`'s history on every commit, forever, to serve one transient reading. In `git log` a bare URL\n * is self-evidently the way back and needs no explanation. The permanent surface wins.\n * Then the risk score (always). Then ONLY the non-green flags, under a heading that SAYS the full list\n * is in the 1st comment — a `git log` reader has no page to glance at, so the heading has to carry\n * that itself rather than leaving \"why is this list short?\" to the pointer bullet alone. Then the\n * summary capped at 4 sentences, then the footer naming the build command that vouched for it.\n *\n * The hidden gate-token marker is appended AFTER this by the caller, so it is the very last line of\n * the description and therefore of the commit. It stays an HTML comment — invisible on the PR page,\n * visible in `git log` — which is a deliberate accepted trade: the alternative was reshaping a live\n * CI-critical surface that two consumer repos verify on every PR, to save one line of history.\n */\n renderPrBody(input: DashboardInput, prUrl: string): string {\n const lines: string[] = [];\n if (prUrl !== '') {\n lines.push(prUrl);\n lines.push('');\n }\n lines.push(\n `Risk: ${this.riskBar(input.review.riskScore)} ${input.review.riskScore}/100 ${input.review.riskEmoji} (${input.review.riskLevel})`,\n );\n lines.push('');\n const flags = this.nonGreenFlags(input);\n lines.push(flags.length === 0\n ? 'Flags: 🟢 all green'\n : 'Non-green Flags (full list in first comment to avoid large git logs)');\n for (const flag of flags) lines.push(`- ${flag}`);\n // ALWAYS the last bullet, green case included. Without it the compact body reads as the whole\n // record, and the reader never learns that every green row, the full summary and each reviewer's\n // output are one click away.\n lines.push(`- ${DETAIL_POINTER}`);\n const summary = this.firstSentences(input.review.summary.trim(), 4);\n if (summary !== '') {\n lines.push('');\n lines.push(summary);\n }\n lines.push('');\n lines.push(this.prBodyFooter(input.buildCommand));\n return lines.join('\\n');\n }\n\n /**\n * The footer, naming the build command from `commands.pr-gate.buildCommand` VERBATIM.\n *\n * No backticks: this string's primary home is `git log` in a terminal, where markdown ticks are\n * literal punctuation. \"run locally\" is the honest claim — this is the LOCAL gate's receipt, and the\n * server-side proof is the gate token in the 1st comment, not this sentence.\n */\n private prBodyFooter(buildCommand: string): string {\n const cmd = buildCommand.trim();\n const ran = cmd === '' ? '' : ` (${cmd} run locally)`;\n return `Generated by webpieces-ts wp-finish-upsert-pr${ran}`;\n }\n\n // Every dashboard row that is NOT green, as a flat bullet list for the commit body. Green rows\n // (build passed, gate did not match, zero disables/violations) are intentionally omitted.\n private nonGreenFlags(input: DashboardInput): string[] {\n const flags: string[] = [];\n if (!input.buildPassed) flags.push('Build (nx affected): 🔴 Failed');\n if (input.review.violations.length > 0)\n flags.push(`Pattern Violations: 🟡 ${input.review.violations.length} violation(s)`);\n for (const result of input.gateResults) {\n if (result.matchedFiles.length === 0) continue;\n const emoji = result.warningColor === 'red' ? '🔴' : '🟡';\n flags.push(`${result.name}: ${emoji} ${result.matchedFiles.length} file(s)`);\n }\n if (input.disables.webpiecesCount > 0) {\n const which =\n input.disables.webpiecesRules.length > 0\n ? ` — ${input.disables.webpiecesRules.join(', ')}`\n : '';\n flags.push(\n `Webpieces Disables Added: 🟡 ${input.disables.webpiecesCount} line(s)${which}`,\n );\n }\n if (input.disables.eslintCount > 0)\n flags.push(`ESLint Disables Added: 🟡 ${input.disables.eslintCount} line(s)`);\n // A triggered checklist is noteworthy in main's history — carry each into the commit body.\n for (const row of input.checklists) {\n flags.push(`Checklist — ${row.title}: ${this.checklistStatusText(row)}`);\n }\n return flags;\n }\n\n // Emoji + words for a checklist verdict, shared by the dashboard row and the commit-body flag.\n // Every state is matched EXPLICITLY and the fallthrough names the status it did not recognize. The\n // previous `return '🟢 passed'` fallthrough meant any newly-added verdict rendered as a clean pass on\n // both the PR body and main's commit history until someone noticed — exactly the wrong default.\n private checklistStatusText(row: ChecklistRow): string {\n if (row.status === CK_OVERRIDDEN) {\n const why = row.detail.trim() !== '' ? ` — override: ${row.detail.trim()}` : '';\n return `🟠 OVERRIDDEN${why}`;\n }\n if (row.status === CK_WARN) return '🟡 passed with concerns';\n if (row.status === CK_FAIL) return '🔴 FAILED review';\n if (row.status === CK_MISSING) return '⚪ not reviewed';\n if (row.status === CK_PASS) return '🟢 passed';\n return `⚪ unknown verdict (${row.status})`;\n }\n\n // First `max` sentences of `text`. A sentence ends at `. ! ?` ONLY when followed by whitespace or\n // end-of-string, so interior dots in filenames/paths/versions (dependencies.json, runtime-graph.ts,\n // 0.4.447) do NOT split — and, unlike a greedy `[^.!?]+` regex, no text is ever dropped when such a\n // dot appears (that footgun silently deleted the run of prose up to the next real boundary). Keeps\n // the commit body scannable; the full summary still lives in the PR-body dashboard.\n private firstSentences(text: string, max: number): string {\n if (text === '') return '';\n const sentences: string[] = [];\n let start = 0;\n for (let i = 0; i < text.length && sentences.length < max; i++) {\n const ch = text[i];\n const isTerminator = ch === '.' || ch === '!' || ch === '?';\n const next = text[i + 1];\n if (isTerminator && (next === undefined || /\\s/.test(next))) {\n sentences.push(text.slice(start, i + 1).trim());\n start = i + 1;\n }\n }\n // Trailing text with no terminator still counts as a sentence (up to the cap).\n if (sentences.length < max && start < text.length) {\n const tail = text.slice(start).trim();\n if (tail !== '') sentences.push(tail);\n }\n return sentences.join(' ').trim();\n }\n\n // Self-contained glob matcher (** , * , ?) so pr-gate needs no extra runtime dependency.\n private globToRegex(pattern: string): RegExp {\n let re = '';\n let i = 0;\n while (i < pattern.length) {\n const ch = pattern[i];\n if (ch === '*' && pattern[i + 1] === '*') {\n re += '.*';\n i += 2;\n if (pattern[i] === '/') i += 1;\n continue;\n }\n if (ch === '*') {\n re += '[^/]*';\n i += 1;\n continue;\n }\n if (ch === '?') {\n re += '[^/]';\n i += 1;\n continue;\n }\n if ('.+^$(){}|[]\\\\'.includes(ch)) {\n re += '\\\\' + ch;\n i += 1;\n continue;\n }\n re += ch;\n i += 1;\n }\n return new RegExp('^' + re + '$');\n }\n\n private matchesAny(patterns: string[], file: string): boolean {\n for (const pattern of patterns) {\n if (this.globToRegex(pattern).test(file)) return true;\n }\n return false;\n }\n\n private gateLine(result: GateResult): string {\n if (result.matchedFiles.length === 0) return `**${result.name}:** 🟢 No`;\n const emoji = result.warningColor === 'red' ? '🔴' : '🟡';\n return `**${result.name}:** ${emoji} Yes (${result.matchedFiles.length} file(s))`;\n }\n\n /**\n * The ONE rolled-up **Checklists:** row, coloured worst-of across every checklist that ran, in the same\n * shape as its neighbours (`**Build (nx affected):** 🟢 Passed`). Counts and names only — never a line\n * of reviewer output or override justification, which is the whole reason this row exists.\n */\n private checklistRollupLine(rows: readonly ChecklistRow[]): string {\n const buckets = this.rollupBuckets(rows);\n const filled = buckets.filter((b: RollupBucket): boolean => b.titles.length > 0);\n // NO reviewer ran ⇒ the SKIPPED icon, never green. Green claims something was checked and came back\n // clean; ⚪ says nobody looked, which is what a reader has to be able to tell apart at a glance.\n if (filled.length === 0) {\n return '**Checklists:** ⚪ 0 ran — no review checklist matched this PR · see the checklist comment';\n }\n const allPassed = filled.length === 1 && !filled[0].named;\n const detail = allPassed\n ? 'all passed'\n : filled.map((b: RollupBucket): string => this.bucketPhrase(b)).join(', ');\n return `**Checklists:** ${filled[0].emoji} ${rows.length} ran — ${detail} · per-checklist detail in the checklist comment`;\n }\n\n /**\n * The four severity buckets, WORST FIRST, so `filled[0]` is the roll-up colour: red beats orange beats\n * yellow beats green. The colours are the same vocabulary the comment already uses (see verdictEmoji).\n *\n * OVERRIDDEN is deliberately NOT folded into passed: an override is a human knowingly accepting a red\n * verdict, and a dashboard that painted that green would hide the single most review-worthy thing on\n * the PR. The blocking bucket is the FALLTHROUGH — CK_FAIL, CK_MISSING, CK_BAD_FORMAT and any verdict\n * added later all land there, matching review.json's own \"not pass|warn|overridden ⇒ refuse\" rule\n * rather than a second list that could silently drift green.\n */\n private rollupBuckets(rows: readonly ChecklistRow[]): RollupBucket[] {\n const blocking = new RollupBucket('blocking', '🔴', true);\n const overridden = new RollupBucket('overridden', '🟠', true);\n const warned = new RollupBucket('with concerns', '🟡', true);\n const passed = new RollupBucket('passed', '🟢', false);\n for (const row of rows) {\n if (row.status === CK_PASS) passed.titles.push(row.title);\n else if (row.status === CK_WARN) warned.titles.push(row.title);\n else if (row.status === CK_OVERRIDDEN) overridden.titles.push(row.title);\n else blocking.titles.push(row.title);\n }\n return [blocking, overridden, warned, passed];\n }\n\n // `2 overridden (a, b)` for the buckets a reviewer must act on; a bare `3 passed` for the one they need\n // no list of. Names are capped so a many-checklist repo still gets a ONE-LINE row.\n private bucketPhrase(bucket: RollupBucket): string {\n if (!bucket.named) return `${bucket.titles.length} ${bucket.label}`;\n return `${bucket.titles.length} ${bucket.label} (${this.compactNames(bucket.titles)})`;\n }\n\n private compactNames(titles: readonly string[]): string {\n const max = 4;\n if (titles.length <= max) return titles.join(', ');\n return `${titles.slice(0, max).join(', ')} +${titles.length - max} more`;\n }\n\n // 10-cell risk bar colored by band (🟩 ≤25, 🟨 ≤50, 🟧 ≤75, 🟥 >75), at least one filled cell.\n private riskBar(score: number): string {\n const clamped = Math.max(0, Math.min(100, score));\n const cell = clamped <= 25 ? '🟩' : clamped <= 50 ? '🟨' : clamped <= 75 ? '🟧' : '🟥';\n const filled = Math.max(1, Math.min(10, Math.round(clamped / 10)));\n return cell.repeat(filled) + '⬜'.repeat(10 - filled);\n }\n\n // RISK section (the AI half): Risk Score bar, Risk Level, Pattern Violations.\n private riskLines(review: ReviewJson): string[] {\n const violations = review.violations.length;\n const violationLine = violations === 0 ? '🟢 No' : `🟡 Yes (${violations} violation(s))`;\n return [\n `**Risk Score:** ${this.riskBar(review.riskScore)} **${review.riskScore}/100** ${review.riskEmoji}`,\n `**Risk Level:** ${review.riskEmoji} **${review.riskLevel}**`,\n `**Pattern Violations:** ${violationLine}`,\n ];\n }\n\n private disableLine(disables: DisableCounts): string {\n if (disables.webpiecesCount === 0) return '**Webpieces Disables Added:** 🟢 No';\n const which =\n disables.webpiecesRules.length > 0 ? ` — ${disables.webpiecesRules.join(', ')}` : '';\n return `**Webpieces Disables Added:** 🟡 ${disables.webpiecesCount} line(s)${which}`;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"dashboard.js","sourceRoot":"","sources":["../../../../../../packages/tooling/pr-gate/src/dashboard/dashboard.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n GateDefinition,\n WEBPIECES_DISABLE,\n RULE_NAMES,\n ReviewJson,\n CK_PASS,\n CK_WARN,\n CK_OVERRIDDEN,\n CK_FAIL,\n CK_MISSING,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\n\n/**\n * Hidden marker on the FULL-DASHBOARD comment — the \"1st comment\" the PR description points at — so\n * finish PATCHes its own comment on every push instead of appending a new one, exactly like the checklist\n * comment (see ChecklistCommentRenderer, which owns the 2nd).\n *\n * This comment did not exist before the PR description became the git-log body: the dashboard WAS the\n * description. Moving it here is what keeps a risk table out of every squash commit on main.\n */\nexport const DETAIL_COMMENT_MARKER = '<!-- webpieces-pr-detail v1 -->';\n\n/**\n * Footer on the full-dashboard comment. Names the tooling by REPO URL so a reader landing on a\n * generated comment in any consumer repo can find what generated it.\n *\n * It deliberately no longer asserts \"build ran via nx affected — not self-attested\": that sentence was\n * hard-coded and therefore wrong on every repo whose `buildCommand` is not nx. The build command is now\n * named for real, from config, in the PR-body footer — see {@link Dashboard.renderPrBody}.\n */\nconst DETAIL_COMMENT_FOOTER = 'Generated by https://github.com/deanhiller/webpieces-ts wp-finish-upsert-pr';\n\n/**\n * The last bullet of the PR description, and therefore of every squash commit body.\n *\n * Positional (\"1st\"/\"2nd\") on purpose — it is what a reader of `git log` can actually act on, far more\n * useful than naming an HTML marker they cannot see. On a PR opened by an OLDER release the checklist\n * comment already exists, so the dashboard comment lands second and this reads one off for that PR's\n * remaining life; it is cosmetic and self-corrects on the next PR.\n */\nconst DETAIL_POINTER = '(Full dashboard in 1st comment, reviewer checklist in 2nd — kept out of git log)';\n\nexport class GateResult {\n name: string;\n warningColor: string; // 'yellow' | 'red' — the color shown WHEN files matched (green is implicit)\n matchedFiles: string[];\n\n constructor(name: string, warningColor: string, matchedFiles: string[]) {\n this.name = name;\n this.warningColor = warningColor;\n this.matchedFiles = matchedFiles;\n }\n}\n\n// One row for a consumer review checklist the branch triggered. `status` is the resolved verdict (one of\n// CK_PASS | CK_WARN | CK_OVERRIDDEN | CK_FAIL | CK_MISSING | CK_BAD_FORMAT); `detail` is the reviewer\n// output / override justification. A row is always PASS/WARN/OVERRIDDEN by the time it renders — a failed,\n// missing or unreadable verdict throws before the dashboard is built. Rendered into the PR body so the\n// verdict reaches the server — the PR body is the artifact of the local flow that leaves the checkout\n// (alongside the HMAC gate token that proves the flow ran).\nexport class ChecklistRow {\n title: string; // the checklist id (= reviewer subagent name)\n status: string; // CK_* verdict\n detail: string; // reviewer output / override justification (surfaced for OVERRIDDEN)\n\n constructor(title: string, status: string, detail = '') {\n this.title = title;\n this.status = status;\n this.detail = detail;\n }\n}\n\n/**\n * One severity bucket of the rolled-up **Checklists** dashboard row: its emoji, the words that describe it,\n * and which checklists landed in it. Data-only.\n *\n * A bucket knows whether it should NAME its members: the non-green ones do (a reader has to know WHICH\n * reviewer to go read), the passed bucket does not (a list of what went fine is noise on a one-line row).\n */\nclass RollupBucket {\n label: string; // 'blocking' | 'overridden' | 'with concerns' | 'passed'\n emoji: string;\n named: boolean; // list the checklist ids on the row, or report a bare count\n titles: string[];\n\n constructor(label: string, emoji: string, named: boolean) {\n this.label = label;\n this.emoji = emoji;\n this.named = named;\n this.titles = [];\n }\n}\n\nexport class DisableCounts {\n webpiecesCount: number;\n eslintCount: number;\n webpiecesRules: string[];\n\n constructor(webpiecesCount: number, eslintCount: number, webpiecesRules: string[]) {\n this.webpiecesCount = webpiecesCount;\n this.eslintCount = eslintCount;\n this.webpiecesRules = webpiecesRules;\n }\n}\n\nexport class DashboardInput {\n title: string;\n gateResults: GateResult[];\n disables: DisableCounts;\n buildPassed: boolean;\n forkPoint: string;\n featureHead: string;\n mainHead: string;\n review: ReviewJson; // AI-authored risk/violations/summary (from review.json)\n checklists: ChecklistRow[]; // consumer checklists this branch triggered; [] for non-adopting repos\n /**\n * `commands.pr-gate.buildCommand` VERBATIM, named in the PR-body footer so `git log` records WHICH\n * command vouched for the commit. Read from config rather than hard-coded because the footer used to\n * assert \"build ran via nx affected\" on every repo, including those whose buildCommand is not nx.\n *\n * REQUIRED, with no default, and `checklists` lost its `= []` for the same reason. A defaulted\n * `buildCommand: string = ''` let every pre-existing 9-argument construction keep compiling while\n * silently rendering a footer that claims nothing — an absence that quietly means \"no build was\n * named\", which is the widening-by-omission the compatibility policy calls out. Making it required\n * means every caller states what vouched for the commit, and the empty string stays available for a\n * repo that genuinely has no build command, but only when someone writes it down.\n */\n buildCommand: string;\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(\n title: string,\n gateResults: GateResult[],\n disables: DisableCounts,\n buildPassed: boolean,\n forkPoint: string,\n featureHead: string,\n mainHead: string,\n review: ReviewJson,\n checklists: ChecklistRow[],\n buildCommand: string,\n ) {\n this.title = title;\n this.gateResults = gateResults;\n this.disables = disables;\n this.buildPassed = buildPassed;\n this.forkPoint = forkPoint;\n this.featureHead = featureHead;\n this.mainHead = mainHead;\n this.review = review;\n this.checklists = checklists;\n this.buildCommand = buildCommand;\n }\n}\n\n/** Renders the PR-gate dashboard markdown (gates × changed files, disables, risk, 3-point hashes). */\n@injectable(bindingScopeValues.Singleton)\nexport class Dashboard {\n // Disabled gates are in-file examples (JSON has no comments) — skip them entirely.\n computeGateResults(gates: GateDefinition[], changedFiles: string[]): GateResult[] {\n return gates\n .filter((gate: GateDefinition): boolean => !gate.disabled)\n .map((gate: GateDefinition): GateResult => {\n const matched = changedFiles.filter((file: string): boolean =>\n this.matchesAny(gate.patterns, file),\n );\n return new GateResult(gate.name, gate.warningColor, matched);\n });\n }\n\n // Count disables ADDED in this PR by scanning added (`+`) lines of the diff patch. Rule-aware:\n // reports which webpieces rules were disabled, using the canonical RULE_NAMES vocabulary.\n countAddedDisables(patch: string): DisableCounts {\n let webpiecesCount = 0;\n let eslintCount = 0;\n const rules = new Set<string>();\n const allRuleTokens = Object.keys(RULE_NAMES).map(\n (key: string): string => (RULE_NAMES as Record<string, string>)[key],\n );\n\n for (const line of patch.split('\\n')) {\n if (!line.startsWith('+') || line.startsWith('+++')) continue;\n if (line.includes(WEBPIECES_DISABLE)) {\n webpiecesCount += 1;\n for (const token of allRuleTokens) {\n if (line.includes(token)) rules.add(token);\n }\n }\n if (line.includes('eslint-disable')) eslintCount += 1;\n }\n return new DisableCounts(webpiecesCount, eslintCount, Array.from(rules).sort());\n }\n\n /**\n * The FULL dashboard — every row (green included), the whole summary, the 3-point hash points.\n *\n * This is the **1st PR comment**, not the PR description. It used to be the description, and that is\n * exactly what put it into main's history: GitHub's `squash_merge_commit_message: PR_BODY` copies the\n * description verbatim into the squash commit, so every `git log` entry carried the risk table, the\n * hash points and the gate token. The description now holds {@link renderPrBody}'s compact form, and\n * everything long-form lives here where a reader can open it and `git log` never sees it.\n *\n * Machine-facing content belongs HERE too — this comment carries the HMAC gate token. That is the\n * whole rule that keeps the description clean: an HTML comment is invisible in rendered markdown but\n * perfectly visible in `git log`, so nothing hidden may live in the description.\n */\n renderDetailComment(input: DashboardInput): string {\n const lines: string[] = [];\n lines.push('## 🚦 PR Gate Dashboard');\n lines.push('');\n for (const line of this.riskLines(input.review)) lines.push(line);\n lines.push(`**Build (nx affected):** ${input.buildPassed ? '🟢 Passed' : '🔴 Failed'}`);\n for (const result of input.gateResults) lines.push(this.gateLine(result));\n lines.push(this.disableLine(input.disables));\n const eslintEmoji =\n input.disables.eslintCount === 0 ? '🟢 No' : `🟡 ${input.disables.eslintCount} line(s)`;\n lines.push(`**ESLint Disables Added:** ${eslintEmoji}`);\n // ONE rolled-up row for ALL triggered consumer checklists — a worst-of colour and a count, sitting\n // with the other status rows. It used to be one row PER checklist, each inlining that checklist's\n // whole verdict/override paragraph: on a six-checklist PR that was the same ~90-word override\n // repeated six times, burying the signal rows above it — and every word of it was already in the\n // checklist COMMENT, per reviewer, in full. ALWAYS emitted, including the zero case: \"no reviewer\n // looked at this PR\" is a fact a reader must be told, and an absent row silently reads as a green\n // all-clear (see checklistRollupLine).\n lines.push(this.checklistRollupLine(input.checklists));\n lines.push('');\n if (input.review.summary.trim() !== '') {\n lines.push('### Summary');\n lines.push(input.review.summary.trim());\n lines.push('');\n }\n lines.push('### 🔍 3-Point Hash Points');\n lines.push(`- Fork point (A): \\`${input.forkPoint.slice(0, 12)}\\``);\n lines.push(`- Feature HEAD (B): \\`${input.featureHead.slice(0, 12)}\\``);\n lines.push(`- Main HEAD (C): \\`${input.mainHead.slice(0, 12)}\\``);\n lines.push('');\n lines.push(`<sub>🤖 ${DETAIL_COMMENT_FOOTER}</sub>`);\n return lines.join('\\n');\n }\n\n /**\n * The PR DESCRIPTION — which is also, byte for byte, the squash-commit body that lands in main.\n *\n * ─── Why one string serves both ─────────────────────────────────────────────────────────────────\n * There used to be two: a long dashboard in the description and this compact form passed to\n * `gh pr merge --body-file`. That made the good `git log` reachable ONLY through an explicit\n * `--body-file` merge, because every other route (the GitHub Merge button, a bare `gh pr merge`)\n * takes its body from the repo's `squash_merge_commit_message`, and on `PR_BODY` that copied the\n * whole dashboard into history. Two repos ran that way for months.\n *\n * Making the DESCRIPTION the compact form inverts it: `PR_BODY` now yields exactly the right commit,\n * so the UI button, a bare `gh pr merge`, `wp-land-pr` and finish's own auto-merge all converge on\n * identical bytes. It rests on two GitHub repo settings — `squash_merge_commit_title: PR_TITLE` and\n * `squash_merge_commit_message: PR_BODY` — which are NOT a consumer's job: they are server-side, so no\n * config key can express them, and SquashSettingsEnforcer pins them on every run. That convergence is\n * the point, and `pr-body-is-merge-body.spec.ts` pins it.\n *\n * ─── The shape, and why each part earns its line ────────────────────────────────────────────────\n * The URL leads, BARE — no caption. It used to carry `(for git log)`, which was aimed at a reader on\n * the PR page (where the link is redundant, being the page you are already on) and was meant to stop\n * them deleting it. But this string is also the squash-commit body, so that caption landed verbatim in\n * `main`'s history on every commit, forever, to serve one transient reading. In `git log` a bare URL\n * is self-evidently the way back and needs no explanation. The permanent surface wins.\n * Then the risk score (always). Then ONLY the non-green flags, under a heading that SAYS the full list\n * is in the 1st comment — a `git log` reader has no page to glance at, so the heading has to carry\n * that itself rather than leaving \"why is this list short?\" to the pointer bullet alone. Then the\n * summary capped at 4 sentences, then the footer naming the build command that vouched for it.\n *\n * The hidden gate-token marker is appended AFTER this by the caller, so it is the very last line of\n * the description and therefore of the commit. It stays an HTML comment — invisible on the PR page,\n * visible in `git log` — which is a deliberate accepted trade: the alternative was reshaping a live\n * CI-critical surface that two consumer repos verify on every PR, to save one line of history.\n */\n renderPrBody(input: DashboardInput, prUrl: string): string {\n const lines: string[] = [];\n if (prUrl !== '') {\n lines.push(prUrl);\n lines.push('');\n }\n lines.push(\n `Risk: ${this.riskBar(input.review.riskScore)} ${input.review.riskScore}/100 ${input.review.riskEmoji} (${input.review.riskLevel})`,\n );\n lines.push('');\n const flags = this.nonGreenFlags(input);\n lines.push(flags.length === 0\n ? 'Flags: 🟢 all green'\n : 'Non-green Flags (full list in first comment to avoid large git logs)');\n for (const flag of flags) lines.push(`- ${flag}`);\n // ALWAYS the last bullet, green case included. Without it the compact body reads as the whole\n // record, and the reader never learns that every green row, the full summary and each reviewer's\n // output are one click away.\n lines.push(`- ${DETAIL_POINTER}`);\n const summary = this.firstSentences(input.review.summary.trim(), 4);\n if (summary !== '') {\n lines.push('');\n lines.push(summary);\n }\n lines.push('');\n lines.push(this.prBodyFooter(input.buildCommand));\n return this.gitLogSafe(lines.join('\\n'));\n }\n\n /**\n * Makes THIS renderer own the invariant `wp-land-pr` checks: the compact body carries no markdown\n * heading and no table pipe.\n *\n * It is applied to the WHOLE joined string rather than to each author-supplied field, on purpose.\n * Every field this body interpolates — the review summary, an override justification, a checklist\n * title, a disabled rule name, `commands.pr-gate.buildCommand` — is text this package did not write,\n * and a `|` reaches all of them for entirely ordinary non-markdown reasons: a TypeScript union\n * (`'open' | 'paused'`), a regex alternation, a quoted shell pipeline, an OR in prose. Sanitizing at\n * the single exit point means a field added later is covered without anyone remembering to wrap it,\n * and none of this renderer's own literals contain either marker, so nothing of ours changes.\n *\n * Before this, `wp-finish-upsert-pr` would happily POST such a body and report success, and then\n * `wp-land-pr` refused those exact bytes — two commands in one flow disagreeing about what the\n * renderer produces, with a refusal that blamed an old release and prescribed re-running finish,\n * which re-rendered the identical pipe from the unchanged `review.json`. An infinite loop costing a\n * CI cycle per turn.\n *\n * `¦` (BROKEN BAR) rather than `\\|`: this string's home is `git log` in a terminal, where a\n * backslash is literal punctuation and a markdown escape means nothing. The substitution is visible\n * and reads correctly; the full, unaltered text is one click away in the 1st comment, which this\n * body's last bullet always points at. `##` collapses to `#` for the same reason — a heading in a\n * summary is markdown a plain-text history cannot carry either.\n */\n private gitLogSafe(body: string): string {\n return body.replace(/\\|/g, '\\u00a6').replace(/#{2,}/g, '#');\n }\n\n /**\n * The footer, naming the build command from `commands.pr-gate.buildCommand` VERBATIM.\n *\n * No backticks: this string's primary home is `git log` in a terminal, where markdown ticks are\n * literal punctuation. \"run locally\" is the honest claim — this is the LOCAL gate's receipt, and the\n * server-side proof is the gate token in the 1st comment, not this sentence.\n */\n private prBodyFooter(buildCommand: string): string {\n const cmd = buildCommand.trim();\n const ran = cmd === '' ? '' : ` (${cmd} run locally)`;\n return `Generated by webpieces-ts wp-finish-upsert-pr${ran}`;\n }\n\n // Every dashboard row that is NOT green, as a flat bullet list for the commit body. Green rows\n // (build passed, gate did not match, zero disables/violations) are intentionally omitted.\n private nonGreenFlags(input: DashboardInput): string[] {\n const flags: string[] = [];\n if (!input.buildPassed) flags.push('Build (nx affected): 🔴 Failed');\n if (input.review.violations.length > 0)\n flags.push(`Pattern Violations: 🟡 ${input.review.violations.length} violation(s)`);\n for (const result of input.gateResults) {\n if (result.matchedFiles.length === 0) continue;\n const emoji = result.warningColor === 'red' ? '🔴' : '🟡';\n flags.push(`${result.name}: ${emoji} ${result.matchedFiles.length} file(s)`);\n }\n if (input.disables.webpiecesCount > 0) {\n const which =\n input.disables.webpiecesRules.length > 0\n ? ` — ${input.disables.webpiecesRules.join(', ')}`\n : '';\n flags.push(\n `Webpieces Disables Added: 🟡 ${input.disables.webpiecesCount} line(s)${which}`,\n );\n }\n if (input.disables.eslintCount > 0)\n flags.push(`ESLint Disables Added: 🟡 ${input.disables.eslintCount} line(s)`);\n // A triggered checklist is noteworthy in main's history — carry each into the commit body.\n for (const row of input.checklists) {\n flags.push(`Checklist — ${row.title}: ${this.checklistStatusText(row)}`);\n }\n return flags;\n }\n\n // Emoji + words for a checklist verdict, shared by the dashboard row and the commit-body flag.\n // Every state is matched EXPLICITLY and the fallthrough names the status it did not recognize. The\n // previous `return '🟢 passed'` fallthrough meant any newly-added verdict rendered as a clean pass on\n // both the PR body and main's commit history until someone noticed — exactly the wrong default.\n private checklistStatusText(row: ChecklistRow): string {\n if (row.status === CK_OVERRIDDEN) {\n const why = row.detail.trim() !== '' ? ` — override: ${row.detail.trim()}` : '';\n return `🟠 OVERRIDDEN${why}`;\n }\n if (row.status === CK_WARN) return '🟡 passed with concerns';\n if (row.status === CK_FAIL) return '🔴 FAILED review';\n if (row.status === CK_MISSING) return '⚪ not reviewed';\n if (row.status === CK_PASS) return '🟢 passed';\n return `⚪ unknown verdict (${row.status})`;\n }\n\n // First `max` sentences of `text`. A sentence ends at `. ! ?` ONLY when followed by whitespace or\n // end-of-string, so interior dots in filenames/paths/versions (dependencies.json, runtime-graph.ts,\n // 0.4.447) do NOT split — and, unlike a greedy `[^.!?]+` regex, no text is ever dropped when such a\n // dot appears (that footgun silently deleted the run of prose up to the next real boundary). Keeps\n // the commit body scannable; the full summary still lives in the PR-body dashboard.\n private firstSentences(text: string, max: number): string {\n if (text === '') return '';\n const sentences: string[] = [];\n let start = 0;\n for (let i = 0; i < text.length && sentences.length < max; i++) {\n const ch = text[i];\n const isTerminator = ch === '.' || ch === '!' || ch === '?';\n const next = text[i + 1];\n if (isTerminator && (next === undefined || /\\s/.test(next))) {\n sentences.push(text.slice(start, i + 1).trim());\n start = i + 1;\n }\n }\n // Trailing text with no terminator still counts as a sentence (up to the cap).\n if (sentences.length < max && start < text.length) {\n const tail = text.slice(start).trim();\n if (tail !== '') sentences.push(tail);\n }\n return sentences.join(' ').trim();\n }\n\n // Self-contained glob matcher (** , * , ?) so pr-gate needs no extra runtime dependency.\n private globToRegex(pattern: string): RegExp {\n let re = '';\n let i = 0;\n while (i < pattern.length) {\n const ch = pattern[i];\n if (ch === '*' && pattern[i + 1] === '*') {\n re += '.*';\n i += 2;\n if (pattern[i] === '/') i += 1;\n continue;\n }\n if (ch === '*') {\n re += '[^/]*';\n i += 1;\n continue;\n }\n if (ch === '?') {\n re += '[^/]';\n i += 1;\n continue;\n }\n if ('.+^$(){}|[]\\\\'.includes(ch)) {\n re += '\\\\' + ch;\n i += 1;\n continue;\n }\n re += ch;\n i += 1;\n }\n return new RegExp('^' + re + '$');\n }\n\n private matchesAny(patterns: string[], file: string): boolean {\n for (const pattern of patterns) {\n if (this.globToRegex(pattern).test(file)) return true;\n }\n return false;\n }\n\n private gateLine(result: GateResult): string {\n if (result.matchedFiles.length === 0) return `**${result.name}:** 🟢 No`;\n const emoji = result.warningColor === 'red' ? '🔴' : '🟡';\n return `**${result.name}:** ${emoji} Yes (${result.matchedFiles.length} file(s))`;\n }\n\n /**\n * The ONE rolled-up **Checklists:** row, coloured worst-of across every checklist that ran, in the same\n * shape as its neighbours (`**Build (nx affected):** 🟢 Passed`). Counts and names only — never a line\n * of reviewer output or override justification, which is the whole reason this row exists.\n */\n private checklistRollupLine(rows: readonly ChecklistRow[]): string {\n const buckets = this.rollupBuckets(rows);\n const filled = buckets.filter((b: RollupBucket): boolean => b.titles.length > 0);\n // NO reviewer ran ⇒ the SKIPPED icon, never green. Green claims something was checked and came back\n // clean; ⚪ says nobody looked, which is what a reader has to be able to tell apart at a glance.\n if (filled.length === 0) {\n return '**Checklists:** ⚪ 0 ran — no review checklist matched this PR · see the checklist comment';\n }\n const allPassed = filled.length === 1 && !filled[0].named;\n const detail = allPassed\n ? 'all passed'\n : filled.map((b: RollupBucket): string => this.bucketPhrase(b)).join(', ');\n return `**Checklists:** ${filled[0].emoji} ${rows.length} ran — ${detail} · per-checklist detail in the checklist comment`;\n }\n\n /**\n * The four severity buckets, WORST FIRST, so `filled[0]` is the roll-up colour: red beats orange beats\n * yellow beats green. The colours are the same vocabulary the comment already uses (see verdictEmoji).\n *\n * OVERRIDDEN is deliberately NOT folded into passed: an override is a human knowingly accepting a red\n * verdict, and a dashboard that painted that green would hide the single most review-worthy thing on\n * the PR. The blocking bucket is the FALLTHROUGH — CK_FAIL, CK_MISSING, CK_BAD_FORMAT and any verdict\n * added later all land there, matching review.json's own \"not pass|warn|overridden ⇒ refuse\" rule\n * rather than a second list that could silently drift green.\n */\n private rollupBuckets(rows: readonly ChecklistRow[]): RollupBucket[] {\n const blocking = new RollupBucket('blocking', '🔴', true);\n const overridden = new RollupBucket('overridden', '🟠', true);\n const warned = new RollupBucket('with concerns', '🟡', true);\n const passed = new RollupBucket('passed', '🟢', false);\n for (const row of rows) {\n if (row.status === CK_PASS) passed.titles.push(row.title);\n else if (row.status === CK_WARN) warned.titles.push(row.title);\n else if (row.status === CK_OVERRIDDEN) overridden.titles.push(row.title);\n else blocking.titles.push(row.title);\n }\n return [blocking, overridden, warned, passed];\n }\n\n // `2 overridden (a, b)` for the buckets a reviewer must act on; a bare `3 passed` for the one they need\n // no list of. Names are capped so a many-checklist repo still gets a ONE-LINE row.\n private bucketPhrase(bucket: RollupBucket): string {\n if (!bucket.named) return `${bucket.titles.length} ${bucket.label}`;\n return `${bucket.titles.length} ${bucket.label} (${this.compactNames(bucket.titles)})`;\n }\n\n private compactNames(titles: readonly string[]): string {\n const max = 4;\n if (titles.length <= max) return titles.join(', ');\n return `${titles.slice(0, max).join(', ')} +${titles.length - max} more`;\n }\n\n // 10-cell risk bar colored by band (🟩 ≤25, 🟨 ≤50, 🟧 ≤75, 🟥 >75), at least one filled cell.\n private riskBar(score: number): string {\n const clamped = Math.max(0, Math.min(100, score));\n const cell = clamped <= 25 ? '🟩' : clamped <= 50 ? '🟨' : clamped <= 75 ? '🟧' : '🟥';\n const filled = Math.max(1, Math.min(10, Math.round(clamped / 10)));\n return cell.repeat(filled) + '⬜'.repeat(10 - filled);\n }\n\n // RISK section (the AI half): Risk Score bar, Risk Level, Pattern Violations.\n private riskLines(review: ReviewJson): string[] {\n const violations = review.violations.length;\n const violationLine = violations === 0 ? '🟢 No' : `🟡 Yes (${violations} violation(s))`;\n return [\n `**Risk Score:** ${this.riskBar(review.riskScore)} **${review.riskScore}/100** ${review.riskEmoji}`,\n `**Risk Level:** ${review.riskEmoji} **${review.riskLevel}**`,\n `**Pattern Violations:** ${violationLine}`,\n ];\n }\n\n private disableLine(disables: DisableCounts): string {\n if (disables.webpiecesCount === 0) return '**Webpieces Disables Added:** 🟢 No';\n const which =\n disables.webpiecesRules.length > 0 ? ` — ${disables.webpiecesRules.join(', ')}` : '';\n return `**Webpieces Disables Added:** 🟡 ${disables.webpiecesCount} line(s)${which}`;\n }\n}\n"]}
|
|
@@ -77,11 +77,25 @@ export declare class LandPrCommand {
|
|
|
77
77
|
*
|
|
78
78
|
* It keeps earning its place after the transition: nothing stops a human editing a PR description
|
|
79
79
|
* in GitHub's textarea, and this is the only point between that edit and main's history.
|
|
80
|
+
*
|
|
81
|
+
* What it must NOT be read as is proof that the bytes did not come from `renderPrBody`. They can:
|
|
82
|
+
* the compact body interpolates author text, and a `|` reaches that text from a TypeScript union, a
|
|
83
|
+
* regex alternation or a quoted shell pipeline. `Dashboard.gitLogSafe` substitutes both markers at
|
|
84
|
+
* the render exit so a freshly-rendered body no longer trips this — which is where that invariant
|
|
85
|
+
* belongs, since the renderer is the only thing that can fix it without an extra CI cycle.
|
|
80
86
|
*/
|
|
81
87
|
private notFitForGitLog;
|
|
82
88
|
/**
|
|
83
|
-
* The refusal when the description is not the compact gated body
|
|
84
|
-
*
|
|
89
|
+
* The refusal when the description is not the compact gated body.
|
|
90
|
+
*
|
|
91
|
+
* It names BOTH ways bytes get here, because naming only one sent readers down a path that did not
|
|
92
|
+
* exist. It used to assert an old release as "the usual cause" and prescribe re-running finish — and
|
|
93
|
+
* when the marker had come from AUTHOR TEXT instead (a `|` in a summary, from a TypeScript union or a
|
|
94
|
+
* regex alternation), finish re-rendered the identical character from the unchanged `review.json` and
|
|
95
|
+
* landing refused again: a loop costing a CI cycle per turn, escapable only by guessing that one
|
|
96
|
+
* character in your prose was the problem. `Dashboard.gitLogSafe` now substitutes both markers at the
|
|
97
|
+
* render exit, so a freshly-rendered body cannot reach here at all — which leaves a HAND-EDITED
|
|
98
|
+
* description and a genuinely old PR as the two remaining causes, and this says so.
|
|
85
99
|
*/
|
|
86
100
|
private descriptionUnfitForGitLog;
|
|
87
101
|
/**
|
|
@@ -146,6 +146,12 @@ let LandPrCommand = class LandPrCommand {
|
|
|
146
146
|
*
|
|
147
147
|
* It keeps earning its place after the transition: nothing stops a human editing a PR description
|
|
148
148
|
* in GitHub's textarea, and this is the only point between that edit and main's history.
|
|
149
|
+
*
|
|
150
|
+
* What it must NOT be read as is proof that the bytes did not come from `renderPrBody`. They can:
|
|
151
|
+
* the compact body interpolates author text, and a `|` reaches that text from a TypeScript union, a
|
|
152
|
+
* regex alternation or a quoted shell pipeline. `Dashboard.gitLogSafe` substitutes both markers at
|
|
153
|
+
* the render exit so a freshly-rendered body no longer trips this — which is where that invariant
|
|
154
|
+
* belongs, since the renderer is the only thing that can fix it without an extra CI cycle.
|
|
149
155
|
*/
|
|
150
156
|
notFitForGitLog(body) {
|
|
151
157
|
if (body.includes('##'))
|
|
@@ -155,18 +161,31 @@ let LandPrCommand = class LandPrCommand {
|
|
|
155
161
|
return '';
|
|
156
162
|
}
|
|
157
163
|
/**
|
|
158
|
-
* The refusal when the description is not the compact gated body
|
|
159
|
-
*
|
|
164
|
+
* The refusal when the description is not the compact gated body.
|
|
165
|
+
*
|
|
166
|
+
* It names BOTH ways bytes get here, because naming only one sent readers down a path that did not
|
|
167
|
+
* exist. It used to assert an old release as "the usual cause" and prescribe re-running finish — and
|
|
168
|
+
* when the marker had come from AUTHOR TEXT instead (a `|` in a summary, from a TypeScript union or a
|
|
169
|
+
* regex alternation), finish re-rendered the identical character from the unchanged `review.json` and
|
|
170
|
+
* landing refused again: a loop costing a CI cycle per turn, escapable only by guessing that one
|
|
171
|
+
* character in your prose was the problem. `Dashboard.gitLogSafe` now substitutes both markers at the
|
|
172
|
+
* render exit, so a freshly-rendered body cannot reach here at all — which leaves a HAND-EDITED
|
|
173
|
+
* description and a genuinely old PR as the two remaining causes, and this says so.
|
|
160
174
|
*/
|
|
161
175
|
descriptionUnfitForGitLog(ref, marker) {
|
|
162
176
|
return new rules_config_1.InformAiError('\n' + SEP + `❌ PR #${ref.number}'s description is not a git-log commit body\n` + SEP + '\n' +
|
|
163
177
|
`It contains ${marker}, which the compact body rendered by \`pnpm wp-finish-upsert-pr\` never\n` +
|
|
164
|
-
'does — so these bytes are not the gated summary
|
|
165
|
-
'Dashboard (or hand-written markdown) into main\'s
|
|
166
|
-
'
|
|
167
|
-
'
|
|
168
|
-
'
|
|
169
|
-
'
|
|
178
|
+
'does — it substitutes both markers as it renders — so these bytes are not the gated summary,\n' +
|
|
179
|
+
'and landing them would put a PR Gate Dashboard (or hand-written markdown) into main\'s\n' +
|
|
180
|
+
'history permanently.\n\n' +
|
|
181
|
+
'There are two causes, and they need different fixes:\n\n' +
|
|
182
|
+
' 1. The description was EDITED BY HAND on GitHub after finish posted it. Re-running finish\n' +
|
|
183
|
+
' overwrites that edit with the compact body, so landing works.\n' +
|
|
184
|
+
' 2. The PR was posted by a webpieces release OLDER than the one that made the description\n' +
|
|
185
|
+
' the commit body, so it is still the full dashboard. The dashboard now lives in the PR\'s\n' +
|
|
186
|
+
' 1st comment instead, and re-running finish moves it there.\n\n' +
|
|
187
|
+
`Either way the cure is the same, and it does NOT require removing ${marker.trim()} from your review\n` +
|
|
188
|
+
'text — a pipe or a heading in a summary is rendered safe, not rejected:\n' +
|
|
170
189
|
' pnpm wp-start-upsert-pr && pnpm wp-review-upsert-pr && pnpm wp-finish-upsert-pr\n' + SEP);
|
|
171
190
|
}
|
|
172
191
|
/**
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"land-pr-command.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/commands/land-pr-command.ts"],"names":[],"mappings":"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","sourcesContent":["import { execSync, spawnSync } from 'child_process';\nimport {\n InformAiError, RepoRootFinder, MERGE_MODE_AUTO, loadAndValidate,\n BranchArchiver, BRANCH_RETENTION_ARCHIVE_TAG, BRANCH_RETENTION_KEEP, toError,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { AiBranchName } from '../workflow/git-readAiBranchName';\nimport { BranchNaming } from '../workflow/branch-naming';\nimport { LandedWorktreeReaper, WorktreeReapHandoff } from '../workflow/landed-worktree-reaper';\nimport { MergeBodyTempFile } from '../workflow/merge-body-temp-file';\nimport { ArchiveRecord, MergeInfoIndex } from '../workflow/merge-info-index';\nimport { MergeIntent, PrMerger } from '../workflow/pr-merger';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n/**\n * `wp-land-pr`: squash-merge THIS branch's already-posted PR into main with the compact commit body.\n *\n * It exists because a merge clicked in the GitHub UI cannot set the squash SUBJECT to `<title> (#N)`\n * and cannot archive the pre-squash tip. So when a merge has to happen outside `wp-finish-upsert-pr`\n * (a mergeMode=NONE repo, or a PR whose checks were still running when finish ran), this is the command\n * that keeps main's history consistent and finishes the branch's bookkeeping.\n *\n * ─── Where the bytes come from: THE PR ITSELF ──────────────────────────────────────────────────────\n * `wp-finish-upsert-pr` renders ONE string and publishes it as the PR DESCRIPTION, and the full\n * dashboard lives in the PR's comments (see `pr-body-is-merge-body.spec.ts`, which pins that the\n * description and the merge body are one renderer's output). So the description IS the gated squash\n * body, held by GitHub, and landing reads it back with `gh pr view --json body`.\n *\n * THE BYTES THAT LAND ARE THE BYTES FINISH PRODUCED — that invariant is unchanged; what changed is that\n * GitHub, not this machine, is the thing holding them. That removes the whole machine-global receipt\n * store: a cache of a fact the remote already owns, which could only ever be missing, stale, or on the\n * wrong computer. Landing from a second clone, a fresh clone, or another machine entirely now just\n * works. See `decisions/0005-the-pr-description-is-the-merge-body.md`.\n *\n * It still deliberately does NOT re-render the body: re-deriving it at land time would be a second\n * authoritative gate whose result nobody reads, and it could silently disagree with what was reviewed.\n *\n * ─── Two scopes, and only one of them is the tree's ────────────────────────────────────────────────\n * 1. THE MERGE, which belongs to the PR — and is therefore reachable from anywhere.\n * 2. THE BOOKKEEPING — archiving the pre-squash tip as `archive/<date>/<branch>`, promoting merge-info,\n * and reaping the landed worktree. That half belongs to the tree whose `<branch>` really is the\n * commit being squashed, and it must NOT be attempted from anywhere else: another clone's\n * `<branch>` is a different commit, so archiving it there would tag the wrong objects under the\n * right name. {@link LandPrCommand.bookkeeping} tests that by comparing this tree's `<branch>`\n * against the PR's own `headRefOid`, and when they disagree the merge still happens and the\n * bookkeeping is SKIPPED OUT LOUD.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class LandPrCommand {\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(\n private readonly repoRootFinder: RepoRootFinder,\n private readonly aiBranchName: AiBranchName,\n private readonly branchNaming: BranchNaming,\n private readonly prMerger: PrMerger,\n private readonly archiver: BranchArchiver,\n private readonly mergeInfoIndex: MergeInfoIndex,\n private readonly landedWorktree: LandedWorktreeReaper,\n private readonly bodyFile: MergeBodyTempFile,\n ) {}\n\n async run(): Promise<void> {\n const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());\n const base = this.branchNaming.baseBranchName(execSync('git branch --show-current', { encoding: 'utf8' }).trim());\n\n const ref = this.readPr(base);\n if (ref === null) {\n throw new InformAiError(\n '\\n' + SEP + '❌ No open PR found for this branch\\n' + SEP + '\\n' +\n `No open PR has head branch \"${base}\". Nothing to land.\\n` +\n 'If the PR is already merged, run `pnpm wp-cleanup`.\\n' +\n 'If it was never posted, run the gated flow — it posts the PR AND writes the description\\n' +\n 'that becomes this commit body:\\n' +\n ' pnpm wp-start-upsert-pr && pnpm wp-review-upsert-pr && pnpm wp-finish-upsert-pr\\n' + SEP,\n );\n }\n if (ref.body === '') throw this.emptyDescription(ref);\n const unfit = this.notFitForGitLog(ref.body);\n if (unfit !== '') throw this.descriptionUnfitForGitLog(ref, unfit);\n\n process.stdout.write('\\n' + SEP + `🚀 Landing PR #${ref.number}\\n` + SEP + '\\n');\n const mergeBodyFile = this.bodyFile.write(ref.body);\n\n const config = loadAndValidate(repoRoot).prGate;\n const policy = config.mergeMode;\n // Reuse the SAME merge logic wp-finish-upsert-pr uses, so a PR lands identically whichever\n // command lands it — including the auto-merge fallback when the checks are still running.\n //\n // `commanded: true` — running THIS command IS the intent to merge, so it is not gated on\n // pr-gate.mergeMode (a NONE repo runs this precisely to land one PR by hand). The real `policy`\n // travels alongside it rather than being replaced by a literal AUTO, so PrMerger's diagnostics\n // never assert a config value nobody set — that is exactly what used to print a CONFIG MISMATCH\n // about `mergeMode: AUTO` two lines above this command printing `mergeMode is NONE`.\n const outcome = this.prMerger.merge(\n base, `${ref.title} (#${ref.number})`, mergeBodyFile, new MergeIntent(policy ?? '', true),\n );\n\n const bookkeeping = outcome.merged\n ? this.bookkeeping(repoRoot, base, ref, config.landPr.branchRetention)\n : '';\n process.stdout.write(\n '\\n' + SEP + (outcome.merged ? '✅ Landed\\n' : 'ℹ️ Not landed yet\\n') + SEP + '\\n' +\n ` ${outcome.message}\\n` +\n bookkeeping +\n (policy === MERGE_MODE_AUTO\n ? ''\n : ` (pr-gate.mergeMode is ${policy} — wp-finish-upsert-pr will keep leaving PRs for a human.)\\n`) +\n '\\n',\n );\n }\n\n /**\n * The refusal when the PR exists but its description is empty.\n *\n * This is the ONE remaining \"no gated body\" case, and it is now a property of the PR rather than of\n * this machine — which is why the cure is a single command with no human judgement call attached.\n * There is deliberately no `--fallback-title-only` escape any more: that flag existed because the\n * bytes could be on a DIFFERENT COMPUTER from the one landing, which cannot happen when GitHub holds\n * them. Re-running finish is always available, always correct, and lands the reviewed bytes.\n */\n private emptyDescription(ref: PrIdentity): InformAiError {\n return new InformAiError(\n '\\n' + SEP + `❌ PR #${ref.number} has an EMPTY description\\n` + SEP + '\\n' +\n 'The PR description IS the squash-commit body — `pnpm wp-finish-upsert-pr` renders one string\\n' +\n 'and publishes it as the description, and landing reads it straight back with\\n' +\n '`gh pr view --json body`. An empty description means the gated flow never published one\\n' +\n '(or a human cleared it), so there is nothing reviewed to land.\\n\\n' +\n 'Re-run the gated flow — it re-renders the description and re-posts it to this same PR:\\n' +\n ' pnpm wp-start-upsert-pr # update from main (3-point merge). No push, no build gate.\\n' +\n ' pnpm wp-review-upsert-pr # validate the merge, build gate, extract the diff, brief reviewers\\n' +\n ' # write review.json at the path wp-review-upsert-pr prints\\n' +\n ' pnpm wp-finish-upsert-pr # build gate, dashboard, create/update the PR\\n' +\n 'Then re-run `pnpm wp-land-pr`.\\n' + SEP,\n );\n }\n\n /**\n * '' when the description is a compact gated body; otherwise the marker that proves it is not.\n *\n * This reads the SAME invariant `pr-body-is-merge-body.spec.ts` asserts from the other end: the\n * compact body \"contains nothing a plain-text git log cannot carry\" — no markdown heading, no table\n * pipe. It is therefore not a heuristic about what a dashboard looks like; it is the renderer's own\n * pinned property, checked against bytes that arrived from outside this process.\n *\n * WHY IT IS NEEDED, and why it is not a compatibility fallback: a PR posted by a release OLDER than\n * the surface swap still has the FULL DASHBOARD as its description. Measured on this repo,\n * 2026-08-07 — PR #613 (posted after the swap) has a description byte-identical to its squash body,\n * while PR #614 (still open, posted minutes before) begins `## 🚦 PR Gate Dashboard`. Landing that\n * one would dump a risk table into main, which is precisely the defect\n * `decisions/0004` § 4.1 warned about. So this REFUSES and names the one command that fixes it. It\n * never falls back, never rewrites, and never reaches for a second source of bytes.\n *\n * It keeps earning its place after the transition: nothing stops a human editing a PR description\n * in GitHub's textarea, and this is the only point between that edit and main's history.\n */\n private notFitForGitLog(body: string): string {\n if (body.includes('##')) return '## (a markdown heading)';\n if (body.includes('|')) return '| (a markdown table)';\n return '';\n }\n\n /**\n * The refusal when the description is not the compact gated body — almost always an OLD PR posted\n * before the surface swap, whose description is still the full dashboard.\n */\n private descriptionUnfitForGitLog(ref: PrIdentity, marker: string): InformAiError {\n return new InformAiError(\n '\\n' + SEP + `❌ PR #${ref.number}'s description is not a git-log commit body\\n` + SEP + '\\n' +\n `It contains ${marker}, which the compact body rendered by \\`pnpm wp-finish-upsert-pr\\` never\\n` +\n 'does — so these bytes are not the gated summary, and landing them would put a PR Gate\\n' +\n 'Dashboard (or hand-written markdown) into main\\'s history permanently.\\n\\n' +\n 'The usual cause is a PR posted by a webpieces release OLDER than the one that made the\\n' +\n 'description the commit body. The dashboard now lives in the PR\\'s 1st comment instead.\\n\\n' +\n 'Re-run finish — it re-renders the description in the compact form and re-posts it to this\\n' +\n 'same PR, then landing works:\\n' +\n ' pnpm wp-start-upsert-pr && pnpm wp-review-upsert-pr && pnpm wp-finish-upsert-pr\\n' + SEP,\n );\n }\n\n /**\n * The landed branch's post-merge bookkeeping — but ONLY when this tree's `<branch>` IS the commit\n * that was squashed.\n *\n * The fact being tested is \"does this working tree hold the objects the PR merged\", and the PR\n * answers it authoritatively: `headRefOid` is the tip GitHub squashed. A second clone's `<branch>`\n * is a different commit, so `archive/<date>/<branch>` there would tag the wrong tip under the right\n * name; `merge-info/staged/<feature>` lives in the posting tree's state, not ours; and the worktree\n * to reap is not this one. So the merge stands and the bookkeeping is declined ALOUD.\n *\n * This used to be read out of an `origin.json` sidecar recording which tree posted the PR. Comparing\n * SHAs is strictly better: it is a fact rather than a recorded claim, it needs no stored state, and\n * it is more precise in both directions — a second clone sitting on the SAME commit can safely\n * archive it, and a tree that has committed further work since finish ran correctly declines.\n */\n private bookkeeping(repoRoot: string, base: string, ref: PrIdentity, retention: string): string {\n const local = this.revParse(repoRoot, base);\n if (ref.headRefOid !== '' && local !== '' && local !== ref.headRefOid) {\n return this.notTheLandedTipNotice(base, local, ref);\n }\n return this.archiveAndPromote(repoRoot, base, ref, retention) + this.nextStep(repoRoot, base);\n }\n\n /** What was skipped, why, and what the two SHAs are — so the reader can tell WHICH cause it was. */\n private notTheLandedTipNotice(base: string, local: string, ref: PrIdentity): string {\n return '\\n ⚠️ Archive + worktree cleanup SKIPPED — this tree\\'s branch is not the commit that landed:\\n' +\n ` ${base} here → ${local}\\n` +\n ` PR #${ref.number} squashed → ${ref.headRefOid}\\n` +\n ' Archiving from here would tag the wrong objects under the right name, and the\\n' +\n ` merge-info record and (if any) the worktree holding ${base} live with the other tip.\\n` +\n ' Either this is a second clone of the repo — finish the bookkeeping in the tree that\\n' +\n ' posted the PR with `pnpm wp-cleanup` — or this tree has commits made after\\n' +\n ' `pnpm wp-finish-upsert-pr` ran, which the PR does not contain.\\n';\n }\n\n /**\n * What happens next — which is NOT the same act when you landed from a worktree.\n *\n * Landing from a linked worktree always used to leave a corpse: the merged branch is checked out\n * here, so `git branch -D` refuses, `wp-cleanup`'s branch pass spares it as in-use, and nothing\n * removed the worktree — so the pair sat there until branch-creation-guard hit its cap. #512 made\n * worktrees reapable and printed `cd <primary> && pnpm wp-cleanup`; that instruction was correct\n * and it was the most-skipped step in the flow, because the PR is already landed and the work\n * feels done.\n *\n * So the reap is no longer an instruction. This command still refuses to remove the directory it\n * is standing in — that rail is untouched — and instead HANDS THE REAP to a child process rooted\n * in the primary clone, which is a tree nobody is deleting. See LandedWorktreeReaper. When that\n * hand-off is not safely achievable the #512 notice is printed unchanged, because an honest\n * limitation beats a command that deletes its own working directory mid-run.\n */\n private nextStep(repoRoot: string, base: string): string {\n const handoff: WorktreeReapHandoff | null = this.landedWorktree.plan(repoRoot, base);\n if (handoff === null) return ' Next: `pnpm wp-cleanup` to delete the merged branch.\\n';\n if (!handoff.canReap) return this.landedWorktree.manualNotice(handoff);\n return this.landedWorktree.handOff(handoff);\n }\n\n /**\n * The landed branch's post-merge bookkeeping, in one place:\n * 1. ARCHIVE the pre-squash tip as `archive/<date>/<branch>` — the tag makes the original history\n * permanently restorable (`git checkout -b <branch> <tag>` gives back the exact objects) while\n * costing one ref, so the branch itself no longer has to survive as a `*PreMerge` husk that\n * counts toward the branch cap. See BranchArchiver for why a tag beats a patch or the reflog.\n * 2. PROMOTE `merge-info/staged/<feature>/` to `merge-info/merged/<feature>/` and rebuild\n * `index.json`, so `staged/` holds only branches that are still in flight.\n *\n * Never throws: the PR is already merged by the time we get here, and failing the command after a\n * successful merge would report a landed PR as a failure. Problems are reported in the recap.\n */\n private archiveAndPromote(repoRoot: string, base: string, ref: PrIdentity, retention: string): string {\n if (retention === BRANCH_RETENTION_KEEP) return ' Branch retention is \"keep\" — nothing archived.\\n';\n\n let line = '';\n let tag = '';\n if (retention === BRANCH_RETENTION_ARCHIVE_TAG) {\n const archive = this.archiver.archive(repoRoot, base);\n tag = archive.tag;\n line = archive.ok\n ? ` Archived ${base} → ${archive.tag} (restore: ${this.archiver.restoreCommand(base, archive.tag)})\\n`\n : ` ⚠️ Could not archive ${base}: ${archive.error} — the branch was left alone.\\n`;\n }\n\n const feature = this.aiBranchName.getFeatureName();\n const record = new ArchiveRecord(\n tag, this.revParse(repoRoot, base), this.revParse(repoRoot, 'origin/main'),\n Number(ref.number), new Date().toISOString(),\n );\n if (this.mergeInfoIndex.promoteToMerged(repoRoot, feature, record)) {\n line += ` merge-info: staged/${feature} → merged/${feature} (index.json rebuilt)\\n`;\n }\n return line;\n }\n\n // Best-effort sha of a ref — '' when it cannot resolve. Recorded in archive.json for provenance.\n private revParse(repoRoot: string, ref: string): string {\n const result = spawnSync('git', ['rev-parse', ref], { cwd: repoRoot, encoding: 'utf8' });\n return result.status === 0 ? (result.stdout ?? '').trim() : '';\n }\n\n /**\n * Everything landing needs about the PR, in ONE `gh` read: number, title, URL, DESCRIPTION and the\n * head sha GitHub is squashing. `null` when there is no open PR for this head branch.\n *\n * The TITLE comes from the PR itself, not review.json, so the squash subject matches what a reviewer\n * approved even if review.json was edited afterwards. The BODY is the gated commit body (see the\n * class doc). `headRefOid` is read BEFORE the merge, because it is what the bookkeeping check\n * compares against and the merge is what makes the branch's fate uninteresting to GitHub.\n */\n private readPr(baseBranch: string): PrIdentity | null {\n const result = spawnSync(\n 'gh', ['pr', 'view', baseBranch, '--json', 'number,title,url,body,headRefOid'],\n { encoding: 'utf8' },\n );\n if (result.status !== 0) return null;\n return this.parsePr(result.stdout ?? '');\n }\n\n // `gh --json` output → PrIdentity. Malformed/absent JSON reads as \"no PR\", which the caller turns\n // into the same refusal a missing PR gets: both mean nothing here can be landed.\n private parsePr(stdout: string): PrIdentity | null {\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: unparseable gh output means \"no PR\", never a crash\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n // webpieces-disable no-any-unknown -- gh's JSON is opaque until narrowed field-by-field below\n const raw = JSON.parse(stdout) as Record<string, unknown>;\n const number = typeof raw['number'] === 'number' ? String(raw['number']) : this.str(raw['number']);\n if (number === '') return null;\n // GitHub stores descriptions with CRLF. A commit body must not carry them, and the bytes are\n // otherwise identical to what finish rendered, so the line endings are normalized back here.\n const body = this.str(raw['body']).replace(/\\r\\n/g, '\\n').trim();\n return new PrIdentity(\n number, this.str(raw['title']), this.str(raw['url']), body === '' ? '' : body + '\\n',\n this.str(raw['headRefOid']),\n );\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return null;\n }\n }\n\n // webpieces-disable no-any-unknown -- one opaque JSON field, narrowed to string\n private str(value: unknown): string {\n return typeof value === 'string' ? value : '';\n }\n}\n\n// The open PR as GitHub holds it: its number, title, web URL, DESCRIPTION (= the gated commit body) and\n// the head commit being squashed.\nclass PrIdentity {\n number: string;\n title: string;\n url: string;\n body: string;\n headRefOid: string;\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(number: string, title: string, url: string, body: string, headRefOid: string) {\n this.number = number;\n this.title = title;\n this.url = url;\n this.body = body;\n this.headRefOid = headRefOid;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"land-pr-command.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/commands/land-pr-command.ts"],"names":[],"mappings":"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","sourcesContent":["import { execSync, spawnSync } from 'child_process';\nimport {\n InformAiError, RepoRootFinder, MERGE_MODE_AUTO, loadAndValidate,\n BranchArchiver, BRANCH_RETENTION_ARCHIVE_TAG, BRANCH_RETENTION_KEEP, toError,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { AiBranchName } from '../workflow/git-readAiBranchName';\nimport { BranchNaming } from '../workflow/branch-naming';\nimport { LandedWorktreeReaper, WorktreeReapHandoff } from '../workflow/landed-worktree-reaper';\nimport { MergeBodyTempFile } from '../workflow/merge-body-temp-file';\nimport { ArchiveRecord, MergeInfoIndex } from '../workflow/merge-info-index';\nimport { MergeIntent, PrMerger } from '../workflow/pr-merger';\n\nconst SEP = '━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\\n';\n\n/**\n * `wp-land-pr`: squash-merge THIS branch's already-posted PR into main with the compact commit body.\n *\n * It exists because a merge clicked in the GitHub UI cannot set the squash SUBJECT to `<title> (#N)`\n * and cannot archive the pre-squash tip. So when a merge has to happen outside `wp-finish-upsert-pr`\n * (a mergeMode=NONE repo, or a PR whose checks were still running when finish ran), this is the command\n * that keeps main's history consistent and finishes the branch's bookkeeping.\n *\n * ─── Where the bytes come from: THE PR ITSELF ──────────────────────────────────────────────────────\n * `wp-finish-upsert-pr` renders ONE string and publishes it as the PR DESCRIPTION, and the full\n * dashboard lives in the PR's comments (see `pr-body-is-merge-body.spec.ts`, which pins that the\n * description and the merge body are one renderer's output). So the description IS the gated squash\n * body, held by GitHub, and landing reads it back with `gh pr view --json body`.\n *\n * THE BYTES THAT LAND ARE THE BYTES FINISH PRODUCED — that invariant is unchanged; what changed is that\n * GitHub, not this machine, is the thing holding them. That removes the whole machine-global receipt\n * store: a cache of a fact the remote already owns, which could only ever be missing, stale, or on the\n * wrong computer. Landing from a second clone, a fresh clone, or another machine entirely now just\n * works. See `decisions/0005-the-pr-description-is-the-merge-body.md`.\n *\n * It still deliberately does NOT re-render the body: re-deriving it at land time would be a second\n * authoritative gate whose result nobody reads, and it could silently disagree with what was reviewed.\n *\n * ─── Two scopes, and only one of them is the tree's ────────────────────────────────────────────────\n * 1. THE MERGE, which belongs to the PR — and is therefore reachable from anywhere.\n * 2. THE BOOKKEEPING — archiving the pre-squash tip as `archive/<date>/<branch>`, promoting merge-info,\n * and reaping the landed worktree. That half belongs to the tree whose `<branch>` really is the\n * commit being squashed, and it must NOT be attempted from anywhere else: another clone's\n * `<branch>` is a different commit, so archiving it there would tag the wrong objects under the\n * right name. {@link LandPrCommand.bookkeeping} tests that by comparing this tree's `<branch>`\n * against the PR's own `headRefOid`, and when they disagree the merge still happens and the\n * bookkeeping is SKIPPED OUT LOUD.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class LandPrCommand {\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(\n private readonly repoRootFinder: RepoRootFinder,\n private readonly aiBranchName: AiBranchName,\n private readonly branchNaming: BranchNaming,\n private readonly prMerger: PrMerger,\n private readonly archiver: BranchArchiver,\n private readonly mergeInfoIndex: MergeInfoIndex,\n private readonly landedWorktree: LandedWorktreeReaper,\n private readonly bodyFile: MergeBodyTempFile,\n ) {}\n\n async run(): Promise<void> {\n const repoRoot = this.repoRootFinder.resolveRepoRoot(process.cwd());\n const base = this.branchNaming.baseBranchName(execSync('git branch --show-current', { encoding: 'utf8' }).trim());\n\n const ref = this.readPr(base);\n if (ref === null) {\n throw new InformAiError(\n '\\n' + SEP + '❌ No open PR found for this branch\\n' + SEP + '\\n' +\n `No open PR has head branch \"${base}\". Nothing to land.\\n` +\n 'If the PR is already merged, run `pnpm wp-cleanup`.\\n' +\n 'If it was never posted, run the gated flow — it posts the PR AND writes the description\\n' +\n 'that becomes this commit body:\\n' +\n ' pnpm wp-start-upsert-pr && pnpm wp-review-upsert-pr && pnpm wp-finish-upsert-pr\\n' + SEP,\n );\n }\n if (ref.body === '') throw this.emptyDescription(ref);\n const unfit = this.notFitForGitLog(ref.body);\n if (unfit !== '') throw this.descriptionUnfitForGitLog(ref, unfit);\n\n process.stdout.write('\\n' + SEP + `🚀 Landing PR #${ref.number}\\n` + SEP + '\\n');\n const mergeBodyFile = this.bodyFile.write(ref.body);\n\n const config = loadAndValidate(repoRoot).prGate;\n const policy = config.mergeMode;\n // Reuse the SAME merge logic wp-finish-upsert-pr uses, so a PR lands identically whichever\n // command lands it — including the auto-merge fallback when the checks are still running.\n //\n // `commanded: true` — running THIS command IS the intent to merge, so it is not gated on\n // pr-gate.mergeMode (a NONE repo runs this precisely to land one PR by hand). The real `policy`\n // travels alongside it rather than being replaced by a literal AUTO, so PrMerger's diagnostics\n // never assert a config value nobody set — that is exactly what used to print a CONFIG MISMATCH\n // about `mergeMode: AUTO` two lines above this command printing `mergeMode is NONE`.\n const outcome = this.prMerger.merge(\n base, `${ref.title} (#${ref.number})`, mergeBodyFile, new MergeIntent(policy ?? '', true),\n );\n\n const bookkeeping = outcome.merged\n ? this.bookkeeping(repoRoot, base, ref, config.landPr.branchRetention)\n : '';\n process.stdout.write(\n '\\n' + SEP + (outcome.merged ? '✅ Landed\\n' : 'ℹ️ Not landed yet\\n') + SEP + '\\n' +\n ` ${outcome.message}\\n` +\n bookkeeping +\n (policy === MERGE_MODE_AUTO\n ? ''\n : ` (pr-gate.mergeMode is ${policy} — wp-finish-upsert-pr will keep leaving PRs for a human.)\\n`) +\n '\\n',\n );\n }\n\n /**\n * The refusal when the PR exists but its description is empty.\n *\n * This is the ONE remaining \"no gated body\" case, and it is now a property of the PR rather than of\n * this machine — which is why the cure is a single command with no human judgement call attached.\n * There is deliberately no `--fallback-title-only` escape any more: that flag existed because the\n * bytes could be on a DIFFERENT COMPUTER from the one landing, which cannot happen when GitHub holds\n * them. Re-running finish is always available, always correct, and lands the reviewed bytes.\n */\n private emptyDescription(ref: PrIdentity): InformAiError {\n return new InformAiError(\n '\\n' + SEP + `❌ PR #${ref.number} has an EMPTY description\\n` + SEP + '\\n' +\n 'The PR description IS the squash-commit body — `pnpm wp-finish-upsert-pr` renders one string\\n' +\n 'and publishes it as the description, and landing reads it straight back with\\n' +\n '`gh pr view --json body`. An empty description means the gated flow never published one\\n' +\n '(or a human cleared it), so there is nothing reviewed to land.\\n\\n' +\n 'Re-run the gated flow — it re-renders the description and re-posts it to this same PR:\\n' +\n ' pnpm wp-start-upsert-pr # update from main (3-point merge). No push, no build gate.\\n' +\n ' pnpm wp-review-upsert-pr # validate the merge, build gate, extract the diff, brief reviewers\\n' +\n ' # write review.json at the path wp-review-upsert-pr prints\\n' +\n ' pnpm wp-finish-upsert-pr # build gate, dashboard, create/update the PR\\n' +\n 'Then re-run `pnpm wp-land-pr`.\\n' + SEP,\n );\n }\n\n /**\n * '' when the description is a compact gated body; otherwise the marker that proves it is not.\n *\n * This reads the SAME invariant `pr-body-is-merge-body.spec.ts` asserts from the other end: the\n * compact body \"contains nothing a plain-text git log cannot carry\" — no markdown heading, no table\n * pipe. It is therefore not a heuristic about what a dashboard looks like; it is the renderer's own\n * pinned property, checked against bytes that arrived from outside this process.\n *\n * WHY IT IS NEEDED, and why it is not a compatibility fallback: a PR posted by a release OLDER than\n * the surface swap still has the FULL DASHBOARD as its description. Measured on this repo,\n * 2026-08-07 — PR #613 (posted after the swap) has a description byte-identical to its squash body,\n * while PR #614 (still open, posted minutes before) begins `## 🚦 PR Gate Dashboard`. Landing that\n * one would dump a risk table into main, which is precisely the defect\n * `decisions/0004` § 4.1 warned about. So this REFUSES and names the one command that fixes it. It\n * never falls back, never rewrites, and never reaches for a second source of bytes.\n *\n * It keeps earning its place after the transition: nothing stops a human editing a PR description\n * in GitHub's textarea, and this is the only point between that edit and main's history.\n *\n * What it must NOT be read as is proof that the bytes did not come from `renderPrBody`. They can:\n * the compact body interpolates author text, and a `|` reaches that text from a TypeScript union, a\n * regex alternation or a quoted shell pipeline. `Dashboard.gitLogSafe` substitutes both markers at\n * the render exit so a freshly-rendered body no longer trips this — which is where that invariant\n * belongs, since the renderer is the only thing that can fix it without an extra CI cycle.\n */\n private notFitForGitLog(body: string): string {\n if (body.includes('##')) return '## (a markdown heading)';\n if (body.includes('|')) return '| (a markdown table)';\n return '';\n }\n\n /**\n * The refusal when the description is not the compact gated body.\n *\n * It names BOTH ways bytes get here, because naming only one sent readers down a path that did not\n * exist. It used to assert an old release as \"the usual cause\" and prescribe re-running finish — and\n * when the marker had come from AUTHOR TEXT instead (a `|` in a summary, from a TypeScript union or a\n * regex alternation), finish re-rendered the identical character from the unchanged `review.json` and\n * landing refused again: a loop costing a CI cycle per turn, escapable only by guessing that one\n * character in your prose was the problem. `Dashboard.gitLogSafe` now substitutes both markers at the\n * render exit, so a freshly-rendered body cannot reach here at all — which leaves a HAND-EDITED\n * description and a genuinely old PR as the two remaining causes, and this says so.\n */\n private descriptionUnfitForGitLog(ref: PrIdentity, marker: string): InformAiError {\n return new InformAiError(\n '\\n' + SEP + `❌ PR #${ref.number}'s description is not a git-log commit body\\n` + SEP + '\\n' +\n `It contains ${marker}, which the compact body rendered by \\`pnpm wp-finish-upsert-pr\\` never\\n` +\n 'does — it substitutes both markers as it renders — so these bytes are not the gated summary,\\n' +\n 'and landing them would put a PR Gate Dashboard (or hand-written markdown) into main\\'s\\n' +\n 'history permanently.\\n\\n' +\n 'There are two causes, and they need different fixes:\\n\\n' +\n ' 1. The description was EDITED BY HAND on GitHub after finish posted it. Re-running finish\\n' +\n ' overwrites that edit with the compact body, so landing works.\\n' +\n ' 2. The PR was posted by a webpieces release OLDER than the one that made the description\\n' +\n ' the commit body, so it is still the full dashboard. The dashboard now lives in the PR\\'s\\n' +\n ' 1st comment instead, and re-running finish moves it there.\\n\\n' +\n `Either way the cure is the same, and it does NOT require removing ${marker.trim()} from your review\\n` +\n 'text — a pipe or a heading in a summary is rendered safe, not rejected:\\n' +\n ' pnpm wp-start-upsert-pr && pnpm wp-review-upsert-pr && pnpm wp-finish-upsert-pr\\n' + SEP,\n );\n }\n\n /**\n * The landed branch's post-merge bookkeeping — but ONLY when this tree's `<branch>` IS the commit\n * that was squashed.\n *\n * The fact being tested is \"does this working tree hold the objects the PR merged\", and the PR\n * answers it authoritatively: `headRefOid` is the tip GitHub squashed. A second clone's `<branch>`\n * is a different commit, so `archive/<date>/<branch>` there would tag the wrong tip under the right\n * name; `merge-info/staged/<feature>` lives in the posting tree's state, not ours; and the worktree\n * to reap is not this one. So the merge stands and the bookkeeping is declined ALOUD.\n *\n * This used to be read out of an `origin.json` sidecar recording which tree posted the PR. Comparing\n * SHAs is strictly better: it is a fact rather than a recorded claim, it needs no stored state, and\n * it is more precise in both directions — a second clone sitting on the SAME commit can safely\n * archive it, and a tree that has committed further work since finish ran correctly declines.\n */\n private bookkeeping(repoRoot: string, base: string, ref: PrIdentity, retention: string): string {\n const local = this.revParse(repoRoot, base);\n if (ref.headRefOid !== '' && local !== '' && local !== ref.headRefOid) {\n return this.notTheLandedTipNotice(base, local, ref);\n }\n return this.archiveAndPromote(repoRoot, base, ref, retention) + this.nextStep(repoRoot, base);\n }\n\n /** What was skipped, why, and what the two SHAs are — so the reader can tell WHICH cause it was. */\n private notTheLandedTipNotice(base: string, local: string, ref: PrIdentity): string {\n return '\\n ⚠️ Archive + worktree cleanup SKIPPED — this tree\\'s branch is not the commit that landed:\\n' +\n ` ${base} here → ${local}\\n` +\n ` PR #${ref.number} squashed → ${ref.headRefOid}\\n` +\n ' Archiving from here would tag the wrong objects under the right name, and the\\n' +\n ` merge-info record and (if any) the worktree holding ${base} live with the other tip.\\n` +\n ' Either this is a second clone of the repo — finish the bookkeeping in the tree that\\n' +\n ' posted the PR with `pnpm wp-cleanup` — or this tree has commits made after\\n' +\n ' `pnpm wp-finish-upsert-pr` ran, which the PR does not contain.\\n';\n }\n\n /**\n * What happens next — which is NOT the same act when you landed from a worktree.\n *\n * Landing from a linked worktree always used to leave a corpse: the merged branch is checked out\n * here, so `git branch -D` refuses, `wp-cleanup`'s branch pass spares it as in-use, and nothing\n * removed the worktree — so the pair sat there until branch-creation-guard hit its cap. #512 made\n * worktrees reapable and printed `cd <primary> && pnpm wp-cleanup`; that instruction was correct\n * and it was the most-skipped step in the flow, because the PR is already landed and the work\n * feels done.\n *\n * So the reap is no longer an instruction. This command still refuses to remove the directory it\n * is standing in — that rail is untouched — and instead HANDS THE REAP to a child process rooted\n * in the primary clone, which is a tree nobody is deleting. See LandedWorktreeReaper. When that\n * hand-off is not safely achievable the #512 notice is printed unchanged, because an honest\n * limitation beats a command that deletes its own working directory mid-run.\n */\n private nextStep(repoRoot: string, base: string): string {\n const handoff: WorktreeReapHandoff | null = this.landedWorktree.plan(repoRoot, base);\n if (handoff === null) return ' Next: `pnpm wp-cleanup` to delete the merged branch.\\n';\n if (!handoff.canReap) return this.landedWorktree.manualNotice(handoff);\n return this.landedWorktree.handOff(handoff);\n }\n\n /**\n * The landed branch's post-merge bookkeeping, in one place:\n * 1. ARCHIVE the pre-squash tip as `archive/<date>/<branch>` — the tag makes the original history\n * permanently restorable (`git checkout -b <branch> <tag>` gives back the exact objects) while\n * costing one ref, so the branch itself no longer has to survive as a `*PreMerge` husk that\n * counts toward the branch cap. See BranchArchiver for why a tag beats a patch or the reflog.\n * 2. PROMOTE `merge-info/staged/<feature>/` to `merge-info/merged/<feature>/` and rebuild\n * `index.json`, so `staged/` holds only branches that are still in flight.\n *\n * Never throws: the PR is already merged by the time we get here, and failing the command after a\n * successful merge would report a landed PR as a failure. Problems are reported in the recap.\n */\n private archiveAndPromote(repoRoot: string, base: string, ref: PrIdentity, retention: string): string {\n if (retention === BRANCH_RETENTION_KEEP) return ' Branch retention is \"keep\" — nothing archived.\\n';\n\n let line = '';\n let tag = '';\n if (retention === BRANCH_RETENTION_ARCHIVE_TAG) {\n const archive = this.archiver.archive(repoRoot, base);\n tag = archive.tag;\n line = archive.ok\n ? ` Archived ${base} → ${archive.tag} (restore: ${this.archiver.restoreCommand(base, archive.tag)})\\n`\n : ` ⚠️ Could not archive ${base}: ${archive.error} — the branch was left alone.\\n`;\n }\n\n const feature = this.aiBranchName.getFeatureName();\n const record = new ArchiveRecord(\n tag, this.revParse(repoRoot, base), this.revParse(repoRoot, 'origin/main'),\n Number(ref.number), new Date().toISOString(),\n );\n if (this.mergeInfoIndex.promoteToMerged(repoRoot, feature, record)) {\n line += ` merge-info: staged/${feature} → merged/${feature} (index.json rebuilt)\\n`;\n }\n return line;\n }\n\n // Best-effort sha of a ref — '' when it cannot resolve. Recorded in archive.json for provenance.\n private revParse(repoRoot: string, ref: string): string {\n const result = spawnSync('git', ['rev-parse', ref], { cwd: repoRoot, encoding: 'utf8' });\n return result.status === 0 ? (result.stdout ?? '').trim() : '';\n }\n\n /**\n * Everything landing needs about the PR, in ONE `gh` read: number, title, URL, DESCRIPTION and the\n * head sha GitHub is squashing. `null` when there is no open PR for this head branch.\n *\n * The TITLE comes from the PR itself, not review.json, so the squash subject matches what a reviewer\n * approved even if review.json was edited afterwards. The BODY is the gated commit body (see the\n * class doc). `headRefOid` is read BEFORE the merge, because it is what the bookkeeping check\n * compares against and the merge is what makes the branch's fate uninteresting to GitHub.\n */\n private readPr(baseBranch: string): PrIdentity | null {\n const result = spawnSync(\n 'gh', ['pr', 'view', baseBranch, '--json', 'number,title,url,body,headRefOid'],\n { encoding: 'utf8' },\n );\n if (result.status !== 0) return null;\n return this.parsePr(result.stdout ?? '');\n }\n\n // `gh --json` output → PrIdentity. Malformed/absent JSON reads as \"no PR\", which the caller turns\n // into the same refusal a missing PR gets: both mean nothing here can be landed.\n private parsePr(stdout: string): PrIdentity | null {\n // webpieces-disable no-unmanaged-exceptions -- chokepoint: unparseable gh output means \"no PR\", never a crash\n // eslint-disable-next-line @webpieces/no-unmanaged-exceptions\n try {\n // webpieces-disable no-any-unknown -- gh's JSON is opaque until narrowed field-by-field below\n const raw = JSON.parse(stdout) as Record<string, unknown>;\n const number = typeof raw['number'] === 'number' ? String(raw['number']) : this.str(raw['number']);\n if (number === '') return null;\n // GitHub stores descriptions with CRLF. A commit body must not carry them, and the bytes are\n // otherwise identical to what finish rendered, so the line endings are normalized back here.\n const body = this.str(raw['body']).replace(/\\r\\n/g, '\\n').trim();\n return new PrIdentity(\n number, this.str(raw['title']), this.str(raw['url']), body === '' ? '' : body + '\\n',\n this.str(raw['headRefOid']),\n );\n } catch (err: unknown) {\n const error = toError(err);\n void error;\n return null;\n }\n }\n\n // webpieces-disable no-any-unknown -- one opaque JSON field, narrowed to string\n private str(value: unknown): string {\n return typeof value === 'string' ? value : '';\n }\n}\n\n// The open PR as GitHub holds it: its number, title, web URL, DESCRIPTION (= the gated commit body) and\n// the head commit being squashed.\nclass PrIdentity {\n number: string;\n title: string;\n url: string;\n body: string;\n headRefOid: string;\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(number: string, title: string, url: string, body: string, headRefOid: string) {\n this.number = number;\n this.title = title;\n this.url = url;\n this.body = body;\n this.headRefOid = headRefOid;\n }\n}\n"]}
|