@patronage/software-factory 1.0.0-alpha.17 → 1.0.0-alpha.19

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.js CHANGED
@@ -14,10 +14,10 @@ import { setImmediate as setImmediate$1, setTimeout as setTimeout$1 } from "node
14
14
  import { Worker } from "node:worker_threads";
15
15
  import picomatch from "picomatch";
16
16
  import { parse } from "yaml";
17
- import { FACTORY_PROOF_GATE_STEP_NAME, GitHubApiError, mintInstallationToken, productionImpactTargetOutput, readVitestProfileDocument } from "@patronage/factory-ci";
17
+ import { FACTORY_PROOF_GATE_APP_ID, FACTORY_PROOF_GATE_STEP_NAME, GitHubApiError, mintInstallationToken, previewProofInventory, productionImpactTargetOutput, readVitestProfileDocument } from "@patronage/factory-ci";
18
18
  import { promisify } from "node:util";
19
19
  //#region package.json
20
- var version = "1.0.0-alpha.17";
20
+ var version = "1.0.0-alpha.19";
21
21
  //#endregion
22
22
  //#region src/review-rungs.ts
23
23
  const EVIDENCE_REVIEW_RUNGS$1 = [
@@ -291,11 +291,11 @@ var boundary_review_proof_exports = /* @__PURE__ */ __exportAll({
291
291
  undispositionedBlockingFindings: () => undispositionedBlockingFindings
292
292
  });
293
293
  const BOUNDARY_REVIEW_PROOF_KIND$1 = "boundary-review-proof";
294
- const shaSchema$5 = z.string().regex(/^[0-9a-f]{7,40}$/u, { message: "must be a 7-40 char lowercase hex git SHA" });
294
+ const shaSchema$6 = z.string().regex(/^[0-9a-f]{7,40}$/u, { message: "must be a 7-40 char lowercase hex git SHA" });
295
295
  const coveredSetEntrySchema$1 = z.object({
296
- headSha: shaSchema$5.optional(),
296
+ headSha: shaSchema$6.optional(),
297
297
  issue: z.number().int().positive().optional(),
298
- mergedSha: shaSchema$5.optional(),
298
+ mergedSha: shaSchema$6.optional(),
299
299
  pr: z.number().int().positive(),
300
300
  state: z.enum(["merged", "open"]),
301
301
  wave: z.string().min(1).optional()
@@ -1198,11 +1198,16 @@ const impactConfigSchema = z.object({
1198
1198
  });
1199
1199
  })
1200
1200
  }).strict();
1201
+ const lifecycleContractSchema = z.object({
1202
+ command: z.string().min(1).describe("Package-script invocation hosted CI runs for the non-credentialed lifecycle-contract lane (for example `pnpm test:factory`). Never a direct Vitest invocation."),
1203
+ jobName: z.string().min(1).optional().describe("Display name for the generated GitHub Actions job. Defaults to Factory / Alchemy Contracts when omitted.")
1204
+ }).strict().describe("Opt-in hosted lifecycle-contract lane. Presence includes the non-credentialed job in generated Verify; absence omits it. factory-ci does not load this object; generators pass it as typed options (ADR 0021).");
1201
1205
  const factoryProjectProfileSchema = z.object({
1202
1206
  $schema: z.string().min(1).optional(),
1203
1207
  extensions: z.record(z.string(), z.unknown()).optional(),
1204
1208
  hq: hqIngestConfigSchema.optional(),
1205
1209
  impact: impactConfigSchema.optional(),
1210
+ lifecycleContract: lifecycleContractSchema.optional(),
1206
1211
  repository: z.object({
1207
1212
  name: z.string().min(1),
1208
1213
  owner: z.string().min(1)
@@ -3699,10 +3704,10 @@ const resolveAuthoringSessionIds = ({ override, recorded }) => {
3699
3704
  //#region src/demand-waiver.ts
3700
3705
  const DEMAND_WAIVER_SCHEMA_VERSION$1 = 1;
3701
3706
  const DEFAULT_DEMAND_WAIVER_PATH = ".factory-memory/demand-waivers.json";
3702
- const shaSchema$4 = z.string().regex(/^[0-9a-f]{40}$/u);
3707
+ const shaSchema$5 = z.string().regex(/^[0-9a-f]{40}$/u);
3703
3708
  const demandWaiverSchema = z.object({
3704
3709
  candidate: z.object({
3705
- headSha: shaSchema$4,
3710
+ headSha: shaSchema$5,
3706
3711
  pr: z.number().int().positive()
3707
3712
  }),
3708
3713
  demand: waiverDemandKeySchema,
@@ -5984,7 +5989,7 @@ function withGateTiming(spec, action) {
5984
5989
  };
5985
5990
  }
5986
5991
  //#endregion
5987
- //#region src/commands/options.ts
5992
+ //#region src/command-options.ts
5988
5993
  function positiveInteger$1(name) {
5989
5994
  return (value) => {
5990
5995
  const parsed = Number(value);
@@ -8533,7 +8538,7 @@ var CandidateIdentityError = class extends Error {
8533
8538
  this.name = "CandidateIdentityError";
8534
8539
  }
8535
8540
  };
8536
- const OBJECT_ID = /^[0-9a-f]{40}$/u;
8541
+ const OBJECT_ID$1 = /^[0-9a-f]{40}$/u;
8537
8542
  /**
8538
8543
  * Private patch-id primitive for this proof-identity owner. It stays local so
8539
8544
  * the ownership boundary is structural rather than an approximation of which
@@ -8565,7 +8570,7 @@ function runStablePatchId(cwd, base) {
8565
8570
  return id;
8566
8571
  }
8567
8572
  function assertObjectId(field, value) {
8568
- if (!OBJECT_ID.test(value)) throw new CandidateIdentityError(`Candidate identity ${field} must be a 40-character object id, got ${JSON.stringify(value)}. Evidence bound to a malformed identity cannot be matched against a candidate, so this fails closed.`);
8573
+ if (!OBJECT_ID$1.test(value)) throw new CandidateIdentityError(`Candidate identity ${field} must be a 40-character object id, got ${JSON.stringify(value)}. Evidence bound to a malformed identity cannot be matched against a candidate, so this fails closed.`);
8569
8574
  return value;
8570
8575
  }
8571
8576
  /**
@@ -8665,16 +8670,16 @@ var external_evidence_exports = /* @__PURE__ */ __exportAll({
8665
8670
  requiredCheckScopeSchema: () => requiredCheckScopeSchema$1,
8666
8671
  resolveAuthoringSessionIds: () => resolveAuthoringSessionIds
8667
8672
  });
8668
- const shaSchema$3 = z.string().regex(/^[0-9a-f]{7,40}$/u, { message: "must be a 7-40 char lowercase hex git SHA" });
8673
+ const shaSchema$4 = z.string().regex(/^[0-9a-f]{7,40}$/u, { message: "must be a 7-40 char lowercase hex git SHA" });
8669
8674
  const evidenceEnvelopeSchema = z.object({
8670
8675
  check: z.string().min(1),
8671
8676
  checkType: z.enum(EVIDENCE_CHECK_TYPES$1),
8672
8677
  findingsPointer: z.string().min(1).optional(),
8673
- headSha: shaSchema$3,
8674
- mergeBaseSha: shaSchema$3,
8678
+ headSha: shaSchema$4,
8679
+ mergeBaseSha: shaSchema$4,
8675
8680
  model: z.string().min(1).optional(),
8676
8681
  outcome: z.enum(["pass", "fail"]),
8677
- patchId: shaSchema$3,
8682
+ patchId: shaSchema$4,
8678
8683
  policyVersion: z.string().min(1).optional(),
8679
8684
  producer: z.string().min(1),
8680
8685
  requestId: z.string().min(1).optional(),
@@ -9081,6 +9086,124 @@ function generationOrderMs(value) {
9081
9086
  return Number.isFinite(parsed) ? parsed : Number.NEGATIVE_INFINITY;
9082
9087
  }
9083
9088
  //#endregion
9089
+ //#region src/pr-verify-status.ts
9090
+ const PR_VERIFY_STATUS_CONTEXT = "patronage-factory/pr-verify";
9091
+ const GITHUB_PUBLISH_TIMEOUT_MS = 5e3;
9092
+ /**
9093
+ * Fire a best-effort async operation without blocking the caller and without
9094
+ * ever surfacing an unhandled rejection. Every check-run/commit-status
9095
+ * publisher in this module and github-check-runs.ts is a fire-and-forget
9096
+ * sink whose own failure (including its diagnostic write) must never affect
9097
+ * the gate result; this is the single detach wrapper they share instead of
9098
+ * each redeclaring the same try/catch-and-void shape.
9099
+ */
9100
+ function runDetachedBestEffort(work) {
9101
+ const safe = async () => {
9102
+ try {
9103
+ await work();
9104
+ } catch {}
9105
+ };
9106
+ safe();
9107
+ }
9108
+ async function runGhStatus(args, cwd) {
9109
+ const { execFile } = await import("node:child_process");
9110
+ await promisify(execFile)("gh", args, {
9111
+ cwd,
9112
+ timeout: GITHUB_PUBLISH_TIMEOUT_MS
9113
+ });
9114
+ }
9115
+ function createGhCommitStatusPoster(output, run = runGhStatus) {
9116
+ return (input) => {
9117
+ const { context = PR_VERIFY_STATUS_CONTEXT, cwd, description, owner, repo, sha, state, suppressFailureDiagnostic = false, targetUrl } = input;
9118
+ const args = [
9119
+ "api",
9120
+ "--method",
9121
+ "POST",
9122
+ `/repos/${owner}/${repo}/statuses/${sha}`,
9123
+ "-f",
9124
+ `state=${state}`,
9125
+ "-f",
9126
+ `context=${context}`,
9127
+ "-f",
9128
+ `description=${description}`,
9129
+ "-f",
9130
+ `target_url=${targetUrl}`
9131
+ ];
9132
+ const publish = async () => {
9133
+ await Promise.resolve();
9134
+ try {
9135
+ await run(args, cwd);
9136
+ if (state === "success") output.stdout.write(context === "patronage-factory/pr-verify" ? `pr:verify posted ${PR_VERIFY_STATUS_CONTEXT}=success\n` : `${context} posted success\n`);
9137
+ } catch (error) {
9138
+ if (suppressFailureDiagnostic) return;
9139
+ const message = error instanceof Error ? error.message : String(error);
9140
+ output.stderr.write(context === "patronage-factory/pr-verify" ? `pr:verify: unable to post ${PR_VERIFY_STATUS_CONTEXT} status: ${message}\n` : `${context}: unable to post commit status: ${message}\n`);
9141
+ }
9142
+ };
9143
+ runDetachedBestEffort(publish);
9144
+ };
9145
+ }
9146
+ //#endregion
9147
+ //#region src/github-app-client.ts
9148
+ const readExchange = async (request, url, init, timeoutMs) => {
9149
+ const response = await request(url, {
9150
+ ...init,
9151
+ headers: {
9152
+ Accept: "application/vnd.github+json",
9153
+ "X-GitHub-Api-Version": "2022-11-28",
9154
+ ...init.headers
9155
+ },
9156
+ signal: init.signal ?? AbortSignal.timeout(timeoutMs)
9157
+ });
9158
+ const etag = response.headers.get("etag") ?? void 0;
9159
+ if (response.status === 204) return {
9160
+ etag,
9161
+ json: void 0,
9162
+ status: 204
9163
+ };
9164
+ if (!response.ok) throw new GitHubApiError(response.status, response.statusText);
9165
+ return {
9166
+ etag,
9167
+ json: await response.json(),
9168
+ status: response.status
9169
+ };
9170
+ };
9171
+ async function requestGitHubJson(request, url, init, timeoutMs = GITHUB_PUBLISH_TIMEOUT_MS) {
9172
+ return (await readExchange(request, url, init, timeoutMs)).json;
9173
+ }
9174
+ /** Mint one repository-scoped installation token for the Patronage Factory App. */
9175
+ const mintFactoryInstallationToken = (repository, config, request, now, timeoutMs = GITHUB_PUBLISH_TIMEOUT_MS) => mintInstallationToken({
9176
+ credentials: config,
9177
+ owner: repository.owner,
9178
+ repo: repository.repo
9179
+ }, {
9180
+ fetch: request,
9181
+ now: () => now,
9182
+ timeoutMs
9183
+ });
9184
+ const resolveFactoryAppToken = async (input, dependencies, request, timeoutMs) => {
9185
+ if (dependencies.token) return dependencies.token;
9186
+ const config = dependencies.githubApp ?? tryUserConfig()?.config.githubApp;
9187
+ if (!config) throw new Error("The Patronage Factory GitHub App is required for App-owned GitHub requests.");
9188
+ return mintFactoryInstallationToken(input, config, request, (dependencies.now ?? Date.now)(), timeoutMs);
9189
+ };
9190
+ const factoryAppInit = (input, token) => ({
9191
+ ...input.body === void 0 ? {} : { body: JSON.stringify(input.body) },
9192
+ headers: {
9193
+ Authorization: `Bearer ${token}`,
9194
+ "Content-Type": "application/json",
9195
+ ...input.headers
9196
+ },
9197
+ method: input.method
9198
+ });
9199
+ /** Perform one repository request as the Patronage Factory GitHub App. */
9200
+ async function requestFactoryApp(input, dependencies = {}) {
9201
+ const request = dependencies.fetch ?? fetch;
9202
+ const timeoutMs = dependencies.timeoutMs ?? 5e3;
9203
+ const token = await resolveFactoryAppToken(input, dependencies, request, timeoutMs);
9204
+ return readExchange(request, `https://api.github.com/repos/${input.owner}/${input.repo}/${input.path}`, factoryAppInit(input, token), timeoutMs);
9205
+ }
9206
+ //#endregion
9084
9207
  //#region src/pr-verify-mode.ts
9085
9208
  const RESOLVED_PR_VERIFY_MODES = new Set([
9086
9209
  "docs-only",
@@ -9142,64 +9265,6 @@ const prVerifyCheckPayloadFor = (proof) => {
9142
9265
  };
9143
9266
  const renderPrVerifyCheckPayloadText = (payload) => `\`\`\`json\n${JSON.stringify(payload, null, 2)}\n\`\`\``;
9144
9267
  //#endregion
9145
- //#region src/pr-verify-status.ts
9146
- const PR_VERIFY_STATUS_CONTEXT = "patronage-factory/pr-verify";
9147
- const GITHUB_PUBLISH_TIMEOUT_MS = 5e3;
9148
- /**
9149
- * Fire a best-effort async operation without blocking the caller and without
9150
- * ever surfacing an unhandled rejection. Every check-run/commit-status
9151
- * publisher in this module and github-check-runs.ts is a fire-and-forget
9152
- * sink whose own failure (including its diagnostic write) must never affect
9153
- * the gate result; this is the single detach wrapper they share instead of
9154
- * each redeclaring the same try/catch-and-void shape.
9155
- */
9156
- function runDetachedBestEffort(work) {
9157
- const safe = async () => {
9158
- try {
9159
- await work();
9160
- } catch {}
9161
- };
9162
- safe();
9163
- }
9164
- async function runGhStatus(args, cwd) {
9165
- const { execFile } = await import("node:child_process");
9166
- await promisify(execFile)("gh", args, {
9167
- cwd,
9168
- timeout: GITHUB_PUBLISH_TIMEOUT_MS
9169
- });
9170
- }
9171
- function createGhCommitStatusPoster(output, run = runGhStatus) {
9172
- return (input) => {
9173
- const { context = PR_VERIFY_STATUS_CONTEXT, cwd, description, owner, repo, sha, state, suppressFailureDiagnostic = false, targetUrl } = input;
9174
- const args = [
9175
- "api",
9176
- "--method",
9177
- "POST",
9178
- `/repos/${owner}/${repo}/statuses/${sha}`,
9179
- "-f",
9180
- `state=${state}`,
9181
- "-f",
9182
- `context=${context}`,
9183
- "-f",
9184
- `description=${description}`,
9185
- "-f",
9186
- `target_url=${targetUrl}`
9187
- ];
9188
- const publish = async () => {
9189
- await Promise.resolve();
9190
- try {
9191
- await run(args, cwd);
9192
- if (state === "success") output.stdout.write(context === "patronage-factory/pr-verify" ? `pr:verify posted ${PR_VERIFY_STATUS_CONTEXT}=success\n` : `${context} posted success\n`);
9193
- } catch (error) {
9194
- if (suppressFailureDiagnostic) return;
9195
- const message = error instanceof Error ? error.message : String(error);
9196
- output.stderr.write(context === "patronage-factory/pr-verify" ? `pr:verify: unable to post ${PR_VERIFY_STATUS_CONTEXT} status: ${message}\n` : `${context}: unable to post commit status: ${message}\n`);
9197
- }
9198
- };
9199
- runDetachedBestEffort(publish);
9200
- };
9201
- }
9202
- //#endregion
9203
9268
  //#region src/github-check-runs.ts
9204
9269
  const FACTORY_CHECK_NAMES = {
9205
9270
  "pr-ready": "patronage-factory/pr-ready",
@@ -9293,19 +9358,6 @@ const prePushVerifyBindingNotice = (sha) => `${FACTORY_CHECK_NAMES["pr-verify"]}
9293
9358
  The proof is complete and stored. After you push, pr:publish binds this check to the same commit and reuses that proof — no verification command runs again.
9294
9359
  Re-verify only if the tree changes: a new commit, an amend, or a rebase.
9295
9360
  `;
9296
- async function githubJson(request, url, init, timeoutMs = GITHUB_PUBLISH_TIMEOUT_MS) {
9297
- const response = await request(url, {
9298
- ...init,
9299
- headers: {
9300
- Accept: "application/vnd.github+json",
9301
- "X-GitHub-Api-Version": "2022-11-28",
9302
- ...init.headers
9303
- },
9304
- signal: init.signal ?? AbortSignal.timeout(timeoutMs)
9305
- });
9306
- if (!response.ok) throw new GitHubApiError(response.status, response.statusText);
9307
- return await response.json();
9308
- }
9309
9361
  /**
9310
9362
  * Mint an installation token for the Patronage Factory App.
9311
9363
  *
@@ -9315,15 +9367,7 @@ async function githubJson(request, url, init, timeoutMs = GITHUB_PUBLISH_TIMEOUT
9315
9367
  * where the credentials come from (`user-config`), and the publish timeout the
9316
9368
  * rest of these calls are bound by. Nothing is cached, as before.
9317
9369
  */
9318
- const installationToken = (repository, config, request, now, timeoutMs = GITHUB_PUBLISH_TIMEOUT_MS) => mintInstallationToken({
9319
- credentials: config,
9320
- owner: repository.owner,
9321
- repo: repository.repo
9322
- }, {
9323
- fetch: request,
9324
- now: () => now,
9325
- timeoutMs
9326
- });
9370
+ const installationToken = (repository, config, request, now, timeoutMs = GITHUB_PUBLISH_TIMEOUT_MS) => mintFactoryInstallationToken(repository, config, request, now, timeoutMs);
9327
9371
  function verifyOutput(proof) {
9328
9372
  const value = proof;
9329
9373
  const commands = value.executedCommands?.length ?? 0;
@@ -9412,7 +9456,7 @@ async function postFactoryCheckRun({ config, deadlineMs, dependencies, detailsUr
9412
9456
  const timeoutMs = dependencies.timeoutMs ?? 5e3;
9413
9457
  try {
9414
9458
  const token = await installationToken(input, config, request, (dependencies.now ?? Date.now)(), timeoutMs);
9415
- return await githubJson(request, `https://api.github.com/repos/${input.owner}/${input.repo}/check-runs`, {
9459
+ return await requestGitHubJson(request, `https://api.github.com/repos/${input.owner}/${input.repo}/check-runs`, {
9416
9460
  body: JSON.stringify(checkRunRequestBody(input, detailsUrl)),
9417
9461
  headers: {
9418
9462
  Authorization: `Bearer ${token}`,
@@ -9509,7 +9553,7 @@ async function servedFactoryCheckRun(input, config, dependencies) {
9509
9553
  filter: "all",
9510
9554
  per_page: "100"
9511
9555
  });
9512
- const served = await githubJson(request, `https://api.github.com/repos/${input.owner}/${input.repo}/commits/${input.sha}/check-runs?${query.toString()}`, {
9556
+ const served = await requestGitHubJson(request, `https://api.github.com/repos/${input.owner}/${input.repo}/commits/${input.sha}/check-runs?${query.toString()}`, {
9513
9557
  headers: { Authorization: `Bearer ${token}` },
9514
9558
  method: "GET"
9515
9559
  }, timeoutMs);
@@ -10183,7 +10227,7 @@ const assertCleanInstallResolution = (repoRoot, dependencies = {}) => {
10183
10227
  //#region src/pr-verify.ts
10184
10228
  const PR_VERIFY_SCHEMA_VERSION = 4;
10185
10229
  const DEFAULT_PR_VERIFY_PROOF_PATH = ".factory-memory/pr-verify.json";
10186
- const PR_VERIFY_PROOF_DESCRIPTOR = {
10230
+ const PR_VERIFY_PROOF_DESCRIPTOR$1 = {
10187
10231
  defaultPath: DEFAULT_PR_VERIFY_PROOF_PATH,
10188
10232
  label: "pr:verify proof",
10189
10233
  parse: validatePrVerifyProof,
@@ -10225,7 +10269,7 @@ function defaultPrVerifyGit() {
10225
10269
  };
10226
10270
  }
10227
10271
  function readPreviousPrVerifyProof(cwd, filePath) {
10228
- return readOptionalProofFor(PR_VERIFY_PROOF_DESCRIPTOR, cwd, filePath);
10272
+ return readOptionalProofFor(PR_VERIFY_PROOF_DESCRIPTOR$1, cwd, filePath);
10229
10273
  }
10230
10274
  function collectBaselineFullProofs({ appendPrevious, previousProof, seedFilter }) {
10231
10275
  if (!previousProof) return;
@@ -10569,7 +10613,7 @@ function runPrVerify(args, dependencies = {}) {
10569
10613
  //#endregion
10570
10614
  //#region src/demand-waive.ts
10571
10615
  const DEMAND_WAIVER_SCHEMA_VERSION = 1;
10572
- const shaSchema$2 = z.string().regex(/^[0-9a-f]{40}$/u);
10616
+ const shaSchema$3 = z.string().regex(/^[0-9a-f]{40}$/u);
10573
10617
  const demandWaiverStorePath = (cwd, filePath) => path.resolve(cwd, filePath ?? ".factory-memory/demand-waivers.json");
10574
10618
  /** Every waiver recorded in this checkout; an absent or unreadable store is
10575
10619
  * no waivers, so a damaged file can only ever block, never permit. */
@@ -10619,7 +10663,7 @@ const requestRefusals = ({ demand, parsedDemand, rationale }) => [...parsedDeman
10619
10663
  * the authoring session read below describes a different candidate.
10620
10664
  */
10621
10665
  const candidateBindingRefusals = ({ headSha, pr, verifyProof }) => {
10622
- if (!headSha || !shaSchema$2.safeParse(headSha).success) return [`demand:waive could not resolve a pushed head SHA for PR #${pr}; a waiver binds to one candidate.`];
10666
+ if (!headSha || !shaSchema$3.safeParse(headSha).success) return [`demand:waive could not resolve a pushed head SHA for PR #${pr}; a waiver binds to one candidate.`];
10623
10667
  return verifyProof && verifyProof.headSha !== headSha ? [`The pr:verify proof in this checkout records head ${verifyProof.headSha}, not the pushed head ${headSha}; re-run pr:verify on the candidate before waiving one of its demands.`] : [];
10624
10668
  };
10625
10669
  /**
@@ -10645,7 +10689,7 @@ const runDemandWaive = (args, dependencies = {}) => {
10645
10689
  pr: args.pr,
10646
10690
  repo: repository.name
10647
10691
  });
10648
- const verifyProof = readOptionalProofFor(PR_VERIFY_PROOF_DESCRIPTOR, cwd);
10692
+ const verifyProof = readOptionalProofFor(PR_VERIFY_PROOF_DESCRIPTOR$1, cwd);
10649
10693
  refusals.push(...candidateBindingRefusals({
10650
10694
  headSha,
10651
10695
  pr: args.pr,
@@ -13184,13 +13228,13 @@ const HOSTED_VERIFY_CHECK_NAME$1 = "verify";
13184
13228
  const GITHUB_ACTIONS_APP_ID = 15368;
13185
13229
  const GITHUB_ACTIONS_APP_SLUG = "github-actions";
13186
13230
  const MERGE_FREEZE_SCHEMA_VERSION = 1;
13187
- const CHECK_RUNS_PER_PAGE = 100;
13188
- const CHECK_RUN_PAGE_LIMIT = 10;
13189
- const shaSchema$1 = z.string().regex(/^[0-9a-f]{40}$/u);
13231
+ const CHECK_RUNS_PER_PAGE$1 = 100;
13232
+ const CHECK_RUN_PAGE_LIMIT$1 = 10;
13233
+ const shaSchema$2 = z.string().regex(/^[0-9a-f]{40}$/u);
13190
13234
  const activeMergeFreezeStateSchema = z.object({
13191
13235
  active: z.literal(true),
13192
13236
  generationId: z.number().int().positive(),
13193
- headSha: shaSchema$1,
13237
+ headSha: shaSchema$2,
13194
13238
  outcome: z.enum(["active", "stale"]),
13195
13239
  reason: z.string().min(1),
13196
13240
  recordedAt: z.iso.datetime(),
@@ -13200,7 +13244,7 @@ const inactiveMergeFreezeStateSchema = z.object({
13200
13244
  active: z.literal(false),
13201
13245
  clearRationale: z.string().min(1).optional(),
13202
13246
  generationId: z.number().int().positive(),
13203
- headSha: shaSchema$1,
13247
+ headSha: shaSchema$2,
13204
13248
  outcome: z.literal("inactive"),
13205
13249
  reason: z.string().min(1),
13206
13250
  recordedAt: z.iso.datetime(),
@@ -13224,7 +13268,7 @@ const mergeFreezeCheckRunListItemSchema = z.object({
13224
13268
  "neutral",
13225
13269
  "success"
13226
13270
  ]).nullable().optional(),
13227
- head_sha: shaSchema$1,
13271
+ head_sha: shaSchema$2,
13228
13272
  id: z.number().int().positive(),
13229
13273
  name: z.literal(MERGE_FREEZE_CHECK_NAME),
13230
13274
  output: z.object({ text: z.string().nullable().optional() }).optional(),
@@ -13249,7 +13293,7 @@ const hostedVerifyCheckRunSchema = z.object({
13249
13293
  id: z.literal(GITHUB_ACTIONS_APP_ID),
13250
13294
  slug: z.literal(GITHUB_ACTIONS_APP_SLUG)
13251
13295
  }),
13252
- head_sha: shaSchema$1,
13296
+ head_sha: shaSchema$2,
13253
13297
  id: z.number().int().positive(),
13254
13298
  name: z.literal(HOSTED_VERIFY_CHECK_NAME$1),
13255
13299
  started_at: z.iso.datetime().nullable(),
@@ -13260,8 +13304,8 @@ const hostedVerifyCheckRunSchema = z.object({
13260
13304
  ])
13261
13305
  });
13262
13306
  const commitParentsSchema = z.object({
13263
- parents: z.array(z.object({ sha: shaSchema$1 })),
13264
- sha: shaSchema$1
13307
+ parents: z.array(z.object({ sha: shaSchema$2 })),
13308
+ sha: shaSchema$2
13265
13309
  });
13266
13310
  function selectListedMergeFreezeGeneration(listed, expectedHeadSha) {
13267
13311
  const runs = z.object({ check_runs: z.array(z.unknown()) }).parse(listed).check_runs.map((run) => mergeFreezeCheckRunListItemSchema.parse(run));
@@ -13395,11 +13439,11 @@ function createGitHubCheckRunMergeFreezeApi(dependencies = {}) {
13395
13439
  return {
13396
13440
  list({ cwd, headSha, name, repository }) {
13397
13441
  const checkRuns = [];
13398
- for (let page = 1; page <= CHECK_RUN_PAGE_LIMIT; page += 1) {
13399
- const response = z.object({ check_runs: z.array(z.unknown()) }).parse((dependencies.readJson ?? runGhJsonAt)(["api", `/repos/${repository.owner}/${repository.name}/commits/${headSha}/check-runs?check_name=${encodeURIComponent(name)}&filter=all&per_page=${CHECK_RUNS_PER_PAGE}&page=${page}`], cwd));
13442
+ for (let page = 1; page <= CHECK_RUN_PAGE_LIMIT$1; page += 1) {
13443
+ const response = z.object({ check_runs: z.array(z.unknown()) }).parse((dependencies.readJson ?? runGhJsonAt)(["api", `/repos/${repository.owner}/${repository.name}/commits/${headSha}/check-runs?check_name=${encodeURIComponent(name)}&filter=all&per_page=${CHECK_RUNS_PER_PAGE$1}&page=${page}`], cwd));
13400
13444
  checkRuns.push(...response.check_runs);
13401
- if (response.check_runs.length < CHECK_RUNS_PER_PAGE) break;
13402
- if (page === CHECK_RUN_PAGE_LIMIT) throw new Error(`${MERGE_FREEZE_CHECK_NAME} check-run pagination limit was exhausted before GitHub returned a final page.`);
13445
+ if (response.check_runs.length < CHECK_RUNS_PER_PAGE$1) break;
13446
+ if (page === CHECK_RUN_PAGE_LIMIT$1) throw new Error(`${MERGE_FREEZE_CHECK_NAME} check-run pagination limit was exhausted before GitHub returned a final page.`);
13403
13447
  }
13404
13448
  return { check_runs: checkRuns };
13405
13449
  },
@@ -13495,7 +13539,7 @@ const selectReviewProof = ({ reviewProof }) => {
13495
13539
  if (isExplicitReviewFailure(reviewProof) || isUntypedReviewProof(reviewProof)) return {};
13496
13540
  return hasReviewProof(reviewProof) ? reviewProof : {};
13497
13541
  };
13498
- const shaSchema = z.string().regex(/^[0-9a-f]{40}$/u);
13542
+ const shaSchema$1 = z.string().regex(/^[0-9a-f]{40}$/u);
13499
13543
  const patchIdSchema = z.string().regex(/^[0-9a-f]{40,64}$/u);
13500
13544
  const reviewCycleStateSchema = z.object({
13501
13545
  autoBlockingFindings: z.number().int().nonnegative(),
@@ -13550,7 +13594,7 @@ const readinessRepairSchema = z.object({
13550
13594
  command: z.string().min(1)
13551
13595
  });
13552
13596
  const managedReadinessLedgerSchema = z.object({
13553
- baseSha: shaSchema,
13597
+ baseSha: shaSchema$1,
13554
13598
  blockingReasons: z.array(z.string()),
13555
13599
  classification: z.enum([
13556
13600
  "docs/process-only",
@@ -13590,13 +13634,13 @@ const managedReadinessLedgerSchema = z.object({
13590
13634
  * branded `patronage-factory/pr-ready` check binds to — GitHub's required
13591
13635
  * checks compare a new head's checks against exactly this SHA (#477).
13592
13636
  */
13593
- headSha: shaSchema,
13637
+ headSha: shaSchema$1,
13594
13638
  /**
13595
13639
  * The merge base used to evaluate tree-scoped external evidence. Optional
13596
13640
  * only for backwards-compatible parsing of readiness ledgers written before
13597
13641
  * #732; a committed verify-type demand cannot pass the merge gate without it.
13598
13642
  */
13599
- mergeBaseSha: shaSchema.optional(),
13643
+ mergeBaseSha: shaSchema$1.optional(),
13600
13644
  patchId: patchIdSchema,
13601
13645
  pr: z.number().int().positive(),
13602
13646
  repairs: z.array(readinessRepairSchema).default([]),
@@ -14312,6 +14356,605 @@ const evaluateReadiness = (input) => {
14312
14356
  };
14313
14357
  };
14314
14358
  //#endregion
14359
+ //#region src/github-pr-status-comment.ts
14360
+ const FACTORY_PR_STATUS_COMMENT_MARKER = "<!-- patronage-factory-pr-status-hud -->";
14361
+ const OBSERVED_MARKER_PREFIX = "<!-- patronage-factory-pr-status-observed:";
14362
+ const OBSERVED_MARKER_SUFFIX = " -->";
14363
+ const UPSERT_ATTEMPT_LIMIT = 5;
14364
+ const writtenCommentSchema = z.object({
14365
+ html_url: z.string().url(),
14366
+ id: z.number().int().positive(),
14367
+ performed_via_github_app: z.object({ id: z.number().int().positive() }).refine((app) => String(app.id) === FACTORY_PROOF_GATE_APP_ID, "PR status HUD mutation was not performed by the Patronage Factory App.")
14368
+ });
14369
+ const issueCommentSchema = writtenCommentSchema.extend({
14370
+ body: z.string().nullable(),
14371
+ performed_via_github_app: z.object({ id: z.number().int().positive() }).nullable().optional()
14372
+ });
14373
+ const managedBody = (body, observedAt) => {
14374
+ const observed = observedAt === void 0 ? "" : `${OBSERVED_MARKER_PREFIX}${observedAt}${OBSERVED_MARKER_SUFFIX}\n`;
14375
+ const trimmed = body.trim();
14376
+ if (trimmed === "" && observed === "") return FACTORY_PR_STATUS_COMMENT_MARKER;
14377
+ return `${FACTORY_PR_STATUS_COMMENT_MARKER}\n${observed}${trimmed}`;
14378
+ };
14379
+ const readObservedAt = (body) => {
14380
+ if (!body) return;
14381
+ const start = body.indexOf(OBSERVED_MARKER_PREFIX);
14382
+ if (start === -1) return;
14383
+ const from = start + 42;
14384
+ const end = body.indexOf(OBSERVED_MARKER_SUFFIX, from);
14385
+ if (end === -1) return;
14386
+ const value = body.slice(from, end);
14387
+ if (Number.isNaN(new Date(value).valueOf())) return;
14388
+ return value;
14389
+ };
14390
+ const existingIsCurrentOrNewer = (existingBody, incomingObservedAt) => {
14391
+ if (incomingObservedAt === void 0) return false;
14392
+ const existingObservedAt = readObservedAt(existingBody);
14393
+ return existingObservedAt !== void 0 && existingObservedAt >= incomingObservedAt;
14394
+ };
14395
+ const isManagedComment = (comment) => String(comment.performed_via_github_app?.id) === FACTORY_PROOF_GATE_APP_ID && Boolean(comment.body?.includes("<!-- patronage-factory-pr-status-hud -->"));
14396
+ const exchangeFor = (dependencies) => {
14397
+ if (dependencies.request) return dependencies.request;
14398
+ if (dependencies.requestJson) {
14399
+ const { requestJson } = dependencies;
14400
+ return async (input) => {
14401
+ return {
14402
+ json: await requestJson(input),
14403
+ status: 200
14404
+ };
14405
+ };
14406
+ }
14407
+ return requestFactoryApp;
14408
+ };
14409
+ const listManagedComments = async (exchange, input, pageNumber = 1, collected = []) => {
14410
+ const response = await exchange({
14411
+ method: "GET",
14412
+ owner: input.owner,
14413
+ path: `issues/${input.pr}/comments?per_page=100&page=${pageNumber}`,
14414
+ repo: input.repo
14415
+ });
14416
+ const page = z.array(issueCommentSchema).parse(response.json);
14417
+ const managed = [...collected, ...page.filter(isManagedComment)];
14418
+ if (page.length < 100) return managed;
14419
+ return listManagedComments(exchange, input, pageNumber + 1, managed);
14420
+ };
14421
+ const resultFrom = (action, comment) => ({
14422
+ action,
14423
+ commentId: comment.id,
14424
+ url: comment.html_url
14425
+ });
14426
+ const isRetryableMutation = (error) => error instanceof GitHubApiError && error.status === 404;
14427
+ const deleteManagedComment = async (exchange, input, commentId) => {
14428
+ try {
14429
+ await exchange({
14430
+ method: "DELETE",
14431
+ owner: input.owner,
14432
+ path: `issues/comments/${commentId}`,
14433
+ repo: input.repo
14434
+ });
14435
+ } catch (error) {
14436
+ if (!(error instanceof GitHubApiError) || error.status !== 404) throw error;
14437
+ }
14438
+ };
14439
+ const newestBody = (comments, incomingBody, incomingObservedAt) => {
14440
+ let body = incomingBody;
14441
+ let observedAt = incomingObservedAt;
14442
+ for (const comment of comments) {
14443
+ const { body: commentBody } = comment;
14444
+ const observed = readObservedAt(commentBody);
14445
+ if (observed !== void 0 && (observedAt === void 0 || observed > observedAt) && commentBody) {
14446
+ body = commentBody;
14447
+ observedAt = observed;
14448
+ }
14449
+ }
14450
+ return body;
14451
+ };
14452
+ const readComment = async (exchange, input, commentId) => {
14453
+ const response = await exchange({
14454
+ method: "GET",
14455
+ owner: input.owner,
14456
+ path: `issues/comments/${commentId}`,
14457
+ repo: input.repo
14458
+ });
14459
+ return issueCommentSchema.parse(response.json);
14460
+ };
14461
+ const patchManaged = async (exchange, input, commentId, body) => {
14462
+ const response = await exchange({
14463
+ body: { body },
14464
+ method: "PATCH",
14465
+ owner: input.owner,
14466
+ path: `issues/comments/${commentId}`,
14467
+ repo: input.repo
14468
+ });
14469
+ return writtenCommentSchema.parse(response.json);
14470
+ };
14471
+ const reconcileManagedComments = async (exchange, input, comments, incomingBody) => {
14472
+ const [first, ...rest] = comments;
14473
+ if (!first) throw new Error("PR status HUD reconcile requires at least one managed comment.");
14474
+ let keeper = first;
14475
+ for (const comment of rest) if (comment.id < keeper.id) keeper = comment;
14476
+ await Promise.all(comments.filter((comment) => comment.id !== keeper.id).map((comment) => deleteManagedComment(exchange, input, comment.id)));
14477
+ const body = newestBody(comments, incomingBody, input.observedAt);
14478
+ if (keeper.body === body) return keeper;
14479
+ const updated = await patchManaged(exchange, input, keeper.id, body);
14480
+ return {
14481
+ ...keeper,
14482
+ ...updated,
14483
+ body
14484
+ };
14485
+ };
14486
+ const attemptUpsert = async (exchange, input, writtenBody, attempt) => {
14487
+ if (attempt >= UPSERT_ATTEMPT_LIMIT) throw new Error("PR status HUD upsert exhausted attempts without a single current comment.");
14488
+ const managed = await listManagedComments(exchange, input);
14489
+ if (managed.length > 1) return resultFrom("updated", await reconcileManagedComments(exchange, input, managed, writtenBody));
14490
+ const [existing] = managed;
14491
+ if (existing) try {
14492
+ const fresh = await readComment(exchange, input, existing.id);
14493
+ if (existingIsCurrentOrNewer(fresh.body, input.observedAt)) return resultFrom("updated", fresh);
14494
+ return resultFrom("updated", await patchManaged(exchange, input, existing.id, writtenBody));
14495
+ } catch (error) {
14496
+ if (isRetryableMutation(error)) return attemptUpsert(exchange, input, writtenBody, attempt + 1);
14497
+ throw error;
14498
+ }
14499
+ try {
14500
+ const createdResponse = await exchange({
14501
+ body: { body: writtenBody },
14502
+ method: "POST",
14503
+ owner: input.owner,
14504
+ path: `issues/${input.pr}/comments`,
14505
+ repo: input.repo
14506
+ });
14507
+ const created = writtenCommentSchema.parse(createdResponse.json);
14508
+ const after = await listManagedComments(exchange, input);
14509
+ if (after.length > 1) {
14510
+ const keeper = await reconcileManagedComments(exchange, input, after, writtenBody);
14511
+ return resultFrom(keeper.id === created.id ? "created" : "updated", keeper);
14512
+ }
14513
+ return resultFrom("created", created);
14514
+ } catch (error) {
14515
+ if (isRetryableMutation(error)) return attemptUpsert(exchange, input, writtenBody, attempt + 1);
14516
+ throw error;
14517
+ }
14518
+ };
14519
+ async function upsertFactoryPrStatusComment(input, dependencies = {}) {
14520
+ return attemptUpsert(exchangeFor(dependencies), input, managedBody(input.body, input.observedAt), 0);
14521
+ }
14522
+ //#endregion
14523
+ //#region src/preview-lifecycle.ts
14524
+ const previewTargetPlanSchema = z.object({
14525
+ basis: z.string(),
14526
+ disposition: z.enum(["deploy", "not-required"]),
14527
+ name: z.string().min(1)
14528
+ });
14529
+ const previewLifecyclePlanSchema = z.object({
14530
+ deploy: z.array(z.string()),
14531
+ notRequired: z.array(previewTargetPlanSchema),
14532
+ stampTrusted: z.boolean(),
14533
+ targets: z.array(previewTargetPlanSchema)
14534
+ }).superRefine((plan, context) => {
14535
+ const deploy = plan.targets.filter((target) => target.disposition === "deploy").map((target) => target.name);
14536
+ const notRequired = plan.targets.filter((target) => target.disposition === "not-required");
14537
+ if (JSON.stringify(plan.deploy) !== JSON.stringify(deploy)) context.addIssue({
14538
+ code: "custom",
14539
+ message: "deploy must match the plan's deploy target dispositions",
14540
+ path: ["deploy"]
14541
+ });
14542
+ if (JSON.stringify(plan.notRequired) !== JSON.stringify(notRequired)) context.addIssue({
14543
+ code: "custom",
14544
+ message: "notRequired must match the plan's not-required target dispositions",
14545
+ path: ["notRequired"]
14546
+ });
14547
+ });
14548
+ /** Validate a serialized plan without deriving or changing its dispositions. */
14549
+ const validatePreviewLifecyclePlan = (value) => previewLifecyclePlanSchema.parse(value);
14550
+ /**
14551
+ * The declared preview stacks: impact targets that are ALSO declared required
14552
+ * checks — i.e. targets whose proof is produced externally, which is exactly
14553
+ * what a preview producer is.
14554
+ *
14555
+ * Deriving the list from the two existing declarations rather than adding a
14556
+ * third is what makes lifecycle and demand unable to disagree: the stacks this
14557
+ * plan can withhold are precisely the demands wave 2 can release, so a stack
14558
+ * can never be skipped while its proof stays demanded.
14559
+ */
14560
+ const previewLifecycleTargets = (profile) => {
14561
+ const demanded = new Set((profile.requiredChecks ?? []).map(({ name }) => name));
14562
+ return (profile.impact?.targets ?? []).map(({ name }) => name).filter((name) => demanded.has(name));
14563
+ };
14564
+ /**
14565
+ * Plan the preview lifecycle for one candidate.
14566
+ *
14567
+ * Trust is resolved here through the SAME identity gate demand release uses
14568
+ * (`trustedImpactStamp`): the proof must have passed and must bind both the
14569
+ * candidate's headSha and its patchId. A stale, aborted, absent, or
14570
+ * differently-bound proof yields no stamp, and every declared stack is planned
14571
+ * for deploy. Total over its INPUTS — no proof, stamp, or profile can make it
14572
+ * throw. A malformed CALL is the deliberate exception: omitting
14573
+ * `vetoedTargets` is API misuse, not data doubt, and throws.
14574
+ */
14575
+ const planPreviewLifecycle = ({ candidate, profile, proof, vetoedTargets }) => {
14576
+ assertVetoedTargets(vetoedTargets, "planPreviewLifecycle");
14577
+ const stamp = trustedImpactStamp({
14578
+ candidate,
14579
+ proof
14580
+ });
14581
+ const vetoed = new Set(vetoedTargets);
14582
+ const targets = previewLifecycleTargets(profile).map((name) => {
14583
+ const decision = impactStampScopeDecision({
14584
+ stamp,
14585
+ surface: "preview-lifecycle",
14586
+ targetName: name
14587
+ });
14588
+ const vetoedHere = decision.scoped && vetoed.has(name);
14589
+ return {
14590
+ basis: vetoedHere ? `veto: a current envelope bound to this candidate records stack "${name}" as FAILING, so the stamp's release is withheld (${decision.reason})` : decision.reason,
14591
+ disposition: decision.scoped && !vetoedHere ? "not-required" : "deploy",
14592
+ name
14593
+ };
14594
+ });
14595
+ return {
14596
+ deploy: targets.filter((target) => target.disposition === "deploy").map((target) => target.name),
14597
+ notRequired: targets.filter((target) => target.disposition === "not-required"),
14598
+ stampTrusted: stamp !== void 0,
14599
+ targets
14600
+ };
14601
+ };
14602
+ //#endregion
14603
+ //#region src/pr-status-hud.ts
14604
+ const statusLabel = (status) => {
14605
+ return `${{
14606
+ Failed: "❌",
14607
+ "In progress": "⏳",
14608
+ Ready: "✅",
14609
+ Skipped: "➖"
14610
+ }[status]} ${status}`;
14611
+ };
14612
+ const utcTimestamp = (value) => {
14613
+ const instant = new Date(value);
14614
+ if (Number.isNaN(instant.valueOf())) throw new TypeError(`PR status HUD requires an ISO timestamp; received ${value}.`);
14615
+ return `${instant.toISOString().slice(0, 16).replace("T", " ")} UTC`;
14616
+ };
14617
+ const markdownText = (value) => value.split(/\r\n?|\n/u).join(" ").replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll("@", "&#64;").replaceAll("\\", "\\\\").replaceAll("`", "\\`").replaceAll("*", "\\*").replaceAll("_", "\\_").replaceAll("[", "\\[").replaceAll("]", "\\]");
14618
+ const markdownCell = (value) => markdownText(value).replaceAll("|", "\\|");
14619
+ const safeMarkdownLink = (label, value) => {
14620
+ if (!value) return "—";
14621
+ try {
14622
+ const url = new URL(value);
14623
+ if (url.protocol !== "https:" && url.protocol !== "http:") return "—";
14624
+ return `[${label}](<${url.href}>)`;
14625
+ } catch {
14626
+ return "—";
14627
+ }
14628
+ };
14629
+ const renderTable = (rows) => [
14630
+ "| Target | Status | Link | Timestamp |",
14631
+ "| --- | --- | --- | --- |",
14632
+ ...rows.map((row) => `| ${markdownCell(row.target)} | ${statusLabel(row.status)} | ${row.link} | ${row.timestamp} |`)
14633
+ ].join("\n");
14634
+ const checkRow = (target, snapshot, renderedAt) => {
14635
+ if (!snapshot) return {
14636
+ detail: "No completed Factory App snapshot is available at this head.",
14637
+ link: "—",
14638
+ status: "In progress",
14639
+ target,
14640
+ timestamp: utcTimestamp(renderedAt)
14641
+ };
14642
+ return {
14643
+ detail: `Completed snapshot: ${snapshot.conclusion}.`,
14644
+ link: safeMarkdownLink("Details", snapshot.detailsUrl),
14645
+ status: snapshot.conclusion === "success" ? "Ready" : "Failed",
14646
+ target,
14647
+ timestamp: utcTimestamp(snapshot.completedAt)
14648
+ };
14649
+ };
14650
+ const previewRow = (target, registration, failure, renderedAt) => {
14651
+ if (target.disposition === "not-required") return {
14652
+ detail: target.basis,
14653
+ link: "—",
14654
+ status: "Skipped",
14655
+ target: target.name,
14656
+ timestamp: utcTimestamp(renderedAt)
14657
+ };
14658
+ if (registration) return {
14659
+ detail: "Factory-owned preview registration is live at this head.",
14660
+ link: safeMarkdownLink("Preview", registration.url),
14661
+ status: "Ready",
14662
+ target: target.name,
14663
+ timestamp: utcTimestamp(renderedAt)
14664
+ };
14665
+ if (failure) return {
14666
+ detail: `Preview deploy concluded ${failure.conclusion}.`,
14667
+ link: safeMarkdownLink("Details", failure.detailsUrl),
14668
+ status: "Failed",
14669
+ target: target.name,
14670
+ timestamp: utcTimestamp(failure.completedAt)
14671
+ };
14672
+ return {
14673
+ detail: target.basis,
14674
+ link: "—",
14675
+ status: "In progress",
14676
+ target: target.name,
14677
+ timestamp: utcTimestamp(renderedAt)
14678
+ };
14679
+ };
14680
+ function renderFactoryPrStatusHud(input) {
14681
+ if (input.planHeadSha !== input.headSha) throw new Error(`PR status HUD plan targets ${input.planHeadSha}, expected current head ${input.headSha}.`);
14682
+ const checks = new Map(Object.entries(input.checks).flatMap(([name, check]) => {
14683
+ if (!check) return [];
14684
+ const expectedSha = name === "patronage-factory/merge-freeze" ? input.baseSha : input.headSha;
14685
+ return check.headSha === expectedSha ? [[name, check]] : [];
14686
+ }));
14687
+ const registrations = new Map(input.previewRegistrations.filter((row) => row.pr === input.pr && row.headSha === input.headSha && row.cleanupStatus === "pending").map((row) => [row.stack, row]));
14688
+ const rows = [
14689
+ checkRow("Verify", checks.get("patronage-factory/pr-verify"), input.renderedAt),
14690
+ checkRow("Ready", checks.get("patronage-factory/pr-ready"), input.renderedAt),
14691
+ ...input.plan.targets.map((target) => previewRow(target, registrations.get(target.name), input.previewFailures?.[target.name], input.renderedAt))
14692
+ ];
14693
+ const mergeFreeze = checks.get("patronage-factory/merge-freeze");
14694
+ if (mergeFreeze) rows.push(checkRow("Merge freeze", mergeFreeze, input.renderedAt));
14695
+ rows.push({
14696
+ detail: "Production deployment is not demanded for a pull request.",
14697
+ link: "—",
14698
+ status: "Skipped",
14699
+ target: "Production deploy",
14700
+ timestamp: utcTimestamp(input.renderedAt)
14701
+ });
14702
+ const active = rows.filter((row) => row.status !== "Skipped");
14703
+ const skipped = rows.filter((row) => row.status === "Skipped");
14704
+ const skippedDetails = skipped.map((row) => `- **${markdownText(row.target)}:** ${markdownText(row.detail)}`).join("\n");
14705
+ return [
14706
+ "## Patronage Factory status",
14707
+ "",
14708
+ renderTable(active),
14709
+ "",
14710
+ "<details>",
14711
+ `<summary>Skipped (${skipped.length})</summary>`,
14712
+ "",
14713
+ renderTable(skipped),
14714
+ "",
14715
+ skippedDetails,
14716
+ "",
14717
+ "</details>",
14718
+ "",
14719
+ `_Observed ${utcTimestamp(input.renderedAt)} at \`${input.headSha.slice(0, 7)}\`. Human-readable presentation only; checks and factory evidence remain authoritative._`
14720
+ ].join("\n");
14721
+ }
14722
+ async function presentFactoryPrStatusHud(input, dependencies) {
14723
+ const identity = {
14724
+ headSha: input.headSha,
14725
+ owner: input.owner,
14726
+ pr: input.pr,
14727
+ repo: input.repo
14728
+ };
14729
+ const sources = await dependencies.readSources(identity);
14730
+ const renderedAt = input.renderedAt ?? (/* @__PURE__ */ new Date()).toISOString();
14731
+ const body = renderFactoryPrStatusHud({
14732
+ ...sources,
14733
+ headSha: input.headSha,
14734
+ plan: input.plan,
14735
+ planHeadSha: input.planHeadSha,
14736
+ pr: input.pr,
14737
+ renderedAt
14738
+ });
14739
+ await dependencies.assertCurrentIdentity({
14740
+ baseSha: sources.baseSha,
14741
+ ...identity
14742
+ });
14743
+ return (dependencies.upsert ?? upsertFactoryPrStatusComment)({
14744
+ body,
14745
+ observedAt: renderedAt,
14746
+ owner: input.owner,
14747
+ pr: input.pr,
14748
+ repo: input.repo
14749
+ });
14750
+ }
14751
+ const checkRunSchema = z.object({
14752
+ app: z.object({ id: z.number().int().positive() }).nullable().optional(),
14753
+ completed_at: z.iso.datetime().nullable(),
14754
+ conclusion: z.string().nullable().optional(),
14755
+ details_url: z.string().nullable().optional(),
14756
+ head_sha: z.string().regex(/^[0-9a-f]{40}$/u),
14757
+ name: z.string(),
14758
+ started_at: z.iso.datetime().nullable(),
14759
+ status: z.enum([
14760
+ "completed",
14761
+ "in_progress",
14762
+ "queued"
14763
+ ])
14764
+ });
14765
+ const CHECK_RUNS_PER_PAGE = 100;
14766
+ const CHECK_RUN_PAGE_LIMIT = 10;
14767
+ const pullIdentitySchema = z.object({
14768
+ base: z.object({ sha: z.string().regex(/^[0-9a-f]{40}$/u) }),
14769
+ head: z.object({ sha: z.string().regex(/^[0-9a-f]{40}$/u) })
14770
+ });
14771
+ const readFactoryPrIdentity = async (input, dependencies) => pullIdentitySchema.parse(await requestGitHubJson(dependencies.fetch ?? fetch, `https://api.github.com/repos/${input.owner}/${input.repo}/pulls/${input.pr}`, {
14772
+ headers: { Authorization: `Bearer ${input.token}` },
14773
+ method: "GET"
14774
+ }, dependencies.timeoutMs));
14775
+ async function assertFactoryPrStatusIdentity(input, dependencies = {}) {
14776
+ const current = await readFactoryPrIdentity(input, dependencies);
14777
+ if (current.head.sha !== input.headSha || current.base.sha !== input.baseSha) throw new Error(`PR status HUD candidate identity moved to base ${current.base.sha} / head ${current.head.sha}; refusing stale presentation for base ${input.baseSha} / head ${input.headSha}.`);
14778
+ }
14779
+ const listFactoryCheckRuns = async (input, name, sha, dependencies) => {
14780
+ const request = dependencies.fetch ?? fetch;
14781
+ const runs = [];
14782
+ for (let page = 1; page <= CHECK_RUN_PAGE_LIMIT; page += 1) {
14783
+ const response = z.object({ check_runs: z.array(checkRunSchema) }).parse(await requestGitHubJson(request, `https://api.github.com/repos/${input.owner}/${input.repo}/commits/${sha}/check-runs?check_name=${encodeURIComponent(name)}&filter=all&per_page=${CHECK_RUNS_PER_PAGE}&page=${page}`, {
14784
+ headers: { Authorization: `Bearer ${input.token}` },
14785
+ method: "GET"
14786
+ }, dependencies.timeoutMs));
14787
+ runs.push(...response.check_runs);
14788
+ if (response.check_runs.length < CHECK_RUNS_PER_PAGE) return runs;
14789
+ }
14790
+ throw new Error(`${name} pagination limit was exhausted before GitHub returned a final page.`);
14791
+ };
14792
+ const completedSnapshot = (name, sha, runs) => {
14793
+ const candidates = runs.filter((run) => String(run.app?.id ?? "") === FACTORY_PROOF_GATE_APP_ID && run.head_sha === sha && run.name === name);
14794
+ if (candidates.some((run) => !Number.isFinite(generationOrderMs(run.started_at)))) throw new Error(`${name} presentation snapshot has a generation with no usable started_at.`);
14795
+ const selected = selectNewestGeneration(candidates.map((run) => ({
14796
+ orderMs: generationOrderMs(run.started_at),
14797
+ run
14798
+ })));
14799
+ if (selected.kind === "ambiguous") throw new Error(`Newest ${name} presentation snapshot is ambiguous: ${selected.tied.length} check runs share the newest timestamp.`);
14800
+ if (selected.kind === "none") return;
14801
+ const { run } = selected.generation;
14802
+ if (run.status !== "completed" || !run.completed_at || !run.conclusion) return;
14803
+ return {
14804
+ completedAt: run.completed_at,
14805
+ conclusion: run.conclusion,
14806
+ ...run.details_url ? { detailsUrl: run.details_url } : {},
14807
+ headSha: run.head_sha,
14808
+ name
14809
+ };
14810
+ };
14811
+ const FAILED_PREVIEW_CONCLUSIONS = new Set([
14812
+ "action_required",
14813
+ "cancelled",
14814
+ "failure",
14815
+ "stale",
14816
+ "startup_failure",
14817
+ "timed_out"
14818
+ ]);
14819
+ const completedFailedPreview = (name, sha, runs) => {
14820
+ const candidates = runs.filter((run) => run.head_sha === sha && run.name === name);
14821
+ if (candidates.some((run) => !Number.isFinite(generationOrderMs(run.started_at)))) throw new Error(`${name} preview failure snapshot has a generation with no usable started_at.`);
14822
+ const selected = selectNewestGeneration(candidates.map((run) => ({
14823
+ orderMs: generationOrderMs(run.started_at),
14824
+ run
14825
+ })));
14826
+ if (selected.kind === "ambiguous") throw new Error(`Newest ${name} preview failure snapshot is ambiguous: ${selected.tied.length} check runs share the newest timestamp.`);
14827
+ if (selected.kind === "none") return;
14828
+ const { run } = selected.generation;
14829
+ if (run.status !== "completed" || !run.completed_at || !run.conclusion || !FAILED_PREVIEW_CONCLUSIONS.has(run.conclusion)) return;
14830
+ return {
14831
+ completedAt: run.completed_at,
14832
+ conclusion: run.conclusion,
14833
+ ...run.details_url ? { detailsUrl: run.details_url } : {},
14834
+ headSha: run.head_sha,
14835
+ name
14836
+ };
14837
+ };
14838
+ async function readFactoryPrStatusSources(input, dependencies = {}) {
14839
+ const listPreviews = dependencies.listPreviewRegistrations ?? ((identity) => previewProofInventory.list(identity, {
14840
+ fetch: dependencies.fetch,
14841
+ timeoutMs: dependencies.timeoutMs,
14842
+ token: identity.token
14843
+ }));
14844
+ const [previewRegistrations, pullIdentity] = await Promise.all([listPreviews(input), readFactoryPrIdentity(input, dependencies)]);
14845
+ if (pullIdentity.head.sha !== input.headSha) throw new Error(`PR status HUD candidate head moved to ${pullIdentity.head.sha}; refusing stale presentation for ${input.headSha}.`);
14846
+ const baseSha = pullIdentity.base.sha;
14847
+ const checkTargets = [
14848
+ ["patronage-factory/pr-verify", input.headSha],
14849
+ ["patronage-factory/pr-ready", input.headSha],
14850
+ ["patronage-factory/merge-freeze", baseSha]
14851
+ ];
14852
+ const listedChecks = await Promise.all(checkTargets.map(([name, sha]) => listFactoryCheckRuns(input, name, sha, dependencies)));
14853
+ const checks = Object.fromEntries(checkTargets.flatMap(([name, sha], index) => {
14854
+ const snapshot = completedSnapshot(name, sha, listedChecks[index] ?? []);
14855
+ return snapshot ? [[name, snapshot]] : [];
14856
+ }));
14857
+ const previewTargets = input.previewTargets ?? [];
14858
+ const listedPreviewChecks = await Promise.all(previewTargets.map((name) => listFactoryCheckRuns(input, name, input.headSha, dependencies)));
14859
+ return {
14860
+ baseSha,
14861
+ checks,
14862
+ previewFailures: Object.fromEntries(previewTargets.flatMap((name, index) => {
14863
+ const snapshot = completedFailedPreview(name, input.headSha, listedPreviewChecks[index] ?? []);
14864
+ return snapshot ? [[name, snapshot]] : [];
14865
+ })),
14866
+ previewRegistrations
14867
+ };
14868
+ }
14869
+ const OBJECT_ID = /^[0-9a-f]{40}$/u;
14870
+ const PR_VERIFY_PROOF_DESCRIPTOR = {
14871
+ defaultPath: ".factory-memory/pr-verify.json",
14872
+ label: "pr:verify proof",
14873
+ parse: validatePrVerifyProof,
14874
+ schemaVersion: 4
14875
+ };
14876
+ const objectIdOrHead = (value, headSha) => value !== void 0 && OBJECT_ID.test(value) ? value : headSha;
14877
+ /**
14878
+ * Consume `planPreviewLifecycle` for the current checkout. No second plan is
14879
+ * invented: dispositions are Ready/Skipped from `deploy` vs `not-required`.
14880
+ * Candidate identity is never copied from the proof. Absent independent
14881
+ * identity or an unbound verify proof is the planner's conservative path.
14882
+ */
14883
+ function planFactoryPrStatusHud(input) {
14884
+ const { profile } = loadProjectProfile({
14885
+ cwd: input.cwd,
14886
+ profilePath: input.profilePath
14887
+ });
14888
+ const proof = readOptionalProofFor(PR_VERIFY_PROOF_DESCRIPTOR, input.cwd);
14889
+ const independentPatchId = input.patchId !== void 0 && OBJECT_ID.test(input.patchId) ? input.patchId : void 0;
14890
+ const boundProof = independentPatchId !== void 0 && proof !== void 0 && proof.headSha === input.headSha ? proof : void 0;
14891
+ const patchId = independentPatchId ?? input.headSha;
14892
+ const mergeBaseSha = objectIdOrHead(input.mergeBaseSha, input.headSha);
14893
+ return planPreviewLifecycle({
14894
+ candidate: {
14895
+ headSha: input.headSha,
14896
+ patchId
14897
+ },
14898
+ profile,
14899
+ proof: boundProof,
14900
+ vetoedTargets: boundFailingCheckNames({
14901
+ candidate: {
14902
+ headSha: input.headSha,
14903
+ mergeBaseSha,
14904
+ patchId
14905
+ },
14906
+ checkType: "verify",
14907
+ envelopes: loadEvidenceEnvelopes(input.cwd)
14908
+ })
14909
+ });
14910
+ }
14911
+ const resolveHudToken = async (input) => {
14912
+ if (input.token) return input.token;
14913
+ const config = tryUserConfig()?.config.githubApp;
14914
+ if (!config) throw new Error("PR status HUD refresh requires the Patronage Factory GitHub App.");
14915
+ return mintFactoryInstallationToken({
14916
+ owner: input.owner,
14917
+ repo: input.repo
14918
+ }, config, fetch, Date.now());
14919
+ };
14920
+ /** Plan from the checkout and present the App-owned HUD. */
14921
+ async function refreshFactoryPrStatusHud(input) {
14922
+ const plan = planFactoryPrStatusHud(input);
14923
+ const token = await resolveHudToken(input);
14924
+ return presentFactoryPrStatusHud({
14925
+ headSha: input.headSha,
14926
+ owner: input.owner,
14927
+ plan,
14928
+ planHeadSha: input.headSha,
14929
+ pr: input.pr,
14930
+ repo: input.repo
14931
+ }, {
14932
+ assertCurrentIdentity: (identity) => assertFactoryPrStatusIdentity({
14933
+ ...identity,
14934
+ token
14935
+ }),
14936
+ readSources: (identity) => readFactoryPrStatusSources({
14937
+ ...identity,
14938
+ previewTargets: plan.deploy,
14939
+ token
14940
+ }),
14941
+ upsert: (upsertInput) => upsertFactoryPrStatusComment(upsertInput, { request: (request) => requestFactoryApp(request, { token }) })
14942
+ });
14943
+ }
14944
+ /**
14945
+ * Presentation must not fail an admission gate. Used after pr:ready publishes
14946
+ * its check so the table is not stuck at first-write.
14947
+ */
14948
+ async function refreshFactoryPrStatusHudSafely(input, onNotice = (message) => {
14949
+ console.warn(message);
14950
+ }) {
14951
+ try {
14952
+ await refreshFactoryPrStatusHud(input);
14953
+ } catch (error) {
14954
+ onNotice(`PR status HUD refresh failed without affecting the gate: ${error instanceof Error ? error.message : String(error)}`);
14955
+ }
14956
+ }
14957
+ //#endregion
14315
14958
  //#region src/pr-ready.ts
14316
14959
  const CHECK_CONCLUSION_BY_READINESS_STATUS = {
14317
14960
  blocked: "failure",
@@ -14583,6 +15226,20 @@ async function runPrReady(args, dependencies = {}) {
14583
15226
  sleep: dependencies.sleep
14584
15227
  });
14585
15228
  let finalCheckPublished = false;
15229
+ let hudMergeBaseSha;
15230
+ let hudPatchId;
15231
+ const presentHud = async () => {
15232
+ await (dependencies.presentStatusHud ?? refreshFactoryPrStatusHudSafely)({
15233
+ cwd,
15234
+ headSha: pr.headRefOid,
15235
+ owner: repository.owner,
15236
+ pr: args.pr,
15237
+ repo: repository.name,
15238
+ ...args.profilePath === void 0 ? {} : { profilePath: args.profilePath },
15239
+ ...hudMergeBaseSha === void 0 ? {} : { mergeBaseSha: hudMergeBaseSha },
15240
+ ...hudPatchId === void 0 ? {} : { patchId: hudPatchId }
15241
+ });
15242
+ };
14586
15243
  try {
14587
15244
  const authoritativeBaseSha = authoritativePrBase({
14588
15245
  args,
@@ -14594,7 +15251,9 @@ async function runPrReady(args, dependencies = {}) {
14594
15251
  cwd,
14595
15252
  git
14596
15253
  });
14597
- const { headSha: localHeadSha, patchId } = candidateIdentityForBase(authoritativeBaseSha);
15254
+ const { headSha: localHeadSha, mergeBaseSha: resolvedMergeBaseSha, patchId } = candidateIdentityForBase(authoritativeBaseSha);
15255
+ hudMergeBaseSha = resolvedMergeBaseSha;
15256
+ hudPatchId = patchId;
14598
15257
  const files = git.changedFiles(cwd, authoritativeBaseSha, pr.headRefOid);
14599
15258
  const committedPolicy = policyProfileAtCommittedHead({
14600
15259
  cwd,
@@ -14778,6 +15437,7 @@ async function runPrReady(args, dependencies = {}) {
14778
15437
  subject: { prNumber: args.pr }
14779
15438
  }, dependencies.hq);
14780
15439
  reportProof(proof, output, Boolean(args.json), args.report ?? true);
15440
+ await presentHud();
14781
15441
  if (proof.status !== "ready" && (args.throwWhenBlocked ?? true)) throw new Error(`pr:ready blocked: ${proof.blockingReasons.join(" ")}`);
14782
15442
  return proof;
14783
15443
  } catch (error) {
@@ -14798,6 +15458,7 @@ async function runPrReady(args, dependencies = {}) {
14798
15458
  sha: pr.headRefOid
14799
15459
  };
14800
15460
  if (!await (dependencies.publishFinalCheckRun ? dependencies.publishFinalCheckRun(failureCheckInput) : ensureFactoryCheckRunPublished(failureCheckInput, { onDiagnostic: (diagnostic) => process.stderr.write(diagnostic) }))) console.warn(`pr:ready could not publish the failing ${FACTORY_CHECK_NAMES["pr-ready"]} check run for ${pr.headRefOid}; the required check is absent rather than red.`);
15461
+ await presentHud();
14801
15462
  }
14802
15463
  throw error;
14803
15464
  }
@@ -15207,7 +15868,7 @@ async function runPrReview(args, dependencies = {}) {
15207
15868
  git
15208
15869
  })(args.base);
15209
15870
  const { additional: additionalOutput, canonical: output } = resolveProofOutputPaths(cwd, DEFAULT_PR_REVIEW_PROOF_PATH, args.output);
15210
- const verifyProof = readOptionalProofFor(PR_VERIFY_PROOF_DESCRIPTOR, cwd, args.verifyProof ?? ".factory-memory/pr-verify.json");
15871
+ const verifyProof = readOptionalProofFor(PR_VERIFY_PROOF_DESCRIPTOR$1, cwd, args.verifyProof ?? ".factory-memory/pr-verify.json");
15211
15872
  const writeProofCopies = (proof) => {
15212
15873
  writeReviewProofCopies(cwd, proof, output, additionalOutput);
15213
15874
  enqueueHqIngest({
@@ -15418,7 +16079,7 @@ function discoverOrCreatePullRequestNumber({ args, branch, cwd, gh }) {
15418
16079
  if (!created[0]) throw new Error(`gh pr create completed but no PR was found for branch ${branch}.`);
15419
16080
  return created[0].number;
15420
16081
  }
15421
- const readOptionalVerifyProof = (cwd, proofPath) => readOptionalProofFor(PR_VERIFY_PROOF_DESCRIPTOR, cwd, proofPath);
16082
+ const readOptionalVerifyProof = (cwd, proofPath) => readOptionalProofFor(PR_VERIFY_PROOF_DESCRIPTOR$1, cwd, proofPath);
15422
16083
  const followUpSummaryLine = (outcome) => {
15423
16084
  if (outcome.status === "succeeded") return `Follow-up succeeded: ${outcome.action.command} — ${outcome.reason}.`;
15424
16085
  if (outcome.status === "skipped") return `Follow-up skipped (${outcome.action.command}): ${outcome.reason}.`;
@@ -16211,6 +16872,67 @@ function createProductionImpactCommand(output) {
16211
16872
  }));
16212
16873
  }
16213
16874
  //#endregion
16875
+ //#region src/pr-status-hud-command.ts
16876
+ const shaSchema = z.string().regex(/^[0-9a-f]{40}$/u);
16877
+ const boundPlanSchema = z.object({
16878
+ headSha: shaSchema,
16879
+ plan: z.unknown()
16880
+ });
16881
+ function resolvePrStatusHudPlan(args) {
16882
+ if (args.planPath !== void 0) {
16883
+ const boundPlan = boundPlanSchema.parse(JSON.parse(readFileSync(path.resolve(args.cwd, args.planPath), "utf-8")));
16884
+ if (boundPlan.headSha !== args.headSha) throw new Error(`PR status HUD plan targets ${boundPlan.headSha}, expected current head ${args.headSha}.`);
16885
+ return {
16886
+ plan: validatePreviewLifecyclePlan(boundPlan.plan),
16887
+ planHeadSha: boundPlan.headSha
16888
+ };
16889
+ }
16890
+ return {
16891
+ plan: planFactoryPrStatusHud({
16892
+ cwd: args.cwd,
16893
+ headSha: args.headSha
16894
+ }),
16895
+ planHeadSha: args.headSha
16896
+ };
16897
+ }
16898
+ async function runPrStatusHud(args) {
16899
+ const { plan, planHeadSha } = resolvePrStatusHudPlan(args);
16900
+ return presentFactoryPrStatusHud({
16901
+ headSha: args.headSha,
16902
+ owner: args.owner,
16903
+ plan,
16904
+ planHeadSha,
16905
+ pr: args.pr,
16906
+ repo: args.repo
16907
+ }, {
16908
+ assertCurrentIdentity: (identity) => assertFactoryPrStatusIdentity({
16909
+ ...identity,
16910
+ token: args.token
16911
+ }),
16912
+ readSources: (identity) => readFactoryPrStatusSources({
16913
+ ...identity,
16914
+ token: args.token
16915
+ }),
16916
+ upsert: (input) => upsertFactoryPrStatusComment(input, { request: (request) => requestFactoryApp(request, { token: args.token }) })
16917
+ });
16918
+ }
16919
+ function createPrStatusHudCommand(output, action = runPrStatusHud, env = process.env) {
16920
+ return markCwdOptionDefault(new Command("pr:status-hud").description("Present the App-owned PR status HUD from planPreviewLifecycle").requiredOption("--head <sha>", "candidate head commit SHA").requiredOption("--owner <owner>", "GitHub repository owner").requiredOption("--repo <repo>", "GitHub repository name").requiredOption("--pr <number>", "pull request number", positiveInteger$1("--pr")).option("--plan <path>", "optional candidate-bound JSON envelope; when omitted, planPreviewLifecycle is consumed from the checkout").option("--cwd <path>", "repository working directory", collectCwdOption).action(async (options) => {
16921
+ const token = env.FACTORY_GITHUB_APP_TOKEN;
16922
+ if (!token) throw new Error("pr:status-hud requires FACTORY_GITHUB_APP_TOKEN from actions/create-github-app-token; a workflow GITHUB_TOKEN cannot own the Factory App comment.");
16923
+ const result = await action({
16924
+ cwd: resolveCwdOption(options.cwd),
16925
+ headSha: shaSchema.parse(options.head),
16926
+ owner: options.owner,
16927
+ ...options.plan === void 0 ? {} : { planPath: options.plan },
16928
+ pr: options.pr,
16929
+ repo: options.repo,
16930
+ token
16931
+ });
16932
+ output.stdout.write(`PR status HUD ${result.action}: ${result.url} (comment ${result.commentId})\n`);
16933
+ }));
16934
+ }
16935
+ //#endregion
16214
16936
  //#region src/worktree-scratch-files.ts
16215
16937
  var worktree_scratch_files_exports = /* @__PURE__ */ __exportAll({
16216
16938
  checkWorktreeScratchFiles: () => checkWorktreeScratchFiles,
@@ -16427,60 +17149,6 @@ const assembleReviewPrompt = ({ base, changedFiles, headSha, issueFocus, kind, p
16427
17149
  };
16428
17150
  };
16429
17151
  //#endregion
16430
- //#region src/preview-lifecycle.ts
16431
- /**
16432
- * The declared preview stacks: impact targets that are ALSO declared required
16433
- * checks — i.e. targets whose proof is produced externally, which is exactly
16434
- * what a preview producer is.
16435
- *
16436
- * Deriving the list from the two existing declarations rather than adding a
16437
- * third is what makes lifecycle and demand unable to disagree: the stacks this
16438
- * plan can withhold are precisely the demands wave 2 can release, so a stack
16439
- * can never be skipped while its proof stays demanded.
16440
- */
16441
- const previewLifecycleTargets = (profile) => {
16442
- const demanded = new Set((profile.requiredChecks ?? []).map(({ name }) => name));
16443
- return (profile.impact?.targets ?? []).map(({ name }) => name).filter((name) => demanded.has(name));
16444
- };
16445
- /**
16446
- * Plan the preview lifecycle for one candidate.
16447
- *
16448
- * Trust is resolved here through the SAME identity gate demand release uses
16449
- * (`trustedImpactStamp`): the proof must have passed and must bind both the
16450
- * candidate's headSha and its patchId. A stale, aborted, absent, or
16451
- * differently-bound proof yields no stamp, and every declared stack is planned
16452
- * for deploy. Total over its INPUTS — no proof, stamp, or profile can make it
16453
- * throw. A malformed CALL is the deliberate exception: omitting
16454
- * `vetoedTargets` is API misuse, not data doubt, and throws.
16455
- */
16456
- const planPreviewLifecycle = ({ candidate, profile, proof, vetoedTargets }) => {
16457
- assertVetoedTargets(vetoedTargets, "planPreviewLifecycle");
16458
- const stamp = trustedImpactStamp({
16459
- candidate,
16460
- proof
16461
- });
16462
- const vetoed = new Set(vetoedTargets);
16463
- const targets = previewLifecycleTargets(profile).map((name) => {
16464
- const decision = impactStampScopeDecision({
16465
- stamp,
16466
- surface: "preview-lifecycle",
16467
- targetName: name
16468
- });
16469
- const vetoedHere = decision.scoped && vetoed.has(name);
16470
- return {
16471
- basis: vetoedHere ? `veto: a current envelope bound to this candidate records stack "${name}" as FAILING, so the stamp's release is withheld (${decision.reason})` : decision.reason,
16472
- disposition: decision.scoped && !vetoedHere ? "not-required" : "deploy",
16473
- name
16474
- };
16475
- });
16476
- return {
16477
- deploy: targets.filter((target) => target.disposition === "deploy").map((target) => target.name),
16478
- notRequired: targets.filter((target) => target.disposition === "not-required"),
16479
- stampTrusted: stamp !== void 0,
16480
- targets
16481
- };
16482
- };
16483
- //#endregion
16484
17152
  //#region src/index.ts
16485
17153
  function createProgram(options = {}) {
16486
17154
  const factoryCliInvocation = options.factoryCliInvocation ? FactoryCliInvocationSchema.parse(options.factoryCliInvocation) : defaultFactoryCliInvocation(import.meta.filename);
@@ -16507,6 +17175,7 @@ function createProgram(options = {}) {
16507
17175
  program.addCommand(createEpicPublishStructureCommand(output));
16508
17176
  program.addCommand(createPrReviewCommand(output, options.actions?.prReview));
16509
17177
  program.addCommand(createPrReadyCommand(output, options.actions?.prReady));
17178
+ program.addCommand(createPrStatusHudCommand(output, options.actions?.prStatusHud));
16510
17179
  program.addCommand(createPrPublishCommand(output, prPublishAction));
16511
17180
  program.addCommand(createDemandWaiveCommand(output, options.actions?.demandWaive));
16512
17181
  return program;
@@ -16531,4 +17200,4 @@ if (isDirectCliExecution()) try {
16531
17200
  process.exit(1);
16532
17201
  }
16533
17202
  //#endregion
16534
- export { DEFAULT_DEMAND_WAIVER_PATH, DEFAULT_FACTORY_REPOSITORY, DEFAULT_ROOT_SHARED_GLOBS, DemandWaiveRefusalError, EMPTY_TREE_OBJECT_HASH, EPIC_STRUCTURE_NODE_STATUSES, EPIC_STRUCTURE_SCHEMA_VERSION, EpicStructureValidationError, FACTORY_BASE_REF_ENV, FACTORY_CHANGED_FILES_ENV, FACTORY_CHANGED_FILES_FILE_ENV, FactoryCliInvocationSchema, FollowUpActionSchema, MERGE_FREEZE_APP_SLUG, MERGE_FREEZE_CHECK_NAME, PrPublishFollowUpError, REVIEW_FOCUS_SECTION, WorkerCheckoutGuardError, appendReviewLadderStageTraceEvent, appendWithTraceSinks, applyDemandWaiver, assembleReviewPrompt, assertWorkerCheckoutAllowed, authorizeDemandWaiver, batteryScopeSummaryLines, blockingLadderFindings, boundary_manifest_exports as boundaryManifest, boundary_review_proof_exports as boundaryReviewProof, buildEpicStructureEvent, buildEpicStructurePayload, buildEvidenceEnvelope, buildReviewGateNotRequiredTraceEvent, buildReviewLadderStageTraceEvent, comment_provenance_exports as commentProvenance, createLocalJsonlTraceSink, createProgram, createVerificationRunContext, doctorProjectProfile, epicStructureEventId, evaluateReviewLadder, evidenceEnvelopeFilename, findingKey, followUpFromArgv, impactStampScopeDecision, inferFixedInThreadDispositions, isProductionHqUrl, loadProjectProfile, normalizeIssueComments, openLadderFindings, planPreviewLifecycle, planPublishFollowUp, planVerificationBattery, readiness_evaluation_exports as prReadinessEvaluation, external_evidence_exports as prReadinessExternalEvidence, pr_body_renderer_exports as prReadinessPrBodyRenderer, proof_identity_exports as prReadinessProofIdentity, review_proof_exports as prReadinessReviewProof, status_check_rollup_exports as prReadinessStatusChecks, verification_proof_exports as prReadinessVerificationProof, previewLifecycleTargets, publishEpicStructure, readDemandWaivers, readPrReadyProof, readPrReviewProof, resolveFactoryRepository, resolveFindingBlocking, resolveReviewFindingCategory, resolveReviewFindingSeverity, resolveReviewLadderPolicy, resolveTraceWriteSinks, reviewCycleStateFor, reviewFocusFromIssueBody, reviewPromptSectionSchema, reviewPromptSectionsSchema, run, runBoundaryCheck, runDemandWaive, runEvidenceEmit, runPrPublish, runPrReady, runPrReview, runPrVerify, scanFactoryTraceDiagnostics, scanFactoryTraceEvents, selectWaiversForCandidate, staleRepeatLadderFindings, toFactoryTraceEnvelope, tryAppendReviewGateNotRequiredTraceEvent, tryAppendReviewLadderStageTraceEvent, validateBoundaryCheckProof, validateDagDocument, validateDemandWaiverStore, validateFactoryTraceEvent, validateMergeFreezeState, validatePrReadyProof, validatePrReviewProof, validatePrVerifyProof, waivedDemandNotice, waivedDemandSchema, worktree_scratch_files_exports as worktreeScratchFiles };
17203
+ export { DEFAULT_DEMAND_WAIVER_PATH, DEFAULT_FACTORY_REPOSITORY, DEFAULT_ROOT_SHARED_GLOBS, DemandWaiveRefusalError, EMPTY_TREE_OBJECT_HASH, EPIC_STRUCTURE_NODE_STATUSES, EPIC_STRUCTURE_SCHEMA_VERSION, EpicStructureValidationError, FACTORY_BASE_REF_ENV, FACTORY_CHANGED_FILES_ENV, FACTORY_CHANGED_FILES_FILE_ENV, FactoryCliInvocationSchema, FollowUpActionSchema, MERGE_FREEZE_APP_SLUG, MERGE_FREEZE_CHECK_NAME, PrPublishFollowUpError, REVIEW_FOCUS_SECTION, WorkerCheckoutGuardError, appendReviewLadderStageTraceEvent, appendWithTraceSinks, applyDemandWaiver, assembleReviewPrompt, assertFactoryPrStatusIdentity, assertWorkerCheckoutAllowed, authorizeDemandWaiver, batteryScopeSummaryLines, blockingLadderFindings, boundary_manifest_exports as boundaryManifest, boundary_review_proof_exports as boundaryReviewProof, buildEpicStructureEvent, buildEpicStructurePayload, buildEvidenceEnvelope, buildReviewGateNotRequiredTraceEvent, buildReviewLadderStageTraceEvent, comment_provenance_exports as commentProvenance, createLocalJsonlTraceSink, createProgram, createVerificationRunContext, doctorProjectProfile, epicStructureEventId, evaluateReviewLadder, evidenceEnvelopeFilename, findingKey, followUpFromArgv, impactStampScopeDecision, inferFixedInThreadDispositions, isProductionHqUrl, loadProjectProfile, normalizeIssueComments, openLadderFindings, planFactoryPrStatusHud, planPreviewLifecycle, planPublishFollowUp, planVerificationBattery, readiness_evaluation_exports as prReadinessEvaluation, external_evidence_exports as prReadinessExternalEvidence, pr_body_renderer_exports as prReadinessPrBodyRenderer, proof_identity_exports as prReadinessProofIdentity, review_proof_exports as prReadinessReviewProof, status_check_rollup_exports as prReadinessStatusChecks, verification_proof_exports as prReadinessVerificationProof, presentFactoryPrStatusHud, previewLifecycleTargets, publishEpicStructure, readDemandWaivers, readFactoryPrStatusSources, readPrReadyProof, readPrReviewProof, refreshFactoryPrStatusHud, refreshFactoryPrStatusHudSafely, renderFactoryPrStatusHud, resolveFactoryRepository, resolveFindingBlocking, resolveReviewFindingCategory, resolveReviewFindingSeverity, resolveReviewLadderPolicy, resolveTraceWriteSinks, reviewCycleStateFor, reviewFocusFromIssueBody, reviewPromptSectionSchema, reviewPromptSectionsSchema, run, runBoundaryCheck, runDemandWaive, runEvidenceEmit, runPrPublish, runPrReady, runPrReview, runPrVerify, scanFactoryTraceDiagnostics, scanFactoryTraceEvents, selectWaiversForCandidate, staleRepeatLadderFindings, toFactoryTraceEnvelope, tryAppendReviewGateNotRequiredTraceEvent, tryAppendReviewLadderStageTraceEvent, validateBoundaryCheckProof, validateDagDocument, validateDemandWaiverStore, validateFactoryTraceEvent, validateMergeFreezeState, validatePrReadyProof, validatePrReviewProof, validatePrVerifyProof, validatePreviewLifecyclePlan, waivedDemandNotice, waivedDemandSchema, worktree_scratch_files_exports as worktreeScratchFiles };