@patronage/software-factory 1.0.0-alpha.40 → 1.0.0-alpha.42
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 +51 -18
- package/dist/index.js +566 -99
- package/dist/schemas.d.ts +2 -2
- package/dist/schemas.js +1 -0
- package/package.json +3 -3
- /package/{CONTEXT.md → GLOSSARY.md} +0 -0
package/dist/index.d.ts
CHANGED
|
@@ -138,8 +138,8 @@ declare const boundaryManifestBaseSchema: z.ZodObject<{
|
|
|
138
138
|
integrationBranch: z.ZodOptional<z.ZodString>;
|
|
139
139
|
schemaVersion: z.ZodLiteral<1>;
|
|
140
140
|
topology: z.ZodEnum<{
|
|
141
|
-
flagged: "flagged";
|
|
142
141
|
"each-to-main": "each-to-main";
|
|
142
|
+
flagged: "flagged";
|
|
143
143
|
stacked: "stacked";
|
|
144
144
|
"each-to-epic": "each-to-epic";
|
|
145
145
|
}>;
|
|
@@ -167,8 +167,8 @@ declare const boundaryManifestSchema: z.ZodObject<{
|
|
|
167
167
|
integrationBranch: z.ZodOptional<z.ZodString>;
|
|
168
168
|
schemaVersion: z.ZodLiteral<1>;
|
|
169
169
|
topology: z.ZodEnum<{
|
|
170
|
-
flagged: "flagged";
|
|
171
170
|
"each-to-main": "each-to-main";
|
|
171
|
+
flagged: "flagged";
|
|
172
172
|
stacked: "stacked";
|
|
173
173
|
"each-to-epic": "each-to-epic";
|
|
174
174
|
}>;
|
|
@@ -228,7 +228,7 @@ interface GithubIssueComment {
|
|
|
228
228
|
}
|
|
229
229
|
declare const normalizeIssueComments: (value: GithubIssueCommentApi[] | GithubIssueCommentApi[][]) => GithubIssueComment[];
|
|
230
230
|
declare namespace boundary_review_proof_d_exports {
|
|
231
|
-
export { BOUNDARY_REVIEW_PROOF_KIND, BoundaryReviewProof, BoundaryReviewProofComment, boundaryReviewProofAuthorsSeen, boundaryReviewProofSchema, selectBoundaryReviewProof, undispositionedBlockingFindings };
|
|
231
|
+
export { BOUNDARY_REVIEW_PROOF_KIND, BoundaryReviewProof, BoundaryReviewProofComment, BoundaryReviewProofParseFailure, boundaryReviewProofAuthorsSeen, boundaryReviewProofParseFailures, boundaryReviewProofSchema, selectBoundaryReviewProof, undispositionedBlockingFindings };
|
|
232
232
|
}
|
|
233
233
|
declare const BOUNDARY_REVIEW_PROOF_KIND = "boundary-review-proof";
|
|
234
234
|
declare const boundaryReviewProofBaseSchema: z.ZodObject<{
|
|
@@ -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
|
-
merged: "merged";
|
|
243
242
|
open: "open";
|
|
243
|
+
merged: "merged";
|
|
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
|
-
merged: "merged";
|
|
287
286
|
open: "open";
|
|
287
|
+
merged: "merged";
|
|
288
288
|
}>;
|
|
289
289
|
wave: z.ZodOptional<z.ZodString>;
|
|
290
290
|
}, z.core.$loose>>;
|
|
@@ -325,8 +325,14 @@ interface BoundaryReviewProofComment {
|
|
|
325
325
|
commentId?: string | number;
|
|
326
326
|
commentUrl?: string;
|
|
327
327
|
}
|
|
328
|
+
interface BoundaryReviewProofParseFailure {
|
|
329
|
+
commentAuthor: string;
|
|
330
|
+
commentUrl?: string;
|
|
331
|
+
parseError: string;
|
|
332
|
+
}
|
|
328
333
|
declare const selectBoundaryReviewProof: (comments: GithubIssueComment[], boundary: string, declaredBy: string) => BoundaryReviewProofComment | undefined;
|
|
329
334
|
declare const boundaryReviewProofAuthorsSeen: (comments: GithubIssueComment[], boundary: string) => string[];
|
|
335
|
+
declare const boundaryReviewProofParseFailures: (comments: GithubIssueComment[], boundary: string) => BoundaryReviewProofParseFailure[];
|
|
330
336
|
declare const undispositionedBlockingFindings: (proof: BoundaryReviewProof) => string[];
|
|
331
337
|
//#endregion
|
|
332
338
|
//#region src/boundary-membership-graph.d.ts
|
|
@@ -796,7 +802,7 @@ interface LadderCycleRef {
|
|
|
796
802
|
stage: LadderCycleStage;
|
|
797
803
|
}
|
|
798
804
|
type LadderFindingReport = ReviewFindingAttributes;
|
|
799
|
-
/** One finding as recorded in one cycle (
|
|
805
|
+
/** One finding as recorded in one cycle (GLOSSARY.md: finding occurrence). */
|
|
800
806
|
interface ReviewOccurrenceRecord {
|
|
801
807
|
address: string;
|
|
802
808
|
/** The ladder-assigned cycle number the occurrence was recorded in. */
|
|
@@ -813,7 +819,7 @@ declare const POLICY_DECLARATION_RULES: readonly ["cycle-2-severity-floor", "ide
|
|
|
813
819
|
type PolicyDeclarationRule = (typeof POLICY_DECLARATION_RULES)[number];
|
|
814
820
|
type DeclarationAuthority = "operator" | "reviewer" | "policy";
|
|
815
821
|
/**
|
|
816
|
-
* A disposition declaration as stored (
|
|
822
|
+
* A disposition declaration as stored (GLOSSARY.md: disposition declaration):
|
|
817
823
|
* one occurrence, one disposition, the authority that made it and its
|
|
818
824
|
* reference (operator, reviewer) or rule (policy).
|
|
819
825
|
*/
|
|
@@ -1735,14 +1741,19 @@ interface ExactHeadRunSource {
|
|
|
1735
1741
|
//#endregion
|
|
1736
1742
|
//#region src/merge-freeze.d.ts
|
|
1737
1743
|
declare const MERGE_FREEZE_CHECK_NAME = "patronage-factory/merge-freeze";
|
|
1744
|
+
/**
|
|
1745
|
+
* The Factory App's slug, as GitHub serves it on the runs the writer posts.
|
|
1746
|
+
* It is not a trust predicate: an App chooses its own slug, so the reader
|
|
1747
|
+
* trusts the pinned numeric App id instead (ADR 0022, #1393).
|
|
1748
|
+
*/
|
|
1738
1749
|
declare const MERGE_FREEZE_APP_SLUG = "patronage-factory";
|
|
1739
1750
|
declare const mergeFreezeStateSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
1740
1751
|
active: z.ZodLiteral<true>;
|
|
1741
1752
|
generationId: z.ZodNumber;
|
|
1742
1753
|
headSha: z.ZodString;
|
|
1743
1754
|
outcome: z.ZodEnum<{
|
|
1744
|
-
stale: "stale";
|
|
1745
1755
|
active: "active";
|
|
1756
|
+
stale: "stale";
|
|
1746
1757
|
}>;
|
|
1747
1758
|
reason: z.ZodString;
|
|
1748
1759
|
recordedAt: z.ZodISODateTime;
|
|
@@ -2029,10 +2040,10 @@ declare const managedReadinessLedgerSchema: z.ZodObject<{
|
|
|
2029
2040
|
reviewedHeadSha: z.ZodOptional<z.ZodString>;
|
|
2030
2041
|
reviewedPatchId: z.ZodOptional<z.ZodString>;
|
|
2031
2042
|
status: z.ZodEnum<{
|
|
2043
|
+
stale: "stale";
|
|
2032
2044
|
blocked: "blocked";
|
|
2033
2045
|
"not-required": "not-required";
|
|
2034
2046
|
current: "current";
|
|
2035
|
-
stale: "stale";
|
|
2036
2047
|
missing: "missing";
|
|
2037
2048
|
}>;
|
|
2038
2049
|
}, z.core.$strip>;
|
|
@@ -2041,10 +2052,10 @@ declare const managedReadinessLedgerSchema: z.ZodObject<{
|
|
|
2041
2052
|
reviewedHeadSha: z.ZodOptional<z.ZodString>;
|
|
2042
2053
|
reviewedPatchId: z.ZodOptional<z.ZodString>;
|
|
2043
2054
|
status: z.ZodEnum<{
|
|
2055
|
+
stale: "stale";
|
|
2044
2056
|
blocked: "blocked";
|
|
2045
2057
|
"not-required": "not-required";
|
|
2046
2058
|
current: "current";
|
|
2047
|
-
stale: "stale";
|
|
2048
2059
|
missing: "missing";
|
|
2049
2060
|
}>;
|
|
2050
2061
|
}, z.core.$strip>>;
|
|
@@ -2495,6 +2506,12 @@ interface CompletedFactoryCheckSnapshot {
|
|
|
2495
2506
|
readonly detailsUrl?: string;
|
|
2496
2507
|
readonly headSha: string;
|
|
2497
2508
|
readonly name: FactoryPrStatusCheckName;
|
|
2509
|
+
/**
|
|
2510
|
+
* Merge freeze only (#1501): the cancellation-only generations, newest
|
|
2511
|
+
* first, that the freeze selection read past to reach this snapshot's
|
|
2512
|
+
* generation (#1158). Absent when the newest generation stands.
|
|
2513
|
+
*/
|
|
2514
|
+
readonly readPastRunIds?: readonly number[];
|
|
2498
2515
|
}
|
|
2499
2516
|
interface FailedPreviewSnapshot {
|
|
2500
2517
|
readonly completedAt: string;
|
|
@@ -3114,10 +3131,10 @@ declare const loadEvidenceEnvelopes: (cwd: string) => LoadedEvidenceEnvelope[];
|
|
|
3114
3131
|
//#region src/review-proof-applicability.d.ts
|
|
3115
3132
|
declare const REVIEW_STATUS_VALUES: readonly ["not-required", "current", "stale", "missing", "blocked"];
|
|
3116
3133
|
declare const reviewStatusSchema: z.ZodEnum<{
|
|
3134
|
+
stale: "stale";
|
|
3117
3135
|
blocked: "blocked";
|
|
3118
3136
|
"not-required": "not-required";
|
|
3119
3137
|
current: "current";
|
|
3120
|
-
stale: "stale";
|
|
3121
3138
|
missing: "missing";
|
|
3122
3139
|
}>;
|
|
3123
3140
|
type ReviewStatus = z.infer<typeof reviewStatusSchema>;
|
|
@@ -3225,8 +3242,8 @@ declare const boundaryCheckProofSchema: z.ZodObject<{
|
|
|
3225
3242
|
pr: z.ZodNumber;
|
|
3226
3243
|
sha: z.ZodString;
|
|
3227
3244
|
state: z.ZodEnum<{
|
|
3228
|
-
merged: "merged";
|
|
3229
3245
|
open: "open";
|
|
3246
|
+
merged: "merged";
|
|
3230
3247
|
}>;
|
|
3231
3248
|
wave: z.ZodString;
|
|
3232
3249
|
}, z.core.$strip>>;
|
|
@@ -3628,6 +3645,12 @@ interface PrVerifyDependencies {
|
|
|
3628
3645
|
}
|
|
3629
3646
|
declare function runPrVerify(args: PrVerifyArgs, dependencies?: PrVerifyDependencies): PrVerifyProof;
|
|
3630
3647
|
//#endregion
|
|
3648
|
+
//#region src/pull-request-fill.d.ts
|
|
3649
|
+
interface CommitMessage {
|
|
3650
|
+
body: string;
|
|
3651
|
+
subject: string;
|
|
3652
|
+
}
|
|
3653
|
+
//#endregion
|
|
3631
3654
|
//#region src/pr-publish.d.ts
|
|
3632
3655
|
type PrPublishArgs = Omit<PrReadyArgs, "pr" | "throwWhenBlocked"> & {
|
|
3633
3656
|
/** A clean-session findings file to validate into the review proof. */findings?: string;
|
|
@@ -3726,6 +3749,12 @@ interface PrPublishDependencies extends PrReadyDependencies {
|
|
|
3726
3749
|
ensureVerifyCheckRun?: typeof ensureFactoryCheckRunPublished;
|
|
3727
3750
|
factoryCliInvocation?: FactoryCliInvocation;
|
|
3728
3751
|
gh?: GhRunner;
|
|
3752
|
+
/**
|
|
3753
|
+
* The branch's commits into its base, oldest first, that a pull request
|
|
3754
|
+
* publish creates is titled and described from (#1366). Tests only;
|
|
3755
|
+
* production reads them from Git.
|
|
3756
|
+
*/
|
|
3757
|
+
pullRequestCommits?: PullRequestCommits;
|
|
3729
3758
|
github?: PrReadyDependencies["github"] & {
|
|
3730
3759
|
editPullRequestBody?: (input: {
|
|
3731
3760
|
body: string;
|
|
@@ -3741,7 +3770,10 @@ interface PrPublishDependencies extends PrReadyDependencies {
|
|
|
3741
3770
|
* carries the publisher for the verdict it records (#1323).
|
|
3742
3771
|
*/
|
|
3743
3772
|
runPrReview?: (args: PrReviewArgs, dependencies?: ProducerPublisher) => Promise<PrReviewProof>;
|
|
3744
|
-
/**
|
|
3773
|
+
/**
|
|
3774
|
+
* Settles asynchronously scheduled sink work: after a review cycle publish
|
|
3775
|
+
* records from `--findings` (#1382), and before the handoff drain.
|
|
3776
|
+
*/
|
|
3745
3777
|
awaitPendingIngest?: () => Promise<void>;
|
|
3746
3778
|
/**
|
|
3747
3779
|
* The handoff drain (#390). Advisory everywhere: publish reports what it
|
|
@@ -3767,6 +3799,7 @@ interface PrPublishDependencies extends PrReadyDependencies {
|
|
|
3767
3799
|
*/
|
|
3768
3800
|
sleep?: (ms: number) => Promise<void>;
|
|
3769
3801
|
}
|
|
3802
|
+
type PullRequestCommits = (cwd: string, base: string) => CommitMessage[];
|
|
3770
3803
|
/**
|
|
3771
3804
|
* What became of the durable `pr:verify` binding for this publish. Reported in
|
|
3772
3805
|
* the publish summary: a binding that silently failed to land is
|
|
@@ -4647,13 +4680,13 @@ declare const evaluateReadiness: (input: EvaluationInput) => {
|
|
|
4647
4680
|
reviews: {
|
|
4648
4681
|
correctness: {
|
|
4649
4682
|
required: boolean;
|
|
4650
|
-
status: "blocked" | "not-required" | "current" | "
|
|
4683
|
+
status: "stale" | "blocked" | "not-required" | "current" | "missing";
|
|
4651
4684
|
reviewedHeadSha?: string | undefined;
|
|
4652
4685
|
reviewedPatchId?: string | undefined;
|
|
4653
4686
|
};
|
|
4654
4687
|
security?: {
|
|
4655
4688
|
required: boolean;
|
|
4656
|
-
status: "blocked" | "not-required" | "current" | "
|
|
4689
|
+
status: "stale" | "blocked" | "not-required" | "current" | "missing";
|
|
4657
4690
|
reviewedHeadSha?: string | undefined;
|
|
4658
4691
|
reviewedPatchId?: string | undefined;
|
|
4659
4692
|
} | undefined;
|
|
@@ -4766,13 +4799,13 @@ declare const evaluateReadiness: (input: EvaluationInput) => {
|
|
|
4766
4799
|
reviews: {
|
|
4767
4800
|
correctness: {
|
|
4768
4801
|
required: boolean;
|
|
4769
|
-
status: "blocked" | "not-required" | "current" | "
|
|
4802
|
+
status: "stale" | "blocked" | "not-required" | "current" | "missing";
|
|
4770
4803
|
reviewedHeadSha?: string | undefined;
|
|
4771
4804
|
reviewedPatchId?: string | undefined;
|
|
4772
4805
|
};
|
|
4773
4806
|
security?: {
|
|
4774
4807
|
required: boolean;
|
|
4775
|
-
status: "blocked" | "not-required" | "current" | "
|
|
4808
|
+
status: "stale" | "blocked" | "not-required" | "current" | "missing";
|
|
4776
4809
|
reviewedHeadSha?: string | undefined;
|
|
4777
4810
|
reviewedPatchId?: string | undefined;
|
|
4778
4811
|
} | undefined;
|
|
@@ -4927,7 +4960,7 @@ declare const EPIC_STRUCTURE_SCHEMA_VERSION = 1;
|
|
|
4927
4960
|
/**
|
|
4928
4961
|
* DAG-node PLANNING statuses — the planning-plane vocabulary a planner authors
|
|
4929
4962
|
* on a `dag.yml` node. Carried forward by ADR 0018 and defined by the epic
|
|
4930
|
-
* skill (`reference/epic-artifacts.md`) and both `
|
|
4963
|
+
* skill (`reference/epic-artifacts.md`) and both `GLOSSARY.md` glossaries.
|
|
4931
4964
|
*
|
|
4932
4965
|
* A DAG node is a unit of *plan*; an HQ lane is a unit of *runtime execution*.
|
|
4933
4966
|
* These are two planes with two vocabularies and must not be conflated — the
|
package/dist/index.js
CHANGED
|
@@ -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.
|
|
25
|
+
var version = "1.0.0-alpha.42";
|
|
26
26
|
//#endregion
|
|
27
27
|
//#region src/git-globals-parse.ts
|
|
28
28
|
const VALUE_TAKING_GIT_OPTIONS = new Set([
|
|
@@ -2507,6 +2507,7 @@ const coveredSetDivergences = (computed, proof) => {
|
|
|
2507
2507
|
var boundary_review_proof_exports = /* @__PURE__ */ __exportAll({
|
|
2508
2508
|
BOUNDARY_REVIEW_PROOF_KIND: () => BOUNDARY_REVIEW_PROOF_KIND$1,
|
|
2509
2509
|
boundaryReviewProofAuthorsSeen: () => boundaryReviewProofAuthorsSeen,
|
|
2510
|
+
boundaryReviewProofParseFailures: () => boundaryReviewProofParseFailures,
|
|
2510
2511
|
boundaryReviewProofSchema: () => boundaryReviewProofSchema$1,
|
|
2511
2512
|
selectBoundaryReviewProof: () => selectBoundaryReviewProof,
|
|
2512
2513
|
undispositionedBlockingFindings: () => undispositionedBlockingFindings
|
|
@@ -2561,8 +2562,11 @@ const boundaryReviewProofSchema$1 = z.object({
|
|
|
2561
2562
|
}).passthrough().optional()
|
|
2562
2563
|
}).passthrough();
|
|
2563
2564
|
const FENCED_JSON_PATTERN = /```json[ \t]*\r?\n(?<inner>[\s\S]*?)```/gu;
|
|
2564
|
-
const
|
|
2565
|
-
const
|
|
2565
|
+
const scanBoundaryReviewProofs = (comments, boundary) => {
|
|
2566
|
+
const scan = {
|
|
2567
|
+
failed: [],
|
|
2568
|
+
valid: []
|
|
2569
|
+
};
|
|
2566
2570
|
for (const comment of comments) {
|
|
2567
2571
|
const commentAuthor = comment.author.login;
|
|
2568
2572
|
if (!commentAuthor) continue;
|
|
@@ -2576,8 +2580,17 @@ const collectValidBoundaryReviewProofs = (comments, boundary) => {
|
|
|
2576
2580
|
}
|
|
2577
2581
|
if (typeof raw !== "object" || raw === null || raw.kind !== "boundary-review-proof") continue;
|
|
2578
2582
|
const parsed = boundaryReviewProofSchema$1.safeParse(raw);
|
|
2579
|
-
if (!parsed.success
|
|
2580
|
-
|
|
2583
|
+
if (!parsed.success) {
|
|
2584
|
+
const rawBoundary = raw.boundary;
|
|
2585
|
+
if (typeof rawBoundary !== "string" || rawBoundary === boundary) scan.failed.push({
|
|
2586
|
+
commentAuthor,
|
|
2587
|
+
...comment.url === void 0 ? {} : { commentUrl: comment.url },
|
|
2588
|
+
parseError: formatZodIssues(parsed.error)
|
|
2589
|
+
});
|
|
2590
|
+
continue;
|
|
2591
|
+
}
|
|
2592
|
+
if (parsed.data.boundary !== boundary) continue;
|
|
2593
|
+
scan.valid.push({
|
|
2581
2594
|
commentAuthor,
|
|
2582
2595
|
...comment.id === void 0 ? {} : { commentId: comment.id },
|
|
2583
2596
|
...comment.url === void 0 ? {} : { commentUrl: comment.url },
|
|
@@ -2585,12 +2598,12 @@ const collectValidBoundaryReviewProofs = (comments, boundary) => {
|
|
|
2585
2598
|
});
|
|
2586
2599
|
}
|
|
2587
2600
|
}
|
|
2588
|
-
return
|
|
2601
|
+
return scan;
|
|
2589
2602
|
};
|
|
2590
2603
|
const selectBoundaryReviewProof = (comments, boundary, declaredBy) => {
|
|
2591
2604
|
let selected;
|
|
2592
2605
|
const declaredByLower = declaredBy.toLowerCase();
|
|
2593
|
-
for (const entry of
|
|
2606
|
+
for (const entry of scanBoundaryReviewProofs(comments, boundary).valid) {
|
|
2594
2607
|
if (entry.commentAuthor.toLowerCase() !== declaredByLower) continue;
|
|
2595
2608
|
selected = {
|
|
2596
2609
|
...entry.commentId === void 0 ? {} : { commentId: entry.commentId },
|
|
@@ -2602,9 +2615,10 @@ const selectBoundaryReviewProof = (comments, boundary, declaredBy) => {
|
|
|
2602
2615
|
};
|
|
2603
2616
|
const boundaryReviewProofAuthorsSeen = (comments, boundary) => {
|
|
2604
2617
|
const authors = /* @__PURE__ */ new Set();
|
|
2605
|
-
for (const entry of
|
|
2618
|
+
for (const entry of scanBoundaryReviewProofs(comments, boundary).valid) authors.add(entry.commentAuthor);
|
|
2606
2619
|
return [...authors];
|
|
2607
2620
|
};
|
|
2621
|
+
const boundaryReviewProofParseFailures = (comments, boundary) => scanBoundaryReviewProofs(comments, boundary).failed;
|
|
2608
2622
|
const undispositionedBlockingFindings = (proof) => proof.findings.filter((finding) => (finding.category !== "maintainability" || finding.blockingAfterCap === true) && finding.disposition === void 0).map((finding) => `boundary finding "${finding.title}" (category ${finding.category ?? "unknown"}) has no disposition; fix it or record waived / follow-up-filed`);
|
|
2609
2623
|
//#endregion
|
|
2610
2624
|
//#region src/boundary-topology.ts
|
|
@@ -3030,6 +3044,42 @@ const waivedDemandSchema = z.object({
|
|
|
3030
3044
|
session: z.string().trim().min(1).optional(),
|
|
3031
3045
|
unmetReasons: z.array(z.string().min(1)).min(1)
|
|
3032
3046
|
});
|
|
3047
|
+
/**
|
|
3048
|
+
* The demands an operator waiver can reach (#354; moved out of `pr:ready` by
|
|
3049
|
+
* #1492 so `demand:waive` refuses by the same contract every reader folds by,
|
|
3050
|
+
* and can never record an authorization nothing applies).
|
|
3051
|
+
*
|
|
3052
|
+
* `pr:ready` folds the merge freeze, the profile's external required checks
|
|
3053
|
+
* and the boundary wave's review rung — the demands the resolver puts *in
|
|
3054
|
+
* force* for one candidate — plus the review-round limit (#1280), which exists
|
|
3055
|
+
* only to be waived and which `pr:review --assemble` folds too.
|
|
3056
|
+
* `boundary:check --final` folds the closeout pin `child-issue-open` (#901);
|
|
3057
|
+
* readiness never raises that demand, so naming it here widens nothing at
|
|
3058
|
+
* `pr:ready`.
|
|
3059
|
+
*
|
|
3060
|
+
* Everything else stays unwaivable. Structural refusals — candidate identity,
|
|
3061
|
+
* proof binding, an unreadable demand authority, wave membership — were never
|
|
3062
|
+
* waivable at merge time and are not waivable now; in `pr:ready` most of them
|
|
3063
|
+
* refuse by throwing before any blocker list exists. The evidence gates
|
|
3064
|
+
* readiness has always owned (verify, review modes, draft, human blockers) and
|
|
3065
|
+
* GitHub's own check and merge-state rollups are satisfied or not; a waiver is
|
|
3066
|
+
* not a second way to pass them.
|
|
3067
|
+
*/
|
|
3068
|
+
const WAIVABLE_DEMAND_KEYS = [
|
|
3069
|
+
DEMAND_KEYS.mergeFreeze,
|
|
3070
|
+
DEMAND_KEYS.reviewRoundLimit,
|
|
3071
|
+
DEMAND_KEYS.childIssueOpen
|
|
3072
|
+
];
|
|
3073
|
+
/** Every instance of these qualified families is waivable at `pr:ready`. */
|
|
3074
|
+
const WAIVABLE_QUALIFIED_DEMAND_FAMILIES = ["required-check", "review-rung"];
|
|
3075
|
+
const isWaivableDemand = (demand) => WAIVABLE_DEMAND_KEYS.includes(demand) || WAIVABLE_QUALIFIED_DEMAND_FAMILIES.some((family) => demand.startsWith(`${family}:`));
|
|
3076
|
+
/**
|
|
3077
|
+
* Why `demand:waive` refuses a well-formed key outside that contract. It names
|
|
3078
|
+
* the key and the waivable set, built from the same lists the predicate reads,
|
|
3079
|
+
* and says nothing about the demand itself: whether it is in force or met is
|
|
3080
|
+
* the reader's to decide, and a refused waiver changes neither.
|
|
3081
|
+
*/
|
|
3082
|
+
const ineligibleWaiverRefusal = (demand) => `demand:waive refuses demand key "${demand}": no factory command applies a waiver of it, so a recorded waiver would authorize nothing. A waiver can name ${WAIVABLE_DEMAND_KEYS.join(", ")}, or a ${WAIVABLE_QUALIFIED_DEMAND_FAMILIES.map((family) => `${family}:<value>`).join(" or ")} key.`;
|
|
3033
3083
|
/** The waivers that bind one candidate: same PR, same head. */
|
|
3034
3084
|
const selectWaiversForCandidate = ({ headSha, pr, waivers }) => headSha === void 0 ? [] : waivers.filter((waiver) => waiver.candidate.pr === pr && waiver.candidate.headSha === headSha);
|
|
3035
3085
|
/**
|
|
@@ -4353,7 +4403,7 @@ function resolveCandidateIdentity({ base, cwd, git }) {
|
|
|
4353
4403
|
* and `resolveCandidateIdentity`'s own 40-character object-id check would
|
|
4354
4404
|
* reject a legitimate 64-character patch id from a SHA-256 repository, which
|
|
4355
4405
|
* the original direct call never rejected. This is a narrower view over the
|
|
4356
|
-
* same owned machine — not a second primitive and not a new `
|
|
4406
|
+
* same owned machine — not a second primitive and not a new `GLOSSARY.md`
|
|
4357
4407
|
* entry.
|
|
4358
4408
|
*
|
|
4359
4409
|
* Takes the primitive itself, not a `git` object to read it off: the original
|
|
@@ -7305,7 +7355,7 @@ const headShaMatches = (recorded, candidate) => Boolean(recorded.headSha && cand
|
|
|
7305
7355
|
* describes the candidate.
|
|
7306
7356
|
*
|
|
7307
7357
|
* Time is not an input and never will be: adding one would contradict the
|
|
7308
|
-
* `freshness` definition in root `
|
|
7358
|
+
* `freshness` definition in root `GLOSSARY.md`.
|
|
7309
7359
|
*/
|
|
7310
7360
|
function evidenceFresh({ candidate, recorded, rule }) {
|
|
7311
7361
|
if (patchIdMatches(recorded, candidate)) return true;
|
|
@@ -8835,7 +8885,7 @@ const readTransportedVerifyProof = (value, text) => {
|
|
|
8835
8885
|
*/
|
|
8836
8886
|
const parsePrVerifyCheckPayload = (text) => {
|
|
8837
8887
|
if (!text?.trim()) return;
|
|
8838
|
-
const candidate = (text.match(/```(?:json)?\s*(?<body>[\s\S]*?)```/u)?.groups?.body ?? text).trim();
|
|
8888
|
+
const candidate = (text.match(/```(?:json)?\s*(?<body>[\s\S]*?)\n```/u)?.groups?.body ?? text).trim();
|
|
8839
8889
|
try {
|
|
8840
8890
|
const parsed = JSON.parse(candidate);
|
|
8841
8891
|
if (parsed.schemaVersion !== PR_VERIFY_CHECK_PAYLOAD_SCHEMA_VERSION || parsed.kind !== "pr-verify-proof-binding" || typeof parsed.headSha !== "string" || parsed.headSha.length === 0 || !isResolvedPrVerifyMode(parsed.mode) || !isOutcome(parsed.outcome)) return;
|
|
@@ -9632,6 +9682,200 @@ const completeCheckRunPages = (pages) => {
|
|
|
9632
9682
|
const runs = pages.flatMap((page) => page.check_runs);
|
|
9633
9683
|
return pages.every((page) => page.total_count === runs.length) ? runs : void 0;
|
|
9634
9684
|
};
|
|
9685
|
+
/** The only instant shape either pr-verify reader takes from a typed proof. */
|
|
9686
|
+
const CANONICAL_UTC = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/u;
|
|
9687
|
+
/** An instant in epoch milliseconds, rounded down to its whole second. */
|
|
9688
|
+
const wholeSecondMs = (ms) => Math.floor(ms / 1e3) * 1e3;
|
|
9689
|
+
/**
|
|
9690
|
+
* When the verification a transported typed proof records started: its
|
|
9691
|
+
* `endedAt` minus its `durationMs`, rounded down to the whole second. Nothing
|
|
9692
|
+
* when `endedAt` is not canonical UTC or `durationMs` is not a finite
|
|
9693
|
+
* non-negative number; the run is then ordered like one without a typed proof.
|
|
9694
|
+
*/
|
|
9695
|
+
const proofStartMs = (proof) => {
|
|
9696
|
+
if (proof === void 0 || !CANONICAL_UTC.test(proof.endedAt)) return;
|
|
9697
|
+
const { durationMs } = proof;
|
|
9698
|
+
if (!(Number.isFinite(durationMs) && durationMs >= 0)) return;
|
|
9699
|
+
const startMs = Date.parse(proof.endedAt) - durationMs;
|
|
9700
|
+
return Number.isFinite(startMs) ? wholeSecondMs(startMs) : void 0;
|
|
9701
|
+
};
|
|
9702
|
+
/**
|
|
9703
|
+
* Whether a run is a pass: completed with conclusion `success`, and its bound
|
|
9704
|
+
* payload has outcome `passed`. The hosted gate's jq calls this `passing`.
|
|
9705
|
+
*/
|
|
9706
|
+
const isPassingRun = (run, payload) => payload !== void 0 && run.status === "completed" && run.conclusion === "success" && payload.outcome === "passed";
|
|
9707
|
+
/**
|
|
9708
|
+
* When a passing run's verification started, by its transported typed proof
|
|
9709
|
+
* (`proofStartMs`). Nothing for any other run, which is ordered at its
|
|
9710
|
+
* `started_at` whatever proof it carries (#1506).
|
|
9711
|
+
*/
|
|
9712
|
+
const passingProofStartMs = (run, payload) => isPassingRun(run, payload) ? proofStartMs(payload.verificationProof) : void 0;
|
|
9713
|
+
/** The same bound payload with the same status and conclusion. */
|
|
9714
|
+
const sameVerdict = (one, other) => one.payload !== void 0 && (one.run.status ?? "") === (other.run.status ?? "") && (one.run.conclusion ?? "") === (other.run.conclusion ?? "") && isDeepStrictEqual(one.payload, other.payload);
|
|
9715
|
+
/**
|
|
9716
|
+
* Every generation among the served runs, ordered and grouped by the rule
|
|
9717
|
+
* `newestVerifyProofGeneration` states, and every run's post with the
|
|
9718
|
+
* generation it belongs to.
|
|
9719
|
+
*/
|
|
9720
|
+
const verifyProofGenerations = (runs, bound) => {
|
|
9721
|
+
const generations = [];
|
|
9722
|
+
const posts = [];
|
|
9723
|
+
for (const run of runs) {
|
|
9724
|
+
const parsed = parsePrVerifyCheckPayload(run.output?.text) ?? parsePrVerifyCheckPayload(run.output?.summary);
|
|
9725
|
+
const payload = run.head_sha === bound.sha && parsed?.headSha === bound.sha && parsed.repository === bound.repository ? parsed : void 0;
|
|
9726
|
+
const postedMs = wholeSecondMs(generationOrderMs(run.started_at));
|
|
9727
|
+
const startMs = passingProofStartMs(run, payload);
|
|
9728
|
+
if (payload === void 0 || startMs === void 0) {
|
|
9729
|
+
const generation = {
|
|
9730
|
+
orderMs: postedMs,
|
|
9731
|
+
...payload === void 0 ? {} : { payload },
|
|
9732
|
+
recorded: false,
|
|
9733
|
+
run
|
|
9734
|
+
};
|
|
9735
|
+
generations.push(generation);
|
|
9736
|
+
posts.push({
|
|
9737
|
+
generation,
|
|
9738
|
+
postedMs
|
|
9739
|
+
});
|
|
9740
|
+
continue;
|
|
9741
|
+
}
|
|
9742
|
+
const generation = {
|
|
9743
|
+
orderMs: Math.min(startMs, postedMs),
|
|
9744
|
+
payload,
|
|
9745
|
+
recorded: true,
|
|
9746
|
+
run
|
|
9747
|
+
};
|
|
9748
|
+
const earlier = generations.find((candidate) => candidate.recorded && sameVerdict(candidate, generation));
|
|
9749
|
+
if (earlier === void 0) generations.push(generation);
|
|
9750
|
+
else earlier.orderMs = Math.min(earlier.orderMs, generation.orderMs);
|
|
9751
|
+
posts.push({
|
|
9752
|
+
generation: earlier ?? generation,
|
|
9753
|
+
postedMs
|
|
9754
|
+
});
|
|
9755
|
+
}
|
|
9756
|
+
return {
|
|
9757
|
+
generations,
|
|
9758
|
+
posts
|
|
9759
|
+
};
|
|
9760
|
+
};
|
|
9761
|
+
/**
|
|
9762
|
+
* The newest generation of the `pr:verify` proof at one head, or nothing when
|
|
9763
|
+
* no single generation can be trusted. A passing generation is ordered by when
|
|
9764
|
+
* its verification started, never later than when its run reached GitHub and
|
|
9765
|
+
* never by when its verification ended (#1409, #1490). Every other run is
|
|
9766
|
+
* ordered by when it reached GitHub (#1506):
|
|
9767
|
+
*
|
|
9768
|
+
* - A passing run is completed with conclusion `success`, and its bound
|
|
9769
|
+
* payload has outcome `passed`. When that payload transports a typed proof
|
|
9770
|
+
* whose start is computable (`proofStartMs`: its `endedAt` minus its
|
|
9771
|
+
* `durationMs`), the run is ordered at that start, or at its own
|
|
9772
|
+
* `started_at` when that is earlier. Runs that carry the same such payload
|
|
9773
|
+
* with the same status and conclusion are one generation posted more than
|
|
9774
|
+
* once (`pr:verify` posts, `pr:publish` re-posts), ordered at the earliest of
|
|
9775
|
+
* its members' instants, whatever order GitHub serves them in. A re-post
|
|
9776
|
+
* does not move the proof's start, so an older pass re-posted after a newer
|
|
9777
|
+
* failure does not outrank it, and the normal post plus re-post is not a
|
|
9778
|
+
* tie.
|
|
9779
|
+
* - Any other run is its own generation at its `started_at`: one still
|
|
9780
|
+
* running, a failure or cancellation whether or not it carries a proof, one
|
|
9781
|
+
* with no bound payload (including one whose transported proof fails the
|
|
9782
|
+
* proof schema, which drops the whole payload), or a pass with no usable
|
|
9783
|
+
* typed proof (over the text cap, or with no computable start). A run is
|
|
9784
|
+
* created no earlier than its verification began, so `started_at` is an
|
|
9785
|
+
* upper bound on that start. Ranking there can only make such a run newer
|
|
9786
|
+
* than it is, so it vetoes more, never less.
|
|
9787
|
+
* - Neither `endedAt` nor a passing run's `started_at` is its start. Every
|
|
9788
|
+
* writer posts its `pr-verify` run only once verification has finished
|
|
9789
|
+
* (`pr:verify` on success or failure, `pr:publish` on re-post), so both say
|
|
9790
|
+
* when it ended, and an older pass ordered there would hide a run that
|
|
9791
|
+
* started after the pass began: still running, or failed (#1490). A pass's
|
|
9792
|
+
* `started_at` only caps its proof's start. A failure's `started_at` is when
|
|
9793
|
+
* it was posted, not when it started, and that is deliberate: a veto's
|
|
9794
|
+
* instant only has to be no earlier than its start.
|
|
9795
|
+
* - Only a pass is ordered by its proof because the hosted gate cannot run the
|
|
9796
|
+
* proof schema this reader runs (#1506). Ordered by its proof's claimed
|
|
9797
|
+
* start there, a failure carrying a malformed proof ranked under an older
|
|
9798
|
+
* pass, and the gate reused the pass where this reader refused. For an
|
|
9799
|
+
* honest proof, `started_at` is no earlier than `endedAt`, which is no
|
|
9800
|
+
* earlier than the start, so the cap changes nothing for a valid pass, and
|
|
9801
|
+
* it also caps a schema-valid proof from a skewed clock.
|
|
9802
|
+
* - A pass is reused only when its verification started strictly after every
|
|
9803
|
+
* other run at this head was posted (#1506). Its start is its instant
|
|
9804
|
+
* (`orderMs`): its proof's start, capped as above, or its `started_at` when
|
|
9805
|
+
* it is ordered there. Every other run's post is its `started_at`, rounded
|
|
9806
|
+
* down to the whole second. Only the pass's own generation is left out: its
|
|
9807
|
+
* re-post group, or, for a pass with no typed proof, that run alone. At most
|
|
9808
|
+
* one generation can satisfy this, and that one is the newest, so this
|
|
9809
|
+
* reader selects the newest generation and, when it is a pass
|
|
9810
|
+
* (`isPassingRun`), trusts nothing unless every other run was posted before
|
|
9811
|
+
* its instant (`postedSinceItStarted`). When the newest generation is not a
|
|
9812
|
+
* pass, the caller refuses reuse anyway, so it is returned as before.
|
|
9813
|
+
* - The check exists because a run may be ordered earlier than its post by
|
|
9814
|
+
* its proof. This reader validates proofs, so for it the check only refuses
|
|
9815
|
+
* a valid pass that started before another pass was posted. The hosted gate
|
|
9816
|
+
* cannot validate them: without the check, a successful run whose malformed
|
|
9817
|
+
* proof claimed an early start ranked under an older pass there, whether
|
|
9818
|
+
* that pass was ordered by its proof or at its `started_at`, and the gate
|
|
9819
|
+
* reused the older pass where this reader, which drops that payload and
|
|
9820
|
+
* ranks the run at its `started_at`, refused. Both readers apply the check
|
|
9821
|
+
* so they agree. With it, whether a pass may be reused never consults
|
|
9822
|
+
* another run's proof fields, only when each other run was posted, which
|
|
9823
|
+
* GitHub records. In the hosted gate those fields can only cause a refusal;
|
|
9824
|
+
* they never hide a veto. The pass's own clock is capped at its post, so it
|
|
9825
|
+
* can only count against that pass.
|
|
9826
|
+
* - Two changes on valid input follow, and both only add refusals, each
|
|
9827
|
+
* costing one fresh verification in a rare case. A failure carrying a proof
|
|
9828
|
+
* is ordered when it was posted instead of when it started, so an older
|
|
9829
|
+
* failure that finished after a newer pass began now refuses reuse. Two pass
|
|
9830
|
+
* generations whose verifications overlapped now refuse, as does an older
|
|
9831
|
+
* pass re-posted after a newer pass started. Passes that ran one after the
|
|
9832
|
+
* other, and a pass with its own re-posts, are reused as before.
|
|
9833
|
+
* - The hosted gate's trust boundary is a class. The gate does not
|
|
9834
|
+
* reimplement this reader's payload parser. On payloads the factory's
|
|
9835
|
+
* writer emits, both readers select the same pass or both refuse: the
|
|
9836
|
+
* 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,
|
|
9838
|
+
* the two can differ in either direction. The known instances where the
|
|
9839
|
+
* hosted gate reuses and this reader refuses are: (a) a reused run whose
|
|
9840
|
+
* typed proof fails `validatePrVerifyProof`; (b) a newest pass that repeats
|
|
9841
|
+
* another run's verdict only once this reader normalizes that run's
|
|
9842
|
+
* payload, say by dropping a key; (c) a payload in `output.summary` beside
|
|
9843
|
+
* a text payload this reader cannot read, which it reads from the summary
|
|
9844
|
+
* and the hosted gate from the text; (d) text the gate's strip-and-`fromjson`
|
|
9845
|
+
* reads and this reader's fence match or `JSON.parse` refuses, such as a
|
|
9846
|
+
* closing fence with no opening one, a doubled opening fence, a fence line
|
|
9847
|
+
* inside the JSON, a payload split across two fenced blocks, or a number
|
|
9848
|
+
* literal such as `NaN` where the gate's jq accepts it (jq 1.8 does).
|
|
9849
|
+
* Mirroring any of it would turn the gate's jq into a parser. Only the
|
|
9850
|
+
* factory App can post the run, and that identity bounds the whole class
|
|
9851
|
+
* (#1511).
|
|
9852
|
+
* - A run not ordered by a typed proof carries no generation identity, so it
|
|
9853
|
+
* is never grouped. Two separate executions with the same command names and
|
|
9854
|
+
* verdict post the same compact payload, and so does a re-post. When the
|
|
9855
|
+
* newest generation is one of these and repeats another run's verdict
|
|
9856
|
+
* (`sameVerdict`), it could be a re-post of an older verdict or a new one,
|
|
9857
|
+
* and nothing is trusted. Proofs that size are rare, so refusing costs one
|
|
9858
|
+
* fresh verification.
|
|
9859
|
+
* - Two different generations at the newest instant refuse, by the shared
|
|
9860
|
+
* sequencing owner's rule.
|
|
9861
|
+
*
|
|
9862
|
+
* Every instant is rounded down to its whole second (`wholeSecondMs`), as the
|
|
9863
|
+
* hosted gate's jq must: it compares epoch seconds. Rounding down is monotonic,
|
|
9864
|
+
* so it can merge two instants into a tie, which refuses, but never invert
|
|
9865
|
+
* them. It also fails closed: a proof's start never moves later, so it never
|
|
9866
|
+
* outranks an unbound run in the same second.
|
|
9867
|
+
*
|
|
9868
|
+
* Factory-ci's hosted proof-reuse gate applies the same rule in jq.
|
|
9869
|
+
*/
|
|
9870
|
+
const newestVerifyProofGeneration = (runs, bound) => {
|
|
9871
|
+
const { generations, posts } = verifyProofGenerations(runs, bound);
|
|
9872
|
+
const selected = selectNewestGeneration(generations);
|
|
9873
|
+
if (selected.kind !== "newest") return;
|
|
9874
|
+
const newest = selected.generation;
|
|
9875
|
+
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;
|
|
9878
|
+
};
|
|
9635
9879
|
/**
|
|
9636
9880
|
* Read the machine-readable `pr:verify` binding for one commit (#247).
|
|
9637
9881
|
*
|
|
@@ -9659,20 +9903,18 @@ function fetchPrVerifyProofBinding({ expectedAppId, githubApp, owner, repo, sha,
|
|
|
9659
9903
|
}
|
|
9660
9904
|
const runs = completeCheckRunPages(pages);
|
|
9661
9905
|
if (!runs) return;
|
|
9662
|
-
const
|
|
9906
|
+
const owned = runs.filter((run) => run?.name === checkName && checkRunProducedByFactoryApp({
|
|
9663
9907
|
app: run.app,
|
|
9664
9908
|
expectedAppId: resolvedAppId
|
|
9665
|
-
})).map((run) => ({
|
|
9666
|
-
orderMs: generationOrderMs(run.started_at),
|
|
9667
|
-
run
|
|
9668
9909
|
}));
|
|
9669
|
-
if (
|
|
9670
|
-
const
|
|
9671
|
-
|
|
9672
|
-
|
|
9673
|
-
|
|
9674
|
-
|
|
9675
|
-
|
|
9910
|
+
if (owned.some((run) => !Number.isFinite(generationOrderMs(run.started_at)))) return;
|
|
9911
|
+
const newest = newestVerifyProofGeneration(owned, {
|
|
9912
|
+
repository: `${owner}/${repo}`,
|
|
9913
|
+
sha
|
|
9914
|
+
});
|
|
9915
|
+
if (newest === void 0) return;
|
|
9916
|
+
const { payload, run } = newest;
|
|
9917
|
+
if (run.status !== "completed" || !payload) return;
|
|
9676
9918
|
const producer = checkRunProducedByFactoryApp({
|
|
9677
9919
|
app: run.app,
|
|
9678
9920
|
expectedAppId: resolvedAppId
|
|
@@ -10947,8 +11189,21 @@ var DemandWaiveRefusalError = class extends Error {
|
|
|
10947
11189
|
this.refusals = refusals;
|
|
10948
11190
|
}
|
|
10949
11191
|
};
|
|
10950
|
-
/**
|
|
10951
|
-
|
|
11192
|
+
/**
|
|
11193
|
+
* Refusals about the named key: malformed, or well-formed but outside the one
|
|
11194
|
+
* eligibility contract the readers fold by (#1492). An ineligible key refuses
|
|
11195
|
+
* here, before anything is written, so the store never holds an apparent
|
|
11196
|
+
* operator authorization that no command applies.
|
|
11197
|
+
*/
|
|
11198
|
+
const demandKeyRefusals = ({ demand, parsedDemand }) => {
|
|
11199
|
+
if (!parsedDemand.success) return [`demand:waive refuses demand key "${demand}": a demand key is a lowercase family, optionally qualified by its resolved value (e.g. review-rung:human).`];
|
|
11200
|
+
return isWaivableDemand(parsedDemand.data) ? [] : [ineligibleWaiverRefusal(parsedDemand.data)];
|
|
11201
|
+
};
|
|
11202
|
+
/** Refusals about the request itself: the key, a missing rationale. */
|
|
11203
|
+
const requestRefusals = ({ demand, parsedDemand, rationale }) => [...demandKeyRefusals({
|
|
11204
|
+
demand,
|
|
11205
|
+
parsedDemand
|
|
11206
|
+
}), ...rationale.length === 0 ? ["demand:waive requires a non-empty --rationale."] : []];
|
|
10952
11207
|
/**
|
|
10953
11208
|
* Refusals about the candidate the waiver would bind to. A waiver names one
|
|
10954
11209
|
* pushed head. For ordinary demands that head must be the one verification
|
|
@@ -12264,10 +12519,14 @@ const defaultResolveRepo = (cwd) => runCapture("gh", [
|
|
|
12264
12519
|
"--jq",
|
|
12265
12520
|
".nameWithOwner"
|
|
12266
12521
|
], cwd).stdout.trim();
|
|
12267
|
-
const
|
|
12522
|
+
const parseFailureReason = ({ commentAuthor, commentUrl, parseError }) => `found a boundary-review-proof block from ${commentAuthor}${commentUrl === void 0 ? "" : ` (${commentUrl})`} that failed to parse: ${parseError}; edit that comment so the block matches the boundary-review proof schema — an unparseable proof satisfies nothing`;
|
|
12523
|
+
const evaluateProofAgainstDemands = ({ boundary, comments, computed, declaredBy, demandedRung, manifestHash, selected }) => {
|
|
12268
12524
|
if (!selected) {
|
|
12269
|
-
|
|
12270
|
-
|
|
12525
|
+
const authorsSeen = boundaryReviewProofAuthorsSeen(comments, boundary);
|
|
12526
|
+
const blockingReasons = boundaryReviewProofParseFailures(comments, boundary).map(parseFailureReason);
|
|
12527
|
+
if (authorsSeen.length > 0) blockingReasons.push(`boundary-review proof block(s) found on the epic issue, but none authored by declaredBy "${declaredBy}" (authors seen: ${authorsSeen.join(", ")}); run the boundary review at the ${demandedRung} rung from that account, or correct declaredBy if it names the wrong one`);
|
|
12528
|
+
if (blockingReasons.length === 0) blockingReasons.push(`no boundary-review proof found on the epic issue; run the boundary review at the ${demandedRung} rung and post its fenced JSON proof comment — absence fails closed`);
|
|
12529
|
+
return { blockingReasons };
|
|
12271
12530
|
}
|
|
12272
12531
|
const { proof } = selected;
|
|
12273
12532
|
const blockingReasons = [];
|
|
@@ -12294,7 +12553,8 @@ function runBoundaryCheck(args, dependencies = {}) {
|
|
|
12294
12553
|
const comments = github.fetchIssueComments(repo, args.epic);
|
|
12295
12554
|
const selected = selectBoundaryReviewProof(comments, parsed.manifest.boundary, parsed.manifest.declaredBy);
|
|
12296
12555
|
const { blockingReasons } = evaluateProofAgainstDemands({
|
|
12297
|
-
|
|
12556
|
+
boundary: parsed.manifest.boundary,
|
|
12557
|
+
comments,
|
|
12298
12558
|
computed,
|
|
12299
12559
|
declaredBy: parsed.manifest.declaredBy,
|
|
12300
12560
|
demandedRung,
|
|
@@ -14606,8 +14866,9 @@ function buildEpicCloseoutReport(input) {
|
|
|
14606
14866
|
*
|
|
14607
14867
|
* `factory:closeout` reads the lane's native at-rest provider logs by its
|
|
14608
14868
|
* session join keys and records what they say as closeout metric rows. The
|
|
14609
|
-
* rows land in the one local closeout markdown,
|
|
14610
|
-
*
|
|
14869
|
+
* rows land in the one local closeout markdown, and the closeout artifact
|
|
14870
|
+
* carries them to HQ as an advisory handoff. The weekly look-back no longer
|
|
14871
|
+
* reads token cost (#1433).
|
|
14611
14872
|
*
|
|
14612
14873
|
* Harvest NEVER throws: a missing or unreadable log degrades to an absent
|
|
14613
14874
|
* family plus a data gap, so telemetry content can never fail the closeout.
|
|
@@ -16686,7 +16947,7 @@ const EPIC_STRUCTURE_SCHEMA_VERSION = 1;
|
|
|
16686
16947
|
/**
|
|
16687
16948
|
* DAG-node PLANNING statuses — the planning-plane vocabulary a planner authors
|
|
16688
16949
|
* on a `dag.yml` node. Carried forward by ADR 0018 and defined by the epic
|
|
16689
|
-
* skill (`reference/epic-artifacts.md`) and both `
|
|
16950
|
+
* skill (`reference/epic-artifacts.md`) and both `GLOSSARY.md` glossaries.
|
|
16690
16951
|
*
|
|
16691
16952
|
* A DAG node is a unit of *plan*; an HQ lane is a unit of *runtime execution*.
|
|
16692
16953
|
* These are two planes with two vocabularies and must not be conflated — the
|
|
@@ -17756,6 +18017,11 @@ function armAutoMerge(input, dependencies = {}) {
|
|
|
17756
18017
|
//#endregion
|
|
17757
18018
|
//#region src/merge-freeze.ts
|
|
17758
18019
|
const MERGE_FREEZE_CHECK_NAME = "patronage-factory/merge-freeze";
|
|
18020
|
+
/**
|
|
18021
|
+
* The Factory App's slug, as GitHub serves it on the runs the writer posts.
|
|
18022
|
+
* It is not a trust predicate: an App chooses its own slug, so the reader
|
|
18023
|
+
* trusts the pinned numeric App id instead (ADR 0022, #1393).
|
|
18024
|
+
*/
|
|
17759
18025
|
const MERGE_FREEZE_APP_SLUG = "patronage-factory";
|
|
17760
18026
|
const MERGE_FREEZE_SCHEMA_VERSION = 1;
|
|
17761
18027
|
const CHECK_RUNS_PER_PAGE$1 = 100;
|
|
@@ -17806,7 +18072,10 @@ function validateMergeFreezeState(value) {
|
|
|
17806
18072
|
return mergeFreezeStateSchema.parse(value);
|
|
17807
18073
|
}
|
|
17808
18074
|
const mergeFreezeCheckRunListItemSchema = z.object({
|
|
17809
|
-
app: z.object({
|
|
18075
|
+
app: z.object({
|
|
18076
|
+
id: z.number(),
|
|
18077
|
+
slug: z.string().nullable().optional()
|
|
18078
|
+
}),
|
|
17810
18079
|
conclusion: z.enum([
|
|
17811
18080
|
"failure",
|
|
17812
18081
|
"neutral",
|
|
@@ -17855,43 +18124,161 @@ const commitParentsSchema = z.object({
|
|
|
17855
18124
|
parents: z.array(z.object({ sha: shaSchema$2 })),
|
|
17856
18125
|
sha: shaSchema$2
|
|
17857
18126
|
});
|
|
17858
|
-
|
|
17859
|
-
|
|
17860
|
-
|
|
18127
|
+
/**
|
|
18128
|
+
* The verdict a completed generation carries. A malformed payload throws, and
|
|
18129
|
+
* a payload that disagrees with its own check run is unreadable.
|
|
18130
|
+
*/
|
|
18131
|
+
function settleCompletedGeneration(run) {
|
|
18132
|
+
const completed = completedMergeFreezeCheckRunSchema.parse(run);
|
|
18133
|
+
const state = mergeFreezeStateSchema.parse(JSON.parse(completed.output.text));
|
|
18134
|
+
if (state.generationId !== completed.id || state.headSha !== completed.head_sha || state.outcome === "active" && completed.conclusion !== "failure" || state.outcome === "stale" && completed.conclusion !== "neutral" || !state.active && completed.conclusion !== "success") return {
|
|
18135
|
+
kind: "unreadable",
|
|
18136
|
+
reason: `Newest ${MERGE_FREEZE_CHECK_NAME} check run has inconsistent state.`
|
|
18137
|
+
};
|
|
18138
|
+
return {
|
|
18139
|
+
kind: "settled",
|
|
18140
|
+
state
|
|
18141
|
+
};
|
|
18142
|
+
}
|
|
18143
|
+
/**
|
|
18144
|
+
* Whether a generation's recorded verification was cancelled and nothing in it
|
|
18145
|
+
* failed (#1158): at least one `cancelled` job and no `failure`. It reads only
|
|
18146
|
+
* the recorded job facts and never infers why a run was cancelled. A legacy
|
|
18147
|
+
* schema-1 record without `verification` is never cancellation-only, because
|
|
18148
|
+
* the reader cannot tell why it froze.
|
|
18149
|
+
*/
|
|
18150
|
+
function isCancellationOnly(state) {
|
|
18151
|
+
const results = Object.values(state.verification?.jobs ?? {});
|
|
18152
|
+
return results.includes("cancelled") && !results.includes("failure");
|
|
18153
|
+
}
|
|
18154
|
+
/**
|
|
18155
|
+
* The verdict behind a newest generation whose verification was only
|
|
18156
|
+
* cancelled (#1158).
|
|
18157
|
+
*
|
|
18158
|
+
* The freeze reads the newest completed generation at the exact tip whose
|
|
18159
|
+
* verification was not cancellation-only. A cancellation-only generation means
|
|
18160
|
+
* verification did not finish, so it says nothing about the code. A duplicate
|
|
18161
|
+
* push or a concurrency group can cancel a later Verify run at a tip an earlier
|
|
18162
|
+
* run already judged, and that later run must not override the earlier verdict.
|
|
18163
|
+
*
|
|
18164
|
+
* So the reader steps back, newest first by the same `started_at` rule, over
|
|
18165
|
+
* cancellation-only generations to the first older one that decides. A failed
|
|
18166
|
+
* or mixed failed-and-cancelled generation decides, and so does a legacy
|
|
18167
|
+
* record without job facts. If no older generation decides, `newest` stands,
|
|
18168
|
+
* which keeps the freeze active and fail-closed. That covers three cases:
|
|
18169
|
+
* every completed generation at the tip is cancellation-only; the next older
|
|
18170
|
+
* generation is still running, since an older green never outranks a newer
|
|
18171
|
+
* running one; and the next older generation is for another tip, is malformed,
|
|
18172
|
+
* or shares its `started_at` with another.
|
|
18173
|
+
*
|
|
18174
|
+
* `steppedPast` carries the cancellation-only runs walked so far, newest first,
|
|
18175
|
+
* starting with the newest run itself. The selection reports them only when an
|
|
18176
|
+
* older verdict decides; when `newest` stands, it read past nothing.
|
|
18177
|
+
*/
|
|
18178
|
+
function verdictBehindCancellation(newest, olderRuns, expectedHeadSha, steppedPast) {
|
|
18179
|
+
const standing = {
|
|
18180
|
+
...newest,
|
|
18181
|
+
readPast: []
|
|
18182
|
+
};
|
|
18183
|
+
const selected = selectNewestGeneration(olderRuns.map((run) => ({
|
|
18184
|
+
orderMs: generationOrderMs(run.started_at),
|
|
18185
|
+
run
|
|
18186
|
+
})));
|
|
18187
|
+
if (selected.kind !== "newest") return standing;
|
|
18188
|
+
const { run } = selected.generation;
|
|
18189
|
+
if (run.head_sha !== expectedHeadSha || run.status !== "completed") return standing;
|
|
18190
|
+
let older;
|
|
18191
|
+
try {
|
|
18192
|
+
older = settleCompletedGeneration(run);
|
|
18193
|
+
} catch {
|
|
18194
|
+
return standing;
|
|
18195
|
+
}
|
|
18196
|
+
if (older.kind !== "settled") return standing;
|
|
18197
|
+
if (!isCancellationOnly(older.state)) return {
|
|
18198
|
+
generation: older,
|
|
18199
|
+
readPast: steppedPast,
|
|
18200
|
+
run
|
|
18201
|
+
};
|
|
18202
|
+
return verdictBehindCancellation(newest, olderRuns.filter((candidate) => candidate !== run), expectedHeadSha, [...steppedPast, run]);
|
|
18203
|
+
}
|
|
18204
|
+
/**
|
|
18205
|
+
* The generation the freeze selects among the runs listed at one tip: the one
|
|
18206
|
+
* rule behind `readGeneration`, `read`, and the PR status HUD's freeze row
|
|
18207
|
+
* (#1501). It is pure. Its caller has already decided which runs to trust; the
|
|
18208
|
+
* reader refuses the whole read on a run the pinned App did not write.
|
|
18209
|
+
*
|
|
18210
|
+
* GitHub documents check-run IDs only as unique, not ordered. The factory
|
|
18211
|
+
* deliberately relies on GitHub-stamped `started_at`, and equal newest instants
|
|
18212
|
+
* stay ambiguous and fail closed. `filter=all` in the API reader is required
|
|
18213
|
+
* because `latest` can hide a live generation (#522, #524).
|
|
18214
|
+
*
|
|
18215
|
+
* A malformed newest payload throws, as `settleCompletedGeneration` does.
|
|
18216
|
+
*/
|
|
18217
|
+
function selectMergeFreezeRun(runs, expectedHeadSha) {
|
|
18218
|
+
if (runs.length === 0) return {
|
|
18219
|
+
generation: { kind: "missing" },
|
|
18220
|
+
readPast: []
|
|
18221
|
+
};
|
|
17861
18222
|
const selected = selectNewestGeneration(runs.map((run) => ({
|
|
17862
18223
|
orderMs: generationOrderMs(run.started_at),
|
|
17863
18224
|
run
|
|
17864
18225
|
})));
|
|
17865
18226
|
if (selected.kind === "ambiguous") return {
|
|
17866
|
-
|
|
17867
|
-
|
|
18227
|
+
generation: {
|
|
18228
|
+
kind: "unreadable",
|
|
18229
|
+
reason: `Newest ${MERGE_FREEZE_CHECK_NAME} generation is ambiguous: ${selected.tied.length} check runs share ${new Date(selected.orderMs).toISOString()}.`
|
|
18230
|
+
},
|
|
18231
|
+
readPast: []
|
|
18232
|
+
};
|
|
18233
|
+
if (selected.kind === "none") return {
|
|
18234
|
+
generation: { kind: "missing" },
|
|
18235
|
+
readPast: []
|
|
17868
18236
|
};
|
|
17869
|
-
if (selected.kind === "none") return { kind: "missing" };
|
|
17870
18237
|
const newest = selected.generation.run;
|
|
17871
18238
|
if (newest.head_sha !== expectedHeadSha) return {
|
|
17872
|
-
|
|
17873
|
-
|
|
18239
|
+
generation: {
|
|
18240
|
+
kind: "unreadable",
|
|
18241
|
+
reason: `Newest ${MERGE_FREEZE_CHECK_NAME} check run targets ${newest.head_sha}, expected ${expectedHeadSha}.`
|
|
18242
|
+
},
|
|
18243
|
+
readPast: []
|
|
17874
18244
|
};
|
|
17875
18245
|
if (newest.status !== "completed") return {
|
|
17876
|
-
|
|
17877
|
-
|
|
17878
|
-
|
|
17879
|
-
|
|
17880
|
-
|
|
17881
|
-
|
|
17882
|
-
|
|
18246
|
+
generation: {
|
|
18247
|
+
kind: "settling",
|
|
18248
|
+
phase: "running-generation",
|
|
18249
|
+
reason: `Authoritative merge freeze generation ${newest.id} is in progress.`,
|
|
18250
|
+
witness: {
|
|
18251
|
+
headSha: newest.head_sha,
|
|
18252
|
+
runId: newest.id
|
|
18253
|
+
}
|
|
18254
|
+
},
|
|
18255
|
+
readPast: []
|
|
17883
18256
|
};
|
|
17884
|
-
const
|
|
17885
|
-
|
|
17886
|
-
|
|
17887
|
-
|
|
17888
|
-
reason: `Newest ${MERGE_FREEZE_CHECK_NAME} check run has inconsistent state.`
|
|
18257
|
+
const verdict = settleCompletedGeneration(newest);
|
|
18258
|
+
if (verdict.kind !== "settled") return {
|
|
18259
|
+
generation: verdict,
|
|
18260
|
+
readPast: []
|
|
17889
18261
|
};
|
|
18262
|
+
if (isCancellationOnly(verdict.state)) return verdictBehindCancellation({
|
|
18263
|
+
generation: verdict,
|
|
18264
|
+
run: newest
|
|
18265
|
+
}, runs.filter((run) => run !== newest), expectedHeadSha, [newest]);
|
|
17890
18266
|
return {
|
|
17891
|
-
|
|
17892
|
-
|
|
18267
|
+
generation: verdict,
|
|
18268
|
+
readPast: [],
|
|
18269
|
+
run: newest
|
|
17893
18270
|
};
|
|
17894
18271
|
}
|
|
18272
|
+
function selectListedMergeFreezeGeneration(listed, expectedHeadSha) {
|
|
18273
|
+
return selectMergeFreezeRun(z.object({ check_runs: z.array(z.unknown()) }).parse(listed).check_runs.map((run) => {
|
|
18274
|
+
const listedRun = mergeFreezeCheckRunListItemSchema.parse(run);
|
|
18275
|
+
if (!checkRunProducedByFactoryApp({
|
|
18276
|
+
app: listedRun.app,
|
|
18277
|
+
expectedAppId: FACTORY_PROOF_GATE_APP_ID
|
|
18278
|
+
})) throw new Error(`${MERGE_FREEZE_CHECK_NAME} check run ${listedRun.id} was not written by the pinned Factory App ${FACTORY_PROOF_GATE_APP_ID}.`);
|
|
18279
|
+
return listedRun;
|
|
18280
|
+
}), expectedHeadSha).generation;
|
|
18281
|
+
}
|
|
17895
18282
|
function unconfiguredMergeFreeze(input, detail) {
|
|
17896
18283
|
return {
|
|
17897
18284
|
kind: "unreadable",
|
|
@@ -18763,26 +19150,9 @@ const reviewRoundLimitBlockers = (input) => {
|
|
|
18763
19150
|
}];
|
|
18764
19151
|
};
|
|
18765
19152
|
/**
|
|
18766
|
-
* The demands an operator waiver can reach (#354, moved here from
|
|
18767
|
-
* `pr:merge-check` by #477). Exactly the set the merge-time site could waive:
|
|
18768
|
-
* the merge freeze, the profile's external required checks, and the boundary
|
|
18769
|
-
* wave's review rung — the demands the resolver puts *in force* for one
|
|
18770
|
-
* candidate — plus the review-round limit (#1280), which exists only to be
|
|
18771
|
-
* waived.
|
|
18772
|
-
*
|
|
18773
|
-
* Everything else stays unwaivable, and relocating the consumption site does
|
|
18774
|
-
* not widen the operator's override. Structural refusals — candidate identity,
|
|
18775
|
-
* proof binding, an unreadable demand authority, wave membership — were never
|
|
18776
|
-
* waivable at merge time and are not waivable here; in `pr:ready` most of them
|
|
18777
|
-
* refuse by throwing before any blocker list exists. The evidence gates
|
|
18778
|
-
* readiness has always owned (verify, review modes, draft, human blockers) are
|
|
18779
|
-
* satisfied or not; a waiver is not a second way to pass them.
|
|
18780
|
-
*/
|
|
18781
|
-
const WAIVABLE_QUALIFIED_DEMAND_FAMILIES = ["required-check", "review-rung"];
|
|
18782
|
-
const isWaivableDemand = (demand) => demand === DEMAND_KEYS.mergeFreeze || demand === DEMAND_KEYS.reviewRoundLimit || WAIVABLE_QUALIFIED_DEMAND_FAMILIES.some((family) => demand.startsWith(`${family}:`));
|
|
18783
|
-
/**
|
|
18784
19153
|
* Fold the operator's waivers through the blockers, one call per demand so
|
|
18785
|
-
* `demand-waiver.ts` stays the single authority on what a waiver means
|
|
19154
|
+
* `demand-waiver.ts` stays the single authority on what a waiver means and
|
|
19155
|
+
* which demands it can reach — the contract `demand:waive` refuses by. A
|
|
18786
19156
|
* waived demand keeps every refusal it made, verbatim, on the `WaivedDemand`
|
|
18787
19157
|
* record — it is never rewritten as satisfied.
|
|
18788
19158
|
*/
|
|
@@ -19221,6 +19591,8 @@ const renderTable = (rows) => [
|
|
|
19221
19591
|
"| --- | --- | --- | --- |",
|
|
19222
19592
|
...rows.map((row) => `| ${markdownCell(row.target)} | ${statusLabel(row.status)} | ${row.link} | ${row.timestamp} |`)
|
|
19223
19593
|
].join("\n");
|
|
19594
|
+
/** Name the cancellation-only runs a merge-freeze row read past (#1501). */
|
|
19595
|
+
const withReadPastNote = (target, runIds) => runIds && runIds.length > 0 ? `${target} (read past cancellation-only ${runIds.length === 1 ? "run" : "runs"} ${runIds.join(", ")})` : target;
|
|
19224
19596
|
const checkRow = (target, snapshot, renderedAt) => {
|
|
19225
19597
|
if (!snapshot) return {
|
|
19226
19598
|
detail: "No completed Factory App snapshot is available at this head.",
|
|
@@ -19233,7 +19605,7 @@ const checkRow = (target, snapshot, renderedAt) => {
|
|
|
19233
19605
|
detail: `Completed snapshot: ${snapshot.conclusion}.`,
|
|
19234
19606
|
link: safeMarkdownLink("Details", snapshot.detailsUrl),
|
|
19235
19607
|
status: snapshot.conclusion === "success" ? "Ready" : "Failed",
|
|
19236
|
-
target,
|
|
19608
|
+
target: withReadPastNote(target, snapshot.readPastRunIds),
|
|
19237
19609
|
timestamp: utcTimestamp(snapshot.completedAt)
|
|
19238
19610
|
};
|
|
19239
19611
|
};
|
|
@@ -19345,7 +19717,9 @@ const checkRunSchema = z.object({
|
|
|
19345
19717
|
conclusion: z.string().nullable().optional(),
|
|
19346
19718
|
details_url: z.string().nullable().optional(),
|
|
19347
19719
|
head_sha: z.string().regex(/^[0-9a-f]{40}$/u),
|
|
19720
|
+
id: z.number().int().positive(),
|
|
19348
19721
|
name: z.string(),
|
|
19722
|
+
output: z.object({ text: z.string().nullable().optional() }).nullable().optional(),
|
|
19349
19723
|
started_at: z.iso.datetime().nullable(),
|
|
19350
19724
|
status: z.enum([
|
|
19351
19725
|
"completed",
|
|
@@ -19416,16 +19790,13 @@ const listFactoryCheckRuns = async (input, name, sha, dependencies) => {
|
|
|
19416
19790
|
}
|
|
19417
19791
|
throw new Error(`${name} pagination limit was exhausted before GitHub returned a final page.`);
|
|
19418
19792
|
};
|
|
19419
|
-
|
|
19793
|
+
/** The Factory App's generations of one check at one SHA, each orderable. */
|
|
19794
|
+
const factoryGenerations = (name, sha, runs) => {
|
|
19420
19795
|
const candidates = runs.filter((run) => String(run.app?.id ?? "") === FACTORY_PROOF_GATE_APP_ID && run.head_sha === sha && run.name === name);
|
|
19421
19796
|
if (candidates.some((run) => !Number.isFinite(generationOrderMs(run.started_at)))) throw new Error(`${name} presentation snapshot has a generation with no usable started_at.`);
|
|
19422
|
-
|
|
19423
|
-
|
|
19424
|
-
|
|
19425
|
-
})));
|
|
19426
|
-
if (selected.kind === "ambiguous") throw new Error(`Newest ${name} presentation snapshot is ambiguous: ${selected.tied.length} check runs share the newest timestamp.`);
|
|
19427
|
-
if (selected.kind === "none") return;
|
|
19428
|
-
const { run } = selected.generation;
|
|
19797
|
+
return candidates;
|
|
19798
|
+
};
|
|
19799
|
+
const runSnapshot = (name, run) => {
|
|
19429
19800
|
if (run.status !== "completed" || !run.completed_at || !run.conclusion) return;
|
|
19430
19801
|
return {
|
|
19431
19802
|
completedAt: run.completed_at,
|
|
@@ -19435,6 +19806,45 @@ const completedSnapshot = (name, sha, runs) => {
|
|
|
19435
19806
|
name
|
|
19436
19807
|
};
|
|
19437
19808
|
};
|
|
19809
|
+
const newestSnapshot = (name, generations) => {
|
|
19810
|
+
const selected = selectNewestGeneration(generations.map((run) => ({
|
|
19811
|
+
orderMs: generationOrderMs(run.started_at),
|
|
19812
|
+
run
|
|
19813
|
+
})));
|
|
19814
|
+
if (selected.kind === "ambiguous") throw new Error(`Newest ${name} presentation snapshot is ambiguous: ${selected.tied.length} check runs share the newest timestamp.`);
|
|
19815
|
+
if (selected.kind === "none") return;
|
|
19816
|
+
return runSnapshot(name, selected.generation.run);
|
|
19817
|
+
};
|
|
19818
|
+
const completedSnapshot = (name, sha, runs) => newestSnapshot(name, factoryGenerations(name, sha, runs));
|
|
19819
|
+
const MERGE_FREEZE_CHECK = "patronage-factory/merge-freeze";
|
|
19820
|
+
/**
|
|
19821
|
+
* The merge-freeze row shows the generation readiness reads (#1501), chosen by
|
|
19822
|
+
* the freeze's own selection rather than a second copy of it. That selection
|
|
19823
|
+
* reads past a cancellation-only generation to an older verdict at the same
|
|
19824
|
+
* tip (#1158). The newest completed run alone would show that cancellation as
|
|
19825
|
+
* a freeze readiness does not apply, so the row names the runs it read past.
|
|
19826
|
+
*
|
|
19827
|
+
* 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.
|
|
19832
|
+
*/
|
|
19833
|
+
const mergeFreezeSnapshot = (sha, runs) => {
|
|
19834
|
+
const generations = factoryGenerations(MERGE_FREEZE_CHECK, sha, runs);
|
|
19835
|
+
const newest = newestSnapshot(MERGE_FREEZE_CHECK, generations);
|
|
19836
|
+
if (!newest) return newest;
|
|
19837
|
+
try {
|
|
19838
|
+
const { readPast, run } = selectMergeFreezeRun(generations, sha);
|
|
19839
|
+
const selected = run && readPast.length > 0 ? runSnapshot(MERGE_FREEZE_CHECK, run) : void 0;
|
|
19840
|
+
return selected ? {
|
|
19841
|
+
...selected,
|
|
19842
|
+
readPastRunIds: readPast.map((past) => past.id)
|
|
19843
|
+
} : newest;
|
|
19844
|
+
} catch {
|
|
19845
|
+
return newest;
|
|
19846
|
+
}
|
|
19847
|
+
};
|
|
19438
19848
|
const FAILED_PREVIEW_CONCLUSIONS = new Set([
|
|
19439
19849
|
"action_required",
|
|
19440
19850
|
"cancelled",
|
|
@@ -19483,7 +19893,8 @@ async function readFactoryPrStatusSources(input, dependencies = {}) {
|
|
|
19483
19893
|
];
|
|
19484
19894
|
const listedChecks = await Promise.all(checkTargets.map(([name, sha]) => listFactoryCheckRuns(input, name, sha, dependencies)));
|
|
19485
19895
|
const checks = Object.fromEntries(checkTargets.flatMap(([name, sha], index) => {
|
|
19486
|
-
const
|
|
19896
|
+
const runs = listedChecks[index] ?? [];
|
|
19897
|
+
const snapshot = name === MERGE_FREEZE_CHECK ? mergeFreezeSnapshot(sha, runs) : completedSnapshot(name, sha, runs);
|
|
19487
19898
|
return snapshot ? [[name, snapshot]] : [];
|
|
19488
19899
|
}));
|
|
19489
19900
|
const previewTargets = input.previewTargets ?? [];
|
|
@@ -21346,11 +21757,20 @@ const assertEpicPullRequestOpen = ({ args, base }) => {
|
|
|
21346
21757
|
* prompt for another patch, with no ladder cycle for that patch, is a review
|
|
21347
21758
|
* that nobody recorded, and no ladder control can see it. Assembling again at
|
|
21348
21759
|
* the same patch only rewrites that prompt.
|
|
21760
|
+
*
|
|
21761
|
+
* The prompt also counts as recorded when the checkout's own review proof
|
|
21762
|
+
* holds a cycle at exactly the patch and head the prompt names, whatever the
|
|
21763
|
+
* lineage (#1415). A squash merge of the previous pull request takes that head
|
|
21764
|
+
* off the next branch, so the ladder this candidate continues is empty, yet
|
|
21765
|
+
* the review of that prompt is on record. Only this question reads
|
|
21766
|
+
* `checkoutLadder`; the next review still continues `priorLadder`.
|
|
21349
21767
|
*/
|
|
21350
|
-
const assertPriorPromptRecorded = ({ existing, patchId, priorLadder }) => {
|
|
21768
|
+
const assertPriorPromptRecorded = ({ checkoutLadder, existing, patchId, priorLadder }) => {
|
|
21351
21769
|
if (existing.status !== "ok" || existing.artifact.patchId === patchId) return;
|
|
21352
21770
|
const prompted = existing.artifact;
|
|
21353
|
-
|
|
21771
|
+
const recordedOnContinuedLadder = (priorLadder?.cycles ?? []).some((cycle) => cycle.identity?.patchId === prompted.patchId);
|
|
21772
|
+
const recordedAtPromptedHead = (checkoutLadder?.cycles ?? []).some((cycle) => cycle.identity?.patchId === prompted.patchId && cycle.identity.headSha === prompted.headSha);
|
|
21773
|
+
if (recordedOnContinuedLadder || recordedAtPromptedHead) return;
|
|
21354
21774
|
throw new Error(`pr:review --assemble refuses: ${DEFAULT_ASSEMBLED_REVIEW_PROMPT_PATH} holds a prompt for patch ${prompted.patchId} at ${prompted.headSha}, and the review ladder has no cycle for that patch. The candidate is now patch ${patchId}. Record the review of that patch first: patronage-factory pr:review --historical --findings <findings file for patch ${prompted.patchId}>. If no reviewer read that prompt, delete ${DEFAULT_ASSEMBLED_REVIEW_PROMPT_PATH} and assemble again.`);
|
|
21355
21775
|
};
|
|
21356
21776
|
/**
|
|
@@ -21397,14 +21817,16 @@ const assertAssembleMayPrepareReview = ({ args, base, cwd, git, headSha, output,
|
|
|
21397
21817
|
args,
|
|
21398
21818
|
base
|
|
21399
21819
|
});
|
|
21820
|
+
const prior = readOptionalProofFor(PR_REVIEW_PROOF_DESCRIPTOR, cwd, output);
|
|
21400
21821
|
const priorLadder = priorLadderStateFor({
|
|
21401
21822
|
base: base.ref,
|
|
21402
21823
|
headSha,
|
|
21403
21824
|
isAncestor: (ancestor, descendant) => git.isAncestor(cwd, ancestor, descendant),
|
|
21404
|
-
prior
|
|
21825
|
+
prior
|
|
21405
21826
|
});
|
|
21406
21827
|
const existing = readAssembledReviewPrompt(path.resolve(cwd, DEFAULT_ASSEMBLED_REVIEW_PROMPT_PATH));
|
|
21407
21828
|
assertPriorPromptRecorded({
|
|
21829
|
+
checkoutLadder: prior?.ladder,
|
|
21408
21830
|
existing,
|
|
21409
21831
|
patchId,
|
|
21410
21832
|
priorLadder
|
|
@@ -22143,6 +22565,35 @@ const admittedPullRequestBodyEdit = (admission) => async ({ body, owner, repo, p
|
|
|
22143
22565
|
repo
|
|
22144
22566
|
}), process.cwd(), extraEnv === void 0 ? {} : { extraEnv });
|
|
22145
22567
|
};
|
|
22568
|
+
/**
|
|
22569
|
+
* `gh pr create` for the draft (#1366). With a base, the title and body come
|
|
22570
|
+
* from the branch's commits, built as the Cursor VM's REST create builds them
|
|
22571
|
+
* (#1327). gh's own `--fill` keeps only the commit subjects once a branch has
|
|
22572
|
+
* two commits, which dropped every `Closes #N` line in a commit body, and
|
|
22573
|
+
* membership derives from those lines. With no base there is no range to
|
|
22574
|
+
* read, so gh fills from the repository default branch, every commit body
|
|
22575
|
+
* included (`--fill-verbose`).
|
|
22576
|
+
*/
|
|
22577
|
+
const ghArgsForPullRequestCreate = ({ base, commits, cwd }) => {
|
|
22578
|
+
if (base === void 0) return [
|
|
22579
|
+
"pr",
|
|
22580
|
+
"create",
|
|
22581
|
+
"--draft",
|
|
22582
|
+
"--fill-verbose"
|
|
22583
|
+
];
|
|
22584
|
+
const { body, title } = pullRequestFill(commits(cwd, base));
|
|
22585
|
+
return [
|
|
22586
|
+
"pr",
|
|
22587
|
+
"create",
|
|
22588
|
+
"--draft",
|
|
22589
|
+
"--title",
|
|
22590
|
+
title,
|
|
22591
|
+
"--body",
|
|
22592
|
+
body,
|
|
22593
|
+
"--base",
|
|
22594
|
+
base
|
|
22595
|
+
];
|
|
22596
|
+
};
|
|
22146
22597
|
/** The REST create a Cursor VM uses in place of `gh pr create` (#1327). */
|
|
22147
22598
|
const ghApiArgsForPullRequestCreate = ({ base, body, head, owner, repo, title }) => [
|
|
22148
22599
|
"api",
|
|
@@ -22169,13 +22620,18 @@ const ghApiArgsForPullRequestCreate = ({ base, body, head, owner, repo, title })
|
|
|
22169
22620
|
* reads both branch refs, which is why the admission scope carries
|
|
22170
22621
|
* `contents: read` (#1375). The title and body come from the branch's
|
|
22171
22622
|
* commits, and the head is the already-pushed branch, so nothing is pushed.
|
|
22172
|
-
* Off a Cursor VM the call is `gh pr create`,
|
|
22623
|
+
* Off a Cursor VM the call is `gh pr create`, with the same title and body
|
|
22624
|
+
* (#1366).
|
|
22173
22625
|
*/
|
|
22174
|
-
const admittedPullRequestCreate = (admission, repository) => async ({
|
|
22626
|
+
const admittedPullRequestCreate = (admission, repository, commits) => async ({ base, branch, cwd }) => {
|
|
22175
22627
|
const extraEnv = await admission.ghEnv("create-pull-request", repository);
|
|
22176
|
-
if (extraEnv === void 0) return runCapture("gh",
|
|
22628
|
+
if (extraEnv === void 0) return runCapture("gh", ghArgsForPullRequestCreate({
|
|
22629
|
+
base,
|
|
22630
|
+
commits,
|
|
22631
|
+
cwd
|
|
22632
|
+
}), cwd).stdout;
|
|
22177
22633
|
if (base === void 0) throw new Error("pr:publish on a Cursor VM needs a base branch to create the pull request against, and none was passed or recorded. Run pr:verify, which records its base, or pass --base.");
|
|
22178
|
-
const { body, title } = pullRequestFill(
|
|
22634
|
+
const { body, title } = pullRequestFill(commits(cwd, base));
|
|
22179
22635
|
return runCapture("gh", ghApiArgsForPullRequestCreate({
|
|
22180
22636
|
base,
|
|
22181
22637
|
body,
|
|
@@ -22194,9 +22650,14 @@ const admittedPullRequestCreate = (admission, repository) => async ({ args, base
|
|
|
22194
22650
|
*/
|
|
22195
22651
|
const publishGithubAdapters = (dependencies, repository) => {
|
|
22196
22652
|
const admission = dependencies.admission ?? processAdmissionCredential;
|
|
22653
|
+
const commits = dependencies.pullRequestCommits ?? commitsSinceBase;
|
|
22197
22654
|
const injectedGh = dependencies.gh;
|
|
22198
22655
|
return {
|
|
22199
|
-
create: injectedGh ? ({
|
|
22656
|
+
create: injectedGh ? ({ base, cwd }) => injectedGh(ghArgsForPullRequestCreate({
|
|
22657
|
+
base,
|
|
22658
|
+
commits,
|
|
22659
|
+
cwd
|
|
22660
|
+
}), cwd) : admittedPullRequestCreate(admission, repository, commits),
|
|
22200
22661
|
github: dependencies.github ?? {
|
|
22201
22662
|
...defaultPrReadyGithub(),
|
|
22202
22663
|
editPullRequestBody: admittedPullRequestBodyEdit(admission)
|
|
@@ -22289,15 +22750,7 @@ async function discoverOrCreatePullRequest({ args, branch, create, cwd, gh, revi
|
|
|
22289
22750
|
reviewProofPath,
|
|
22290
22751
|
verifyProofPath
|
|
22291
22752
|
});
|
|
22292
|
-
const createArgs = [
|
|
22293
|
-
"pr",
|
|
22294
|
-
"create",
|
|
22295
|
-
"--draft",
|
|
22296
|
-
"--fill"
|
|
22297
|
-
];
|
|
22298
|
-
if (creationBase !== void 0) createArgs.push("--base", creationBase);
|
|
22299
22753
|
await create({
|
|
22300
|
-
args: createArgs,
|
|
22301
22754
|
base: creationBase,
|
|
22302
22755
|
branch,
|
|
22303
22756
|
cwd
|
|
@@ -22469,6 +22922,18 @@ async function ensureVerifyProof({ args, cwd, dependencies, headSha, pr, verifyP
|
|
|
22469
22922
|
};
|
|
22470
22923
|
}
|
|
22471
22924
|
const reviewNotRequired = (proof) => proof.reviewRequirement?.status === "not-required";
|
|
22925
|
+
/**
|
|
22926
|
+
* Let the `pr-review-proof` event a cycle recorded from `--findings` reach HQ
|
|
22927
|
+
* or the spool before publish goes on (#1382). The sink's setup budget starts
|
|
22928
|
+
* when `pr:review` enqueues the event, and its first step waits for the event
|
|
22929
|
+
* loop. Standalone `pr:review` exits next, so the loop is free. Publish goes
|
|
22930
|
+
* straight on to synchronous `gh` calls, which held the loop past that budget,
|
|
22931
|
+
* so the sink gave up before spooling anything. Bounded by the sink's own
|
|
22932
|
+
* budgets.
|
|
22933
|
+
*/
|
|
22934
|
+
const settleRecordedReviewEvent = async (dependencies) => {
|
|
22935
|
+
await (dependencies.awaitPendingIngest ?? awaitPendingHqIngest)();
|
|
22936
|
+
};
|
|
22472
22937
|
async function ensureReviewProof({ args, cwd, dependencies, currentReviewIdentity, pr, reviewProofPath }) {
|
|
22473
22938
|
const proof = readOptionalReviewProof(cwd, reviewProofPath);
|
|
22474
22939
|
const ladderPolicy = resolveReviewLadderPolicy();
|
|
@@ -22497,9 +22962,11 @@ async function ensureReviewProof({ args, cwd, dependencies, currentReviewIdentit
|
|
|
22497
22962
|
verifyProof: args.verifyProof
|
|
22498
22963
|
}, publisher.dependencies);
|
|
22499
22964
|
} catch (error) {
|
|
22965
|
+
await settleRecordedReviewEvent(dependencies);
|
|
22500
22966
|
await publisher.post()?.settled;
|
|
22501
22967
|
throw error;
|
|
22502
22968
|
}
|
|
22969
|
+
await settleRecordedReviewEvent(dependencies);
|
|
22503
22970
|
const producerPost = publisher.post();
|
|
22504
22971
|
return {
|
|
22505
22972
|
...producerPost === void 0 ? {} : { producerPost },
|
package/dist/schemas.d.ts
CHANGED
|
@@ -66,7 +66,7 @@ type EvidenceReviewRung = (typeof EVIDENCE_REVIEW_RUNGS)[number];
|
|
|
66
66
|
//#region src/review-ladder-ledger.d.ts
|
|
67
67
|
type LadderCycleStage = "interior" | "gate";
|
|
68
68
|
type LadderFindingReport = ReviewFindingAttributes;
|
|
69
|
-
/** One finding as recorded in one cycle (
|
|
69
|
+
/** One finding as recorded in one cycle (GLOSSARY.md: finding occurrence). */
|
|
70
70
|
interface ReviewOccurrenceRecord {
|
|
71
71
|
address: string;
|
|
72
72
|
/** The ladder-assigned cycle number the occurrence was recorded in. */
|
|
@@ -82,7 +82,7 @@ interface ReviewOccurrenceRecord {
|
|
|
82
82
|
declare const POLICY_DECLARATION_RULES: readonly ["cycle-2-severity-floor", "identity-repeat"];
|
|
83
83
|
type PolicyDeclarationRule = (typeof POLICY_DECLARATION_RULES)[number];
|
|
84
84
|
/**
|
|
85
|
-
* A disposition declaration as stored (
|
|
85
|
+
* A disposition declaration as stored (GLOSSARY.md: disposition declaration):
|
|
86
86
|
* one occurrence, one disposition, the authority that made it and its
|
|
87
87
|
* reference (operator, reviewer) or rule (policy).
|
|
88
88
|
*/
|
package/dist/schemas.js
CHANGED
|
@@ -252,6 +252,7 @@ const waivedDemandSchema = z.object({
|
|
|
252
252
|
session: z.string().trim().min(1).optional(),
|
|
253
253
|
unmetReasons: z.array(z.string().min(1)).min(1)
|
|
254
254
|
});
|
|
255
|
+
DEMAND_KEYS.mergeFreeze, DEMAND_KEYS.reviewRoundLimit, DEMAND_KEYS.childIssueOpen;
|
|
255
256
|
//#endregion
|
|
256
257
|
//#region src/diff-classification.ts
|
|
257
258
|
const DIFF_CLASSIFICATIONS = [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@patronage/software-factory",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.42",
|
|
4
4
|
"description": "Shared Patronage software factory CLI and project-profile validation tools",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"files": [
|
|
16
16
|
"dist",
|
|
17
17
|
"README.md",
|
|
18
|
-
"
|
|
18
|
+
"GLOSSARY.md"
|
|
19
19
|
],
|
|
20
20
|
"type": "module",
|
|
21
21
|
"exports": {
|
|
@@ -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.
|
|
40
|
+
"@patronage/factory-ci": "1.0.0-alpha.42"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@types/node": "24.13.3",
|
|
File without changes
|