@patronage/software-factory 1.0.0-alpha.10 → 1.0.0-alpha.13

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/dist/index.d.ts CHANGED
@@ -743,8 +743,8 @@ declare const mergeFreezeStateSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
743
743
  generationId: z.ZodNumber;
744
744
  headSha: z.ZodString;
745
745
  outcome: z.ZodEnum<{
746
- stale: "stale";
747
746
  active: "active";
747
+ stale: "stale";
748
748
  }>;
749
749
  reason: z.ZodString;
750
750
  recordedAt: z.ZodISODateTime;
@@ -1243,8 +1243,8 @@ declare const managedReadinessLedgerSchema: z.ZodObject<{
1243
1243
  status: z.ZodEnum<{
1244
1244
  blocked: "blocked";
1245
1245
  "not-required": "not-required";
1246
- current: "current";
1247
1246
  stale: "stale";
1247
+ current: "current";
1248
1248
  missing: "missing";
1249
1249
  }>;
1250
1250
  }, z.core.$strip>;
@@ -1255,8 +1255,8 @@ declare const managedReadinessLedgerSchema: z.ZodObject<{
1255
1255
  status: z.ZodEnum<{
1256
1256
  blocked: "blocked";
1257
1257
  "not-required": "not-required";
1258
- current: "current";
1259
1258
  stale: "stale";
1259
+ current: "current";
1260
1260
  missing: "missing";
1261
1261
  }>;
1262
1262
  }, z.core.$strip>>;
@@ -1267,8 +1267,8 @@ declare const managedReadinessLedgerSchema: z.ZodObject<{
1267
1267
  docsOnlyDeltaAccepted: z.ZodOptional<z.ZodBoolean>;
1268
1268
  docsOnlyVerifiedHeadSha: z.ZodOptional<z.ZodString>;
1269
1269
  prVerify: z.ZodEnum<{
1270
- passed: "passed";
1271
1270
  stale: "stale";
1271
+ passed: "passed";
1272
1272
  missing: "missing";
1273
1273
  }>;
1274
1274
  trivialDeltaAccepted: z.ZodOptional<z.ZodBoolean>;
@@ -1772,8 +1772,8 @@ declare const REVIEW_STATUS_VALUES: readonly ["not-required", "current", "stale"
1772
1772
  declare const reviewStatusSchema: z.ZodEnum<{
1773
1773
  blocked: "blocked";
1774
1774
  "not-required": "not-required";
1775
- current: "current";
1776
1775
  stale: "stale";
1776
+ current: "current";
1777
1777
  missing: "missing";
1778
1778
  }>;
1779
1779
  type ReviewStatus = z.infer<typeof reviewStatusSchema>;
@@ -4074,13 +4074,13 @@ declare const evaluateReadiness: (input: EvaluationInput) => {
4074
4074
  reviews: {
4075
4075
  correctness: {
4076
4076
  required: boolean;
4077
- status: "blocked" | "not-required" | "current" | "stale" | "missing";
4077
+ status: "blocked" | "not-required" | "stale" | "current" | "missing";
4078
4078
  reviewedHeadSha?: string | undefined;
4079
4079
  reviewedPatchId?: string | undefined;
4080
4080
  };
4081
4081
  security?: {
4082
4082
  required: boolean;
4083
- status: "blocked" | "not-required" | "current" | "stale" | "missing";
4083
+ status: "blocked" | "not-required" | "stale" | "current" | "missing";
4084
4084
  reviewedHeadSha?: string | undefined;
4085
4085
  reviewedPatchId?: string | undefined;
4086
4086
  } | undefined;
@@ -4088,7 +4088,7 @@ declare const evaluateReadiness: (input: EvaluationInput) => {
4088
4088
  schemaVersion: 1;
4089
4089
  verification: {
4090
4090
  command: "patronage-factory pr:verify";
4091
- prVerify: "passed" | "stale" | "missing";
4091
+ prVerify: "stale" | "passed" | "missing";
4092
4092
  docsOnlyDeltaAccepted?: boolean | undefined;
4093
4093
  docsOnlyVerifiedHeadSha?: string | undefined;
4094
4094
  trivialDeltaAccepted?: boolean | undefined;
package/dist/index.js CHANGED
@@ -17,7 +17,7 @@ import { parse } from "yaml";
17
17
  import { promisify } from "node:util";
18
18
  import { FACTORY_PROOF_GATE_STEP_NAME, GitHubApiError, mintInstallationToken, productionImpactTargetOutput, readVitestProfileDocument } from "@patronage/factory-ci";
19
19
  //#region package.json
20
- var version = "1.0.0-alpha.10";
20
+ var version = "1.0.0-alpha.13";
21
21
  //#endregion
22
22
  //#region src/review-rungs.ts
23
23
  const EVIDENCE_REVIEW_RUNGS$1 = [
@@ -268,8 +268,9 @@ const pullRequestTopologyReasons = ({ manifest, pullRequest }) => {
268
268
  };
269
269
  /**
270
270
  * Validate the complete declared member set at boundary closeout. Callers
271
- * supply only issue-linked and explicit-override PRs; branch names classify
272
- * those authoritative members' roles but never discover membership.
271
+ * supply only issue-linked PRs and unlinked manifest-fallback PRs; branch
272
+ * names classify those authoritative members' roles but never discover
273
+ * membership.
273
274
  */
274
275
  const boundaryTopologyReasons = ({ manifest, pullRequests }) => {
275
276
  const integrationBranch = eachToEpicBranch(manifest);
@@ -5319,6 +5320,24 @@ z.object({
5319
5320
  //#endregion
5320
5321
  //#region src/boundary-check.ts
5321
5322
  const DEFAULT_BOUNDARY_CHECK_PROOF_PATH = ".factory-memory/boundary-check.json";
5323
+ const liveMembershipLabel = (entry) => entry.issue === void 0 ? `PR #${entry.pr} (wave ${entry.wave})` : `PR #${entry.pr} membership for issue #${entry.issue} (wave ${entry.wave})`;
5324
+ const removedMembershipMessage = (reviewed) => reviewed.issue === void 0 ? `PR #${reviewed.pr} was covered by the proof but is no longer in the delivery boundary's live covered set` : `PR #${reviewed.pr} membership for issue #${reviewed.issue} was covered by the proof but is no longer in the delivery boundary's live covered set`;
5325
+ const coveredEntriesByPr = (source) => {
5326
+ const byPr = /* @__PURE__ */ new Map();
5327
+ for (const entry of source) {
5328
+ const entries = byPr.get(entry.pr);
5329
+ if (entries) entries.push(entry);
5330
+ else byPr.set(entry.pr, [entry]);
5331
+ }
5332
+ return byPr;
5333
+ };
5334
+ const contributionDivergences = (entry, reviewed) => {
5335
+ const label = liveMembershipLabel(entry);
5336
+ if (reviewed.state !== entry.state) return [`${label} changed state since the proof (reviewed ${reviewed.state}, now ${entry.state})`];
5337
+ const reviewedSha = reviewed.mergedSha ?? reviewed.headSha;
5338
+ if (!reviewedSha || !sameHeadSha(reviewedSha, entry.sha)) return [entry.state === "open" ? `${label} open head moved since the proof (reviewed ${reviewedSha ?? "unknown"}, now ${entry.sha})` : `${label} merged SHA does not match the proof (reviewed ${reviewedSha ?? "unknown"}, now ${entry.sha})`];
5339
+ return [];
5340
+ };
5322
5341
  const CLOSING_PRS_QUERY = `query($owner: String!, $repo: String!, $issue: Int!) {
5323
5342
  repository(owner: $owner, name: $repo) {
5324
5343
  issue(number: $issue) {
@@ -5420,49 +5439,65 @@ const coveredEntryFromRef = (ref, wave, issue, notices) => {
5420
5439
  };
5421
5440
  const computeCoveredSet = (manifest, repo, github) => {
5422
5441
  const notices = [];
5423
- const byPr = /* @__PURE__ */ new Map();
5424
- const pullRequests = /* @__PURE__ */ new Map();
5442
+ const entries = [];
5443
+ const linkedPrs = /* @__PURE__ */ new Set();
5444
+ const pullRequests = [];
5425
5445
  for (const wave of manifest.waves) for (const issue of wave.issues) {
5426
5446
  const refs = github.fetchClosingPullRequests(repo, issue);
5427
5447
  if (refs.length === 0) notices.push(`issue #${issue} (wave ${wave.name}) has no linked closing PR yet`);
5428
5448
  for (const ref of refs) {
5429
- if (!pullRequests.has(ref.number)) pullRequests.set(ref.number, ref);
5449
+ pullRequests.push(ref);
5450
+ linkedPrs.add(ref.number);
5430
5451
  const entry = coveredEntryFromRef(ref, wave.name, issue, notices);
5431
- if (entry && !byPr.has(entry.pr)) byPr.set(entry.pr, entry);
5452
+ if (entry) entries.push(entry);
5432
5453
  }
5433
5454
  }
5434
5455
  for (const [prKey, waveName] of Object.entries(manifest.prs ?? {})) {
5435
5456
  const prNumber = Number(prKey);
5436
- if (byPr.has(prNumber)) continue;
5457
+ if (linkedPrs.has(prNumber)) continue;
5437
5458
  const ref = github.fetchPullRequest(repo, prNumber);
5438
- pullRequests.set(prNumber, ref);
5459
+ pullRequests.push(ref);
5439
5460
  const entry = coveredEntryFromRef(ref, waveName, void 0, notices);
5440
- if (entry) byPr.set(entry.pr, entry);
5461
+ if (entry) entries.push(entry);
5441
5462
  }
5442
5463
  return {
5443
- entries: [...byPr.values()].toSorted((a, b) => a.pr - b.pr),
5464
+ entries: entries.toSorted((a, b) => a.pr - b.pr || (a.issue ?? 0) - (b.issue ?? 0)),
5444
5465
  notices,
5445
- pullRequests: [...pullRequests.values()].toSorted((a, b) => a.number - b.number)
5466
+ pullRequests: pullRequests.toSorted((a, b) => a.number - b.number)
5446
5467
  };
5447
5468
  };
5448
5469
  const coveredSetDivergences = (computed, proof) => {
5449
5470
  const divergences = [];
5450
- const proofByPr = new Map(proof.coveredSet.map((entry) => [entry.pr, entry]));
5451
- for (const entry of computed) {
5452
- const reviewed = proofByPr.get(entry.pr);
5453
- if (!reviewed) {
5454
- divergences.push(`PR #${entry.pr} (wave ${entry.wave}) joined the boundary after the proof was minted`);
5471
+ const computedByPr = coveredEntriesByPr(computed);
5472
+ const proofByPr = coveredEntriesByPr(proof.coveredSet);
5473
+ for (const [pr, entries] of computedByPr) {
5474
+ const reviewedEntries = proofByPr.get(pr) ?? [];
5475
+ if (reviewedEntries.length === 0) {
5476
+ divergences.push(...entries.map((entry) => `${liveMembershipLabel(entry)} joined the delivery boundary after the proof was minted`));
5455
5477
  continue;
5456
5478
  }
5457
- if (reviewed.state !== entry.state) {
5458
- divergences.push(`PR #${entry.pr} changed state since the proof (reviewed ${reviewed.state}, now ${entry.state})`);
5479
+ const [firstEntry] = entries;
5480
+ const [firstReviewed] = reviewedEntries;
5481
+ if (!(entries.length > 1 || reviewedEntries.length > 1 || firstEntry?.issue !== firstReviewed?.issue)) {
5482
+ if (firstEntry && firstReviewed) divergences.push(...contributionDivergences(firstEntry, firstReviewed));
5459
5483
  continue;
5460
5484
  }
5461
- const reviewedSha = reviewed.mergedSha ?? reviewed.headSha;
5462
- if (!reviewedSha || !sameHeadSha(reviewedSha, entry.sha)) divergences.push(entry.state === "open" ? `PR #${entry.pr} open head moved since the proof (reviewed ${reviewedSha ?? "unknown"}, now ${entry.sha})` : `PR #${entry.pr} merged SHA does not match the proof (reviewed ${reviewedSha ?? "unknown"}, now ${entry.sha})`);
5485
+ const unmatchedReviewed = [...reviewedEntries];
5486
+ for (const entry of entries) {
5487
+ const reviewedIndex = unmatchedReviewed.findIndex((candidate) => candidate.issue === entry.issue);
5488
+ if (reviewedIndex === -1) {
5489
+ divergences.push(`${liveMembershipLabel(entry)} joined the delivery boundary after the proof was minted`);
5490
+ continue;
5491
+ }
5492
+ const [reviewed] = unmatchedReviewed.splice(reviewedIndex, 1);
5493
+ if (reviewed) divergences.push(...contributionDivergences(entry, reviewed));
5494
+ }
5495
+ for (const reviewed of unmatchedReviewed) divergences.push(removedMembershipMessage(reviewed));
5496
+ }
5497
+ for (const [pr, reviewedEntries] of proofByPr) {
5498
+ if (computedByPr.has(pr)) continue;
5499
+ divergences.push(...reviewedEntries.map(removedMembershipMessage));
5463
5500
  }
5464
- const computedPrs = new Set(computed.map((entry) => entry.pr));
5465
- for (const reviewed of proof.coveredSet) if (!computedPrs.has(reviewed.pr)) divergences.push(`PR #${reviewed.pr} was covered by the proof but is no longer in the boundary's live covered set`);
5466
5501
  return divergences;
5467
5502
  };
5468
5503
  const defaultResolveRepo = (cwd) => runCapture("gh", [
@@ -12087,24 +12122,12 @@ const resolveCandidateDemands = ({ changedFiles, profile }) => ({
12087
12122
  });
12088
12123
  /**
12089
12124
  * The wave demand that binds one PR under a boundary manifest — moved from the
12090
- * private resolution inside `pr:merge-check` (#351). A manifest `prs` override
12091
- * wins; otherwise the PR must close exactly one issue in exactly one wave.
12092
- * Every refusal is a named reason, fail closed.
12125
+ * private resolution inside `pr:merge-check` (#351). Closing issue links are
12126
+ * authoritative: the PR must close exactly one declared issue in exactly one
12127
+ * wave. A manifest `prs` entry is consulted only when no declared closing link
12128
+ * exists. Every refusal is a named reason, fail closed.
12093
12129
  */
12094
12130
  const waveDemandForPullRequest = ({ fetchClosingPullRequests, manifest, owner, pr, repo }) => {
12095
- const override = manifest.prs?.[String(pr)];
12096
- if (override) {
12097
- const wave = manifest.waves.find((candidate) => candidate.name === override);
12098
- if (!wave) return { reasons: [`PR #${pr} override names unknown wave "${override}".`] };
12099
- return {
12100
- demand: {
12101
- autoMerge: wave.autoMerge === true,
12102
- review: wave.review,
12103
- wave: wave.name
12104
- },
12105
- reasons: []
12106
- };
12107
- }
12108
12131
  const matches = manifest.waves.flatMap((wave) => wave.issues.filter((issue) => fetchClosingPullRequests({
12109
12132
  issue,
12110
12133
  owner,
@@ -12113,10 +12136,9 @@ const waveDemandForPullRequest = ({ fetchClosingPullRequests, manifest, owner, p
12113
12136
  issue,
12114
12137
  wave
12115
12138
  })));
12116
- if (matches.length !== 1) return { reasons: [`PR #${pr} must close exactly one issue in exactly one boundary wave; found ${matches.length} matching issue${matches.length === 1 ? "" : "s"}.`] };
12139
+ if (matches.length > 1) return { reasons: [`PR #${pr} must close exactly one issue in exactly one boundary wave; found ${matches.length} matching issue${matches.length === 1 ? "" : "s"}.`] };
12117
12140
  const [match] = matches;
12118
- if (!match) return { reasons: [`PR #${pr} has no boundary-wave membership.`] };
12119
- return {
12141
+ if (match) return {
12120
12142
  demand: {
12121
12143
  autoMerge: match.wave.autoMerge === true,
12122
12144
  review: match.wave.review,
@@ -12124,6 +12146,18 @@ const waveDemandForPullRequest = ({ fetchClosingPullRequests, manifest, owner, p
12124
12146
  },
12125
12147
  reasons: []
12126
12148
  };
12149
+ const fallback = manifest.prs?.[String(pr)];
12150
+ if (!fallback) return { reasons: [`PR #${pr} must close exactly one issue in exactly one boundary wave; found 0 matching issues.`] };
12151
+ const wave = manifest.waves.find((candidate) => candidate.name === fallback);
12152
+ if (!wave) return { reasons: [`PR #${pr} fallback names unknown wave "${fallback}".`] };
12153
+ return {
12154
+ demand: {
12155
+ autoMerge: wave.autoMerge === true,
12156
+ review: wave.review,
12157
+ wave: wave.name
12158
+ },
12159
+ reasons: []
12160
+ };
12127
12161
  };
12128
12162
  const closingPullRequestsQuery = `query($owner: String!, $repo: String!, $issue: Int!) {
12129
12163
  repository(owner: $owner, name: $repo) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@patronage/software-factory",
3
- "version": "1.0.0-alpha.10",
3
+ "version": "1.0.0-alpha.13",
4
4
  "description": "Shared Patronage software factory CLI and project-profile validation tools",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -37,7 +37,7 @@
37
37
  "picomatch": "^4.0.5",
38
38
  "yaml": "^2.9.0",
39
39
  "zod": "4.4.3",
40
- "@patronage/factory-ci": "1.0.0-alpha.10"
40
+ "@patronage/factory-ci": "1.0.0-alpha.13"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@types/node": "24.13.3",