@patronage/software-factory 1.0.0-alpha.42 → 1.0.0-alpha.44

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
@@ -239,8 +239,8 @@ declare const boundaryReviewProofBaseSchema: z.ZodObject<{
239
239
  mergedSha: z.ZodOptional<z.ZodString>;
240
240
  pr: z.ZodNumber;
241
241
  state: z.ZodEnum<{
242
- open: "open";
243
242
  merged: "merged";
243
+ open: "open";
244
244
  }>;
245
245
  wave: z.ZodOptional<z.ZodString>;
246
246
  }, z.core.$loose>>;
@@ -283,8 +283,8 @@ declare const boundaryReviewProofSchema: z.ZodObject<{
283
283
  mergedSha: z.ZodOptional<z.ZodString>;
284
284
  pr: z.ZodNumber;
285
285
  state: z.ZodEnum<{
286
- open: "open";
287
286
  merged: "merged";
287
+ open: "open";
288
288
  }>;
289
289
  wave: z.ZodOptional<z.ZodString>;
290
290
  }, z.core.$loose>>;
@@ -1752,8 +1752,8 @@ declare const mergeFreezeStateSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
1752
1752
  generationId: z.ZodNumber;
1753
1753
  headSha: z.ZodString;
1754
1754
  outcome: z.ZodEnum<{
1755
- active: "active";
1756
1755
  stale: "stale";
1756
+ active: "active";
1757
1757
  }>;
1758
1758
  reason: z.ZodString;
1759
1759
  recordedAt: z.ZodISODateTime;
@@ -2040,10 +2040,10 @@ declare const managedReadinessLedgerSchema: z.ZodObject<{
2040
2040
  reviewedHeadSha: z.ZodOptional<z.ZodString>;
2041
2041
  reviewedPatchId: z.ZodOptional<z.ZodString>;
2042
2042
  status: z.ZodEnum<{
2043
- stale: "stale";
2044
2043
  blocked: "blocked";
2045
2044
  "not-required": "not-required";
2046
2045
  current: "current";
2046
+ stale: "stale";
2047
2047
  missing: "missing";
2048
2048
  }>;
2049
2049
  }, z.core.$strip>;
@@ -2052,10 +2052,10 @@ declare const managedReadinessLedgerSchema: z.ZodObject<{
2052
2052
  reviewedHeadSha: z.ZodOptional<z.ZodString>;
2053
2053
  reviewedPatchId: z.ZodOptional<z.ZodString>;
2054
2054
  status: z.ZodEnum<{
2055
- stale: "stale";
2056
2055
  blocked: "blocked";
2057
2056
  "not-required": "not-required";
2058
2057
  current: "current";
2058
+ stale: "stale";
2059
2059
  missing: "missing";
2060
2060
  }>;
2061
2061
  }, z.core.$strip>>;
@@ -2089,7 +2089,7 @@ type ReadinessStatus = "ready" | "blocked";
2089
2089
  type ManagedReadinessLedger = z.infer<typeof managedReadinessLedgerSchema>;
2090
2090
  declare function validateManagedReadinessLedger(value: unknown): ManagedReadinessLedger;
2091
2091
  declare namespace status_check_rollup_d_exports {
2092
- export { CheckState, HOSTED_VERIFY_CHECK_NAME, HOSTED_VERIFY_DRAFT_CHECK_NAME, StatusCheckRollup, hostedVerifyCheckState, isDraftHostedVerifyCheck, isExcludedFromAdmissionChecks, isFactoryReadyCheck, isHostedVerifyCheck, isSupersededHostedVerifyCheck, pendingCheckNames, statusCheckState };
2092
+ export { CheckState, HOSTED_VERIFY_CHECK_NAME, HOSTED_VERIFY_DRAFT_CHECK_NAME, StatusCheckRollup, hostedVerifyCheckState, isCancelledPrStatusHudCheck, isDraftHostedVerifyCheck, isExcludedFromAdmissionChecks, isFactoryReadyCheck, isHostedVerifyCheck, isSupersededHostedVerifyCheck, pendingCheckNames, statusCheckState };
2093
2093
  }
2094
2094
  type CheckState = "passed" | "failed" | "pending" | "none" | "unknown";
2095
2095
  interface StatusCheckRollup {
@@ -2150,10 +2150,27 @@ declare const HOSTED_VERIFY_DRAFT_CHECK_NAME = "verify (draft)";
2150
2150
  * `verify` row already answers for, so admission does not judge it.
2151
2151
  */
2152
2152
  declare const isDraftHostedVerifyCheck: (check: StatusCheckRollup) => boolean;
2153
+ /**
2154
+ * Is this a cancelled run of the PR status HUD job (#1056)?
2155
+ *
2156
+ * The HUD job keeps one stable name across Verify generations, so a superseded
2157
+ * run and its replacement share a context and `effectiveChecks` keeps only the
2158
+ * newer. But the Verify workflow cancels an in-flight pull-request run when the
2159
+ * next one starts (`ready_for_review` is the next one), and until the
2160
+ * replacement's HUD job appears, the rollup carries only the CANCELLED row. A
2161
+ * readiness read in that window would report a failed required check that
2162
+ * names a job nobody broke.
2163
+ *
2164
+ * The HUD only presents. It verifies nothing and never gates, so its
2165
+ * cancellation says nothing about the candidate. Only a cancellation is
2166
+ * excluded: a HUD run that failed still reads as failed, and every
2167
+ * verification and deploy check keeps its own state.
2168
+ */
2169
+ declare const isCancelledPrStatusHudCheck: (check: StatusCheckRollup) => boolean;
2153
2170
  /**
2154
2171
  * The rollup rows an admission read must not judge: the factory's own branded
2155
- * checks (it would be judging itself) and the draft-head summary residue
2156
- * above.
2172
+ * checks (it would be judging itself), the draft-head summary residue above,
2173
+ * and a cancelled PR status HUD run.
2157
2174
  */
2158
2175
  declare const isExcludedFromAdmissionChecks: (check: StatusCheckRollup) => boolean;
2159
2176
  /**
@@ -2512,6 +2529,12 @@ interface CompletedFactoryCheckSnapshot {
2512
2529
  * generation (#1158). Absent when the newest generation stands.
2513
2530
  */
2514
2531
  readonly readPastRunIds?: readonly number[];
2532
+ /**
2533
+ * Merge freeze only (#1503): why readiness refuses to read the freeze at
2534
+ * this tip, when it does. The row then presents that refusal, never the
2535
+ * run's own conclusion.
2536
+ */
2537
+ readonly refusal?: string;
2515
2538
  }
2516
2539
  interface FailedPreviewSnapshot {
2517
2540
  readonly completedAt: string;
@@ -3131,10 +3154,10 @@ declare const loadEvidenceEnvelopes: (cwd: string) => LoadedEvidenceEnvelope[];
3131
3154
  //#region src/review-proof-applicability.d.ts
3132
3155
  declare const REVIEW_STATUS_VALUES: readonly ["not-required", "current", "stale", "missing", "blocked"];
3133
3156
  declare const reviewStatusSchema: z.ZodEnum<{
3134
- stale: "stale";
3135
3157
  blocked: "blocked";
3136
3158
  "not-required": "not-required";
3137
3159
  current: "current";
3160
+ stale: "stale";
3138
3161
  missing: "missing";
3139
3162
  }>;
3140
3163
  type ReviewStatus = z.infer<typeof reviewStatusSchema>;
@@ -3242,8 +3265,8 @@ declare const boundaryCheckProofSchema: z.ZodObject<{
3242
3265
  pr: z.ZodNumber;
3243
3266
  sha: z.ZodString;
3244
3267
  state: z.ZodEnum<{
3245
- open: "open";
3246
3268
  merged: "merged";
3269
+ open: "open";
3247
3270
  }>;
3248
3271
  wave: z.ZodString;
3249
3272
  }, z.core.$strip>>;
@@ -4680,13 +4703,13 @@ declare const evaluateReadiness: (input: EvaluationInput) => {
4680
4703
  reviews: {
4681
4704
  correctness: {
4682
4705
  required: boolean;
4683
- status: "stale" | "blocked" | "not-required" | "current" | "missing";
4706
+ status: "blocked" | "not-required" | "current" | "stale" | "missing";
4684
4707
  reviewedHeadSha?: string | undefined;
4685
4708
  reviewedPatchId?: string | undefined;
4686
4709
  };
4687
4710
  security?: {
4688
4711
  required: boolean;
4689
- status: "stale" | "blocked" | "not-required" | "current" | "missing";
4712
+ status: "blocked" | "not-required" | "current" | "stale" | "missing";
4690
4713
  reviewedHeadSha?: string | undefined;
4691
4714
  reviewedPatchId?: string | undefined;
4692
4715
  } | undefined;
@@ -4799,13 +4822,13 @@ declare const evaluateReadiness: (input: EvaluationInput) => {
4799
4822
  reviews: {
4800
4823
  correctness: {
4801
4824
  required: boolean;
4802
- status: "stale" | "blocked" | "not-required" | "current" | "missing";
4825
+ status: "blocked" | "not-required" | "current" | "stale" | "missing";
4803
4826
  reviewedHeadSha?: string | undefined;
4804
4827
  reviewedPatchId?: string | undefined;
4805
4828
  };
4806
4829
  security?: {
4807
4830
  required: boolean;
4808
- status: "stale" | "blocked" | "not-required" | "current" | "missing";
4831
+ status: "blocked" | "not-required" | "current" | "stale" | "missing";
4809
4832
  reviewedHeadSha?: string | undefined;
4810
4833
  reviewedPatchId?: string | undefined;
4811
4834
  } | undefined;
package/dist/index.js CHANGED
@@ -5,7 +5,7 @@ import { execFile, execFileSync, spawn, spawnSync } from "node:child_process";
5
5
  import { EventEmitter, once } from "node:events";
6
6
  import * as nodeFs from "node:fs";
7
7
  import { accessSync, appendFileSync, closeSync, constants, cpSync, existsSync, fsyncSync, mkdirSync, mkdtempSync, openSync, readFileSync, readdirSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync } from "node:fs";
8
- import { FACTORY_APP_BROKER_ORIGIN, FACTORY_APP_BROKER_PATH, FACTORY_CURSOR_OIDC_AUDIENCE, FACTORY_PROOF_GATE_APP_ID, FACTORY_PROOF_GATE_STEP_NAME, GitHubApiError, mintInstallationToken, previewProofInventory, productionImpactTargetOutput, readVitestProfileDocument } from "@patronage/factory-ci";
8
+ import { FACTORY_APP_BROKER_ORIGIN, FACTORY_APP_BROKER_PATH, FACTORY_CURSOR_OIDC_AUDIENCE, FACTORY_PROOF_GATE_APP_ID, FACTORY_PROOF_GATE_STEP_NAME, FACTORY_PR_STATUS_HUD_JOB_NAME, GitHubApiError, assertPreviewCleanupStackPolicy, assertPreviewCleanupTargetsAbsent, discoverOrphanedPreviewCleanupTargets, discoverPreviewCleanupTargets, mintInstallationToken, parsePreviewCleanupResidue, parsePreviewCleanupTargets, previewCleanupGithubOutput, previewCleanupResiduePullRequests, previewProofInventory, productionImpactTargetOutput, readPullRequestStates, readVitestProfileDocument, recordPreviewCleanupOutcome } from "@patronage/factory-ci";
9
9
  import http from "node:http";
10
10
  import { isDeepStrictEqual, promisify } from "node:util";
11
11
  import os, { homedir, tmpdir } from "node:os";
@@ -22,7 +22,7 @@ import { AsyncLocalStorage } from "node:async_hooks";
22
22
  import { pathToFileURL } from "node:url";
23
23
  //#region package.json
24
24
  var name = "@patronage/software-factory";
25
- var version = "1.0.0-alpha.42";
25
+ var version = "1.0.0-alpha.44";
26
26
  //#endregion
27
27
  //#region src/git-globals-parse.ts
28
28
  const VALUE_TAKING_GIT_OPTIONS = new Set([
@@ -9784,6 +9784,15 @@ const verifyProofGenerations = (runs, bound) => {
9784
9784
  * created no earlier than its verification began, so `started_at` is an
9785
9785
  * upper bound on that start. Ranking there can only make such a run newer
9786
9786
  * than it is, so it vetoes more, never less.
9787
+ * - A pass with no usable typed proof is trusted only when it is the only run
9788
+ * at this head (`startUnknownBesideAnotherRun`, #1514). Its verification
9789
+ * started at some unknown instant no later than its `started_at`, so
9790
+ * another run posted before that `started_at` may still have been posted
9791
+ * after the pass began: a pass verified before a failure but posted after
9792
+ * it would otherwise outrank the failure. Ranked at its post, such a pass
9793
+ * can only veto more as an older run; as the newest, nothing is trusted
9794
+ * beside any other run. Proofs over the text cap are rare, so refusing
9795
+ * costs one fresh verification.
9787
9796
  * - Neither `endedAt` nor a passing run's `started_at` is its start. Every
9788
9797
  * writer posts its `pr-verify` run only once verification has finished
9789
9798
  * (`pr:verify` on success or failure, `pr:publish` on re-post), so both say
@@ -9834,7 +9843,7 @@ const verifyProofGenerations = (runs, bound) => {
9834
9843
  * reimplement this reader's payload parser. On payloads the factory's
9835
9844
  * writer emits, both readers select the same pass or both refuse: the
9836
9845
  * differential suite, `pr-verify-reuse-differential.test.ts`, asserts it
9837
- * over 9,653 generated run sets. On App-signed text the writer never emits,
9846
+ * over 10,561 generated run sets. On App-signed text the writer never emits,
9838
9847
  * the two can differ in either direction. The known instances where the
9839
9848
  * hosted gate reuses and this reader refuses are: (a) a reused run whose
9840
9849
  * typed proof fails `validatePrVerifyProof`; (b) a newest pass that repeats
@@ -9873,8 +9882,10 @@ const newestVerifyProofGeneration = (runs, bound) => {
9873
9882
  if (selected.kind !== "newest") return;
9874
9883
  const newest = selected.generation;
9875
9884
  const repeatsAnotherRun = !newest.recorded && newest.payload !== void 0 && generations.some((generation) => generation !== newest && sameVerdict(generation, newest));
9876
- const postedSinceItStarted = isPassingRun(newest.run, newest.payload) && posts.some((post) => post.generation !== newest && post.postedMs >= newest.orderMs);
9877
- return repeatsAnotherRun || postedSinceItStarted ? void 0 : newest;
9885
+ const passing = isPassingRun(newest.run, newest.payload);
9886
+ const startUnknownBesideAnotherRun = passing && !newest.recorded && posts.some((post) => post.generation !== newest);
9887
+ const postedSinceItStarted = passing && posts.some((post) => post.generation !== newest && post.postedMs >= newest.orderMs);
9888
+ return repeatsAnotherRun || startUnknownBesideAnotherRun || postedSinceItStarted ? void 0 : newest;
9878
9889
  };
9879
9890
  /**
9880
9891
  * Read the machine-readable `pr:verify` binding for one commit (#247).
@@ -17738,6 +17749,7 @@ var status_check_rollup_exports = /* @__PURE__ */ __exportAll({
17738
17749
  HOSTED_VERIFY_CHECK_NAME: () => HOSTED_VERIFY_CHECK_NAME,
17739
17750
  HOSTED_VERIFY_DRAFT_CHECK_NAME: () => HOSTED_VERIFY_DRAFT_CHECK_NAME,
17740
17751
  hostedVerifyCheckState: () => hostedVerifyCheckState,
17752
+ isCancelledPrStatusHudCheck: () => isCancelledPrStatusHudCheck,
17741
17753
  isDraftHostedVerifyCheck: () => isDraftHostedVerifyCheck,
17742
17754
  isExcludedFromAdmissionChecks: () => isExcludedFromAdmissionChecks,
17743
17755
  isFactoryReadyCheck: () => isFactoryReadyCheck,
@@ -17812,11 +17824,28 @@ const HOSTED_VERIFY_DRAFT_CHECK_NAME = `${HOSTED_VERIFY_CHECK_NAME} (draft)`;
17812
17824
  */
17813
17825
  const isDraftHostedVerifyCheck = (check) => check.name === HOSTED_VERIFY_DRAFT_CHECK_NAME && Boolean(check.workflowName);
17814
17826
  /**
17827
+ * Is this a cancelled run of the PR status HUD job (#1056)?
17828
+ *
17829
+ * The HUD job keeps one stable name across Verify generations, so a superseded
17830
+ * run and its replacement share a context and `effectiveChecks` keeps only the
17831
+ * newer. But the Verify workflow cancels an in-flight pull-request run when the
17832
+ * next one starts (`ready_for_review` is the next one), and until the
17833
+ * replacement's HUD job appears, the rollup carries only the CANCELLED row. A
17834
+ * readiness read in that window would report a failed required check that
17835
+ * names a job nobody broke.
17836
+ *
17837
+ * The HUD only presents. It verifies nothing and never gates, so its
17838
+ * cancellation says nothing about the candidate. Only a cancellation is
17839
+ * excluded: a HUD run that failed still reads as failed, and every
17840
+ * verification and deploy check keeps its own state.
17841
+ */
17842
+ const isCancelledPrStatusHudCheck = (check) => check.name === FACTORY_PR_STATUS_HUD_JOB_NAME && Boolean(check.workflowName) && (check.conclusion ?? check.state) === "CANCELLED";
17843
+ /**
17815
17844
  * The rollup rows an admission read must not judge: the factory's own branded
17816
- * checks (it would be judging itself) and the draft-head summary residue
17817
- * above.
17845
+ * checks (it would be judging itself), the draft-head summary residue above,
17846
+ * and a cancelled PR status HUD run.
17818
17847
  */
17819
- const isExcludedFromAdmissionChecks = (check) => isFactoryReadyCheck(check) || isDraftHostedVerifyCheck(check);
17848
+ const isExcludedFromAdmissionChecks = (check) => isFactoryReadyCheck(check) || isDraftHostedVerifyCheck(check) || isCancelledPrStatusHudCheck(check);
17820
17849
  /**
17821
17850
  * The hosted `verify` gate's state on this head, read by presence (#477).
17822
17851
  *
@@ -17882,6 +17911,15 @@ const effectiveChecks = (rollup) => {
17882
17911
  }
17883
17912
  return currentChecks;
17884
17913
  };
17914
+ /**
17915
+ * The current generation of each check, less the excluded rows.
17916
+ *
17917
+ * Generations collapse before the exclusion runs. An exclusion that depends on
17918
+ * a run's state, like the cancelled HUD run (#1056), must judge only the
17919
+ * newest generation: dropping it first would let an older, superseded run of
17920
+ * the same job stand in as current (#1550).
17921
+ */
17922
+ const currentIncludedChecks = (rollup, exclude) => effectiveChecks(rollup ?? []).filter((check) => !exclude?.(check));
17885
17923
  const terminalState = (check) => check.conclusion ?? check.state ?? "";
17886
17924
  const progressState = (check) => check.status ?? check.state ?? "";
17887
17925
  /**
@@ -17894,11 +17932,10 @@ const progressState = (check) => check.status ?? check.state ?? "";
17894
17932
  * rather than dropped: a wait must not look settled because one row was
17895
17933
  * unprintable.
17896
17934
  */
17897
- const pendingCheckNames = (rollup, exclude) => effectiveChecks((rollup ?? []).filter((check) => !exclude?.(check))).filter((check) => PENDING_STATES.has(progressState(check))).map((check) => checkContext(check) ?? check.name ?? "unnamed check");
17935
+ const pendingCheckNames = (rollup, exclude) => currentIncludedChecks(rollup, exclude).filter((check) => PENDING_STATES.has(progressState(check))).map((check) => checkContext(check) ?? check.name ?? "unnamed check");
17898
17936
  const statusCheckState = (rollup, exclude) => {
17899
- const included = rollup?.filter((check) => !exclude?.(check));
17900
- if (!included || included.length === 0) return "none";
17901
- const currentChecks = effectiveChecks(included);
17937
+ const currentChecks = currentIncludedChecks(rollup, exclude);
17938
+ if (currentChecks.length === 0) return "none";
17902
17939
  if (currentChecks.every((check) => PASSED_STATES.has(terminalState(check)))) return "passed";
17903
17940
  if (currentChecks.some((check) => FAILED_STATES.has(terminalState(check)))) return "failed";
17904
17941
  if (currentChecks.some((check) => PENDING_STATES.has(progressState(check)))) return "pending";
@@ -18092,6 +18129,13 @@ const mergeFreezeCheckRunListItemSchema = z.object({
18092
18129
  "queued"
18093
18130
  ])
18094
18131
  });
18132
+ /**
18133
+ * Whether the reader can parse one listed run (#1550). The reader parses every
18134
+ * listed run before it selects one, so a single run it cannot parse refuses
18135
+ * the whole read, even behind a newer green generation. The PR status HUD
18136
+ * applies the same rule through this export rather than a copy of the schema.
18137
+ */
18138
+ const isReadableMergeFreezeListedRun = (run) => mergeFreezeCheckRunListItemSchema.safeParse(run).success;
18095
18139
  const completedMergeFreezeCheckRunSchema = mergeFreezeCheckRunListItemSchema.extend({
18096
18140
  conclusion: z.enum([
18097
18141
  "failure",
@@ -19601,6 +19645,13 @@ const checkRow = (target, snapshot, renderedAt) => {
19601
19645
  target,
19602
19646
  timestamp: utcTimestamp(renderedAt)
19603
19647
  };
19648
+ if (snapshot.refusal) return {
19649
+ detail: snapshot.refusal,
19650
+ link: safeMarkdownLink("Details", snapshot.detailsUrl),
19651
+ status: "Failed",
19652
+ target: `${target} (${snapshot.refusal})`,
19653
+ timestamp: utcTimestamp(snapshot.completedAt)
19654
+ };
19604
19655
  return {
19605
19656
  detail: `Completed snapshot: ${snapshot.conclusion}.`,
19606
19657
  link: safeMarkdownLink("Details", snapshot.detailsUrl),
@@ -19790,9 +19841,10 @@ const listFactoryCheckRuns = async (input, name, sha, dependencies) => {
19790
19841
  }
19791
19842
  throw new Error(`${name} pagination limit was exhausted before GitHub returned a final page.`);
19792
19843
  };
19844
+ const writtenByFactoryApp = (run) => String(run.app?.id ?? "") === FACTORY_PROOF_GATE_APP_ID;
19793
19845
  /** The Factory App's generations of one check at one SHA, each orderable. */
19794
19846
  const factoryGenerations = (name, sha, runs) => {
19795
- const candidates = runs.filter((run) => String(run.app?.id ?? "") === FACTORY_PROOF_GATE_APP_ID && run.head_sha === sha && run.name === name);
19847
+ const candidates = runs.filter((run) => writtenByFactoryApp(run) && run.head_sha === sha && run.name === name);
19796
19848
  if (candidates.some((run) => !Number.isFinite(generationOrderMs(run.started_at)))) throw new Error(`${name} presentation snapshot has a generation with no usable started_at.`);
19797
19849
  return candidates;
19798
19850
  };
@@ -19824,25 +19876,52 @@ const MERGE_FREEZE_CHECK = "patronage-factory/merge-freeze";
19824
19876
  * tip (#1158). The newest completed run alone would show that cancellation as
19825
19877
  * a freeze readiness does not apply, so the row names the runs it read past.
19826
19878
  *
19879
+ * Where readiness refuses to read the freeze at all, the row says so rather
19880
+ * than showing a conclusion readiness never trusts (#1503). A listed run the
19881
+ * pinned Factory App did not write refuses the whole read, as it does in the
19882
+ * reader (#1393): filtering it out could let an older generation win. So does
19883
+ * any listed run the reader cannot parse, such as an older one concluded
19884
+ * `cancelled` (#1550). A newest payload the selection cannot parse, or one
19885
+ * that contradicts its own check run, is unreadable.
19886
+ *
19827
19887
  * Otherwise the row is the newest completed snapshot, as for every other check:
19828
- * running, missing and ambiguous generations present as before, and so does a
19829
- * newest generation the selection cannot settle. Every case where the freeze
19830
- * stays fail-closed keeps the newest generation, so the row stays red. The HUD
19831
- * is advisory and never gates.
19888
+ * running, missing and ambiguous generations present as before. Every case
19889
+ * where the freeze stays fail-closed keeps the row red. The HUD is advisory and
19890
+ * never gates.
19832
19891
  */
19833
19892
  const mergeFreezeSnapshot = (sha, runs) => {
19893
+ const foreign = runs.find((run) => !writtenByFactoryApp(run));
19894
+ const unparseable = runs.find((run) => !isReadableMergeFreezeListedRun(run));
19895
+ const refusing = foreign ?? unparseable;
19896
+ if (refusing) {
19897
+ const at = refusing.completed_at ?? refusing.started_at;
19898
+ if (!at) throw new Error(`${MERGE_FREEZE_CHECK} presentation snapshot has a refused run with no usable timestamp.`);
19899
+ return {
19900
+ completedAt: at,
19901
+ conclusion: refusing.conclusion ?? refusing.status,
19902
+ ...refusing.details_url ? { detailsUrl: refusing.details_url } : {},
19903
+ headSha: sha,
19904
+ name: MERGE_FREEZE_CHECK,
19905
+ refusal: foreign ? `refused: run ${String(foreign.id)} was not written by the Factory App` : `refused: run ${String(refusing.id)} cannot be read`
19906
+ };
19907
+ }
19834
19908
  const generations = factoryGenerations(MERGE_FREEZE_CHECK, sha, runs);
19835
19909
  const newest = newestSnapshot(MERGE_FREEZE_CHECK, generations);
19836
19910
  if (!newest) return newest;
19911
+ const unreadable = {
19912
+ ...newest,
19913
+ refusal: "unreadable newest payload"
19914
+ };
19837
19915
  try {
19838
- const { readPast, run } = selectMergeFreezeRun(generations, sha);
19916
+ const { generation, readPast, run } = selectMergeFreezeRun(generations, sha);
19917
+ if (generation.kind === "unreadable") return unreadable;
19839
19918
  const selected = run && readPast.length > 0 ? runSnapshot(MERGE_FREEZE_CHECK, run) : void 0;
19840
19919
  return selected ? {
19841
19920
  ...selected,
19842
19921
  readPastRunIds: readPast.map((past) => past.id)
19843
19922
  } : newest;
19844
19923
  } catch {
19845
- return newest;
19924
+ return unreadable;
19846
19925
  }
19847
19926
  };
19848
19927
  const FAILED_PREVIEW_CONCLUSIONS = new Set([
@@ -24807,6 +24886,155 @@ function createPrStatusHudCommand(output, action = runPrStatusHud, env = process
24807
24886
  }));
24808
24887
  }
24809
24888
  //#endregion
24889
+ //#region src/preview-cleanup-command.ts
24890
+ const ACTIONS = new Set([
24891
+ "audit",
24892
+ "discover",
24893
+ "record",
24894
+ "sweep"
24895
+ ]);
24896
+ const collect = (value, previous = []) => [...previous, value];
24897
+ const parseAction = (value) => {
24898
+ if (!ACTIONS.has(value)) throw new InvalidArgumentError("must be discover, sweep, record, or audit");
24899
+ return value;
24900
+ };
24901
+ const policyFrom = (options) => {
24902
+ const stackAliases = {};
24903
+ for (const entry of options.stackAlias) {
24904
+ const separator = entry.indexOf("=");
24905
+ if (separator <= 0 || separator === entry.length - 1) throw new Error(`--stack-alias must be <alias>=<stack>, got "${entry}".`);
24906
+ stackAliases[entry.slice(0, separator)] = entry.slice(separator + 1);
24907
+ }
24908
+ return {
24909
+ ...options.hostedStagePrefix === void 0 ? {} : { hostedStagePrefix: options.hostedStagePrefix },
24910
+ stackAliases,
24911
+ stacks: options.stack
24912
+ };
24913
+ };
24914
+ const repositoryFrom = (options, env) => {
24915
+ const [owner, repo, extra] = (options.repository ?? env.GITHUB_REPOSITORY ?? "").split("/");
24916
+ if (!(owner && repo) || extra !== void 0) throw new Error("preview:cleanup requires --repository <owner/repo> or GITHUB_REPOSITORY.");
24917
+ return {
24918
+ owner,
24919
+ repo
24920
+ };
24921
+ };
24922
+ const requireValue = (name, value) => {
24923
+ if (value === void 0 || value === "") throw new Error(`preview:cleanup requires ${name} for this action.`);
24924
+ return value;
24925
+ };
24926
+ const requireToken = (env, name) => {
24927
+ const token = env[name]?.trim();
24928
+ if (!token) throw new Error(`preview:cleanup requires ${name} for this action.`);
24929
+ return token;
24930
+ };
24931
+ const describeTargets = (count, label, deferred = 0) => `${label}: ${count} cleanup target${count === 1 ? "" : "s"}${deferred > 0 ? `, ${deferred} deferred to the next sweep` : ""}\n`;
24932
+ const recordOutcome = async (options, policy, deps) => {
24933
+ const { env, readFile } = deps;
24934
+ const outcome = requireValue("--outcome", options.outcome);
24935
+ if (outcome !== "passed" && outcome !== "failed") throw new Error("--outcome must be passed or failed.");
24936
+ const targetStack = requireValue("--target-stack", options.targetStack);
24937
+ assertPreviewCleanupStackPolicy(policy);
24938
+ if (!policy.stacks.includes(targetStack)) throw new Error(`--target-stack "${targetStack}" is not a declared --stack.`);
24939
+ const { owner, repo } = repositoryFrom(options, env);
24940
+ const evidence = readFile(requireValue("--evidence-file", options.evidenceFile)).split("\n").map((line) => line.trim()).filter((line) => line.length > 0);
24941
+ const server = env.GITHUB_SERVER_URL?.trim();
24942
+ const runId = env.GITHUB_RUN_ID?.trim();
24943
+ const transport = deps.fetch === void 0 ? {} : { fetch: deps.fetch };
24944
+ const result = await recordPreviewCleanupOutcome({
24945
+ evidence,
24946
+ ...options.headSha ? { headSha: options.headSha } : {},
24947
+ outcome,
24948
+ owner,
24949
+ pr: requireValue("--pr", options.pr),
24950
+ repo,
24951
+ ...server && runId ? { runUrl: `${server}/${owner}/${repo}/actions/runs/${runId}` } : {},
24952
+ stack: targetStack,
24953
+ stackAliases: policy.stackAliases ?? {},
24954
+ stage: requireValue("--target-stage", options.targetStage)
24955
+ }, {
24956
+ read: {
24957
+ ...transport,
24958
+ token: requireToken(env, "GITHUB_TOKEN")
24959
+ },
24960
+ write: {
24961
+ ...transport,
24962
+ token: requireToken(env, "FACTORY_GITHUB_APP_TOKEN")
24963
+ }
24964
+ });
24965
+ return `Cleanup ${outcome} for ${targetStack}/${options.targetStage}: ${result.kind}\n`;
24966
+ };
24967
+ async function runPreviewCleanup(options, deps = {}) {
24968
+ const env = deps.env ?? process.env;
24969
+ const readFile = deps.readFile ?? ((path) => readFileSync(path, "utf-8"));
24970
+ const writeOutput = deps.writeOutput ?? appendFileSync;
24971
+ const policy = policyFrom(options);
24972
+ const readResidue = () => parsePreviewCleanupResidue(JSON.parse(readFile(requireValue("--residue", options.residue))));
24973
+ const publish = (text) => {
24974
+ if (options.githubOutput !== void 0) writeOutput(options.githubOutput, text);
24975
+ };
24976
+ if (options.action === "discover") {
24977
+ const pr = requireValue("--pr", options.pr);
24978
+ const { owner, repo } = repositoryFrom(options, env);
24979
+ const registrations = await (deps.listRegistrations === void 0 ? previewProofInventory.list({
24980
+ owner,
24981
+ pr,
24982
+ repo
24983
+ }, {
24984
+ ...deps.fetch === void 0 ? {} : { fetch: deps.fetch },
24985
+ token: requireToken(env, "GITHUB_TOKEN")
24986
+ }) : deps.listRegistrations({
24987
+ owner,
24988
+ pr,
24989
+ repo
24990
+ }));
24991
+ const targets = discoverPreviewCleanupTargets({
24992
+ ...policy,
24993
+ pr,
24994
+ registrations,
24995
+ residue: readResidue()
24996
+ });
24997
+ const text = previewCleanupGithubOutput(policy, targets);
24998
+ publish(text);
24999
+ return `${describeTargets(targets.length, `PR #${pr}`)}${text}`;
25000
+ }
25001
+ if (options.action === "sweep") {
25002
+ const { owner, repo } = repositoryFrom(options, env);
25003
+ const residue = readResidue();
25004
+ const pullRequestStates = await readPullRequestStates({
25005
+ owner,
25006
+ prs: previewCleanupResiduePullRequests(policy, residue),
25007
+ repo
25008
+ }, {
25009
+ ...deps.fetch === void 0 ? {} : { fetch: deps.fetch },
25010
+ token: requireToken(env, "GITHUB_TOKEN")
25011
+ });
25012
+ const sweep = discoverOrphanedPreviewCleanupTargets({
25013
+ ...policy,
25014
+ ...options.maxTargets === void 0 ? {} : { maxTargets: options.maxTargets },
25015
+ pullRequestStates,
25016
+ residue
25017
+ });
25018
+ const text = previewCleanupGithubOutput(policy, sweep.targets);
25019
+ publish(text);
25020
+ return `${describeTargets(sweep.targets.length, "Sweep", sweep.deferred)}${text}`;
25021
+ }
25022
+ if (options.action === "record") return recordOutcome(options, policy, {
25023
+ ...deps,
25024
+ env,
25025
+ readFile
25026
+ });
25027
+ const targets = parsePreviewCleanupTargets(policy, JSON.parse(requireValue("--targets", options.targets)));
25028
+ assertPreviewCleanupTargetsAbsent(targets, readResidue());
25029
+ return `No residue remains for ${targets.length} cleanup target${targets.length === 1 ? "" : "s"}\n`;
25030
+ }
25031
+ function createPreviewCleanupCommand(output, deps = {}) {
25032
+ return new Command("preview:cleanup").description("Discover, sweep, record, or audit disposable preview stage cleanup for factory-ci's generated cleanup workflow").requiredOption("--action <action>", "discover (one closed PR), sweep (closed PRs' residue), record, or audit", parseAction).option("--stack <name>", "declared cleanup stack; repeat for each stack", collect, []).option("--stack-alias <alias=stack>", "another name for a declared stack; repeat as needed", collect, []).option("--hosted-stage-prefix <prefix>", "the hosted per-PR stage prefix, such as pr-").option("--repository <owner/repo>", "repository; defaults to GITHUB_REPOSITORY").option("--residue <path>", "residue JSON: an array of stack and stage rows").option("--pr <number>", "pull request number", positiveInteger$1("--pr")).option("--max-targets <count>", "sweep targets per run", positiveInteger$1("--max-targets")).option("--target-stack <stack>", "the destroyed stack, for record").option("--target-stage <stage>", "the destroyed stage, for record").option("--outcome <outcome>", "passed or failed, for record").option("--evidence-file <path>", "evidence lines, for record").option("--head-sha <sha>", "registered head SHA, for record").option("--targets <json>", "the discover job's targets output, for audit").option("--github-output <path>", "append discover or sweep outputs to the Actions output file").action(async (options) => {
25033
+ if (options.stack.length === 0) throw new Error("preview:cleanup requires at least one --stack.");
25034
+ output.stdout.write(await runPreviewCleanup(options, deps));
25035
+ });
25036
+ }
25037
+ //#endregion
24810
25038
  //#region src/preview-reaper.ts
24811
25039
  const previewBindingReferenceSchema = z.object({
24812
25040
  binding: z.string().min(1),
@@ -25619,6 +25847,7 @@ function createProgram(options = {}) {
25619
25847
  program.addCommand(createProductionImpactCommand(output));
25620
25848
  program.addCommand(createPreviewReceiptCommand(output));
25621
25849
  program.addCommand(createPreviewReapCommand(output));
25850
+ program.addCommand(createPreviewCleanupCommand(output));
25622
25851
  program.addCommand(createCandidateImpactCommand(output));
25623
25852
  program.addCommand(createAffectedCommand(output));
25624
25853
  program.addCommand(createAppTokenExecCommand());
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@patronage/software-factory",
3
- "version": "1.0.0-alpha.42",
3
+ "version": "1.0.0-alpha.44",
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.42"
40
+ "@patronage/factory-ci": "1.0.0-alpha.44"
41
41
  },
42
42
  "devDependencies": {
43
43
  "@types/node": "24.13.3",