@rmartz/pr-lifecycle 9.0.0 → 9.1.0

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.
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  runCli
4
- } from "../chunk-K5K64YZP.js";
4
+ } from "../chunk-4GTGNP35.js";
5
5
 
6
6
  // src/bin/pr-lifecycle.ts
7
7
  process.exitCode = await runCli(
@@ -184,7 +184,8 @@ function parseReconcile(args) {
184
184
  case "--pr":
185
185
  case "--trusted-authors":
186
186
  case "--hold-checks":
187
- case "--ignore-checks": {
187
+ case "--ignore-checks":
188
+ case "--prose-paths": {
188
189
  const value = args[index + 1];
189
190
  if (value === void 0 || value.startsWith("--")) {
190
191
  return usageError(`${arg} requires a value`);
@@ -212,6 +213,7 @@ function parseReconcile(args) {
212
213
  }
213
214
  const holdChecks = listOption(values["--hold-checks"]);
214
215
  const ignoredChecks = listOption(values["--ignore-checks"]);
216
+ const prosePatterns = listOption(values["--prose-paths"]);
215
217
  return {
216
218
  command: "reconcile",
217
219
  owner: repoMatch[1],
@@ -227,7 +229,8 @@ function parseReconcile(args) {
227
229
  },
228
230
  dryRun,
229
231
  json,
230
- tokenAdvisory
232
+ tokenAdvisory,
233
+ ...prosePatterns === void 0 ? {} : { prosePatterns }
231
234
  };
232
235
  }
233
236
  function listOption(value) {
@@ -909,6 +912,25 @@ async function updateBranch(release, target) {
909
912
  }
910
913
  }
911
914
 
915
+ // src/lineage/prose.ts
916
+ var DEFAULT_PROSE_PATTERNS = ["*.md", "!index.md"];
917
+ function compile(pattern) {
918
+ const escaped = pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replaceAll("*", ".*");
919
+ return new RegExp(`^${escaped}$`);
920
+ }
921
+ function proseMatcher(patterns) {
922
+ const include = patterns.filter((pattern) => !pattern.startsWith("!")).map(compile);
923
+ const exclude = patterns.filter((pattern) => pattern.startsWith("!")).map((pattern) => compile(pattern.slice(1)));
924
+ return (path) => {
925
+ const basename2 = path.slice(path.lastIndexOf("/") + 1);
926
+ return include.some((regex) => regex.test(basename2)) && !exclude.some((regex) => regex.test(basename2));
927
+ };
928
+ }
929
+ function overlappingProse(ours, theirs, isProse) {
930
+ const theirSet = new Set(theirs);
931
+ return ours.filter((path) => theirSet.has(path) && isProse(path)).sort();
932
+ }
933
+
912
934
  // src/lineage/git.ts
913
935
  import { execFile } from "child_process";
914
936
  var TIMEOUT_MS = 12e4;
@@ -1020,6 +1042,23 @@ async function createMergeVerifier(git, source) {
1020
1042
  ]);
1021
1043
  return merged.code === 0 && merged.stdout.split("\n")[0]?.trim() === step.tree;
1022
1044
  },
1045
+ async changedPaths(from, to) {
1046
+ if (!OBJECT_ID.test(from) || !OBJECT_ID.test(to)) {
1047
+ return void 0;
1048
+ }
1049
+ const diff = await git.run([
1050
+ "--git-dir",
1051
+ gitDir,
1052
+ "diff-tree",
1053
+ "-r",
1054
+ "-z",
1055
+ "--name-only",
1056
+ "--no-renames",
1057
+ from,
1058
+ to
1059
+ ]);
1060
+ return diff.code === 0 ? diff.stdout.split("\0").filter((path) => path !== "") : void 0;
1061
+ },
1023
1062
  async dispose() {
1024
1063
  await rm(gitDir, { recursive: true, force: true });
1025
1064
  }
@@ -1039,11 +1078,22 @@ async function onBaseBranch(client, commit, baseHeadSha) {
1039
1078
  const { status } = await client.compareCommits(commit, baseHeadSha);
1040
1079
  return status === "identical" || status === "ahead";
1041
1080
  }
1081
+ async function proseOverlapReason(verifier, step, isProse) {
1082
+ const ours = await verifier.changedPaths(step.mergeBase, step.first);
1083
+ const theirs = await verifier.changedPaths(step.mergeBase, step.second);
1084
+ if (ours === void 0 || theirs === void 0) {
1085
+ return "could not list the files each side changed";
1086
+ }
1087
+ const overlap = overlappingProse(ours, theirs, isProse);
1088
+ return overlap.length === 0 ? void 0 : `both sides edited the same prose (${overlap.join(", ")})`;
1089
+ }
1042
1090
  async function walk(client, git, input, wanted) {
1043
1091
  const verifier = await createMergeVerifier(git, input.source);
1044
1092
  if (verifier === void 0) {
1045
1093
  return { cleanAncestors: [], stoppedBecause: "git unavailable or older than 2.40" };
1046
1094
  }
1095
+ const patterns = input.prosePatterns ?? DEFAULT_PROSE_PATTERNS;
1096
+ const isProse = patterns.length === 0 ? void 0 : proseMatcher(patterns);
1047
1097
  const cleanAncestors = [];
1048
1098
  try {
1049
1099
  let current = input.headSha;
@@ -1057,15 +1107,14 @@ async function walk(client, git, input, wanted) {
1057
1107
  return { cleanAncestors, stoppedBecause: `${current} merges a commit not on the base` };
1058
1108
  }
1059
1109
  const { mergeBaseSha } = await client.compareCommits(first, second);
1060
- const clean = await verifier.verify({
1061
- mergeBase: mergeBaseSha,
1062
- first,
1063
- second,
1064
- tree: commit.treeSha
1065
- });
1066
- if (!clean) {
1110
+ const mergeStep = { mergeBase: mergeBaseSha, first, second, tree: commit.treeSha };
1111
+ if (!await verifier.verify(mergeStep)) {
1067
1112
  return { cleanAncestors, stoppedBecause: `${current} is not the clean automatic merge` };
1068
1113
  }
1114
+ const overlap = isProse === void 0 ? void 0 : await proseOverlapReason(verifier, mergeStep, isProse);
1115
+ if (overlap !== void 0) {
1116
+ return { cleanAncestors, stoppedBecause: `${current}: ${overlap}` };
1117
+ }
1069
1118
  cleanAncestors.push(first);
1070
1119
  current = first;
1071
1120
  if ([...wanted].every((sha) => cleanAncestors.includes(sha))) {
@@ -1262,7 +1311,8 @@ async function gatherLineageFor(client, pull, reviews, options) {
1262
1311
  url: pull.cloneUrl,
1263
1312
  ...options.lineage.token === void 0 ? {} : { token: options.lineage.token }
1264
1313
  },
1265
- reviews
1314
+ reviews,
1315
+ ...options.lineage.prosePatterns === void 0 ? {} : { prosePatterns: options.lineage.prosePatterns }
1266
1316
  });
1267
1317
  } catch (error) {
1268
1318
  const message = error instanceof Error ? error.message : String(error);
@@ -1423,6 +1473,8 @@ reconcile options:
1423
1473
  --skip-copilot-review Don't wait for requested bot reviewers (deprecated)
1424
1474
  --hold-checks <a,b> Required checks whose pending is a hold (default: pr-policy)
1425
1475
  --ignore-checks <a,b> Required checks CI never counts (default: merge-safety)
1476
+ --prose-paths <a,b> Basename patterns whose overlapping edits in a base merge
1477
+ need a new review (default: *.md,!index.md)
1426
1478
  --dry-run Compute the plan without writing anything
1427
1479
  --json Print one JSON object (schemaVersion 1) on stdout
1428
1480
  --no-token-advisory Don't comment on the PR when arming lacks a release token
@@ -1466,7 +1518,11 @@ async function runCli(argv, io, deps) {
1466
1518
  try {
1467
1519
  const result2 = await reconcilePullRequest(client, args.pr, args.policy, {
1468
1520
  dryRun: args.dryRun,
1469
- lineage: { git: deps.git ?? createGitRunner(), token },
1521
+ lineage: {
1522
+ git: deps.git ?? createGitRunner(),
1523
+ token,
1524
+ ...args.prosePatterns === void 0 ? {} : { prosePatterns: args.prosePatterns }
1525
+ },
1470
1526
  // Only arming, merging, and updating use the release token (never reads).
1471
1527
  // Without one they are skipped, not done with GITHUB_TOKEN; see docs/cli.md.
1472
1528
  release: hasReleaseToken ? clientFor(releaseToken) : "unavailable"
@@ -1546,6 +1602,9 @@ export {
1546
1602
  isRebasePending,
1547
1603
  OWNED_LABEL_DEFINITIONS,
1548
1604
  executePlan,
1605
+ DEFAULT_PROSE_PATTERNS,
1606
+ proseMatcher,
1607
+ overlappingProse,
1549
1608
  createGitRunner,
1550
1609
  parseGitVersion,
1551
1610
  createMergeVerifier,
package/dist/index.d.ts CHANGED
@@ -420,7 +420,7 @@ interface CliDeps {
420
420
  /** Runs git for approval carry-over; tests inject one. Defaults to the real binary. */
421
421
  git?: GitRunner;
422
422
  }
423
- declare const USAGE = "Usage: ai-pr-lifecycle <command> [options]\n\nCommands:\n reconcile Recompute a PR's lifecycle state and converge its labels\n help Show this message\n\nreconcile options:\n --repo <owner/repo> Repository (required)\n --pr <number> Pull request number (required)\n --arm-auto-merge Arm/disarm native auto-merge from the state\n --auto-update Update approved PRs merge-safety flags 'update required'\n (Dependabot PRs are asked to rebase, never updated)\n --trusted-authors <a,b> Only these logins (with write access) may cast verdicts\n --skip-copilot-review Don't wait for requested bot reviewers (deprecated)\n --hold-checks <a,b> Required checks whose pending is a hold (default: pr-policy)\n --ignore-checks <a,b> Required checks CI never counts (default: merge-safety)\n --dry-run Compute the plan without writing anything\n --json Print one JSON object (schemaVersion 1) on stdout\n --no-token-advisory Don't comment on the PR when arming lacks a release token\n\nEnvironment:\n GITHUB_TOKEN Token for reads and writes (required)\n PR_LIFECYCLE_TOKEN\n Real-actor token for arming, merging, and updating;\n without it, labels are kept but those are skipped\n GITHUB_API_URL API base URL (GitHub Enterprise Server)";
423
+ declare const USAGE = "Usage: ai-pr-lifecycle <command> [options]\n\nCommands:\n reconcile Recompute a PR's lifecycle state and converge its labels\n help Show this message\n\nreconcile options:\n --repo <owner/repo> Repository (required)\n --pr <number> Pull request number (required)\n --arm-auto-merge Arm/disarm native auto-merge from the state\n --auto-update Update approved PRs merge-safety flags 'update required'\n (Dependabot PRs are asked to rebase, never updated)\n --trusted-authors <a,b> Only these logins (with write access) may cast verdicts\n --skip-copilot-review Don't wait for requested bot reviewers (deprecated)\n --hold-checks <a,b> Required checks whose pending is a hold (default: pr-policy)\n --ignore-checks <a,b> Required checks CI never counts (default: merge-safety)\n --prose-paths <a,b> Basename patterns whose overlapping edits in a base merge\n need a new review (default: *.md,!index.md)\n --dry-run Compute the plan without writing anything\n --json Print one JSON object (schemaVersion 1) on stdout\n --no-token-advisory Don't comment on the PR when arming lacks a release token\n\nEnvironment:\n GITHUB_TOKEN Token for reads and writes (required)\n PR_LIFECYCLE_TOKEN\n Real-actor token for arming, merging, and updating;\n without it, labels are kept but those are skipped\n GITHUB_API_URL API base URL (GitHub Enterprise Server)";
424
424
  declare function runCli(argv: readonly string[], io: CliIo, deps: CliDeps): Promise<number>;
425
425
 
426
426
  /**
@@ -438,6 +438,8 @@ interface ReconcileArgs {
438
438
  json: boolean;
439
439
  /** Comment on the PR when arming is skipped for want of a release token. */
440
440
  tokenAdvisory: boolean;
441
+ /** Prose basename patterns for carry-over; absent means the default. */
442
+ prosePatterns?: string[];
441
443
  }
442
444
  type ParsedArgs = ReconcileArgs | {
443
445
  command: 'help';
@@ -515,6 +517,11 @@ interface RepoSource {
515
517
  interface MergeVerifier {
516
518
  /** True only when the step is a verified clean merge; false on any doubt. */
517
519
  verify(step: MergeStep): Promise<boolean>;
520
+ /**
521
+ * Paths that differ between two commits a `verify` call already fetched;
522
+ * undefined on any error, so the caller can fail closed.
523
+ */
524
+ changedPaths(from: string, to: string): Promise<string[] | undefined>;
518
525
  dispose(): Promise<void>;
519
526
  }
520
527
  /**
@@ -528,7 +535,9 @@ declare function createMergeVerifier(git: GitRunner, source: RepoSource): Promis
528
535
  * Finds the head's clean ancestors: commits reached by walking back from the head
529
536
  * through a chain of **verified clean base merges**, so reviews bound to them
530
537
  * still describe the change being merged (docs/reconciler-design.md §Approval
531
- * carry-over). Fails closed: any doubt or error ends the chain, and the cost is
538
+ * carry-over). A clean merge in which both sides edited the same prose file also
539
+ * ends the chain: nothing automatic checks that the merged page still reads
540
+ * correctly. Fails closed: any doubt or error ends the chain, and the cost is
532
541
  * an extra review, never a false approval.
533
542
  */
534
543
  /** A longer chain of base merges than this is unusual enough to review again. */
@@ -540,6 +549,11 @@ interface LineageInput {
540
549
  baseHeadSha: string;
541
550
  source: RepoSource;
542
551
  reviews: readonly ReviewData[];
552
+ /**
553
+ * Basename patterns for prose files (see src/lineage/prose.ts); default
554
+ * `DEFAULT_PROSE_PATTERNS`. Empty turns the prose-overlap check off.
555
+ */
556
+ prosePatterns?: readonly string[];
543
557
  }
544
558
  interface Lineage {
545
559
  cleanAncestors: string[];
@@ -573,6 +587,7 @@ interface GatherOptions {
573
587
  lineage?: {
574
588
  git: GitRunner;
575
589
  token?: string;
590
+ prosePatterns?: readonly string[];
576
591
  };
577
592
  }
578
593
  declare function gatherFacts(client: GitHubClient, pr: number, policy?: ReconcilePolicy, options?: GatherOptions): Promise<GatheredPullRequest>;
@@ -748,6 +763,22 @@ declare function buildTokenAdvisoryBody(skipped: 'arm' | 'merge'): string;
748
763
  */
749
764
  declare function postTokenAdvisory(client: GitHubClient, pr: number, skipped: 'arm' | 'merge'): Promise<boolean>;
750
765
 
766
+ /**
767
+ * Which files are prose for approval carry-over: a clean base merge in which both
768
+ * sides edited the same prose file doesn't carry an approval over, because no CI
769
+ * check can tell whether two concurrent edits to a page still read correctly
770
+ * together (docs/reconciler-design.md §Approval carry-over).
771
+ *
772
+ * Patterns match a file's **basename** with `*` as the only wildcard; a leading
773
+ * `!` excludes. A file is prose when it matches some pattern and no exclusion.
774
+ */
775
+ /** Every Markdown file except index pages, which take routine concurrent appends. */
776
+ declare const DEFAULT_PROSE_PATTERNS: readonly string[];
777
+ type ProseMatcher = (path: string) => boolean;
778
+ declare function proseMatcher(patterns: readonly string[]): ProseMatcher;
779
+ /** Prose paths changed on both sides of a merge, sorted. */
780
+ declare function overlappingProse(ours: readonly string[], theirs: readonly string[], isProse: ProseMatcher): string[];
781
+
751
782
  /**
752
783
  * Verdict parsing and trust. A review is a verdict when it carries a /review
753
784
  * `skill-meta` marker with a verdict outcome, or (with no marker) a native
@@ -768,4 +799,4 @@ declare function isTrustedAuthor(author: ReviewAuthor, policy: ReconcilePolicy):
768
799
  /** The latest verdict that counts for the PR's current head, if any. */
769
800
  declare function currentVerdict(facts: PullRequestFacts, policy: ReconcilePolicy): Verdict | undefined;
770
801
 
771
- export { ACTOR_TYPES, AUTO_MERGE_LABEL, type ActorType, type AutoMergeAction, BRANCH_UPDATERS, type BotEligibility, type BotPrCommit, type BotPrFacts, type BotPrType, type BranchUpdater, type ChangedFile, type CliDeps, type CliIo, type CollaboratorPermission, type CommitData, DEFAULT_SETTLE_MS, DEPENDABOT_LOGIN, DEPENDABOT_REBASE_COMMAND, DEPENDABOT_REBASING_LABEL, DEPENDABOT_REBASING_NOTICE, DEPENDABOT_RECREATING_NOTICE, DEPENDABOT_UPDATE_TYPES, type DependabotUpdateType, type GatherOptions, type GatheredPullRequest, GitHubApiError, type GitHubClient, type GitResult, type GitRunner, type HttpClientOptions, LIFECYCLE_LABELS, LIFECYCLE_STATES, type LabelDefinition, type LifecycleLabel, type LifecycleState, type Lineage, type LineageInput, MAX_CHAIN_STEPS, type MergeStep, type MergeVerifier, OWNED_LABEL_DEFINITIONS, PR_STATUSES, type ParsedArgs, type ParsedVerdict, type PlanTarget, type PullRequestData, type PullRequestFacts, type PullRequestStatus, RELEASE_PLEASE_BRANCH_PREFIX, RELEASE_TOKEN_DOCS_URL, REPO_PERMISSIONS, REVIEW_STATES, type ReconcileArgs, type ReconcileJson, type ReconcileOptions, type ReconcilePlan, type ReconcilePolicy, type ReconcileResult, type ReconcileTarget, type ReleaseActions, type ReleaseDiffVerdict, type RepoPermission, type RepoSource, type ReviewAuthor, type ReviewData, type ReviewFact, type ReviewState, SCHEMA_VERSION, type SettleOptions, TOKEN_ADVISORY_MARKER, UPDATE_REQUIRED_LABEL, USAGE, type UpdateAction, VERDICTS, type Verdict, buildRebaseRequestBody, buildTokenAdvisoryBody, classifyBotPr, classifyReleaseDiff, computeState, createGitRunner, createHttpClient, createMergeVerifier, currentVerdict, executePlan, formatSummary, gatherFacts, gatherLineage, isApiStatus, isDependabotRebasing, isRebasePending, isTrustedAuthor, isVersionOnlyPatch, parseArgs, parseDependabotUpdateType, parseGitVersion, parseVerdict, planReconcile, postTokenAdvisory, rebaseRequestMarker, reconcilePullRequest, runCli, toReconcileJson, withoutReleaseActions };
802
+ export { ACTOR_TYPES, AUTO_MERGE_LABEL, type ActorType, type AutoMergeAction, BRANCH_UPDATERS, type BotEligibility, type BotPrCommit, type BotPrFacts, type BotPrType, type BranchUpdater, type ChangedFile, type CliDeps, type CliIo, type CollaboratorPermission, type CommitData, DEFAULT_PROSE_PATTERNS, DEFAULT_SETTLE_MS, DEPENDABOT_LOGIN, DEPENDABOT_REBASE_COMMAND, DEPENDABOT_REBASING_LABEL, DEPENDABOT_REBASING_NOTICE, DEPENDABOT_RECREATING_NOTICE, DEPENDABOT_UPDATE_TYPES, type DependabotUpdateType, type GatherOptions, type GatheredPullRequest, GitHubApiError, type GitHubClient, type GitResult, type GitRunner, type HttpClientOptions, LIFECYCLE_LABELS, LIFECYCLE_STATES, type LabelDefinition, type LifecycleLabel, type LifecycleState, type Lineage, type LineageInput, MAX_CHAIN_STEPS, type MergeStep, type MergeVerifier, OWNED_LABEL_DEFINITIONS, PR_STATUSES, type ParsedArgs, type ParsedVerdict, type PlanTarget, type ProseMatcher, type PullRequestData, type PullRequestFacts, type PullRequestStatus, RELEASE_PLEASE_BRANCH_PREFIX, RELEASE_TOKEN_DOCS_URL, REPO_PERMISSIONS, REVIEW_STATES, type ReconcileArgs, type ReconcileJson, type ReconcileOptions, type ReconcilePlan, type ReconcilePolicy, type ReconcileResult, type ReconcileTarget, type ReleaseActions, type ReleaseDiffVerdict, type RepoPermission, type RepoSource, type ReviewAuthor, type ReviewData, type ReviewFact, type ReviewState, SCHEMA_VERSION, type SettleOptions, TOKEN_ADVISORY_MARKER, UPDATE_REQUIRED_LABEL, USAGE, type UpdateAction, VERDICTS, type Verdict, buildRebaseRequestBody, buildTokenAdvisoryBody, classifyBotPr, classifyReleaseDiff, computeState, createGitRunner, createHttpClient, createMergeVerifier, currentVerdict, executePlan, formatSummary, gatherFacts, gatherLineage, isApiStatus, isDependabotRebasing, isRebasePending, isTrustedAuthor, isVersionOnlyPatch, overlappingProse, parseArgs, parseDependabotUpdateType, parseGitVersion, parseVerdict, planReconcile, postTokenAdvisory, proseMatcher, rebaseRequestMarker, reconcilePullRequest, runCli, toReconcileJson, withoutReleaseActions };
package/dist/index.js CHANGED
@@ -2,6 +2,7 @@ import {
2
2
  ACTOR_TYPES,
3
3
  AUTO_MERGE_LABEL,
4
4
  BRANCH_UPDATERS,
5
+ DEFAULT_PROSE_PATTERNS,
5
6
  DEFAULT_SETTLE_MS,
6
7
  DEPENDABOT_LOGIN,
7
8
  DEPENDABOT_REBASE_COMMAND,
@@ -42,22 +43,25 @@ import {
42
43
  isRebasePending,
43
44
  isTrustedAuthor,
44
45
  isVersionOnlyPatch,
46
+ overlappingProse,
45
47
  parseArgs,
46
48
  parseDependabotUpdateType,
47
49
  parseGitVersion,
48
50
  parseVerdict,
49
51
  planReconcile,
50
52
  postTokenAdvisory,
53
+ proseMatcher,
51
54
  rebaseRequestMarker,
52
55
  reconcilePullRequest,
53
56
  runCli,
54
57
  toReconcileJson,
55
58
  withoutReleaseActions
56
- } from "./chunk-K5K64YZP.js";
59
+ } from "./chunk-4GTGNP35.js";
57
60
  export {
58
61
  ACTOR_TYPES,
59
62
  AUTO_MERGE_LABEL,
60
63
  BRANCH_UPDATERS,
64
+ DEFAULT_PROSE_PATTERNS,
61
65
  DEFAULT_SETTLE_MS,
62
66
  DEPENDABOT_LOGIN,
63
67
  DEPENDABOT_REBASE_COMMAND,
@@ -98,12 +102,14 @@ export {
98
102
  isRebasePending,
99
103
  isTrustedAuthor,
100
104
  isVersionOnlyPatch,
105
+ overlappingProse,
101
106
  parseArgs,
102
107
  parseDependabotUpdateType,
103
108
  parseGitVersion,
104
109
  parseVerdict,
105
110
  planReconcile,
106
111
  postTokenAdvisory,
112
+ proseMatcher,
107
113
  rebaseRequestMarker,
108
114
  reconcilePullRequest,
109
115
  runCli,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rmartz/pr-lifecycle",
3
- "version": "9.0.0",
3
+ "version": "9.1.0",
4
4
  "description": "Event-driven PR lifecycle reconciler: recomputes a pull request's lifecycle labels from its current facts on every GitHub event and arms native auto-merge on approval.",
5
5
  "keywords": [
6
6
  "ci",