@webpieces/pr-gate 0.4.593 → 0.4.595
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/checklist-comment-renderer.d.ts +78 -0
- package/src/dashboard/checklist-comment-renderer.js +272 -0
- package/src/dashboard/checklist-comment-renderer.js.map +1 -0
- package/src/dashboard/dashboard.d.ts +66 -52
- package/src/dashboard/dashboard.js +105 -262
- package/src/dashboard/dashboard.js.map +1 -1
- package/src/index.d.ts +3 -1
- package/src/index.js +11 -1
- package/src/index.js.map +1 -1
- package/src/scripts/commands/finish-upsert-pr-command.d.ts +53 -34
- package/src/scripts/commands/finish-upsert-pr-command.js +181 -160
- package/src/scripts/commands/finish-upsert-pr-command.js.map +1 -1
- package/src/scripts/workflow/finish-banner.js +7 -11
- package/src/scripts/workflow/finish-banner.js.map +1 -1
- package/src/scripts/workflow/pr-comment-upserter.d.ts +54 -0
- package/src/scripts/workflow/pr-comment-upserter.js +100 -0
- package/src/scripts/workflow/pr-comment-upserter.js.map +1 -0
- package/src/scripts/workflow/pr-merger.js +13 -8
- package/src/scripts/workflow/pr-merger.js.map +1 -1
- package/src/scripts/workflow/provenance-enforcer.d.ts +68 -0
- package/src/scripts/workflow/provenance-enforcer.js +169 -0
- package/src/scripts/workflow/provenance-enforcer.js.map +1 -0
|
@@ -180,15 +180,20 @@ let PrMerger = class PrMerger {
|
|
|
180
180
|
// posted/updated by this point, which is the whole job in this mode.
|
|
181
181
|
if (!intent.shouldMerge()) {
|
|
182
182
|
const mergeMode = intent.mergeMode;
|
|
183
|
-
//
|
|
184
|
-
//
|
|
185
|
-
//
|
|
186
|
-
//
|
|
183
|
+
// What a UI merge produces, in terms of the repo's OWN two settings.
|
|
184
|
+
//
|
|
185
|
+
// This message said the opposite one release ago: "a UI merge pastes the whole dashboard into
|
|
186
|
+
// git log, land it with wp-land-pr instead". That was true while the PR description held the
|
|
187
|
+
// long dashboard. It is now the compact git-log body (see Dashboard.renderPrBody), so
|
|
188
|
+
// `squash_merge_commit_message: PR_BODY` copies exactly the right text and a UI merge is
|
|
189
|
+
// CORRECT — provided both settings are right. Naming them is the useful thing to say; telling
|
|
190
|
+
// a mergeMode=NONE repo to stop using the UI is not, since using it is the whole policy.
|
|
187
191
|
return new MergeOutcome(false, false, `did NOT merge — pr-gate.mergeMode is ${mergeMode === rules_config_1.MERGE_MODE_NONE ? 'NONE' : `"${mergeMode}"`}, so the PR is left for a human to merge.\n` +
|
|
188
|
-
` A UI merge
|
|
189
|
-
`
|
|
190
|
-
`
|
|
191
|
-
`
|
|
192
|
+
` A UI merge takes the commit message from this repo's own settings, and both must be\n` +
|
|
193
|
+
` set for main's history to read well: squash_merge_commit_title=PR_TITLE (else the\n` +
|
|
194
|
+
` subject is not "${subject}") and squash_merge_commit_message=PR_BODY (which copies\n` +
|
|
195
|
+
` the gated description verbatim — that IS the commit body this flow rendered).\n` +
|
|
196
|
+
` \`pnpm wp-land-pr\` lands the same bytes from the CLI if you would rather not click.`, exports.MERGE_RESULT_LEFT_TO_HUMAN);
|
|
192
197
|
}
|
|
193
198
|
// A direct `gh pr merge --squash --subject --body-file` writes exactly this subject/body to
|
|
194
199
|
// main's history regardless of the repo's squash_merge_commit_title/message defaults — and
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pr-merger.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/pr-merger.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawnSync } from 'child_process';\nimport { MERGE_MODE_AUTO, MERGE_MODE_NONE } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\n\n/**\n * The five terminal shapes a merge attempt can take. The CALLER branches on these — a `message` string\n * is for humans to read, never for code to pattern-match, and the banner that frames it must be able to\n * tell \"done\" from \"stranded\" without parsing prose.\n */\nexport const MERGE_RESULT_MERGED = 'MERGED';\n/** Not merged YET, but queued: auto-merge is enabled and lands it when the checks pass. Self-healing. */\nexport const MERGE_RESULT_AUTO_QUEUED = 'AUTO_QUEUED';\n/** Not merged BY DESIGN: pr-gate.mergeMode is not AUTO, so a person merges. Nothing is owed here. */\nexport const MERGE_RESULT_LEFT_TO_HUMAN = 'LEFT_TO_HUMAN';\n/**\n * Not merged and it NEVER will be without action: `gh pr view --json mergeStateStatus` says BEHIND, i.e.\n * the head branch is out of date with base. Unlike BLOCKED (waiting on checks — self-healing), BEHIND\n * cannot resolve itself; the branch must be re-synced from main. This is the case that used to print a\n * green \"✅ PR finished\" and get abandoned.\n */\nexport const MERGE_RESULT_BEHIND = 'BEHIND';\n/**\n * BEHIND *and* GitHub says the tree merges cleanly (`mergeable: MERGEABLE`). Nobody touched the same\n * lines — somebody simply landed on main first. One clean re-run of ①②③ converges. Split out from the\n * conflicting case because the two need completely different sentences: this one is not the author's\n * problem to solve, it is a queue collision, and telling them \"resolve the conflict\" is a lie.\n */\nexport const MERGE_RESULT_BEHIND_CLEAN = 'BEHIND_CLEAN';\n/**\n * BEHIND *and* `mergeable: CONFLICTING` — the landed work and this branch touch the same lines. Real\n * human/AI judgement is owed, and if others keep landing first it genuinely repeats. That is inherent to\n * concurrent editing, not a defect, and the banner says so rather than pretending a re-run is free.\n */\nexport const MERGE_RESULT_BEHIND_CONFLICTING = 'BEHIND_CONFLICTING';\n/**\n * BEHIND but `mergeable: UNKNOWN` (or unreadable). GitHub computes mergeability ASYNCHRONOUSLY and we ask\n * moments after a force-push, so UNKNOWN is the EXPECTED answer, not an error. Diagnosing from it would be\n * guessing, so this outcome asks for a re-check instead of prescribing a remedy.\n */\nexport const MERGE_RESULT_BEHIND_UNKNOWN = 'BEHIND_UNKNOWN';\n/** Not merged for some other reason (config mismatch, gh error, no PR at all). Read `message`. */\nexport const MERGE_RESULT_FAILED = 'FAILED';\n\n// The `mergeStateStatus` value GitHub reports for \"head branch is not up to date with the base branch\".\nconst GH_STATE_BEHIND = 'BEHIND';\n// `mergeable` values. GitHub returns UNKNOWN while it is still computing the merge — which is most of the\n// time in the seconds after a push — so UNKNOWN must never be read as \"no conflicts\".\nconst GH_MERGEABLE_CLEAN = 'MERGEABLE';\nconst GH_MERGEABLE_CONFLICTING = 'CONFLICTING';\n\n// What actually happened when we tried to land the squash merge. `message` is printed VERBATIM in the\n// final wp-finish-upsert-pr summary, so a merge that did not happen can never be reported as done —\n// the old code ignored `spawnSync().status` entirely and printed \"✅ PR finished\" even when `gh pr\n// merge` had errored out, which is what hid the auto-merge-disabled failure for weeks. `result` is the\n// machine-readable half of the same honesty: the banner picks its HEADER from it, so the frame around\n// the message can no longer say \"finished\" while the message says \"did NOT merge\".\nexport class MergeOutcome {\n merged: boolean;\n autoMergeEnabled: boolean;\n message: string;\n result: string;\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(merged: boolean, autoMergeEnabled: boolean, message: string, result: string) {\n this.merged = merged;\n this.autoMergeEnabled = autoMergeEnabled;\n this.message = message;\n this.result = result;\n }\n\n // TRUE for every flavour of \"the branch is out of date with main\", so the banner can ask that one\n // question once instead of listing four constants at each branch point. Kept here rather than as a\n // module function because `no-function-outside-class` forbids the latter — and PrMergeState below\n // already sets the precedent that a verdict class answers questions about itself.\n isBehind(): boolean {\n return this.result === MERGE_RESULT_BEHIND\n || this.result === MERGE_RESULT_BEHIND_CLEAN\n || this.result === MERGE_RESULT_BEHIND_CONFLICTING\n || this.result === MERGE_RESULT_BEHIND_UNKNOWN;\n }\n}\n\n// GitHub's own verdict on the PR, straight from `gh pr view --json mergeable,mergeStateStatus,state`.\n// All '' when gh could not be asked — an unreadable answer must never be treated as a diagnosis.\nexport class PrMergeState {\n mergeable: string;\n mergeStateStatus: string;\n state: string;\n\n constructor(mergeable: string, mergeStateStatus: string, state: string) {\n this.mergeable = mergeable;\n this.mergeStateStatus = mergeStateStatus;\n this.state = state;\n }\n\n isBehind(): boolean {\n return this.mergeStateStatus === GH_STATE_BEHIND;\n }\n\n /**\n * WHICH kind of BEHIND this is, from the `mergeable` field we have always fetched and never read.\n *\n * This is the whole point of the split. \"Out of date\" and \"conflicting\" are different situations with\n * different costs, and collapsing them told every author the expensive story: a clean queue collision\n * — nobody touched your lines, somebody just landed first — got reported in the same alarming words as\n * a genuine textual conflict.\n *\n * UNKNOWN is its own answer, never folded into CLEAN. GitHub computes mergeability asynchronously and\n * we ask seconds after a force-push, so UNKNOWN is the ordinary reply in exactly our situation.\n * Treating it as \"no conflicts\" would promise a clean re-run we cannot see.\n */\n behindKind(): string {\n if (this.mergeable === GH_MERGEABLE_CLEAN) return MERGE_RESULT_BEHIND_CLEAN;\n if (this.mergeable === GH_MERGEABLE_CONFLICTING) return MERGE_RESULT_BEHIND_CONFLICTING;\n return MERGE_RESULT_BEHIND_UNKNOWN;\n }\n\n // One-line rendering for the failure message, or '' when GitHub could not be asked.\n describe(): string {\n if (this.mergeStateStatus === '') return '';\n return `mergeable=${this.mergeable}, mergeStateStatus=${this.mergeStateStatus}, state=${this.state}`;\n }\n}\n\n/**\n * WHY this run is allowed to merge — the config value, kept separate from the caller's intent.\n *\n * Those two used to be one `mergeMode: string` parameter, and `wp-land-pr` passed a literal\n * `MERGE_MODE_AUTO` through it to mean \"merge it\" rather than \"config says AUTO\". PrMerger could not\n * tell the two apart, so on a `mergeMode: \"NONE\"` repo it reported a CONFIG MISMATCH naming a value\n * nobody had set, and prescribed remedies that were already in place. Keeping the real `mergeMode`\n * alongside a `commanded` flag means no message ever has to assert a config value the code did not read.\n */\nexport class MergeIntent {\n /** pr-gate.mergeMode EXACTLY as read from webpieces.config.json — '' when it could not be read. */\n mergeMode: string;\n /**\n * TRUE when running the command IS the intent to merge (`wp-land-pr`), so the merge happens whatever\n * the config says. FALSE for the policy-driven caller (`wp-finish-upsert-pr`), where only an\n * explicit AUTO merges.\n */\n commanded: boolean;\n\n constructor(mergeMode: string, commanded: boolean) {\n this.mergeMode = mergeMode;\n this.commanded = commanded;\n }\n\n // The gate: a commanded run always proceeds; otherwise only an explicit AUTO does. Anything else\n // (including a repo on a published rules-config that predates the field) is treated as NONE —\n // when the policy is unreadable, do NOT touch main.\n shouldMerge(): boolean {\n return this.commanded || this.mergeMode === MERGE_MODE_AUTO;\n }\n}\n\n/**\n * Lands — or queues — the squash merge with an EXPLICIT subject/body, on BOTH kinds of repo:\n *\n * - auto-merge ALLOWED (`allow_auto_merge: true`): a PR whose checks are still running falls back to\n * the auto-merge queue, carrying the same subject/body so it lands when the checks pass.\n * - auto-merge DISALLOWED (`allow_auto_merge: false`, a deliberate policy control in many orgs): the\n * direct merge still works the moment the PR is mergeable, because `gh pr merge --squash --subject\n * --body-file` does not depend on that setting at all. When the PR is NOT yet mergeable there is no\n * queue to fall back to, so we say so loudly instead of firing a `--auto` that can only fail.\n *\n * WHICH of those a repo gets is not guessed — `pr-gate.mergeMode` is REQUIRED config. AUTO means the\n * tooling lands PRs; NONE means it only posts them and a person merges. No mode can force a queue the\n * repo has turned off, so AUTO on a repo with allow_auto_merge=false is a CONFIG error, reported as\n * one rather than papered over — but ONLY when the AUTO was read from config. A commanded merge\n * (`wp-land-pr`) on a NONE repo is not a mismatch at all; see MergeIntent.\n *\n * Every `gh` status is checked. Nothing here is allowed to fail silently.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class PrMerger {\n /**\n * @param subject the squash-commit subject, normally `<PR title> (#N)`\n * @param mergeBodyFile file holding the squash-commit body (risk/flags/PR link)\n * @param intent the real pr-gate.mergeMode PLUS whether this caller commands the merge\n * regardless of it (see MergeIntent).\n */\n merge(baseBranch: string, subject: string, mergeBodyFile: string, intent: MergeIntent): MergeOutcome {\n // Anything that is not an explicit AUTO leaves the PR alone. The PR itself is already\n // posted/updated by this point, which is the whole job in this mode.\n if (!intent.shouldMerge()) {\n const mergeMode = intent.mergeMode;\n // Name BOTH halves a UI merge substitutes. The subject warning has always been here; the\n // BODY was the half that actually mattered and went unsaid, and two consumer repos on\n // mergeMode=NONE ended up with the full dashboard in every squash commit on main because\n // nothing on this path ever mentioned `wp-land-pr`.\n return new MergeOutcome(false, false,\n `did NOT merge — pr-gate.mergeMode is ${mergeMode === MERGE_MODE_NONE ? 'NONE' : `\"${mergeMode}\"`}, so the PR is left for a human to merge.\\n` +\n ` A UI merge substitutes BOTH halves of the commit message: the subject comes from\\n` +\n ` squash_merge_commit_title (NOT: \"${subject}\") and the body from\\n` +\n ` squash_merge_commit_message, which on PR_BODY is the whole dashboard. Land it with\\n` +\n ` \\`pnpm wp-land-pr\\` instead to write the gated body.`,\n MERGE_RESULT_LEFT_TO_HUMAN);\n }\n\n // A direct `gh pr merge --squash --subject --body-file` writes exactly this subject/body to\n // main's history regardless of the repo's squash_merge_commit_title/message defaults — and\n // regardless of allow_auto_merge. It is the ONLY path that guarantees the good commit message.\n //\n // Its output is CAPTURED, not inherited: this attempt fails BY DESIGN on any repo whose policy\n // forbids a direct merge, and gh's raw `X Pull request #N is not mergeable: …` on the terminal\n // read as a hard failure to every human and agent who saw it. We reprint the reason ourselves,\n // framed as the expected first step of a two-step dance (see announceDirectFailure).\n const direct = this.ghCapture(['pr', 'merge', baseBranch, '--squash', '--subject', subject, '--body-file', mergeBodyFile]);\n if (direct.status === 0) {\n process.stdout.write(direct.output);\n return new MergeOutcome(true, false, `squash-merged the PR as: \"${subject}\"`, MERGE_RESULT_MERGED);\n }\n\n // Past here the PR is not mergeable yet. WHY matters enormously and gh's exit status does not\n // say: checks-still-running (BLOCKED) is self-healing once auto-merge is on, while out-of-date\n // (BEHIND) can NEVER land unattended. Ask GitHub itself — one cheap, failure-tolerant call, made\n // only on the failure path, so a run whose direct merge succeeded costs no extra API call.\n const state = this.prMergeState(baseBranch);\n this.announceDirectFailure(direct.output, state);\n return this.fallBackToAutoMerge(baseBranch, subject, mergeBodyFile, state, intent);\n }\n\n /**\n * The auto-merge queue: the only way to still land a not-yet-mergeable PR unattended. Returns the\n * BEHIND outcome regardless of how queuing went whenever GitHub says the branch is out of date —\n * queuing a BEHIND PR is not an error (a repo that auto-updates branches will still land it), but it\n * is NOT success either, and reporting it as such is precisely the bug this whole file guards.\n */\n // eslint-disable-next-line @typescript-eslint/max-params\n private fallBackToAutoMerge(baseBranch: string, subject: string, mergeBodyFile: string, state: PrMergeState, intent: MergeIntent): MergeOutcome {\n if (!this.autoMergeAllowed()) {\n if (state.isBehind()) return this.behindOutcome(state, false);\n return new MergeOutcome(false, false, this.noQueueMessage(intent), MERGE_RESULT_FAILED);\n }\n\n process.stdout.write(' … retrying with --auto (the auto-merge queue)\\n');\n // gh records the merge subject/body only at the moment auto-merge is FIRST enabled; a second\n // `--auto` on an already-enabled PR silently keeps the OLD body. Disabling first re-stamps the\n // current subject/body on every re-run (a harmless no-op when auto-merge is not enabled).\n this.gh(['pr', 'merge', baseBranch, '--disable-auto'], true);\n const auto = this.gh(['pr', 'merge', baseBranch, '--auto', '--squash', '--subject', subject, '--body-file', mergeBodyFile]);\n if (auto !== 0) {\n if (state.isBehind()) return this.behindOutcome(state, false);\n return new MergeOutcome(false, false,\n '⚠️ did NOT merge and could NOT enable auto-merge either (see the gh error above) —\\n' +\n ' NOTHING is queued. Re-run once the PR is healthy.',\n MERGE_RESULT_FAILED);\n }\n if (state.isBehind()) return this.behindOutcome(state, true);\n return new MergeOutcome(false, true,\n `enabled auto-merge — it will squash-merge as \"${subject}\" when the checks pass`,\n MERGE_RESULT_AUTO_QUEUED);\n }\n\n /**\n * \"Not mergeable yet, and this repo has no queue to park it in\" — in the words that are TRUE for the\n * caller we actually have.\n *\n * A COMMANDED run (`wp-land-pr`) is not a misconfiguration at all: nobody set mergeMode to AUTO, the\n * user asked for a merge by hand, and the checks simply are not green yet. Telling that reader to\n * \"set mergeMode to NONE\" named a value they had already set and sent them investigating a mismatch\n * that did not exist. Nothing is owed here but a re-run.\n *\n * A POLICY-driven run (`wp-finish-upsert-pr`) only reaches here with a config that really does say\n * AUTO on a repo that forbids the queue — the genuine mismatch, where both remedies are real.\n */\n private noQueueMessage(intent: MergeIntent): string {\n if (intent.commanded) {\n return '⚠️ did NOT merge, and queued NOTHING — the PR is not mergeable yet (checks still\\n' +\n ' running, or a required review outstanding), and this repo has allow_auto_merge=false,\\n' +\n ' so there was no queue to fall back to. NOTHING is misconfigured and nothing needs\\n' +\n ' changing — re-run `pnpm wp-land-pr` once the PR is green and it will land.';\n }\n return '⚠️ did NOT merge, and queued NOTHING — CONFIG MISMATCH: pr-gate.mergeMode is AUTO, but\\n' +\n ' this repo has allow_auto_merge=false, so there is no queue to fall back to and the\\n' +\n ' PR is not mergeable yet. `gh pr merge --auto` cannot override a repo that says no.\\n' +\n ' Fix it at ONE of the two ends: turn on \"Allow auto-merge\" in the repo settings, or\\n' +\n ' set commands.pr-gate.mergeMode to \"NONE\" (and set that repo\\'s\\n' +\n ' squash_merge_commit_title to PR_TITLE so a human merge still gets a real subject).';\n }\n\n /**\n * The one outcome that looks queued but is stranded. `queued` says whether auto-merge did get enabled,\n * because \"parked forever\" and \"not queued at all\" need different sentences — but neither is done.\n *\n * The `result` now carries WHICH kind of behind, so the banner can stop describing a queue collision\n * in the vocabulary of a merge conflict. The wording here is deliberately blame-free: nothing the\n * author did caused this, and the PR itself is in perfectly good shape — pushed, bodied, gate-green.\n */\n private behindOutcome(state: PrMergeState, queued: boolean): MergeOutcome {\n const parked = queued ? 'Auto-merge is enabled but parked.' : 'Nothing is queued.';\n return new MergeOutcome(false, queued,\n `did NOT merge — someone else landed on main first, so GitHub wants this branch rebuilt on\\n` +\n ` top of theirs before it will merge (${state.describe()}).\\n` +\n ` This does NOT self-heal: auto-merge never updates your branch. ${parked}`,\n state.behindKind());\n }\n\n // Reprint the expected first-attempt failure as CONTEXT, not as a verdict. gh's own `X …` line is\n // captured (never inherited) so this framing is the only thing on screen.\n private announceDirectFailure(ghOutput: string, state: PrMergeState): void {\n const reason = ghOutput.trim().split('\\n').filter((l: string): boolean => l.trim() !== '').pop() ?? '';\n process.stdout.write(\n 'ℹ️ the direct squash-merge did not go through (expected while checks run, or when repo\\n' +\n ' policy forbids direct merges):\\n' +\n (reason === '' ? '' : ` ${reason}\\n`) +\n (state.describe() === '' ? '' : ` GitHub says: ${state.describe()}\\n`),\n );\n }\n\n /**\n * GitHub's own verdict on the PR. AUTHORITATIVE for the BEHIND check — the alternative is\n * pattern-matching gh's English error prose, which changes between releases. Failure-tolerant on\n * purpose: a gh hiccup returns an all-'' state (never BEHIND) so a transient API blip can only cost\n * us the extra diagnosis, never turn a working run into a crash.\n */\n protected prMergeState(baseBranch: string): PrMergeState {\n const result = spawnSync(\n 'gh',\n ['pr', 'view', baseBranch, '--json', 'mergeable,mergeStateStatus,state',\n '--jq', '\"\\\\(.mergeable)\\\\t\\\\(.mergeStateStatus)\\\\t\\\\(.state)\"'],\n { encoding: 'utf8' },\n );\n if (result.status !== 0) return new PrMergeState('', '', '');\n const parts = (result.stdout ?? '').trim().split('\\t');\n return new PrMergeState(parts[0] ?? '', parts[1] ?? '', parts[2] ?? '');\n }\n\n // Whether `gh pr merge --auto` is even possible on this repo. Many orgs set allow_auto_merge=false\n // as a policy control, where `--auto` can only ever fail with `GraphQL: Auto merge is not allowed\n // for this repository`. One API call beats discovering that from an error string. Anything other\n // than a clean `true` counts as NOT allowed: if the setting cannot be read we must not claim the\n // queue is available.\n protected autoMergeAllowed(): boolean {\n const result = spawnSync('gh', ['api', 'repos/{owner}/{repo}', '--jq', '.allow_auto_merge'], { encoding: 'utf8' });\n return result.status === 0 && (result.stdout ?? '').trim() === 'true';\n }\n\n // Runs `gh`, returning its exit status (-1 when gh could not be spawned at all, which must NOT be\n // mistaken for the 0 that means success).\n protected gh(args: string[], quiet: boolean = false): number {\n const result = spawnSync('gh', args, { stdio: quiet ? 'ignore' : 'inherit' });\n return result.status ?? -1;\n }\n\n // Runs `gh` CAPTURING both streams instead of inheriting them, so an expected-to-fail attempt can be\n // reported in our own words rather than dumping a raw `X …` line the reader reads as fatal.\n protected ghCapture(args: string[]): GhResult {\n const result = spawnSync('gh', args, { encoding: 'utf8' });\n return new GhResult(result.status ?? -1, (result.stdout ?? '') + (result.stderr ?? ''));\n }\n}\n\n// A captured `gh` invocation: its exit status (-1 when gh could not be spawned) and both streams\n// combined, in the order a terminal would have shown them.\nexport class GhResult {\n status: number;\n output: string;\n\n constructor(status: number, output: string) {\n this.status = status;\n this.output = output;\n }\n}\n"]}
|
|
1
|
+
{"version":3,"file":"pr-merger.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/pr-merger.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawnSync } from 'child_process';\nimport { MERGE_MODE_AUTO, MERGE_MODE_NONE } from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\n\n/**\n * The five terminal shapes a merge attempt can take. The CALLER branches on these — a `message` string\n * is for humans to read, never for code to pattern-match, and the banner that frames it must be able to\n * tell \"done\" from \"stranded\" without parsing prose.\n */\nexport const MERGE_RESULT_MERGED = 'MERGED';\n/** Not merged YET, but queued: auto-merge is enabled and lands it when the checks pass. Self-healing. */\nexport const MERGE_RESULT_AUTO_QUEUED = 'AUTO_QUEUED';\n/** Not merged BY DESIGN: pr-gate.mergeMode is not AUTO, so a person merges. Nothing is owed here. */\nexport const MERGE_RESULT_LEFT_TO_HUMAN = 'LEFT_TO_HUMAN';\n/**\n * Not merged and it NEVER will be without action: `gh pr view --json mergeStateStatus` says BEHIND, i.e.\n * the head branch is out of date with base. Unlike BLOCKED (waiting on checks — self-healing), BEHIND\n * cannot resolve itself; the branch must be re-synced from main. This is the case that used to print a\n * green \"✅ PR finished\" and get abandoned.\n */\nexport const MERGE_RESULT_BEHIND = 'BEHIND';\n/**\n * BEHIND *and* GitHub says the tree merges cleanly (`mergeable: MERGEABLE`). Nobody touched the same\n * lines — somebody simply landed on main first. One clean re-run of ①②③ converges. Split out from the\n * conflicting case because the two need completely different sentences: this one is not the author's\n * problem to solve, it is a queue collision, and telling them \"resolve the conflict\" is a lie.\n */\nexport const MERGE_RESULT_BEHIND_CLEAN = 'BEHIND_CLEAN';\n/**\n * BEHIND *and* `mergeable: CONFLICTING` — the landed work and this branch touch the same lines. Real\n * human/AI judgement is owed, and if others keep landing first it genuinely repeats. That is inherent to\n * concurrent editing, not a defect, and the banner says so rather than pretending a re-run is free.\n */\nexport const MERGE_RESULT_BEHIND_CONFLICTING = 'BEHIND_CONFLICTING';\n/**\n * BEHIND but `mergeable: UNKNOWN` (or unreadable). GitHub computes mergeability ASYNCHRONOUSLY and we ask\n * moments after a force-push, so UNKNOWN is the EXPECTED answer, not an error. Diagnosing from it would be\n * guessing, so this outcome asks for a re-check instead of prescribing a remedy.\n */\nexport const MERGE_RESULT_BEHIND_UNKNOWN = 'BEHIND_UNKNOWN';\n/** Not merged for some other reason (config mismatch, gh error, no PR at all). Read `message`. */\nexport const MERGE_RESULT_FAILED = 'FAILED';\n\n// The `mergeStateStatus` value GitHub reports for \"head branch is not up to date with the base branch\".\nconst GH_STATE_BEHIND = 'BEHIND';\n// `mergeable` values. GitHub returns UNKNOWN while it is still computing the merge — which is most of the\n// time in the seconds after a push — so UNKNOWN must never be read as \"no conflicts\".\nconst GH_MERGEABLE_CLEAN = 'MERGEABLE';\nconst GH_MERGEABLE_CONFLICTING = 'CONFLICTING';\n\n// What actually happened when we tried to land the squash merge. `message` is printed VERBATIM in the\n// final wp-finish-upsert-pr summary, so a merge that did not happen can never be reported as done —\n// the old code ignored `spawnSync().status` entirely and printed \"✅ PR finished\" even when `gh pr\n// merge` had errored out, which is what hid the auto-merge-disabled failure for weeks. `result` is the\n// machine-readable half of the same honesty: the banner picks its HEADER from it, so the frame around\n// the message can no longer say \"finished\" while the message says \"did NOT merge\".\nexport class MergeOutcome {\n merged: boolean;\n autoMergeEnabled: boolean;\n message: string;\n result: string;\n\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(merged: boolean, autoMergeEnabled: boolean, message: string, result: string) {\n this.merged = merged;\n this.autoMergeEnabled = autoMergeEnabled;\n this.message = message;\n this.result = result;\n }\n\n // TRUE for every flavour of \"the branch is out of date with main\", so the banner can ask that one\n // question once instead of listing four constants at each branch point. Kept here rather than as a\n // module function because `no-function-outside-class` forbids the latter — and PrMergeState below\n // already sets the precedent that a verdict class answers questions about itself.\n isBehind(): boolean {\n return this.result === MERGE_RESULT_BEHIND\n || this.result === MERGE_RESULT_BEHIND_CLEAN\n || this.result === MERGE_RESULT_BEHIND_CONFLICTING\n || this.result === MERGE_RESULT_BEHIND_UNKNOWN;\n }\n}\n\n// GitHub's own verdict on the PR, straight from `gh pr view --json mergeable,mergeStateStatus,state`.\n// All '' when gh could not be asked — an unreadable answer must never be treated as a diagnosis.\nexport class PrMergeState {\n mergeable: string;\n mergeStateStatus: string;\n state: string;\n\n constructor(mergeable: string, mergeStateStatus: string, state: string) {\n this.mergeable = mergeable;\n this.mergeStateStatus = mergeStateStatus;\n this.state = state;\n }\n\n isBehind(): boolean {\n return this.mergeStateStatus === GH_STATE_BEHIND;\n }\n\n /**\n * WHICH kind of BEHIND this is, from the `mergeable` field we have always fetched and never read.\n *\n * This is the whole point of the split. \"Out of date\" and \"conflicting\" are different situations with\n * different costs, and collapsing them told every author the expensive story: a clean queue collision\n * — nobody touched your lines, somebody just landed first — got reported in the same alarming words as\n * a genuine textual conflict.\n *\n * UNKNOWN is its own answer, never folded into CLEAN. GitHub computes mergeability asynchronously and\n * we ask seconds after a force-push, so UNKNOWN is the ordinary reply in exactly our situation.\n * Treating it as \"no conflicts\" would promise a clean re-run we cannot see.\n */\n behindKind(): string {\n if (this.mergeable === GH_MERGEABLE_CLEAN) return MERGE_RESULT_BEHIND_CLEAN;\n if (this.mergeable === GH_MERGEABLE_CONFLICTING) return MERGE_RESULT_BEHIND_CONFLICTING;\n return MERGE_RESULT_BEHIND_UNKNOWN;\n }\n\n // One-line rendering for the failure message, or '' when GitHub could not be asked.\n describe(): string {\n if (this.mergeStateStatus === '') return '';\n return `mergeable=${this.mergeable}, mergeStateStatus=${this.mergeStateStatus}, state=${this.state}`;\n }\n}\n\n/**\n * WHY this run is allowed to merge — the config value, kept separate from the caller's intent.\n *\n * Those two used to be one `mergeMode: string` parameter, and `wp-land-pr` passed a literal\n * `MERGE_MODE_AUTO` through it to mean \"merge it\" rather than \"config says AUTO\". PrMerger could not\n * tell the two apart, so on a `mergeMode: \"NONE\"` repo it reported a CONFIG MISMATCH naming a value\n * nobody had set, and prescribed remedies that were already in place. Keeping the real `mergeMode`\n * alongside a `commanded` flag means no message ever has to assert a config value the code did not read.\n */\nexport class MergeIntent {\n /** pr-gate.mergeMode EXACTLY as read from webpieces.config.json — '' when it could not be read. */\n mergeMode: string;\n /**\n * TRUE when running the command IS the intent to merge (`wp-land-pr`), so the merge happens whatever\n * the config says. FALSE for the policy-driven caller (`wp-finish-upsert-pr`), where only an\n * explicit AUTO merges.\n */\n commanded: boolean;\n\n constructor(mergeMode: string, commanded: boolean) {\n this.mergeMode = mergeMode;\n this.commanded = commanded;\n }\n\n // The gate: a commanded run always proceeds; otherwise only an explicit AUTO does. Anything else\n // (including a repo on a published rules-config that predates the field) is treated as NONE —\n // when the policy is unreadable, do NOT touch main.\n shouldMerge(): boolean {\n return this.commanded || this.mergeMode === MERGE_MODE_AUTO;\n }\n}\n\n/**\n * Lands — or queues — the squash merge with an EXPLICIT subject/body, on BOTH kinds of repo:\n *\n * - auto-merge ALLOWED (`allow_auto_merge: true`): a PR whose checks are still running falls back to\n * the auto-merge queue, carrying the same subject/body so it lands when the checks pass.\n * - auto-merge DISALLOWED (`allow_auto_merge: false`, a deliberate policy control in many orgs): the\n * direct merge still works the moment the PR is mergeable, because `gh pr merge --squash --subject\n * --body-file` does not depend on that setting at all. When the PR is NOT yet mergeable there is no\n * queue to fall back to, so we say so loudly instead of firing a `--auto` that can only fail.\n *\n * WHICH of those a repo gets is not guessed — `pr-gate.mergeMode` is REQUIRED config. AUTO means the\n * tooling lands PRs; NONE means it only posts them and a person merges. No mode can force a queue the\n * repo has turned off, so AUTO on a repo with allow_auto_merge=false is a CONFIG error, reported as\n * one rather than papered over — but ONLY when the AUTO was read from config. A commanded merge\n * (`wp-land-pr`) on a NONE repo is not a mismatch at all; see MergeIntent.\n *\n * Every `gh` status is checked. Nothing here is allowed to fail silently.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class PrMerger {\n /**\n * @param subject the squash-commit subject, normally `<PR title> (#N)`\n * @param mergeBodyFile file holding the squash-commit body (risk/flags/PR link)\n * @param intent the real pr-gate.mergeMode PLUS whether this caller commands the merge\n * regardless of it (see MergeIntent).\n */\n merge(baseBranch: string, subject: string, mergeBodyFile: string, intent: MergeIntent): MergeOutcome {\n // Anything that is not an explicit AUTO leaves the PR alone. The PR itself is already\n // posted/updated by this point, which is the whole job in this mode.\n if (!intent.shouldMerge()) {\n const mergeMode = intent.mergeMode;\n // What a UI merge produces, in terms of the repo's OWN two settings.\n //\n // This message said the opposite one release ago: \"a UI merge pastes the whole dashboard into\n // git log, land it with wp-land-pr instead\". That was true while the PR description held the\n // long dashboard. It is now the compact git-log body (see Dashboard.renderPrBody), so\n // `squash_merge_commit_message: PR_BODY` copies exactly the right text and a UI merge is\n // CORRECT — provided both settings are right. Naming them is the useful thing to say; telling\n // a mergeMode=NONE repo to stop using the UI is not, since using it is the whole policy.\n return new MergeOutcome(false, false,\n `did NOT merge — pr-gate.mergeMode is ${mergeMode === MERGE_MODE_NONE ? 'NONE' : `\"${mergeMode}\"`}, so the PR is left for a human to merge.\\n` +\n ` A UI merge takes the commit message from this repo's own settings, and both must be\\n` +\n ` set for main's history to read well: squash_merge_commit_title=PR_TITLE (else the\\n` +\n ` subject is not \"${subject}\") and squash_merge_commit_message=PR_BODY (which copies\\n` +\n ` the gated description verbatim — that IS the commit body this flow rendered).\\n` +\n ` \\`pnpm wp-land-pr\\` lands the same bytes from the CLI if you would rather not click.`,\n MERGE_RESULT_LEFT_TO_HUMAN);\n }\n\n // A direct `gh pr merge --squash --subject --body-file` writes exactly this subject/body to\n // main's history regardless of the repo's squash_merge_commit_title/message defaults — and\n // regardless of allow_auto_merge. It is the ONLY path that guarantees the good commit message.\n //\n // Its output is CAPTURED, not inherited: this attempt fails BY DESIGN on any repo whose policy\n // forbids a direct merge, and gh's raw `X Pull request #N is not mergeable: …` on the terminal\n // read as a hard failure to every human and agent who saw it. We reprint the reason ourselves,\n // framed as the expected first step of a two-step dance (see announceDirectFailure).\n const direct = this.ghCapture(['pr', 'merge', baseBranch, '--squash', '--subject', subject, '--body-file', mergeBodyFile]);\n if (direct.status === 0) {\n process.stdout.write(direct.output);\n return new MergeOutcome(true, false, `squash-merged the PR as: \"${subject}\"`, MERGE_RESULT_MERGED);\n }\n\n // Past here the PR is not mergeable yet. WHY matters enormously and gh's exit status does not\n // say: checks-still-running (BLOCKED) is self-healing once auto-merge is on, while out-of-date\n // (BEHIND) can NEVER land unattended. Ask GitHub itself — one cheap, failure-tolerant call, made\n // only on the failure path, so a run whose direct merge succeeded costs no extra API call.\n const state = this.prMergeState(baseBranch);\n this.announceDirectFailure(direct.output, state);\n return this.fallBackToAutoMerge(baseBranch, subject, mergeBodyFile, state, intent);\n }\n\n /**\n * The auto-merge queue: the only way to still land a not-yet-mergeable PR unattended. Returns the\n * BEHIND outcome regardless of how queuing went whenever GitHub says the branch is out of date —\n * queuing a BEHIND PR is not an error (a repo that auto-updates branches will still land it), but it\n * is NOT success either, and reporting it as such is precisely the bug this whole file guards.\n */\n // eslint-disable-next-line @typescript-eslint/max-params\n private fallBackToAutoMerge(baseBranch: string, subject: string, mergeBodyFile: string, state: PrMergeState, intent: MergeIntent): MergeOutcome {\n if (!this.autoMergeAllowed()) {\n if (state.isBehind()) return this.behindOutcome(state, false);\n return new MergeOutcome(false, false, this.noQueueMessage(intent), MERGE_RESULT_FAILED);\n }\n\n process.stdout.write(' … retrying with --auto (the auto-merge queue)\\n');\n // gh records the merge subject/body only at the moment auto-merge is FIRST enabled; a second\n // `--auto` on an already-enabled PR silently keeps the OLD body. Disabling first re-stamps the\n // current subject/body on every re-run (a harmless no-op when auto-merge is not enabled).\n this.gh(['pr', 'merge', baseBranch, '--disable-auto'], true);\n const auto = this.gh(['pr', 'merge', baseBranch, '--auto', '--squash', '--subject', subject, '--body-file', mergeBodyFile]);\n if (auto !== 0) {\n if (state.isBehind()) return this.behindOutcome(state, false);\n return new MergeOutcome(false, false,\n '⚠️ did NOT merge and could NOT enable auto-merge either (see the gh error above) —\\n' +\n ' NOTHING is queued. Re-run once the PR is healthy.',\n MERGE_RESULT_FAILED);\n }\n if (state.isBehind()) return this.behindOutcome(state, true);\n return new MergeOutcome(false, true,\n `enabled auto-merge — it will squash-merge as \"${subject}\" when the checks pass`,\n MERGE_RESULT_AUTO_QUEUED);\n }\n\n /**\n * \"Not mergeable yet, and this repo has no queue to park it in\" — in the words that are TRUE for the\n * caller we actually have.\n *\n * A COMMANDED run (`wp-land-pr`) is not a misconfiguration at all: nobody set mergeMode to AUTO, the\n * user asked for a merge by hand, and the checks simply are not green yet. Telling that reader to\n * \"set mergeMode to NONE\" named a value they had already set and sent them investigating a mismatch\n * that did not exist. Nothing is owed here but a re-run.\n *\n * A POLICY-driven run (`wp-finish-upsert-pr`) only reaches here with a config that really does say\n * AUTO on a repo that forbids the queue — the genuine mismatch, where both remedies are real.\n */\n private noQueueMessage(intent: MergeIntent): string {\n if (intent.commanded) {\n return '⚠️ did NOT merge, and queued NOTHING — the PR is not mergeable yet (checks still\\n' +\n ' running, or a required review outstanding), and this repo has allow_auto_merge=false,\\n' +\n ' so there was no queue to fall back to. NOTHING is misconfigured and nothing needs\\n' +\n ' changing — re-run `pnpm wp-land-pr` once the PR is green and it will land.';\n }\n return '⚠️ did NOT merge, and queued NOTHING — CONFIG MISMATCH: pr-gate.mergeMode is AUTO, but\\n' +\n ' this repo has allow_auto_merge=false, so there is no queue to fall back to and the\\n' +\n ' PR is not mergeable yet. `gh pr merge --auto` cannot override a repo that says no.\\n' +\n ' Fix it at ONE of the two ends: turn on \"Allow auto-merge\" in the repo settings, or\\n' +\n ' set commands.pr-gate.mergeMode to \"NONE\" (and set that repo\\'s\\n' +\n ' squash_merge_commit_title to PR_TITLE so a human merge still gets a real subject).';\n }\n\n /**\n * The one outcome that looks queued but is stranded. `queued` says whether auto-merge did get enabled,\n * because \"parked forever\" and \"not queued at all\" need different sentences — but neither is done.\n *\n * The `result` now carries WHICH kind of behind, so the banner can stop describing a queue collision\n * in the vocabulary of a merge conflict. The wording here is deliberately blame-free: nothing the\n * author did caused this, and the PR itself is in perfectly good shape — pushed, bodied, gate-green.\n */\n private behindOutcome(state: PrMergeState, queued: boolean): MergeOutcome {\n const parked = queued ? 'Auto-merge is enabled but parked.' : 'Nothing is queued.';\n return new MergeOutcome(false, queued,\n `did NOT merge — someone else landed on main first, so GitHub wants this branch rebuilt on\\n` +\n ` top of theirs before it will merge (${state.describe()}).\\n` +\n ` This does NOT self-heal: auto-merge never updates your branch. ${parked}`,\n state.behindKind());\n }\n\n // Reprint the expected first-attempt failure as CONTEXT, not as a verdict. gh's own `X …` line is\n // captured (never inherited) so this framing is the only thing on screen.\n private announceDirectFailure(ghOutput: string, state: PrMergeState): void {\n const reason = ghOutput.trim().split('\\n').filter((l: string): boolean => l.trim() !== '').pop() ?? '';\n process.stdout.write(\n 'ℹ️ the direct squash-merge did not go through (expected while checks run, or when repo\\n' +\n ' policy forbids direct merges):\\n' +\n (reason === '' ? '' : ` ${reason}\\n`) +\n (state.describe() === '' ? '' : ` GitHub says: ${state.describe()}\\n`),\n );\n }\n\n /**\n * GitHub's own verdict on the PR. AUTHORITATIVE for the BEHIND check — the alternative is\n * pattern-matching gh's English error prose, which changes between releases. Failure-tolerant on\n * purpose: a gh hiccup returns an all-'' state (never BEHIND) so a transient API blip can only cost\n * us the extra diagnosis, never turn a working run into a crash.\n */\n protected prMergeState(baseBranch: string): PrMergeState {\n const result = spawnSync(\n 'gh',\n ['pr', 'view', baseBranch, '--json', 'mergeable,mergeStateStatus,state',\n '--jq', '\"\\\\(.mergeable)\\\\t\\\\(.mergeStateStatus)\\\\t\\\\(.state)\"'],\n { encoding: 'utf8' },\n );\n if (result.status !== 0) return new PrMergeState('', '', '');\n const parts = (result.stdout ?? '').trim().split('\\t');\n return new PrMergeState(parts[0] ?? '', parts[1] ?? '', parts[2] ?? '');\n }\n\n // Whether `gh pr merge --auto` is even possible on this repo. Many orgs set allow_auto_merge=false\n // as a policy control, where `--auto` can only ever fail with `GraphQL: Auto merge is not allowed\n // for this repository`. One API call beats discovering that from an error string. Anything other\n // than a clean `true` counts as NOT allowed: if the setting cannot be read we must not claim the\n // queue is available.\n protected autoMergeAllowed(): boolean {\n const result = spawnSync('gh', ['api', 'repos/{owner}/{repo}', '--jq', '.allow_auto_merge'], { encoding: 'utf8' });\n return result.status === 0 && (result.stdout ?? '').trim() === 'true';\n }\n\n // Runs `gh`, returning its exit status (-1 when gh could not be spawned at all, which must NOT be\n // mistaken for the 0 that means success).\n protected gh(args: string[], quiet: boolean = false): number {\n const result = spawnSync('gh', args, { stdio: quiet ? 'ignore' : 'inherit' });\n return result.status ?? -1;\n }\n\n // Runs `gh` CAPTURING both streams instead of inheriting them, so an expected-to-fail attempt can be\n // reported in our own words rather than dumping a raw `X …` line the reader reads as fatal.\n protected ghCapture(args: string[]): GhResult {\n const result = spawnSync('gh', args, { encoding: 'utf8' });\n return new GhResult(result.status ?? -1, (result.stdout ?? '') + (result.stderr ?? ''));\n }\n}\n\n// A captured `gh` invocation: its exit status (-1 when gh could not be spawned) and both streams\n// combined, in the order a terminal would have shown them.\nexport class GhResult {\n status: number;\n output: string;\n\n constructor(status: number, output: string) {\n this.status = status;\n this.output = output;\n }\n}\n"]}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { RequiredChecklist, PrGateConfig, ReviewJsonService, SubagentProvenanceService, ReviewerEvidence, ReviewProvenanceService, ReviewerInstructionsService } from '@webpieces/rules-config';
|
|
2
|
+
import { AiBranchName } from './git-readAiBranchName';
|
|
3
|
+
/**
|
|
4
|
+
* The provenance outcome: whether each reviewer was VERIFIED to have run (the integrity check, which
|
|
5
|
+
* blocks) plus what each one actually read (the quality signal, which is published). Data-only.
|
|
6
|
+
*/
|
|
7
|
+
export declare class ProvenanceReport {
|
|
8
|
+
verified: boolean;
|
|
9
|
+
evidence: ReviewerEvidence[];
|
|
10
|
+
constructor(verified: boolean, evidence: ReviewerEvidence[]);
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Proves each checklist verdict came from its OWN independently-run reviewer subagent, and records what
|
|
14
|
+
* each one read.
|
|
15
|
+
*
|
|
16
|
+
* Split out of FinishUpsertPrCommand when that file crossed the 700-line cap, and it is the right seam
|
|
17
|
+
* twice over: this is a self-contained integrity check with its own spec
|
|
18
|
+
* (`finish-provenance-record.spec.ts`), and that spec previously had to reach through
|
|
19
|
+
* `command as unknown as { enforceProvenance(...) }` to get at it. Now `enforce` is simply public.
|
|
20
|
+
*
|
|
21
|
+
* `@injectable(bindingScopeValues.Singleton)` so it is drawn in the DI design and injected by type.
|
|
22
|
+
*/
|
|
23
|
+
export declare class ProvenanceEnforcer {
|
|
24
|
+
private readonly aiBranchName;
|
|
25
|
+
private readonly provenance;
|
|
26
|
+
private readonly provenanceRecord;
|
|
27
|
+
private readonly reviewerInstructions;
|
|
28
|
+
private readonly reviewJsonService;
|
|
29
|
+
constructor(aiBranchName: AiBranchName, provenance: SubagentProvenanceService, provenanceRecord: ReviewProvenanceService, reviewerInstructions: ReviewerInstructionsService, reviewJsonService: ReviewJsonService);
|
|
30
|
+
/**
|
|
31
|
+
* Archive this round's provenance.json alongside the archived review.json, so an archived review keeps
|
|
32
|
+
* the transcript links belonging to the round that produced it — a record overwritten by the NEXT
|
|
33
|
+
* round audits nothing.
|
|
34
|
+
*
|
|
35
|
+
* Here rather than in the command because this class owns the record; the command only knows WHEN to
|
|
36
|
+
* archive, not what the record is.
|
|
37
|
+
*/
|
|
38
|
+
archiveRecord(prDir: string): void;
|
|
39
|
+
private gitOut;
|
|
40
|
+
enforce(required: readonly RequiredChecklist[], branch: string, repoRoot: string, config: PrGateConfig): ProvenanceReport;
|
|
41
|
+
/**
|
|
42
|
+
* What each credited reviewer actually read. Purely observational here — {@link evidenceErrors} decides
|
|
43
|
+
* whether any of it blocks, and by default none of it does.
|
|
44
|
+
*/
|
|
45
|
+
private gatherEvidence;
|
|
46
|
+
/**
|
|
47
|
+
* Write this round's audit record: `.webpieces/pr-review/<branch>/provenance.json`, linking each verdict
|
|
48
|
+
* to the transcript of the subagent that produced it.
|
|
49
|
+
*
|
|
50
|
+
* A SEPARATE file rather than a field inside review.json / review-<id>.json, for two reasons. The
|
|
51
|
+
* reviewer cannot supply this itself — a subagent's environment exposes the PARENT session id and no
|
|
52
|
+
* agent id, so a self-reported transcript link would be invented — and keeping the AI-authored files
|
|
53
|
+
* byte-untouched means nothing in the record can be mistaken for something a reviewer claimed about
|
|
54
|
+
* itself. Every path here is derived by the tooling from the harness's own artifacts.
|
|
55
|
+
*
|
|
56
|
+
* Never fatal: an unwritable record is a lost audit trail, not a reason to refuse a PR.
|
|
57
|
+
*/
|
|
58
|
+
private writeProvenanceRecord;
|
|
59
|
+
private reviewerPathsFor;
|
|
60
|
+
/**
|
|
61
|
+
* WARN (default) or REFUSE (opt-in) on a reviewer that wrote a verdict without opening the diff.
|
|
62
|
+
*
|
|
63
|
+
* Default-warn because the signal is derived from undocumented Claude Code transcript internals: if the
|
|
64
|
+
* format shifts, a blocking check wedges every PR in every consumer repo with no self-service recovery.
|
|
65
|
+
* `requireDiffEvidence` lets a repo that has watched the warning promote it deliberately.
|
|
66
|
+
*/
|
|
67
|
+
private evidenceErrors;
|
|
68
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ProvenanceEnforcer = exports.ProvenanceReport = void 0;
|
|
4
|
+
const tslib_1 = require("tslib");
|
|
5
|
+
const child_process_1 = require("child_process");
|
|
6
|
+
const path = tslib_1.__importStar(require("path"));
|
|
7
|
+
const rules_config_1 = require("@webpieces/rules-config");
|
|
8
|
+
const inversify_1 = require("inversify");
|
|
9
|
+
const git_readAiBranchName_1 = require("./git-readAiBranchName");
|
|
10
|
+
/**
|
|
11
|
+
* The provenance outcome: whether each reviewer was VERIFIED to have run (the integrity check, which
|
|
12
|
+
* blocks) plus what each one actually read (the quality signal, which is published). Data-only.
|
|
13
|
+
*/
|
|
14
|
+
class ProvenanceReport {
|
|
15
|
+
verified;
|
|
16
|
+
evidence;
|
|
17
|
+
constructor(verified, evidence) {
|
|
18
|
+
this.verified = verified;
|
|
19
|
+
this.evidence = evidence;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
exports.ProvenanceReport = ProvenanceReport;
|
|
23
|
+
/**
|
|
24
|
+
* Proves each checklist verdict came from its OWN independently-run reviewer subagent, and records what
|
|
25
|
+
* each one read.
|
|
26
|
+
*
|
|
27
|
+
* Split out of FinishUpsertPrCommand when that file crossed the 700-line cap, and it is the right seam
|
|
28
|
+
* twice over: this is a self-contained integrity check with its own spec
|
|
29
|
+
* (`finish-provenance-record.spec.ts`), and that spec previously had to reach through
|
|
30
|
+
* `command as unknown as { enforceProvenance(...) }` to get at it. Now `enforce` is simply public.
|
|
31
|
+
*
|
|
32
|
+
* `@injectable(bindingScopeValues.Singleton)` so it is drawn in the DI design and injected by type.
|
|
33
|
+
*/
|
|
34
|
+
let ProvenanceEnforcer = class ProvenanceEnforcer {
|
|
35
|
+
aiBranchName;
|
|
36
|
+
provenance;
|
|
37
|
+
provenanceRecord;
|
|
38
|
+
reviewerInstructions;
|
|
39
|
+
reviewJsonService;
|
|
40
|
+
// eslint-disable-next-line @typescript-eslint/max-params
|
|
41
|
+
constructor(aiBranchName, provenance, provenanceRecord, reviewerInstructions, reviewJsonService) {
|
|
42
|
+
this.aiBranchName = aiBranchName;
|
|
43
|
+
this.provenance = provenance;
|
|
44
|
+
this.provenanceRecord = provenanceRecord;
|
|
45
|
+
this.reviewerInstructions = reviewerInstructions;
|
|
46
|
+
this.reviewJsonService = reviewJsonService;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Archive this round's provenance.json alongside the archived review.json, so an archived review keeps
|
|
50
|
+
* the transcript links belonging to the round that produced it — a record overwritten by the NEXT
|
|
51
|
+
* round audits nothing.
|
|
52
|
+
*
|
|
53
|
+
* Here rather than in the command because this class owns the record; the command only knows WHEN to
|
|
54
|
+
* archive, not what the record is.
|
|
55
|
+
*/
|
|
56
|
+
archiveRecord(prDir) {
|
|
57
|
+
this.provenanceRecord.archive(prDir);
|
|
58
|
+
}
|
|
59
|
+
// HEAD sha for the audit record. Failure-tolerant ('' rather than a throw): a missing sha degrades
|
|
60
|
+
// the audit trail, and refusing a PR over it would be the wrong trade — see writeProvenanceRecord.
|
|
61
|
+
gitOut(args) {
|
|
62
|
+
const result = (0, child_process_1.spawnSync)('git', args, { encoding: 'utf8' });
|
|
63
|
+
return result.status === 0 ? (result.stdout ?? '').trim() : '';
|
|
64
|
+
}
|
|
65
|
+
// Enforce that EACH matched checklist was reviewed by its OWN named subagent, as a DISTINCT run —
|
|
66
|
+
// the coding agent may not self-certify, and one reviewer may not stand in for several. A verified set
|
|
67
|
+
// passes silently; no session id warns but passes; any missing reviewer throws so the PR does not open.
|
|
68
|
+
// eslint-disable-next-line @typescript-eslint/max-params
|
|
69
|
+
enforce(required, branch, repoRoot, config) {
|
|
70
|
+
const errors = [];
|
|
71
|
+
const report = new ProvenanceReport(true, []); // no reviewers to verify ⇒ vacuously verified
|
|
72
|
+
const subagents = required.map((r) => r.subagent.trim()).filter((s) => s !== '');
|
|
73
|
+
// verifyDistinct short-circuits to OK on an empty set, so this runs unconditionally: a repo with no
|
|
74
|
+
// checklists still gets a provenance record naming the session and the main agent's own transcript.
|
|
75
|
+
const result = this.provenance.verifyDistinct(subagents, branch);
|
|
76
|
+
report.verified = result.status === rules_config_1.PROVENANCE_OK;
|
|
77
|
+
if (result.status === rules_config_1.PROVENANCE_MISSING) {
|
|
78
|
+
errors.push(result.detail);
|
|
79
|
+
}
|
|
80
|
+
else if (result.status === rules_config_1.PROVENANCE_SKIPPED) {
|
|
81
|
+
process.stderr.write(`⚠️ ${result.detail}\n`);
|
|
82
|
+
}
|
|
83
|
+
report.evidence = this.gatherEvidence(repoRoot, required, result, branch);
|
|
84
|
+
errors.push(...this.evidenceErrors(report.evidence, config));
|
|
85
|
+
// BEFORE the throw below, deliberately. A refused round is the one most worth auditing, and a record
|
|
86
|
+
// that only ever appeared on success could not answer what the reviewers did the time it was refused.
|
|
87
|
+
this.writeProvenanceRecord(repoRoot, required, result, report.evidence);
|
|
88
|
+
if (errors.length > 0) {
|
|
89
|
+
throw new rules_config_1.InformAiError(`${errors.length} checklist(s) require an independent reviewer subagent that did not run — fix, then re-run pnpm wp-finish-upsert-pr:\n\n` +
|
|
90
|
+
errors.map((e) => ` • ${e}`).join('\n') +
|
|
91
|
+
`\n\nSpawn the named reviewer subagent to review the checklist on THIS branch, then re-run.`);
|
|
92
|
+
}
|
|
93
|
+
return report;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* What each credited reviewer actually read. Purely observational here — {@link evidenceErrors} decides
|
|
97
|
+
* whether any of it blocks, and by default none of it does.
|
|
98
|
+
*/
|
|
99
|
+
// eslint-disable-next-line @typescript-eslint/max-params
|
|
100
|
+
gatherEvidence(repoRoot, required, result, branch) {
|
|
101
|
+
const docPaths = {};
|
|
102
|
+
for (const req of required) {
|
|
103
|
+
if (req.subagent.trim() !== '')
|
|
104
|
+
docPaths[req.subagent] = req.doc.trim() === '' ? '' : path.resolve(repoRoot, req.doc);
|
|
105
|
+
}
|
|
106
|
+
const diffDir = path.join((0, rules_config_1.prDirFor)(repoRoot, this.aiBranchName.getFeatureName()), 'diff');
|
|
107
|
+
return this.provenance.evidenceFor(new rules_config_1.EvidenceRequest(branch, result.agentIds, diffDir, docPaths));
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Write this round's audit record: `.webpieces/pr-review/<branch>/provenance.json`, linking each verdict
|
|
111
|
+
* to the transcript of the subagent that produced it.
|
|
112
|
+
*
|
|
113
|
+
* A SEPARATE file rather than a field inside review.json / review-<id>.json, for two reasons. The
|
|
114
|
+
* reviewer cannot supply this itself — a subagent's environment exposes the PARENT session id and no
|
|
115
|
+
* agent id, so a self-reported transcript link would be invented — and keeping the AI-authored files
|
|
116
|
+
* byte-untouched means nothing in the record can be mistaken for something a reviewer claimed about
|
|
117
|
+
* itself. Every path here is derived by the tooling from the harness's own artifacts.
|
|
118
|
+
*
|
|
119
|
+
* Never fatal: an unwritable record is a lost audit trail, not a reason to refuse a PR.
|
|
120
|
+
*/
|
|
121
|
+
// eslint-disable-next-line @typescript-eslint/max-params
|
|
122
|
+
writeProvenanceRecord(repoRoot, required, result, evidence) {
|
|
123
|
+
const featureName = this.aiBranchName.getFeatureName();
|
|
124
|
+
const prDir = (0, rules_config_1.prDirFor)(repoRoot, featureName);
|
|
125
|
+
const request = new rules_config_1.ProvenanceWriteRequest(prDir, featureName, this.gitOut(['rev-parse', 'HEAD']), result.status);
|
|
126
|
+
request.offered = new rules_config_1.OfferedContext(path.join(prDir, 'diff'), this.reviewerInstructions.instructionsDirFor(repoRoot, featureName));
|
|
127
|
+
request.reviewers = evidence.map((e) => new rules_config_1.ReviewerTranscript(e, this.reviewerPathsFor(repoRoot, featureName, required, e.agentType)));
|
|
128
|
+
const written = this.provenanceRecord.write(request);
|
|
129
|
+
if (written !== '')
|
|
130
|
+
process.stdout.write(` transcript provenance → ${written}\n`);
|
|
131
|
+
}
|
|
132
|
+
// Where ONE reviewer's verdict, instructions and checklist doc live. Keyed by agentType, which IS the
|
|
133
|
+
// checklist id: ChecklistDefinition sets `id = subagent`, so the two never diverge.
|
|
134
|
+
// eslint-disable-next-line @typescript-eslint/max-params
|
|
135
|
+
reviewerPathsFor(repoRoot, featureName, required, agentType) {
|
|
136
|
+
const req = required.find((r) => r.subagent.trim() === agentType);
|
|
137
|
+
const doc = req !== undefined && req.doc.trim() !== '' ? path.resolve(repoRoot, req.doc) : '';
|
|
138
|
+
return new rules_config_1.ReviewerPaths(this.reviewJsonService.checklistResultPath((0, rules_config_1.reviewJsonPath)(repoRoot, featureName), req?.id ?? agentType), this.reviewerInstructions.pathFor(repoRoot, featureName, agentType), doc);
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* WARN (default) or REFUSE (opt-in) on a reviewer that wrote a verdict without opening the diff.
|
|
142
|
+
*
|
|
143
|
+
* Default-warn because the signal is derived from undocumented Claude Code transcript internals: if the
|
|
144
|
+
* format shifts, a blocking check wedges every PR in every consumer repo with no self-service recovery.
|
|
145
|
+
* `requireDiffEvidence` lets a repo that has watched the warning promote it deliberately.
|
|
146
|
+
*/
|
|
147
|
+
evidenceErrors(evidence, config) {
|
|
148
|
+
const blind = evidence.filter((e) => !e.readDiff);
|
|
149
|
+
if (blind.length === 0)
|
|
150
|
+
return [];
|
|
151
|
+
const names = blind.map((e) => e.agentType).join(', ');
|
|
152
|
+
if (!config.requireDiffEvidence) {
|
|
153
|
+
process.stderr.write(`\n⚠️ ${blind.length} reviewer(s) wrote a verdict with no record of opening the extracted diff: ${names}\n` +
|
|
154
|
+
' Published on the PR as a note. Not blocking — set pr-gate.requireDiffEvidence:true to make it one.\n');
|
|
155
|
+
return [];
|
|
156
|
+
}
|
|
157
|
+
return [`these reviewers wrote a verdict without opening the diff (pr-gate.requireDiffEvidence is on): ${names}`];
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
exports.ProvenanceEnforcer = ProvenanceEnforcer;
|
|
161
|
+
exports.ProvenanceEnforcer = ProvenanceEnforcer = tslib_1.__decorate([
|
|
162
|
+
(0, inversify_1.injectable)(inversify_1.bindingScopeValues.Singleton),
|
|
163
|
+
tslib_1.__metadata("design:paramtypes", [git_readAiBranchName_1.AiBranchName,
|
|
164
|
+
rules_config_1.SubagentProvenanceService,
|
|
165
|
+
rules_config_1.ReviewProvenanceService,
|
|
166
|
+
rules_config_1.ReviewerInstructionsService,
|
|
167
|
+
rules_config_1.ReviewJsonService])
|
|
168
|
+
], ProvenanceEnforcer);
|
|
169
|
+
//# sourceMappingURL=provenance-enforcer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"provenance-enforcer.js","sourceRoot":"","sources":["../../../../../../../packages/tooling/pr-gate/src/scripts/workflow/provenance-enforcer.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawnSync } from 'child_process';\nimport * as path from 'path';\nimport {\n prDirFor, reviewJsonPath, RequiredChecklist, PrGateConfig, ReviewJsonService,\n SubagentProvenanceService, PROVENANCE_OK, PROVENANCE_MISSING, PROVENANCE_SKIPPED,\n ProvenanceResult, ReviewerEvidence, EvidenceRequest,\n ReviewProvenanceService, ProvenanceWriteRequest, ReviewerTranscript, ReviewerPaths, OfferedContext,\n ReviewerInstructionsService, InformAiError,\n} from '@webpieces/rules-config';\nimport { injectable, bindingScopeValues } from 'inversify';\nimport { AiBranchName } from './git-readAiBranchName';\n\n/**\n * The provenance outcome: whether each reviewer was VERIFIED to have run (the integrity check, which\n * blocks) plus what each one actually read (the quality signal, which is published). Data-only.\n */\nexport class ProvenanceReport {\n verified: boolean;\n evidence: ReviewerEvidence[];\n\n constructor(verified: boolean, evidence: ReviewerEvidence[]) {\n this.verified = verified;\n this.evidence = evidence;\n }\n}\n\n/**\n * Proves each checklist verdict came from its OWN independently-run reviewer subagent, and records what\n * each one read.\n *\n * Split out of FinishUpsertPrCommand when that file crossed the 700-line cap, and it is the right seam\n * twice over: this is a self-contained integrity check with its own spec\n * (`finish-provenance-record.spec.ts`), and that spec previously had to reach through\n * `command as unknown as { enforceProvenance(...) }` to get at it. Now `enforce` is simply public.\n *\n * `@injectable(bindingScopeValues.Singleton)` so it is drawn in the DI design and injected by type.\n */\n@injectable(bindingScopeValues.Singleton)\nexport class ProvenanceEnforcer {\n // eslint-disable-next-line @typescript-eslint/max-params\n constructor(\n private readonly aiBranchName: AiBranchName,\n private readonly provenance: SubagentProvenanceService,\n private readonly provenanceRecord: ReviewProvenanceService,\n private readonly reviewerInstructions: ReviewerInstructionsService,\n private readonly reviewJsonService: ReviewJsonService,\n ) {}\n\n /**\n * Archive this round's provenance.json alongside the archived review.json, so an archived review keeps\n * the transcript links belonging to the round that produced it — a record overwritten by the NEXT\n * round audits nothing.\n *\n * Here rather than in the command because this class owns the record; the command only knows WHEN to\n * archive, not what the record is.\n */\n archiveRecord(prDir: string): void {\n this.provenanceRecord.archive(prDir);\n }\n\n // HEAD sha for the audit record. Failure-tolerant ('' rather than a throw): a missing sha degrades\n // the audit trail, and refusing a PR over it would be the wrong trade — see writeProvenanceRecord.\n private gitOut(args: string[]): string {\n const result = spawnSync('git', args, { encoding: 'utf8' });\n return result.status === 0 ? (result.stdout ?? '').trim() : '';\n }\n\n // Enforce that EACH matched checklist was reviewed by its OWN named subagent, as a DISTINCT run —\n // the coding agent may not self-certify, and one reviewer may not stand in for several. A verified set\n // passes silently; no session id warns but passes; any missing reviewer throws so the PR does not open.\n // eslint-disable-next-line @typescript-eslint/max-params\n enforce(required: readonly RequiredChecklist[], branch: string, repoRoot: string, config: PrGateConfig): ProvenanceReport {\n const errors: string[] = [];\n const report = new ProvenanceReport(true, []); // no reviewers to verify ⇒ vacuously verified\n const subagents = required.map((r: RequiredChecklist): string => r.subagent.trim()).filter((s: string): boolean => s !== '');\n // verifyDistinct short-circuits to OK on an empty set, so this runs unconditionally: a repo with no\n // checklists still gets a provenance record naming the session and the main agent's own transcript.\n const result = this.provenance.verifyDistinct(subagents, branch);\n report.verified = result.status === PROVENANCE_OK;\n if (result.status === PROVENANCE_MISSING) {\n errors.push(result.detail);\n } else if (result.status === PROVENANCE_SKIPPED) {\n process.stderr.write(`⚠️ ${result.detail}\\n`);\n }\n report.evidence = this.gatherEvidence(repoRoot, required, result, branch);\n errors.push(...this.evidenceErrors(report.evidence, config));\n // BEFORE the throw below, deliberately. A refused round is the one most worth auditing, and a record\n // that only ever appeared on success could not answer what the reviewers did the time it was refused.\n this.writeProvenanceRecord(repoRoot, required, result, report.evidence);\n if (errors.length > 0) {\n throw new InformAiError(\n `${errors.length} checklist(s) require an independent reviewer subagent that did not run — fix, then re-run pnpm wp-finish-upsert-pr:\\n\\n` +\n errors.map((e: string): string => ` • ${e}`).join('\\n') +\n `\\n\\nSpawn the named reviewer subagent to review the checklist on THIS branch, then re-run.`,\n );\n }\n return report;\n }\n\n /**\n * What each credited reviewer actually read. Purely observational here — {@link evidenceErrors} decides\n * whether any of it blocks, and by default none of it does.\n */\n // eslint-disable-next-line @typescript-eslint/max-params\n private gatherEvidence(repoRoot: string, required: readonly RequiredChecklist[], result: ProvenanceResult, branch: string): ReviewerEvidence[] {\n const docPaths: Record<string, string> = {};\n for (const req of required) {\n if (req.subagent.trim() !== '') docPaths[req.subagent] = req.doc.trim() === '' ? '' : path.resolve(repoRoot, req.doc);\n }\n const diffDir = path.join(prDirFor(repoRoot, this.aiBranchName.getFeatureName()), 'diff');\n return this.provenance.evidenceFor(new EvidenceRequest(branch, result.agentIds, diffDir, docPaths));\n }\n\n /**\n * Write this round's audit record: `.webpieces/pr-review/<branch>/provenance.json`, linking each verdict\n * to the transcript of the subagent that produced it.\n *\n * A SEPARATE file rather than a field inside review.json / review-<id>.json, for two reasons. The\n * reviewer cannot supply this itself — a subagent's environment exposes the PARENT session id and no\n * agent id, so a self-reported transcript link would be invented — and keeping the AI-authored files\n * byte-untouched means nothing in the record can be mistaken for something a reviewer claimed about\n * itself. Every path here is derived by the tooling from the harness's own artifacts.\n *\n * Never fatal: an unwritable record is a lost audit trail, not a reason to refuse a PR.\n */\n // eslint-disable-next-line @typescript-eslint/max-params\n private writeProvenanceRecord(repoRoot: string, required: readonly RequiredChecklist[], result: ProvenanceResult, evidence: readonly ReviewerEvidence[]): void {\n const featureName = this.aiBranchName.getFeatureName();\n const prDir = prDirFor(repoRoot, featureName);\n const request = new ProvenanceWriteRequest(prDir, featureName, this.gitOut(['rev-parse', 'HEAD']), result.status);\n request.offered = new OfferedContext(\n path.join(prDir, 'diff'), this.reviewerInstructions.instructionsDirFor(repoRoot, featureName));\n request.reviewers = evidence.map((e: ReviewerEvidence): ReviewerTranscript =>\n new ReviewerTranscript(e, this.reviewerPathsFor(repoRoot, featureName, required, e.agentType)));\n const written = this.provenanceRecord.write(request);\n if (written !== '') process.stdout.write(` transcript provenance → ${written}\\n`);\n }\n\n // Where ONE reviewer's verdict, instructions and checklist doc live. Keyed by agentType, which IS the\n // checklist id: ChecklistDefinition sets `id = subagent`, so the two never diverge.\n // eslint-disable-next-line @typescript-eslint/max-params\n private reviewerPathsFor(repoRoot: string, featureName: string, required: readonly RequiredChecklist[], agentType: string): ReviewerPaths {\n const req = required.find((r: RequiredChecklist): boolean => r.subagent.trim() === agentType);\n const doc = req !== undefined && req.doc.trim() !== '' ? path.resolve(repoRoot, req.doc) : '';\n return new ReviewerPaths(\n this.reviewJsonService.checklistResultPath(reviewJsonPath(repoRoot, featureName), req?.id ?? agentType),\n this.reviewerInstructions.pathFor(repoRoot, featureName, agentType),\n doc,\n );\n }\n\n /**\n * WARN (default) or REFUSE (opt-in) on a reviewer that wrote a verdict without opening the diff.\n *\n * Default-warn because the signal is derived from undocumented Claude Code transcript internals: if the\n * format shifts, a blocking check wedges every PR in every consumer repo with no self-service recovery.\n * `requireDiffEvidence` lets a repo that has watched the warning promote it deliberately.\n */\n private evidenceErrors(evidence: readonly ReviewerEvidence[], config: PrGateConfig): string[] {\n const blind = evidence.filter((e: ReviewerEvidence): boolean => !e.readDiff);\n if (blind.length === 0) return [];\n const names = blind.map((e: ReviewerEvidence): string => e.agentType).join(', ');\n if (!config.requireDiffEvidence) {\n process.stderr.write(\n `\\n⚠️ ${blind.length} reviewer(s) wrote a verdict with no record of opening the extracted diff: ${names}\\n` +\n ' Published on the PR as a note. Not blocking — set pr-gate.requireDiffEvidence:true to make it one.\\n');\n return [];\n }\n return [`these reviewers wrote a verdict without opening the diff (pr-gate.requireDiffEvidence is on): ${names}`];\n }\n}\n"]}
|