@patronage/factory-ci 1.0.0-alpha.37 → 1.0.0-alpha.39

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/README.md CHANGED
@@ -459,7 +459,7 @@ import { previewProofLifecycle } from "@patronage/factory-ci";
459
459
 
460
460
  Events:
461
461
 
462
- - `begin` — the pre-deploy gate. Refuses a draft, a closed PR, a dirty or mismatched worktree, and any stage the disposable grammar does not own for this PR head. Does not create a registration.
462
+ - `begin` — the pre-deploy gate. A mismatched branch name is accepted at the PR head SHA. A dirty tree or a SHA mismatch is refused, as are a draft, a closed PR, and any stage the disposable grammar does not own for this PR head. Does not create a registration.
463
463
  - `register` — the same identity gate, plus passed smoke, passed convergence, and a passing three-way-bound evidence envelope at this head. Writes a registration with `cleanup.outcome: "pending"`. Cleanup, smoke, and convergence status live only on their nested objects, so no top-level copy can diverge from them. Machine evidence is that envelope; this module never posts a PR comment.
464
464
  - `cleanup` — records an explicit passed or failed outcome onto an existing registration. Cleanup is never inferred from whether the stage still exists. `recover` is the failed-to-resolved re-entry of this event.
465
465
 
@@ -1,3 +1,3 @@
1
1
  {
2
- "fingerprint": "d4ebf8af4787cc4426a8c73b8f84098d818fc0f34fab36f6afc21e49e0b619a5"
2
+ "fingerprint": "96dfad08bf2d426a9c9efe5b3b753fc99f7a3cee0d83d36766dd8aa15417f5d1"
3
3
  }
package/dist/index.d.ts CHANGED
@@ -178,7 +178,10 @@ declare const localPreviewStage: (options: LocalPreviewStageOptions) => LocalPre
178
178
  *
179
179
  * Proof sits entirely on one side of GitHub's draft/ready boundary
180
180
  * (paitronage#1089): a draft candidate cannot begin or register. Machine
181
- * evidence is the envelope at head (#355), never a PR comment.
181
+ * evidence is the envelope at head (#355), never a PR comment. The SHA and
182
+ * repository bind that proof (#1312): a detached HEAD or a branch name that
183
+ * differs from the PR head is the same commit, and the registration records
184
+ * `branch: "(detached)"`.
182
185
  */
183
186
  type PreviewProofCleanupStatus = "failed" | "passed" | "pending";
184
187
  interface PreviewProofCandidate {
@@ -206,6 +209,13 @@ interface PreviewProofCleanupOutcome {
206
209
  readonly runUrl?: string;
207
210
  }
208
211
  interface PreviewProofRegistration {
212
+ /**
213
+ * Present only when the checkout is detached or its branch name is not the
214
+ * PR head. The `(detached)` sentinel is on the lifecycle registration
215
+ * `transition` returns, and `previewProofInventory` does not persist it.
216
+ * It is not a branch to check out.
217
+ */
218
+ readonly branch?: "(detached)";
209
219
  readonly cleanup: {
210
220
  readonly evidence: readonly string[];
211
221
  readonly outcome: PreviewProofCleanupStatus;
package/dist/index.js CHANGED
@@ -74,6 +74,12 @@ const factoryCandidateOrPushCondition = (candidateCondition) => {
74
74
  //#endregion
75
75
  //#region src/preview-proof-lifecycle.ts
76
76
  const EMPTY_STATE = Object.freeze({ registrations: Object.freeze([]) });
77
+ const DETACHED_PROOF_BRANCH = "(detached)";
78
+ /**
79
+ * The branch name adds no guarantee. Record the sentinel unless the checkout
80
+ * is actually on the PR head branch.
81
+ */
82
+ const proofBranch = (candidate) => candidate.branch.length > 0 && candidate.branch === candidate.prHeadRef ? void 0 : DETACHED_PROOF_BRANCH;
77
83
  const registrationKey$1 = (row) => `${row.stack}:${row.stage}`;
78
84
  const disposableStage = (stage, candidate) => {
79
85
  const parsed = parseLocalPreviewStage(stage, {
@@ -88,9 +94,7 @@ const assertAdmissibleCandidate = (candidate, stage) => {
88
94
  if (candidate.prState !== "OPEN") throw new Error("Preview proof requires an open pull request.");
89
95
  const errors = [];
90
96
  if (candidate.dirty) errors.push("Worktree must be clean.");
91
- if (!candidate.branch) errors.push("Detached HEAD is not trusted for preview proof.");
92
97
  if (candidate.headSha !== candidate.prHeadSha) errors.push("HEAD is stale for the current PR head.");
93
- if (candidate.branch !== candidate.prHeadRef) errors.push("Current branch does not match the PR head branch.");
94
98
  if (candidate.repository !== candidate.prRepository) errors.push("PR head repository does not match the current repository.");
95
99
  if (errors.length > 0) throw new Error(errors.join("\n"));
96
100
  return disposableStage(stage, candidate);
@@ -116,7 +120,9 @@ const transition = (state, event) => {
116
120
  if (event.convergence.status !== "passed") throw new Error("Preview proof requires passed convergence evidence.");
117
121
  if (!event.stack || !event.url) throw new Error("Preview proof registration requires a stack and URL.");
118
122
  assertPassingEnvelope(event.evidence, event.candidate.headSha);
123
+ const branch = proofBranch(event.candidate);
119
124
  return replaceRegistration$1(state, {
125
+ ...branch === void 0 ? {} : { branch },
120
126
  cleanup: {
121
127
  evidence: [],
122
128
  outcome: "pending"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@patronage/factory-ci",
3
- "version": "1.0.0-alpha.37",
3
+ "version": "1.0.0-alpha.39",
4
4
  "description": "Deep CI and deploy building blocks for Patronage factory projects: workflow source artifacts, hosted diff classification, Alchemy entry execution, and disposable-stage semantics",
5
5
  "keywords": [
6
6
  "alchemy",
@@ -11,7 +11,10 @@ import type { LocalPreviewStage } from "./disposable-stage.ts";
11
11
  *
12
12
  * Proof sits entirely on one side of GitHub's draft/ready boundary
13
13
  * (paitronage#1089): a draft candidate cannot begin or register. Machine
14
- * evidence is the envelope at head (#355), never a PR comment.
14
+ * evidence is the envelope at head (#355), never a PR comment. The SHA and
15
+ * repository bind that proof (#1312): a detached HEAD or a branch name that
16
+ * differs from the PR head is the same commit, and the registration records
17
+ * `branch: "(detached)"`.
15
18
  */
16
19
 
17
20
  export type PreviewProofCleanupStatus = "failed" | "passed" | "pending";
@@ -44,6 +47,13 @@ export interface PreviewProofCleanupOutcome {
44
47
  }
45
48
 
46
49
  export interface PreviewProofRegistration {
50
+ /**
51
+ * Present only when the checkout is detached or its branch name is not the
52
+ * PR head. The `(detached)` sentinel is on the lifecycle registration
53
+ * `transition` returns, and `previewProofInventory` does not persist it.
54
+ * It is not a branch to check out.
55
+ */
56
+ readonly branch?: "(detached)";
47
57
  readonly cleanup: {
48
58
  readonly evidence: readonly string[];
49
59
  readonly outcome: PreviewProofCleanupStatus;
@@ -111,6 +121,19 @@ const EMPTY_STATE: PreviewProofLifecycleState = Object.freeze({
111
121
  registrations: Object.freeze([]),
112
122
  });
113
123
 
124
+ const DETACHED_PROOF_BRANCH = "(detached)" as const;
125
+
126
+ /**
127
+ * The branch name adds no guarantee. Record the sentinel unless the checkout
128
+ * is actually on the PR head branch.
129
+ */
130
+ const proofBranch = (
131
+ candidate: PreviewProofCandidate
132
+ ): typeof DETACHED_PROOF_BRANCH | undefined =>
133
+ candidate.branch.length > 0 && candidate.branch === candidate.prHeadRef
134
+ ? undefined
135
+ : DETACHED_PROOF_BRANCH;
136
+
114
137
  const registrationKey = (row: {
115
138
  readonly stack: string;
116
139
  readonly stage: string;
@@ -149,15 +172,9 @@ const assertAdmissibleCandidate = (
149
172
  if (candidate.dirty) {
150
173
  errors.push("Worktree must be clean.");
151
174
  }
152
- if (!candidate.branch) {
153
- errors.push("Detached HEAD is not trusted for preview proof.");
154
- }
155
175
  if (candidate.headSha !== candidate.prHeadSha) {
156
176
  errors.push("HEAD is stale for the current PR head.");
157
177
  }
158
- if (candidate.branch !== candidate.prHeadRef) {
159
- errors.push("Current branch does not match the PR head branch.");
160
- }
161
178
  if (candidate.repository !== candidate.prRepository) {
162
179
  errors.push("PR head repository does not match the current repository.");
163
180
  }
@@ -233,8 +250,10 @@ const transition = (
233
250
  throw new Error("Preview proof registration requires a stack and URL.");
234
251
  }
235
252
  assertPassingEnvelope(event.evidence, event.candidate.headSha);
253
+ const branch = proofBranch(event.candidate);
236
254
 
237
255
  return replaceRegistration(state, {
256
+ ...(branch === undefined ? {} : { branch }),
238
257
  cleanup: { evidence: [], outcome: "pending" },
239
258
  convergence: {
240
259
  detail: event.convergence.detail,