@patronage/software-factory 1.0.0-alpha.20 → 1.0.0-alpha.22
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 +1 -1
- package/dist/index.d.ts +74 -9
- package/dist/index.js +314 -40
- package/dist/schemas.d.ts +21 -1
- package/dist/schemas.js +17 -1
- package/package.json +5 -10
package/README.md
CHANGED
|
@@ -66,7 +66,7 @@ Generated production workflows may call `psf production:impact --before <sha> --
|
|
|
66
66
|
|
|
67
67
|
Generated pull-request workflows may call `psf candidate:impact --base <sha> --head <sha> --github-output <path>` to route Candidate verification surfaces. The command evaluates the same profile impact declarations and pnpm dependency graph from the exact base/head identity, diffed from their merge base; it is the candidate-named sibling of `production:impact` and replaces repository-owned path filters, never adds a second matcher. The `head` commit must be the checked-out `HEAD`, and both commits must be reachable (`fetch-depth: 0`). Every doubt path — unreadable identity, malformed or unsupported graph data, an unowned profile change, a working-tree profile that differs from the head commit, or classifier refusal — fails closed and leaves every declared target demanded. `production:impact` keeps its merge-push `before`/`after` contract unchanged.
|
|
68
68
|
|
|
69
|
-
For factory workflow, install the four canonical skills
|
|
69
|
+
For factory workflow, install the four canonical skills as the `patronage-software-factory` host plugin (Claude Code, Codex, or Cursor; see [`skills/README.md`](../skills/README.md)). Cloud agents that cannot load host plugins fall back to:
|
|
70
70
|
|
|
71
71
|
```bash
|
|
72
72
|
npx skills add https://github.com/patronage/software-factory -g --all
|
package/dist/index.d.ts
CHANGED
|
@@ -229,6 +229,8 @@ interface BoundaryPullRequestRef {
|
|
|
229
229
|
headRefOid?: string;
|
|
230
230
|
headRefName?: string;
|
|
231
231
|
mergeCommitOid?: string;
|
|
232
|
+
/** GitHub's merge timestamp; the merge-ordering fact topology reads (#821). */
|
|
233
|
+
mergedAt?: string;
|
|
232
234
|
number: number;
|
|
233
235
|
state: "OPEN" | "CLOSED" | "MERGED";
|
|
234
236
|
}
|
|
@@ -1283,7 +1285,7 @@ interface PrReviewProof {
|
|
|
1283
1285
|
cleanedPaths: string[];
|
|
1284
1286
|
ladder?: PrReviewLadderState;
|
|
1285
1287
|
reviewRequirement?: {
|
|
1286
|
-
reason: "no-applicable-mode";
|
|
1288
|
+
reason: "no-applicable-mode" | "faithful-merge";
|
|
1287
1289
|
status: "not-required";
|
|
1288
1290
|
};
|
|
1289
1291
|
reviews: PrReviewResult[];
|
|
@@ -1421,9 +1423,9 @@ declare const managedReadinessLedgerSchema: z.ZodObject<{
|
|
|
1421
1423
|
reviewedHeadSha: z.ZodOptional<z.ZodString>;
|
|
1422
1424
|
reviewedPatchId: z.ZodOptional<z.ZodString>;
|
|
1423
1425
|
status: z.ZodEnum<{
|
|
1426
|
+
blocked: "blocked";
|
|
1424
1427
|
"not-required": "not-required";
|
|
1425
1428
|
stale: "stale";
|
|
1426
|
-
blocked: "blocked";
|
|
1427
1429
|
current: "current";
|
|
1428
1430
|
missing: "missing";
|
|
1429
1431
|
}>;
|
|
@@ -1433,9 +1435,9 @@ declare const managedReadinessLedgerSchema: z.ZodObject<{
|
|
|
1433
1435
|
reviewedHeadSha: z.ZodOptional<z.ZodString>;
|
|
1434
1436
|
reviewedPatchId: z.ZodOptional<z.ZodString>;
|
|
1435
1437
|
status: z.ZodEnum<{
|
|
1438
|
+
blocked: "blocked";
|
|
1436
1439
|
"not-required": "not-required";
|
|
1437
1440
|
stale: "stale";
|
|
1438
|
-
blocked: "blocked";
|
|
1439
1441
|
current: "current";
|
|
1440
1442
|
missing: "missing";
|
|
1441
1443
|
}>;
|
|
@@ -1545,6 +1547,23 @@ interface UpsertFactoryPrStatusCommentResult {
|
|
|
1545
1547
|
}
|
|
1546
1548
|
declare function upsertFactoryPrStatusComment(input: UpsertFactoryPrStatusCommentInput, dependencies?: UpsertFactoryPrStatusCommentDependencies): Promise<UpsertFactoryPrStatusCommentResult>;
|
|
1547
1549
|
//#endregion
|
|
1550
|
+
//#region src/catch-up-recognition-record.d.ts
|
|
1551
|
+
interface CatchUpRecognition {
|
|
1552
|
+
/** The ref the candidate targets, as the recognizing command resolved it. */
|
|
1553
|
+
baseRef: string;
|
|
1554
|
+
/** The live target tip at recognition time; the head's first parent. */
|
|
1555
|
+
baseTipSha: string;
|
|
1556
|
+
/** The head's second parent: the admitted base-side content merged in. */
|
|
1557
|
+
mergedParentSha: string;
|
|
1558
|
+
/**
|
|
1559
|
+
* The tree `git merge-tree --write-tree` produced for the two parents. It
|
|
1560
|
+
* equals the head's own tree; that equality is the faithfulness proof.
|
|
1561
|
+
*/
|
|
1562
|
+
mergedTreeSha: string;
|
|
1563
|
+
/** The ref that proves the second parent is admitted content. */
|
|
1564
|
+
upstreamRef: string;
|
|
1565
|
+
}
|
|
1566
|
+
//#endregion
|
|
1548
1567
|
//#region src/pr-verify-proof-contract.d.ts
|
|
1549
1568
|
declare const SUPPORTED_PR_VERIFY_SCHEMA_VERSIONS: readonly [1, 2, 3, 4];
|
|
1550
1569
|
type PrVerifySchemaVersion = (typeof SUPPORTED_PR_VERIFY_SCHEMA_VERSIONS)[number];
|
|
@@ -1611,6 +1630,7 @@ interface PrVerifyProof {
|
|
|
1611
1630
|
projectKey: string;
|
|
1612
1631
|
repository: string;
|
|
1613
1632
|
}[];
|
|
1633
|
+
catchUpRecognition?: CatchUpRecognition;
|
|
1614
1634
|
changedFiles: string[];
|
|
1615
1635
|
classification: DiffClassification;
|
|
1616
1636
|
classificationReasons: string[];
|
|
@@ -1635,7 +1655,7 @@ interface PrVerifyProof {
|
|
|
1635
1655
|
type PrVerifyBaselineFullProof = NonNullable<PrVerifyProof["baselineFullProofs"]>[number];
|
|
1636
1656
|
declare function validatePrVerifyProof(value: unknown): PrVerifyProof;
|
|
1637
1657
|
declare namespace verification_proof_d_exports {
|
|
1638
|
-
export { PrVerifyBaselineFullProof, PrVerifyCommandResult, PrVerifyExecutedCommand, PrVerifyNotDemandedRecord, PrVerifyNotRequiredCommand, PrVerifyProof, PrVerifySchemaVersion, ResolvedPrVerifyMode, SUPPORTED_PR_VERIFY_SCHEMA_VERSIONS, VerificationHeadShas, VerificationProofState, VerifiedHeadShas, prVerifyFullHeadShasForDiff, prVerifyProofHeadShas, readPrVerifyProof, resolveVerifyProofApplicability, trustedImpactStamp, validatePrVerifyProof, verificationProofApplicabilityFor, verificationProofBlockingReason, verificationProofForReadiness, verificationProofStateFor, verifyProofIdentityMatches, verifyProofPassed };
|
|
1658
|
+
export { PrVerifyBaselineFullProof, PrVerifyCommandResult, PrVerifyExecutedCommand, PrVerifyNotDemandedRecord, PrVerifyNotRequiredCommand, PrVerifyProof, PrVerifySchemaVersion, ResolvedPrVerifyMode, SUPPORTED_PR_VERIFY_SCHEMA_VERSIONS, VerificationHeadShas, VerificationProofState, VerifiedHeadShas, prVerifyFullHeadShasForDiff, prVerifyProofHeadShas, readPrVerifyProof, resolveVerifyProofApplicability, trustedCatchUpRecognition, trustedImpactStamp, validatePrVerifyProof, verificationProofApplicabilityFor, verificationProofBlockingReason, verificationProofForReadiness, verificationProofStateFor, verifyProofIdentityMatches, verifyProofPassed };
|
|
1639
1659
|
}
|
|
1640
1660
|
interface VerifiedHeadShas {
|
|
1641
1661
|
docsOnlyVerifiedHeadShas: string[];
|
|
@@ -1680,6 +1700,24 @@ declare const trustedImpactStamp: ({
|
|
|
1680
1700
|
};
|
|
1681
1701
|
proof: PrVerifyProof | undefined;
|
|
1682
1702
|
}) => ImpactStamp | undefined;
|
|
1703
|
+
/**
|
|
1704
|
+
* #833: the recorded catch-up recognition, consumed ONLY when it is
|
|
1705
|
+
* identity-bound to the current candidate under exactly the rules
|
|
1706
|
+
* `trustedImpactStamp` applies — the proof passed, is v4+, and its recorded
|
|
1707
|
+
* headSha AND patchId both name this candidate. The recognition withdraws
|
|
1708
|
+
* review and preview demands, so it earns the same strict double binding and
|
|
1709
|
+
* the same fail-closed answer (`undefined`) on every other state.
|
|
1710
|
+
*/
|
|
1711
|
+
declare const trustedCatchUpRecognition: ({
|
|
1712
|
+
candidate,
|
|
1713
|
+
proof
|
|
1714
|
+
}: {
|
|
1715
|
+
candidate: {
|
|
1716
|
+
headSha: string;
|
|
1717
|
+
patchId: string;
|
|
1718
|
+
};
|
|
1719
|
+
proof: PrVerifyProof | undefined;
|
|
1720
|
+
}) => CatchUpRecognition | undefined;
|
|
1683
1721
|
declare const prVerifyFullHeadShasForDiff: ({
|
|
1684
1722
|
docsOnlyDeltaAcceptedSince,
|
|
1685
1723
|
files,
|
|
@@ -2194,6 +2232,7 @@ declare const requiredCheckScopeSchema: z.ZodObject<{
|
|
|
2194
2232
|
}, z.core.$strict>;
|
|
2195
2233
|
type RequiredCheckScope = z.infer<typeof requiredCheckScopeSchema>;
|
|
2196
2234
|
interface RequiredCheckScopeContext {
|
|
2235
|
+
catchUpRecognition?: CatchUpRecognition;
|
|
2197
2236
|
classification: DiffClassification | undefined;
|
|
2198
2237
|
impactStamp?: ImpactStamp;
|
|
2199
2238
|
}
|
|
@@ -2376,9 +2415,9 @@ declare const loadEvidenceEnvelopes: (cwd: string) => LoadedEvidenceEnvelope[];
|
|
|
2376
2415
|
//#region src/review-proof-applicability.d.ts
|
|
2377
2416
|
declare const REVIEW_STATUS_VALUES: readonly ["not-required", "current", "stale", "missing", "blocked"];
|
|
2378
2417
|
declare const reviewStatusSchema: z.ZodEnum<{
|
|
2418
|
+
blocked: "blocked";
|
|
2379
2419
|
"not-required": "not-required";
|
|
2380
2420
|
stale: "stale";
|
|
2381
|
-
blocked: "blocked";
|
|
2382
2421
|
current: "current";
|
|
2383
2422
|
missing: "missing";
|
|
2384
2423
|
}>;
|
|
@@ -2527,8 +2566,8 @@ declare const boundaryCheckProofSchema: z.ZodObject<{
|
|
|
2527
2566
|
value: z.ZodString;
|
|
2528
2567
|
}, z.core.$strip>>;
|
|
2529
2568
|
status: z.ZodEnum<{
|
|
2530
|
-
ready: "ready";
|
|
2531
2569
|
blocked: "blocked";
|
|
2570
|
+
ready: "ready";
|
|
2532
2571
|
}>;
|
|
2533
2572
|
}, z.core.$strip>;
|
|
2534
2573
|
type BoundaryCheckProofRecord = z.infer<typeof boundaryCheckProofSchema>;
|
|
@@ -2543,6 +2582,14 @@ interface BoundaryCheckGithub {
|
|
|
2543
2582
|
interface BoundaryCheckArgs {
|
|
2544
2583
|
cwd?: string;
|
|
2545
2584
|
epic: number;
|
|
2585
|
+
/**
|
|
2586
|
+
* Closeout-final mode (#853): a manifest-declared issue whose closing-PR
|
|
2587
|
+
* set is empty is a blocking reason, not a notice. Off by default so the
|
|
2588
|
+
* interior boundary review (run before the terminal release PR exists,
|
|
2589
|
+
* e.g. epic #842's own review of #844) keeps working; the epic closeout
|
|
2590
|
+
* step turns this on as the last check before factory:closeout.
|
|
2591
|
+
*/
|
|
2592
|
+
final?: boolean;
|
|
2546
2593
|
json?: boolean;
|
|
2547
2594
|
output?: string;
|
|
2548
2595
|
repo?: string;
|
|
@@ -2626,6 +2673,19 @@ type HqFlushResult = (HqFlushOrphans & {
|
|
|
2626
2673
|
status: "flushed";
|
|
2627
2674
|
});
|
|
2628
2675
|
//#endregion
|
|
2676
|
+
//#region src/catch-up-recognition.d.ts
|
|
2677
|
+
type CatchUpRecognitionResult = {
|
|
2678
|
+
recognition: CatchUpRecognition;
|
|
2679
|
+
recognized: true;
|
|
2680
|
+
} | {
|
|
2681
|
+
reasons: string[];
|
|
2682
|
+
recognized: false;
|
|
2683
|
+
};
|
|
2684
|
+
type CatchUpRecognizer = (input: {
|
|
2685
|
+
base: string;
|
|
2686
|
+
cwd: string;
|
|
2687
|
+
}) => CatchUpRecognitionResult;
|
|
2688
|
+
//#endregion
|
|
2629
2689
|
//#region src/pr-review.d.ts
|
|
2630
2690
|
interface PrReviewArgs extends LoadProjectProfileInput {
|
|
2631
2691
|
base: string;
|
|
@@ -2641,6 +2701,7 @@ interface PrReviewGitDependencies {
|
|
|
2641
2701
|
changedFiles: (cwd: string, base: string) => string[];
|
|
2642
2702
|
currentHeadSha: (cwd: string) => string;
|
|
2643
2703
|
isAncestor: (cwd: string, ancestor: string, descendant: string) => boolean;
|
|
2704
|
+
recognizeCatchUp?: CatchUpRecognizer;
|
|
2644
2705
|
stablePatchId: (cwd: string, base: string) => string;
|
|
2645
2706
|
statusPorcelain: (cwd: string) => string;
|
|
2646
2707
|
}
|
|
@@ -2680,6 +2741,7 @@ interface PrVerifyGitDependencies {
|
|
|
2680
2741
|
currentHeadSha: (cwd: string) => string;
|
|
2681
2742
|
emptyTreeHash: (cwd: string) => string;
|
|
2682
2743
|
mergeBaseSha: (cwd: string, base: string) => string;
|
|
2744
|
+
recognizeCatchUp?: CatchUpRecognizer;
|
|
2683
2745
|
runVerificationCommand: (command: string, cwd: string, env: Record<string, string>) => VerificationCommandOutcome;
|
|
2684
2746
|
showFileAtRef?: (cwd: string, ref: string, absolutePath: string) => string | undefined;
|
|
2685
2747
|
statusPorcelain: (cwd: string) => string;
|
|
@@ -3034,6 +3096,7 @@ declare const FACTORY_TRACE_EVENT_DEFINITIONS: readonly [DefinedTraceEvent<"revi
|
|
|
3034
3096
|
patchId: z.ZodString;
|
|
3035
3097
|
reason: z.ZodEnum<{
|
|
3036
3098
|
"no-applicable-mode": "no-applicable-mode";
|
|
3099
|
+
"faithful-merge": "faithful-merge";
|
|
3037
3100
|
}>;
|
|
3038
3101
|
reviewCycle: z.ZodNumber;
|
|
3039
3102
|
}, z.core.$strict>, readonly [z.ZodObject<{
|
|
@@ -3056,6 +3119,7 @@ declare const FACTORY_TRACE_EVENT_DEFINITIONS: readonly [DefinedTraceEvent<"revi
|
|
|
3056
3119
|
patchId: z.ZodString;
|
|
3057
3120
|
reason: z.ZodEnum<{
|
|
3058
3121
|
"no-applicable-mode": "no-applicable-mode";
|
|
3122
|
+
"faithful-merge": "faithful-merge";
|
|
3059
3123
|
}>;
|
|
3060
3124
|
reviewCycle: z.ZodNumber;
|
|
3061
3125
|
}, z.core.$strict>], {
|
|
@@ -3069,6 +3133,7 @@ declare const FACTORY_TRACE_EVENT_DEFINITIONS: readonly [DefinedTraceEvent<"revi
|
|
|
3069
3133
|
patchId: z.ZodString;
|
|
3070
3134
|
reason: z.ZodEnum<{
|
|
3071
3135
|
"no-applicable-mode": "no-applicable-mode";
|
|
3136
|
+
"faithful-merge": "faithful-merge";
|
|
3072
3137
|
}>;
|
|
3073
3138
|
reviewCycle: z.ZodNumber;
|
|
3074
3139
|
}, z.core.$strict>;
|
|
@@ -3086,7 +3151,7 @@ declare const FACTORY_TRACE_EVENT_DEFINITIONS: readonly [DefinedTraceEvent<"revi
|
|
|
3086
3151
|
observedAt: string;
|
|
3087
3152
|
outcome: "not-required";
|
|
3088
3153
|
patchId: string;
|
|
3089
|
-
reason: "no-applicable-mode";
|
|
3154
|
+
reason: "no-applicable-mode" | "faithful-merge";
|
|
3090
3155
|
reviewCycle: number;
|
|
3091
3156
|
issue?: number | undefined;
|
|
3092
3157
|
pr?: number | undefined;
|
|
@@ -4090,13 +4155,13 @@ declare const evaluateReadiness: (input: EvaluationInput) => {
|
|
|
4090
4155
|
reviews: {
|
|
4091
4156
|
correctness: {
|
|
4092
4157
|
required: boolean;
|
|
4093
|
-
status: "
|
|
4158
|
+
status: "blocked" | "not-required" | "stale" | "current" | "missing";
|
|
4094
4159
|
reviewedHeadSha?: string | undefined;
|
|
4095
4160
|
reviewedPatchId?: string | undefined;
|
|
4096
4161
|
};
|
|
4097
4162
|
security?: {
|
|
4098
4163
|
required: boolean;
|
|
4099
|
-
status: "
|
|
4164
|
+
status: "blocked" | "not-required" | "stale" | "current" | "missing";
|
|
4100
4165
|
reviewedHeadSha?: string | undefined;
|
|
4101
4166
|
reviewedPatchId?: string | undefined;
|
|
4102
4167
|
} | undefined;
|
package/dist/index.js
CHANGED
|
@@ -17,7 +17,7 @@ import { parse } from "yaml";
|
|
|
17
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.
|
|
20
|
+
var version = "1.0.0-alpha.22";
|
|
21
21
|
//#endregion
|
|
22
22
|
//#region src/review-rungs.ts
|
|
23
23
|
const EVIDENCE_REVIEW_RUNGS$1 = [
|
|
@@ -159,23 +159,48 @@ const DEFAULT_TERMINAL_BASE = "main";
|
|
|
159
159
|
* Derive the membership graph from the manifest's declared issues and their
|
|
160
160
|
* live closing links. A PR observed under several issues keeps one
|
|
161
161
|
* membership per issue; nothing is collapsed here and nothing is refused.
|
|
162
|
+
*
|
|
163
|
+
* A PR that closed without merging contributes NO closing link (#821): it
|
|
164
|
+
* carried nothing into the boundary, so it is not a member and cannot fail
|
|
165
|
+
* one. The dropped link is recorded on the issue. Closeout blocks by default
|
|
166
|
+
* only on that flavor: an issue whose carriers all closed without merging. A
|
|
167
|
+
* declared issue that never had any closing link is reported as a notice,
|
|
168
|
+
* not a blocking reason — except under boundary:check --final, which blocks
|
|
169
|
+
* on that flavor too (#853).
|
|
162
170
|
*/
|
|
163
171
|
const buildBoundaryMembershipGraph = (manifest, fetchClosingPullRequests) => {
|
|
164
172
|
const memberships = [];
|
|
165
173
|
const unlinkedIssues = [];
|
|
174
|
+
const closedWithoutMergeLinks = [];
|
|
166
175
|
for (const wave of manifest.waves) for (const issue of wave.issues) {
|
|
167
176
|
const refs = fetchClosingPullRequests(issue);
|
|
168
|
-
|
|
177
|
+
const live = [];
|
|
178
|
+
const closedWithoutMerge = [];
|
|
179
|
+
for (const ref of refs) {
|
|
180
|
+
if (ref.state === "CLOSED") {
|
|
181
|
+
closedWithoutMerge.push(ref.number);
|
|
182
|
+
closedWithoutMergeLinks.push({
|
|
183
|
+
issue,
|
|
184
|
+
pr: ref.number,
|
|
185
|
+
wave: wave.name
|
|
186
|
+
});
|
|
187
|
+
continue;
|
|
188
|
+
}
|
|
189
|
+
live.push(ref);
|
|
190
|
+
}
|
|
191
|
+
if (live.length === 0) unlinkedIssues.push({
|
|
192
|
+
...closedWithoutMerge.length > 0 ? { closedWithoutMerge } : {},
|
|
169
193
|
issue,
|
|
170
194
|
wave: wave.name
|
|
171
195
|
});
|
|
172
|
-
for (const ref of
|
|
196
|
+
for (const ref of live) memberships.push({
|
|
173
197
|
issue,
|
|
174
198
|
ref,
|
|
175
199
|
wave
|
|
176
200
|
});
|
|
177
201
|
}
|
|
178
202
|
return {
|
|
203
|
+
closedWithoutMergeLinks,
|
|
179
204
|
memberships,
|
|
180
205
|
unlinkedIssues
|
|
181
206
|
};
|
|
@@ -208,7 +233,8 @@ const membershipWaveDemand = (graph, pr) => {
|
|
|
208
233
|
* validation; a repeated PR number is not evidence the memberships agree.
|
|
209
234
|
*/
|
|
210
235
|
const coveredSetProjection = (graph) => {
|
|
211
|
-
const notices = graph.unlinkedIssues.map(({ issue, wave }) => `issue #${issue} (wave ${wave}) has no linked closing PR yet`);
|
|
236
|
+
const notices = graph.unlinkedIssues.map(({ closedWithoutMerge, issue, wave }) => closedWithoutMerge === void 0 ? `issue #${issue} (wave ${wave}) has no linked closing PR yet` : `issue #${issue} (wave ${wave}) has no live closing PR; its only closing link(s) ${closedWithoutMerge.map((pr) => `PR #${pr}`).join(", ")} closed without merging`);
|
|
237
|
+
for (const pr of new Set(graph.closedWithoutMergeLinks.map((l) => l.pr))) notices.push(`PR #${pr} is closed without merge; it contributes nothing to the covered set`);
|
|
212
238
|
const entries = [];
|
|
213
239
|
const pullRequests = [];
|
|
214
240
|
for (const { issue, ref, wave } of graph.memberships) {
|
|
@@ -235,7 +261,6 @@ const coveredSetProjection = (graph) => {
|
|
|
235
261
|
else notices.push(`PR #${ref.number} is open but GitHub reports no head SHA; excluded from the covered set`);
|
|
236
262
|
continue;
|
|
237
263
|
}
|
|
238
|
-
notices.push(`PR #${ref.number} is closed without merge; it contributes nothing to the covered set`);
|
|
239
264
|
}
|
|
240
265
|
return {
|
|
241
266
|
entries: entries.toSorted((a, b) => a.pr - b.pr || a.issue - b.issue),
|
|
@@ -392,6 +417,14 @@ const eachToEpicBranch = (manifest) => manifest.topology === "each-to-epic" ? ma
|
|
|
392
417
|
* Validate only the current member during admission. This deliberately does
|
|
393
418
|
* not inspect sibling membership or require the terminal PR: an epic branch
|
|
394
419
|
* is assembled one admitted interior candidate at a time.
|
|
420
|
+
*
|
|
421
|
+
* A declared member that targets the terminal base is admitted from any head
|
|
422
|
+
* (#821). At admission the candidate has not merged, so merge ordering — the
|
|
423
|
+
* fact that separates the terminal from a post-terminal release member —
|
|
424
|
+
* does not exist yet. Closeout owns that judgment, where the whole graph is
|
|
425
|
+
* visible: it still requires exactly one terminal from the integration
|
|
426
|
+
* branch, and it still refuses a main-targeting member that did not merge
|
|
427
|
+
* after that terminal.
|
|
395
428
|
*/
|
|
396
429
|
const pullRequestTopologyReasons = ({ manifest, pullRequest, terminalBase = DEFAULT_TERMINAL_BASE }) => {
|
|
397
430
|
const integrationBranch = eachToEpicBranch(manifest);
|
|
@@ -399,29 +432,67 @@ const pullRequestTopologyReasons = ({ manifest, pullRequest, terminalBase = DEFA
|
|
|
399
432
|
const { baseRefName, headRefName, number } = pullRequest;
|
|
400
433
|
if (!headRefName) return [`PR #${number} has no live head branch name; each-to-epic admission fails closed until GitHub reports the member's headRefName.`];
|
|
401
434
|
if (baseRefName === integrationBranch && headRefName !== integrationBranch) return [];
|
|
402
|
-
if (baseRefName === terminalBase
|
|
403
|
-
return [`PR #${number} contradicts each-to-epic topology: live head/base ${headRefName ?? "(unknown)"} -> ${baseRefName ?? "(unknown)"}; expected an interior member targeting ${integrationBranch}, or
|
|
435
|
+
if (baseRefName === terminalBase) return [];
|
|
436
|
+
return [`PR #${number} contradicts each-to-epic topology: live head/base ${headRefName ?? "(unknown)"} -> ${baseRefName ?? "(unknown)"}; expected an interior member targeting ${integrationBranch}, or a member targeting ${terminalBase} (the terminal, or a post-terminal release member).`];
|
|
437
|
+
};
|
|
438
|
+
const mergedAtMs = (pullRequest) => {
|
|
439
|
+
if (pullRequest.mergedAt === void 0) return;
|
|
440
|
+
const parsed = Date.parse(pullRequest.mergedAt);
|
|
441
|
+
return Number.isNaN(parsed) ? void 0 : parsed;
|
|
404
442
|
};
|
|
405
443
|
/**
|
|
406
|
-
*
|
|
407
|
-
*
|
|
408
|
-
*
|
|
409
|
-
*
|
|
444
|
+
* Judge a declared member that targets the terminal base from a branch other
|
|
445
|
+
* than the integration branch. It is admitted as a post-terminal release
|
|
446
|
+
* member (#821) exactly when it merged strictly after the one terminal PR:
|
|
447
|
+
* an already-merged integration branch cannot receive later work, so a
|
|
448
|
+
* ratified release slice has nowhere else to target. Every fact this needs is
|
|
449
|
+
* membership plus merge ordering; nothing is declared in the manifest.
|
|
410
450
|
*/
|
|
411
|
-
const
|
|
412
|
-
const
|
|
413
|
-
if (!
|
|
414
|
-
|
|
451
|
+
const postTerminalReleaseReasons = ({ candidate, integrationBranch, terminal, terminalBase }) => {
|
|
452
|
+
const label = `PR #${candidate.number} targets ${terminalBase} from ${candidate.headRefName ?? "(unknown)"}`;
|
|
453
|
+
if (!terminal) return [`${label}; each-to-epic accepts that only as a post-terminal release member, and no terminal ${integrationBranch} -> ${terminalBase} PR was found.`];
|
|
454
|
+
if (candidate.state !== "MERGED") return [`${label} and is not merged (state ${candidate.state ?? "unknown"}); a post-terminal release member must be merged after terminal PR #${terminal.number}.`];
|
|
455
|
+
const candidateMergedAt = mergedAtMs(candidate);
|
|
456
|
+
const terminalMergedAt = mergedAtMs(terminal);
|
|
457
|
+
if (candidateMergedAt === void 0 || terminalMergedAt === void 0) return [`${label}; merge ordering against terminal PR #${terminal.number} cannot be established because GitHub reports no usable merge timestamp for ${candidateMergedAt === void 0 ? `PR #${candidate.number}` : `PR #${terminal.number}`}.`];
|
|
458
|
+
if (candidateMergedAt <= terminalMergedAt) return [`${label} but merged at ${candidate.mergedAt}, not after terminal PR #${terminal.number} (merged ${terminal.mergedAt}); each-to-epic admits a ${terminalBase}-targeting member from another head only as a post-terminal release member.`];
|
|
459
|
+
return [];
|
|
460
|
+
};
|
|
461
|
+
const contradicts = (previous, observation) => previous.baseRefName !== observation.baseRefName || previous.headRefName !== observation.headRefName || previous.state !== observation.state || previous.mergedAt !== observation.mergedAt;
|
|
462
|
+
/**
|
|
463
|
+
* Collapse the repeated observations of one PR. A PR declared under several
|
|
464
|
+
* issues is observed once per issue; identical observations collapse, and any
|
|
465
|
+
* disagreement about that PR's live facts fails closed instead of letting
|
|
466
|
+
* manifest order pick a winner.
|
|
467
|
+
*/
|
|
468
|
+
const collapseObservations = (pullRequests) => {
|
|
415
469
|
const byNumber = /* @__PURE__ */ new Map();
|
|
470
|
+
const reasons = [];
|
|
416
471
|
for (const pullRequest of pullRequests) {
|
|
417
472
|
const previous = byNumber.get(pullRequest.number);
|
|
418
|
-
if (previous && (previous
|
|
419
|
-
reasons.push(`PR #${pullRequest.number} has contradictory live
|
|
473
|
+
if (previous && contradicts(previous, pullRequest)) {
|
|
474
|
+
reasons.push(`PR #${pullRequest.number} has contradictory live data across its declared memberships.`);
|
|
420
475
|
continue;
|
|
421
476
|
}
|
|
422
477
|
byNumber.set(pullRequest.number, pullRequest);
|
|
423
478
|
}
|
|
479
|
+
return {
|
|
480
|
+
byNumber,
|
|
481
|
+
reasons
|
|
482
|
+
};
|
|
483
|
+
};
|
|
484
|
+
/**
|
|
485
|
+
* Validate the complete declared member set at boundary closeout. Callers
|
|
486
|
+
* supply only issue-linked PRs and unlinked manifest-fallback PRs; branch
|
|
487
|
+
* names classify those authoritative members' roles but never discover
|
|
488
|
+
* membership.
|
|
489
|
+
*/
|
|
490
|
+
const boundaryTopologyReasons = ({ manifest, pullRequests, terminalBase = DEFAULT_TERMINAL_BASE }) => {
|
|
491
|
+
const integrationBranch = eachToEpicBranch(manifest);
|
|
492
|
+
if (!integrationBranch) return [];
|
|
493
|
+
const { byNumber, reasons } = collapseObservations(pullRequests);
|
|
424
494
|
const terminalPullRequests = [];
|
|
495
|
+
const releaseCandidates = [];
|
|
425
496
|
for (const pullRequest of byNumber.values()) {
|
|
426
497
|
if (!pullRequest.headRefName) {
|
|
427
498
|
reasons.push(`PR #${pullRequest.number} has no live head branch name; each-to-epic closeout fails closed until GitHub reports headRefName.`);
|
|
@@ -437,16 +508,26 @@ const boundaryTopologyReasons = ({ manifest, pullRequests, terminalBase = DEFAUL
|
|
|
437
508
|
continue;
|
|
438
509
|
}
|
|
439
510
|
if (pullRequest.baseRefName === terminalBase) {
|
|
440
|
-
if (pullRequest.headRefName
|
|
441
|
-
|
|
442
|
-
continue;
|
|
443
|
-
}
|
|
444
|
-
terminalPullRequests.push(pullRequest.number);
|
|
511
|
+
if (pullRequest.headRefName === integrationBranch) terminalPullRequests.push(pullRequest);
|
|
512
|
+
else releaseCandidates.push(pullRequest);
|
|
445
513
|
continue;
|
|
446
514
|
}
|
|
447
515
|
reasons.push(`PR #${pullRequest.number} targets ${pullRequest.baseRefName ?? "(unknown)"}; every each-to-epic interior member must target declared integrationBranch ${integrationBranch}.`);
|
|
448
516
|
}
|
|
449
517
|
if (terminalPullRequests.length !== 1) reasons.push(`each-to-epic requires exactly one terminal member PR from ${integrationBranch} to ${terminalBase}; found ${terminalPullRequests.length}.`);
|
|
518
|
+
const [terminal] = terminalPullRequests;
|
|
519
|
+
const admittedReleaseMembers = [];
|
|
520
|
+
for (const candidate of releaseCandidates) {
|
|
521
|
+
const candidateReasons = postTerminalReleaseReasons({
|
|
522
|
+
candidate,
|
|
523
|
+
integrationBranch,
|
|
524
|
+
terminal: terminalPullRequests.length === 1 ? terminal : void 0,
|
|
525
|
+
terminalBase
|
|
526
|
+
});
|
|
527
|
+
if (candidateReasons.length === 0) admittedReleaseMembers.push(candidate);
|
|
528
|
+
reasons.push(...candidateReasons);
|
|
529
|
+
}
|
|
530
|
+
if (admittedReleaseMembers.length > 1) reasons.push(`each-to-epic admits at most one post-terminal release member targeting ${terminalBase}; found ${admittedReleaseMembers.length}: ${admittedReleaseMembers.map((member) => `PR #${member.number}`).join(", ")}.`);
|
|
450
531
|
return reasons;
|
|
451
532
|
};
|
|
452
533
|
//#endregion
|
|
@@ -1017,6 +1098,72 @@ function isAncestor(cwd, ancestor, descendant) {
|
|
|
1017
1098
|
if (result.status === 1) return false;
|
|
1018
1099
|
return false;
|
|
1019
1100
|
}
|
|
1101
|
+
/** The head commit's parent shas, in order. Empty on a root commit. */
|
|
1102
|
+
function commitParents(cwd, ref) {
|
|
1103
|
+
return runCapture("git", [
|
|
1104
|
+
"rev-list",
|
|
1105
|
+
"--parents",
|
|
1106
|
+
"-n",
|
|
1107
|
+
"1",
|
|
1108
|
+
ref
|
|
1109
|
+
], cwd).stdout.trim().split(/\s+/u).slice(1);
|
|
1110
|
+
}
|
|
1111
|
+
/** The tree object a commit-ish points at. */
|
|
1112
|
+
function treeShaOf(cwd, ref) {
|
|
1113
|
+
return runCapture("git", ["rev-parse", `${ref}^{tree}`], cwd).stdout.trim();
|
|
1114
|
+
}
|
|
1115
|
+
/**
|
|
1116
|
+
* Run the real merge machinery without touching the worktree
|
|
1117
|
+
* (`git merge-tree --write-tree`). Exit 0 is a clean merge, exit 1 is a
|
|
1118
|
+
* conflicted one; both write a tree. Any other outcome (old git, bad refs)
|
|
1119
|
+
* returns `undefined` so the caller fails closed.
|
|
1120
|
+
*/
|
|
1121
|
+
function mergeTreeWriteTree(cwd, firstParent, secondParent) {
|
|
1122
|
+
const result = spawnSync("git", [
|
|
1123
|
+
"merge-tree",
|
|
1124
|
+
"--write-tree",
|
|
1125
|
+
"--no-messages",
|
|
1126
|
+
firstParent,
|
|
1127
|
+
secondParent
|
|
1128
|
+
], {
|
|
1129
|
+
cwd,
|
|
1130
|
+
encoding: "utf-8",
|
|
1131
|
+
env: gitEnv(),
|
|
1132
|
+
stdio: [
|
|
1133
|
+
"ignore",
|
|
1134
|
+
"pipe",
|
|
1135
|
+
"pipe"
|
|
1136
|
+
]
|
|
1137
|
+
});
|
|
1138
|
+
if (result.error || result.status !== 0 && result.status !== 1) return;
|
|
1139
|
+
const [treeSha] = result.stdout.trim().split("\n");
|
|
1140
|
+
if (!treeSha || !/^[0-9a-f]{40,64}$/u.test(treeSha)) return;
|
|
1141
|
+
return {
|
|
1142
|
+
conflicted: result.status === 1,
|
|
1143
|
+
treeSha
|
|
1144
|
+
};
|
|
1145
|
+
}
|
|
1146
|
+
/**
|
|
1147
|
+
* The remote-tracking ref of the repository's default branch (e.g.
|
|
1148
|
+
* `refs/remotes/origin/main`), read from the local `origin/HEAD` symref.
|
|
1149
|
+
* `undefined` when the symref is unset; `git remote set-head origin --auto`
|
|
1150
|
+
* records it.
|
|
1151
|
+
*/
|
|
1152
|
+
function remoteDefaultBranchRef(cwd) {
|
|
1153
|
+
const result = spawnSync("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], {
|
|
1154
|
+
cwd,
|
|
1155
|
+
encoding: "utf-8",
|
|
1156
|
+
env: gitEnv(),
|
|
1157
|
+
stdio: [
|
|
1158
|
+
"ignore",
|
|
1159
|
+
"pipe",
|
|
1160
|
+
"pipe"
|
|
1161
|
+
]
|
|
1162
|
+
});
|
|
1163
|
+
if (result.error || result.status !== 0) return;
|
|
1164
|
+
const ref = result.stdout.trim();
|
|
1165
|
+
return ref.length > 0 ? ref : void 0;
|
|
1166
|
+
}
|
|
1020
1167
|
//#endregion
|
|
1021
1168
|
//#region src/github-issue-comments.ts
|
|
1022
1169
|
const isCommentPage = (value) => Array.isArray(value[0]);
|
|
@@ -3778,6 +3925,16 @@ const authorizeDemandWaiver = ({ authenticatedLogin, session }) => {
|
|
|
3778
3925
|
} };
|
|
3779
3926
|
};
|
|
3780
3927
|
//#endregion
|
|
3928
|
+
//#region src/catch-up-recognition-record.ts
|
|
3929
|
+
const objectShaSchema = z.string().regex(/^[0-9a-f]{40}$/u);
|
|
3930
|
+
const catchUpRecognitionSchema = z.object({
|
|
3931
|
+
baseRef: z.string().min(1),
|
|
3932
|
+
baseTipSha: objectShaSchema,
|
|
3933
|
+
mergedParentSha: objectShaSchema,
|
|
3934
|
+
mergedTreeSha: objectShaSchema,
|
|
3935
|
+
upstreamRef: z.string().min(1)
|
|
3936
|
+
}).strict();
|
|
3937
|
+
//#endregion
|
|
3781
3938
|
//#region src/impact-stamp.ts
|
|
3782
3939
|
const LOCKFILE_PATH = "pnpm-lock.yaml";
|
|
3783
3940
|
const SUPPORTED_LOCKFILE_VERSION = "9.0";
|
|
@@ -4491,6 +4648,11 @@ const assertPrVerifyProofRules = (proof, context) => {
|
|
|
4491
4648
|
message: "notRequiredCommands is a schemaVersion>=4 field; a v1–v3 pr:verify proof must not carry it.",
|
|
4492
4649
|
path: ["notRequiredCommands"]
|
|
4493
4650
|
});
|
|
4651
|
+
if (proof.schemaVersion < 4 && proof.catchUpRecognition !== void 0) context.addIssue({
|
|
4652
|
+
code: "custom",
|
|
4653
|
+
message: "catchUpRecognition is a schemaVersion>=4 field; a v1–v3 pr:verify proof must not carry it.",
|
|
4654
|
+
path: ["catchUpRecognition"]
|
|
4655
|
+
});
|
|
4494
4656
|
if (proof.schemaVersion < 4 && proof.notDemandedRecords !== void 0) context.addIssue({
|
|
4495
4657
|
code: "custom",
|
|
4496
4658
|
message: "notDemandedRecords is a schemaVersion>=4 field; a v1–v3 pr:verify proof must not carry it.",
|
|
@@ -4549,6 +4711,7 @@ const prVerifyProofSchema = z.object({
|
|
|
4549
4711
|
projectKey: z.string().min(1),
|
|
4550
4712
|
repository: z.string().min(1)
|
|
4551
4713
|
})).optional(),
|
|
4714
|
+
catchUpRecognition: catchUpRecognitionSchema.optional(),
|
|
4552
4715
|
changedFiles: z.array(z.string()),
|
|
4553
4716
|
classification: z.enum([
|
|
4554
4717
|
"docs/process-only",
|
|
@@ -4863,7 +5026,7 @@ z.object({
|
|
|
4863
5026
|
patchId: z.string().regex(/^[0-9a-f]{40,64}$/u),
|
|
4864
5027
|
reviewCycle: z.number().int().positive().optional(),
|
|
4865
5028
|
reviewRequirement: z.object({
|
|
4866
|
-
reason: z.enum(["no-applicable-mode"]),
|
|
5029
|
+
reason: z.enum(["no-applicable-mode", "faithful-merge"]),
|
|
4867
5030
|
status: z.literal("not-required")
|
|
4868
5031
|
}).strict().optional(),
|
|
4869
5032
|
reviews: z.array(prReviewResultSchema$1),
|
|
@@ -5183,7 +5346,7 @@ const CLOSING_PRS_QUERY = `query($owner: String!, $repo: String!, $issue: Int!)
|
|
|
5183
5346
|
repository(owner: $owner, name: $repo) {
|
|
5184
5347
|
issue(number: $issue) {
|
|
5185
5348
|
closedByPullRequestsReferences(first: 50, includeClosedPrs: true) {
|
|
5186
|
-
nodes { number state baseRefName headRefName headRefOid mergeCommit { oid } }
|
|
5349
|
+
nodes { number state baseRefName headRefName headRefOid mergedAt mergeCommit { oid } }
|
|
5187
5350
|
}
|
|
5188
5351
|
}
|
|
5189
5352
|
}
|
|
@@ -5215,6 +5378,7 @@ const defaultBoundaryCheckGithub = () => ({
|
|
|
5215
5378
|
...node.headRefOid ? { headRefOid: node.headRefOid } : {},
|
|
5216
5379
|
...node.headRefName ? { headRefName: node.headRefName } : {},
|
|
5217
5380
|
...node.mergeCommit?.oid ? { mergeCommitOid: node.mergeCommit.oid } : {},
|
|
5381
|
+
...node.mergedAt ? { mergedAt: node.mergedAt } : {},
|
|
5218
5382
|
number: node.number,
|
|
5219
5383
|
state: node.state
|
|
5220
5384
|
}));
|
|
@@ -5263,7 +5427,8 @@ function runBoundaryCheck(args, dependencies = {}) {
|
|
|
5263
5427
|
let record;
|
|
5264
5428
|
if (parsed.ok) {
|
|
5265
5429
|
const demandedRung = closeoutRungFor(parsed.manifest);
|
|
5266
|
-
const
|
|
5430
|
+
const graph = buildBoundaryMembershipGraph(parsed.manifest, (issue) => github.fetchClosingPullRequests(repo, issue));
|
|
5431
|
+
const computed = coveredSetProjection(graph);
|
|
5267
5432
|
const comments = github.fetchIssueComments(repo, args.epic);
|
|
5268
5433
|
const selected = selectBoundaryReviewProof(comments, parsed.manifest.boundary, parsed.manifest.declaredBy);
|
|
5269
5434
|
const { blockingReasons } = evaluateProofAgainstDemands({
|
|
@@ -5274,10 +5439,13 @@ function runBoundaryCheck(args, dependencies = {}) {
|
|
|
5274
5439
|
manifestHash: parsed.manifestHash,
|
|
5275
5440
|
selected
|
|
5276
5441
|
});
|
|
5442
|
+
const uncoveredIssueReasons = graph.unlinkedIssues.flatMap((entry) => entry.closedWithoutMerge === void 0 ? [] : [`issue #${entry.issue} (wave ${entry.wave}) is uncovered: its only closing link(s) ${entry.closedWithoutMerge.map((pr) => `PR #${pr}`).join(", ")} closed without merging`]);
|
|
5443
|
+
const finalUncoveredIssueReasons = args.final ? graph.unlinkedIssues.flatMap((entry) => entry.closedWithoutMerge === void 0 ? [`issue #${entry.issue} (wave ${entry.wave}) has no linked closing PR; --final requires every declared issue to be covered before closeout`] : []) : [];
|
|
5277
5444
|
blockingReasons.unshift(...boundaryTopologyReasons({
|
|
5278
5445
|
manifest: parsed.manifest,
|
|
5279
5446
|
pullRequests: computed.pullRequests
|
|
5280
|
-
}));
|
|
5447
|
+
}), ...uncoveredIssueReasons, ...finalUncoveredIssueReasons);
|
|
5448
|
+
const specHashNotices = selected?.proof.specHash !== void 0 && selected.proof.specHash.value !== specHash ? [`boundary-review proof spec sha256 ${selected.proof.specHash.value} does not match current epic spec sha256 ${specHash}; use the specHash from the blocked boundary:check record instead of hashing shell-rendered output`] : [];
|
|
5281
5449
|
record = {
|
|
5282
5450
|
blockingReasons,
|
|
5283
5451
|
boundary: parsed.manifest.boundary,
|
|
@@ -5289,7 +5457,7 @@ function runBoundaryCheck(args, dependencies = {}) {
|
|
|
5289
5457
|
algo: "sha256",
|
|
5290
5458
|
value: parsed.manifestHash
|
|
5291
5459
|
},
|
|
5292
|
-
notices: computed.notices,
|
|
5460
|
+
notices: [...computed.notices, ...specHashNotices],
|
|
5293
5461
|
repo,
|
|
5294
5462
|
...selected ? { reviewProof: {
|
|
5295
5463
|
...selected.commentId === void 0 ? {} : { commentId: selected.commentId },
|
|
@@ -6038,7 +6206,7 @@ const renderReport = (record) => {
|
|
|
6038
6206
|
return `${lines.join("\n")}\n`;
|
|
6039
6207
|
};
|
|
6040
6208
|
function createBoundaryCheckCommand(output, action) {
|
|
6041
|
-
return markCwdOptionDefault(new Command("boundary:check").description("Boundary readiness gate (T8): require a membership-fresh boundary-review proof at the declared closeout rung. Blocks closeout/enablement, never merge.").requiredOption("--epic <number>", "issue number carrying the factory-boundary manifest, which may be the work issue itself (#789)", positiveInteger$1("--epic")).option("--repo <owner/name>", "GitHub repository (default: resolved from --cwd)").option("--cwd <path>", "repository working directory", collectCwdOption).option("--output <path>", "write an additional proof copy to this path").option("--profile <path>", "path to the project profile JSON file").option("--json", "print the full proof record as JSON").action(withGateTiming({
|
|
6209
|
+
return markCwdOptionDefault(new Command("boundary:check").description("Boundary readiness gate (T8): require a membership-fresh boundary-review proof at the declared closeout rung. Blocks closeout/enablement, never merge.").requiredOption("--epic <number>", "issue number carrying the factory-boundary manifest, which may be the work issue itself (#789)", positiveInteger$1("--epic")).option("--repo <owner/name>", "GitHub repository (default: resolved from --cwd)").option("--cwd <path>", "repository working directory", collectCwdOption).option("--output <path>", "write an additional proof copy to this path").option("--profile <path>", "path to the project profile JSON file").option("--json", "print the full proof record as JSON").option("--final", "closeout-final mode (#853): a declared issue with no closing PR blocks instead of only noticing. Run as the last check before factory:closeout.").action(withGateTiming({
|
|
6042
6210
|
gate: "boundary:check",
|
|
6043
6211
|
resolveLedgerRoot: (options) => resolveCwdOption(options.cwd),
|
|
6044
6212
|
stderr: output.stderr
|
|
@@ -6048,7 +6216,8 @@ function createBoundaryCheckCommand(output, action) {
|
|
|
6048
6216
|
epic: options.epic,
|
|
6049
6217
|
...options.repo ? { repo: options.repo } : {},
|
|
6050
6218
|
...options.output ? { output: options.output } : {},
|
|
6051
|
-
...options.json ? { json: true } : {}
|
|
6219
|
+
...options.json ? { json: true } : {},
|
|
6220
|
+
...options.final ? { final: true } : {}
|
|
6052
6221
|
};
|
|
6053
6222
|
const record = action ? action(args) : runBoundaryCheck(args);
|
|
6054
6223
|
output.stdout.write(options.json ? `${JSON.stringify(record, null, 2)}\n` : renderReport(record));
|
|
@@ -7279,7 +7448,7 @@ const ReviewGateTracePayloadShape = z.object({
|
|
|
7279
7448
|
observedAt: isoTimestamp,
|
|
7280
7449
|
outcome: z.literal("not-required"),
|
|
7281
7450
|
patchId: z.string().regex(/^[0-9a-f]{40,64}$/u),
|
|
7282
|
-
reason: z.enum(["no-applicable-mode"]),
|
|
7451
|
+
reason: z.enum(["no-applicable-mode", "faithful-merge"]),
|
|
7283
7452
|
reviewCycle: positiveInteger
|
|
7284
7453
|
}).strict();
|
|
7285
7454
|
const ReviewGateTracePayloadSchema = ReviewGateTracePayloadShape;
|
|
@@ -8638,6 +8807,46 @@ function createCandidateIdentityResolver({ cwd, git }) {
|
|
|
8638
8807
|
};
|
|
8639
8808
|
}
|
|
8640
8809
|
//#endregion
|
|
8810
|
+
//#region src/catch-up-recognition.ts
|
|
8811
|
+
const refused = (reason) => ({
|
|
8812
|
+
reasons: [reason],
|
|
8813
|
+
recognized: false
|
|
8814
|
+
});
|
|
8815
|
+
const recognizeCatchUpCandidate = ({ base, cwd }) => {
|
|
8816
|
+
try {
|
|
8817
|
+
if (statusPorcelain(cwd) !== "") return refused("catch-up recognition refused: the worktree is dirty, so the committed head is not the candidate content.");
|
|
8818
|
+
const parents = commitParents(cwd, "HEAD");
|
|
8819
|
+
if (parents.length !== 2) return refused(`catch-up recognition refused: HEAD has ${parents.length} parent(s); a catch-up head is a merge commit with exactly two.`);
|
|
8820
|
+
const [firstParent, secondParent] = parents;
|
|
8821
|
+
const baseTipSha = runCapture("git", [
|
|
8822
|
+
"rev-parse",
|
|
8823
|
+
"--verify",
|
|
8824
|
+
`${base}^{commit}`
|
|
8825
|
+
], cwd).stdout.trim();
|
|
8826
|
+
if (firstParent !== baseTipSha) return refused(`catch-up recognition refused: the head's first parent ${firstParent} is not the live target tip ${baseTipSha} (${base}); re-merge onto the current tip and re-run.`);
|
|
8827
|
+
const upstreamRef = remoteDefaultBranchRef(cwd);
|
|
8828
|
+
if (upstreamRef === void 0) return refused("catch-up recognition refused: origin/HEAD is unset, so the second parent's provenance cannot be proved; run `git remote set-head origin --auto` and re-run.");
|
|
8829
|
+
if (!isAncestor(cwd, secondParent, upstreamRef)) return refused(`catch-up recognition refused: the head's second parent ${secondParent} is not an ancestor of ${upstreamRef}, so its content is not provably admitted.`);
|
|
8830
|
+
const mergeTree = mergeTreeWriteTree(cwd, firstParent, secondParent);
|
|
8831
|
+
if (mergeTree === void 0) return refused("catch-up recognition refused: `git merge-tree --write-tree` did not produce a tree (git older than 2.38, or unreadable parents).");
|
|
8832
|
+
if (mergeTree.conflicted) return refused("catch-up recognition refused: merging the two parents conflicts, so the head carries manual resolutions; a conflicted catch-up is a full candidate.");
|
|
8833
|
+
const headTreeSha = treeShaOf(cwd, "HEAD");
|
|
8834
|
+
if (headTreeSha !== mergeTree.treeSha) return refused(`catch-up recognition refused: the head tree ${headTreeSha} differs from the faithful merge tree ${mergeTree.treeSha}, so the merge carries content beyond its two parents.`);
|
|
8835
|
+
return {
|
|
8836
|
+
recognition: {
|
|
8837
|
+
baseRef: base,
|
|
8838
|
+
baseTipSha,
|
|
8839
|
+
mergedParentSha: secondParent,
|
|
8840
|
+
mergedTreeSha: mergeTree.treeSha,
|
|
8841
|
+
upstreamRef
|
|
8842
|
+
},
|
|
8843
|
+
recognized: true
|
|
8844
|
+
};
|
|
8845
|
+
} catch (error) {
|
|
8846
|
+
return refused(`catch-up recognition refused: ${error instanceof Error ? error.message : String(error)}`);
|
|
8847
|
+
}
|
|
8848
|
+
};
|
|
8849
|
+
//#endregion
|
|
8641
8850
|
//#region src/evidence-freshness.ts
|
|
8642
8851
|
const patchIdMatches = (recorded, candidate) => Boolean(recorded.patchId) && recorded.patchId === candidate.patchId;
|
|
8643
8852
|
const headShaMatches = (recorded, candidate) => Boolean(recorded.headSha && candidate.headSha && sameHeadSha(candidate.headSha, recorded.headSha));
|
|
@@ -8927,6 +9136,14 @@ const evaluateRequiredChecks = ({ authoringSessionIds, candidate, envelopes, req
|
|
|
8927
9136
|
scopeReason: release.reason,
|
|
8928
9137
|
status: "out-of-scope"
|
|
8929
9138
|
};
|
|
9139
|
+
if (scopeContext.catchUpRecognition && !boundFailureObserved) return {
|
|
9140
|
+
checkType: check.checkType,
|
|
9141
|
+
inScope: false,
|
|
9142
|
+
name: check.name,
|
|
9143
|
+
...check.scope ? { scope: check.scope } : {},
|
|
9144
|
+
scopeReason: `catch-up recognition released: the head is a faithful merge of ${scopeContext.catchUpRecognition.baseTipSha} and admitted base-side content (#833)`,
|
|
9145
|
+
status: "out-of-scope"
|
|
9146
|
+
};
|
|
8930
9147
|
return {
|
|
8931
9148
|
...evaluateInScopeCheck({
|
|
8932
9149
|
authoringSessionIds,
|
|
@@ -9904,6 +10121,7 @@ var verification_proof_exports = /* @__PURE__ */ __exportAll({
|
|
|
9904
10121
|
prVerifyProofHeadShas: () => prVerifyProofHeadShas,
|
|
9905
10122
|
readPrVerifyProof: () => readPrVerifyProof,
|
|
9906
10123
|
resolveVerifyProofApplicability: () => resolveVerifyProofApplicability,
|
|
10124
|
+
trustedCatchUpRecognition: () => trustedCatchUpRecognition,
|
|
9907
10125
|
trustedImpactStamp: () => trustedImpactStamp,
|
|
9908
10126
|
validatePrVerifyProof: () => validatePrVerifyProof,
|
|
9909
10127
|
verificationProofApplicabilityFor: () => verificationProofApplicabilityFor,
|
|
@@ -9973,6 +10191,24 @@ const trustedImpactStamp = ({ candidate, proof }) => {
|
|
|
9973
10191
|
});
|
|
9974
10192
|
return headBound && patchBound ? proof.impactStamp : void 0;
|
|
9975
10193
|
};
|
|
10194
|
+
/**
|
|
10195
|
+
* #833: the recorded catch-up recognition, consumed ONLY when it is
|
|
10196
|
+
* identity-bound to the current candidate under exactly the rules
|
|
10197
|
+
* `trustedImpactStamp` applies — the proof passed, is v4+, and its recorded
|
|
10198
|
+
* headSha AND patchId both name this candidate. The recognition withdraws
|
|
10199
|
+
* review and preview demands, so it earns the same strict double binding and
|
|
10200
|
+
* the same fail-closed answer (`undefined`) on every other state.
|
|
10201
|
+
*/
|
|
10202
|
+
const trustedCatchUpRecognition = ({ candidate, proof }) => {
|
|
10203
|
+
if (!proof || !verifyProofPassed(proof) || proof.schemaVersion < 4) return;
|
|
10204
|
+
const headBound = sameHeadSha(candidate.headSha, proof.headSha);
|
|
10205
|
+
const patchBound = evidenceFresh({
|
|
10206
|
+
candidate: { patchId: candidate.patchId },
|
|
10207
|
+
recorded: { patchId: proof.patchId },
|
|
10208
|
+
rule: "patch-id"
|
|
10209
|
+
});
|
|
10210
|
+
return headBound && patchBound ? proof.catchUpRecognition : void 0;
|
|
10211
|
+
};
|
|
9976
10212
|
const prVerifyFullHeadShasForDiff = ({ docsOnlyDeltaAcceptedSince, files, proof }) => {
|
|
9977
10213
|
if (!verifyProofPassed(proof)) return [];
|
|
9978
10214
|
if (proof.mode === "full") return sameFileList(proof.changedFiles, files) ? [proof.headSha] : [];
|
|
@@ -10254,6 +10490,7 @@ function defaultPrVerifyGit() {
|
|
|
10254
10490
|
currentHeadSha,
|
|
10255
10491
|
emptyTreeHash,
|
|
10256
10492
|
mergeBaseSha,
|
|
10493
|
+
recognizeCatchUp: recognizeCatchUpCandidate,
|
|
10257
10494
|
runVerificationCommand(command, cwd, env) {
|
|
10258
10495
|
const result = runInheritedTee("bash", ["-lc", command], cwd, env);
|
|
10259
10496
|
const counts = parseVitestCounts(result.stdout);
|
|
@@ -10293,11 +10530,12 @@ function baselineFullProofsForDocsOnly({ previousProof, profilePath, projectKey,
|
|
|
10293
10530
|
seedFilter: matchesProfile
|
|
10294
10531
|
});
|
|
10295
10532
|
}
|
|
10296
|
-
function buildPrVerifyProof({ authoringSession, base, baselineFullProofs, classification, commands, endedAt, executedCommands, files, headSha, impactStamp, mode, mergeBaseSha: proofMergeBaseSha, notRequiredCommands, outcome, patchId, profilePath, projectKey, repository, startedAt }) {
|
|
10533
|
+
function buildPrVerifyProof({ authoringSession, base, baselineFullProofs, catchUpRecognition, classification, commands, endedAt, executedCommands, files, headSha, impactStamp, mode, mergeBaseSha: proofMergeBaseSha, notRequiredCommands, outcome, patchId, profilePath, projectKey, repository, startedAt }) {
|
|
10297
10534
|
return {
|
|
10298
10535
|
authoringSession,
|
|
10299
10536
|
base,
|
|
10300
10537
|
...baselineFullProofs ? { baselineFullProofs } : {},
|
|
10538
|
+
...catchUpRecognition ? { catchUpRecognition } : {},
|
|
10301
10539
|
changedFiles: files,
|
|
10302
10540
|
classification: classification.classification,
|
|
10303
10541
|
classificationReasons: classification.reasons,
|
|
@@ -10380,6 +10618,11 @@ function runPrVerify(args, dependencies = {}) {
|
|
|
10380
10618
|
cwd,
|
|
10381
10619
|
git
|
|
10382
10620
|
});
|
|
10621
|
+
const catchUpRecognitionResult = (git.recognizeCatchUp ?? recognizeCatchUpCandidate)({
|
|
10622
|
+
base: args.base,
|
|
10623
|
+
cwd
|
|
10624
|
+
});
|
|
10625
|
+
const catchUpRecognition = catchUpRecognitionResult.recognized ? catchUpRecognitionResult.recognition : void 0;
|
|
10383
10626
|
const readLockfileAtRef = git.showFileAtRef ?? showFileAtRef;
|
|
10384
10627
|
let impactStamp;
|
|
10385
10628
|
try {
|
|
@@ -10425,6 +10668,7 @@ function runPrVerify(args, dependencies = {}) {
|
|
|
10425
10668
|
console.log(`classification: ${classification.classification}`);
|
|
10426
10669
|
for (const line of context.runSummaryLines()) console.log(line);
|
|
10427
10670
|
for (const line of impactStampSummaryLines(impactStamp)) console.log(line);
|
|
10671
|
+
if (catchUpRecognition) console.log(`catch-up recognition (#833): faithful merge of ${catchUpRecognition.baseTipSha} (${catchUpRecognition.baseRef}) and ${catchUpRecognition.mergedParentSha} (admitted via ${catchUpRecognition.upstreamRef}); verification mode and battery scoping are unchanged.`);
|
|
10428
10672
|
const vetoedTargets = boundFailingCheckNames({
|
|
10429
10673
|
candidate: {
|
|
10430
10674
|
headSha,
|
|
@@ -10461,6 +10705,7 @@ function runPrVerify(args, dependencies = {}) {
|
|
|
10461
10705
|
const proofContext = {
|
|
10462
10706
|
authoringSession,
|
|
10463
10707
|
base: args.base,
|
|
10708
|
+
...catchUpRecognition ? { catchUpRecognition } : {},
|
|
10464
10709
|
classification,
|
|
10465
10710
|
commands,
|
|
10466
10711
|
files,
|
|
@@ -10856,7 +11101,7 @@ const closingPullRequestsQuery = `query($owner: String!, $repo: String!, $issue:
|
|
|
10856
11101
|
repository(owner: $owner, name: $repo) {
|
|
10857
11102
|
issue(number: $issue) {
|
|
10858
11103
|
closedByPullRequestsReferences(first: 50, includeClosedPrs: true) {
|
|
10859
|
-
nodes { number }
|
|
11104
|
+
nodes { number state }
|
|
10860
11105
|
}
|
|
10861
11106
|
}
|
|
10862
11107
|
}
|
|
@@ -11818,7 +12063,7 @@ const prReviewProofSchema = z.object({
|
|
|
11818
12063
|
patchId: z.string().regex(/^[0-9a-f]{40,64}$/u),
|
|
11819
12064
|
reviewCycle: z.number().int().positive().optional(),
|
|
11820
12065
|
reviewRequirement: z.object({
|
|
11821
|
-
reason: z.enum(["no-applicable-mode"]),
|
|
12066
|
+
reason: z.enum(["no-applicable-mode", "faithful-merge"]),
|
|
11822
12067
|
status: z.literal("not-required")
|
|
11823
12068
|
}).strict().optional(),
|
|
11824
12069
|
reviews: z.array(prReviewResultSchema),
|
|
@@ -14006,6 +14251,21 @@ const addSecurityBlockingReasons = ({ blockers, securityRequired, securityStatus
|
|
|
14006
14251
|
reason: "Security review proof ran and explicitly failed (open blocking findings or errored run); resolve or disposition the findings and re-run pr:review."
|
|
14007
14252
|
});
|
|
14008
14253
|
};
|
|
14254
|
+
/**
|
|
14255
|
+
* #833: the trusted catch-up recognition for this evaluation's candidate —
|
|
14256
|
+
* `trustedCatchUpRecognition` over the same passed verify proof the impact
|
|
14257
|
+
* stamp is read from, fail closed to `undefined` on every other state.
|
|
14258
|
+
*/
|
|
14259
|
+
const recognitionForCandidate = (input) => {
|
|
14260
|
+
const { verificationProof } = input;
|
|
14261
|
+
return trustedCatchUpRecognition({
|
|
14262
|
+
candidate: {
|
|
14263
|
+
headSha: input.headSha,
|
|
14264
|
+
patchId: input.patchId
|
|
14265
|
+
},
|
|
14266
|
+
proof: verificationProof && "proof" in verificationProof ? verificationProof.proof : void 0
|
|
14267
|
+
});
|
|
14268
|
+
};
|
|
14009
14269
|
const requiredChecksEvaluation = (input) => {
|
|
14010
14270
|
const requiredChecks = input.externalRequiredChecks ?? [];
|
|
14011
14271
|
if (requiredChecks.length === 0) return {
|
|
@@ -14018,6 +14278,7 @@ const requiredChecksEvaluation = (input) => {
|
|
|
14018
14278
|
patchId: input.patchId
|
|
14019
14279
|
};
|
|
14020
14280
|
const { verificationProof } = input;
|
|
14281
|
+
const catchUpRecognition = recognitionForCandidate(input);
|
|
14021
14282
|
const impactStamp = trustedImpactStamp({
|
|
14022
14283
|
candidate: {
|
|
14023
14284
|
headSha: input.headSha,
|
|
@@ -14031,6 +14292,7 @@ const requiredChecksEvaluation = (input) => {
|
|
|
14031
14292
|
envelopes: input.evidenceEnvelopes ?? [],
|
|
14032
14293
|
requiredChecks,
|
|
14033
14294
|
scopeContext: {
|
|
14295
|
+
...catchUpRecognition ? { catchUpRecognition } : {},
|
|
14034
14296
|
classification: input.classification,
|
|
14035
14297
|
...impactStamp ? { impactStamp } : {}
|
|
14036
14298
|
}
|
|
@@ -14243,7 +14505,8 @@ const foldWaivers = ({ blockers, waivers }) => {
|
|
|
14243
14505
|
};
|
|
14244
14506
|
};
|
|
14245
14507
|
const evaluateReadiness = (input) => {
|
|
14246
|
-
const
|
|
14508
|
+
const catchUpRecognition = recognitionForCandidate(input);
|
|
14509
|
+
const reviewModes = catchUpRecognition ? [] : input.reviewModes ?? ["correctness"];
|
|
14247
14510
|
const epoch = reviewEpochModeFor(input.reviewProof ?? {}, input.ladderPolicy);
|
|
14248
14511
|
const verifyGate = verifyGateFor({ input });
|
|
14249
14512
|
const { correctnessProof, correctnessRequired, correctnessStatus, correctnessUntyped, gateCapMismatchBlockers, reviewCycleState, reviewTerminalState, reviewLadder, securityProof, securityRequired, securityStatus, securityUntyped } = resolveReviewRequiredness({
|
|
@@ -14281,6 +14544,7 @@ const evaluateReadiness = (input) => {
|
|
|
14281
14544
|
waivers: input.waivers ?? []
|
|
14282
14545
|
});
|
|
14283
14546
|
const notices = [
|
|
14547
|
+
...catchUpRecognition ? [`Catch-up recognition (#833): the head is a faithful merge of target tip ${catchUpRecognition.baseTipSha} and admitted base-side content (${catchUpRecognition.upstreamRef}); review modes and impact-demanded envelopes are not demanded for this candidate.`] : [],
|
|
14284
14548
|
...input.mergeFreeze?.notices ?? [],
|
|
14285
14549
|
...waivedDemands.map(waivedDemandNotice),
|
|
14286
14550
|
...unusedWaiverNotices({
|
|
@@ -15774,6 +16038,7 @@ const defaultPrReviewGit = () => ({
|
|
|
15774
16038
|
changedFiles,
|
|
15775
16039
|
currentHeadSha,
|
|
15776
16040
|
isAncestor,
|
|
16041
|
+
recognizeCatchUp: recognizeCatchUpCandidate,
|
|
15777
16042
|
stablePatchId: runStablePatchId,
|
|
15778
16043
|
statusPorcelain
|
|
15779
16044
|
});
|
|
@@ -15908,11 +16173,7 @@ async function runPrReview(args, dependencies = {}) {
|
|
|
15908
16173
|
const startedAt = /* @__PURE__ */ new Date();
|
|
15909
16174
|
const findingsFile = args.findings ? parseReviewFindingsFile(args.findings, cwd) : void 0;
|
|
15910
16175
|
const kinds = enabledKinds(profile, args.mode, files, findingsFile?.reviews.map((review) => review.kind) ?? []);
|
|
15911
|
-
|
|
15912
|
-
const reviewRequirement = {
|
|
15913
|
-
reason: "no-applicable-mode",
|
|
15914
|
-
status: "not-required"
|
|
15915
|
-
};
|
|
16176
|
+
const mintNotRequiredProof = (reviewRequirement) => {
|
|
15916
16177
|
const proof = emptyProof({
|
|
15917
16178
|
base: args.base,
|
|
15918
16179
|
cycle: args.cycle,
|
|
@@ -15940,8 +16201,21 @@ async function runPrReview(args, dependencies = {}) {
|
|
|
15940
16201
|
repoRoot: cwd
|
|
15941
16202
|
});
|
|
15942
16203
|
return proof;
|
|
16204
|
+
};
|
|
16205
|
+
if (kinds.length === 0) return mintNotRequiredProof({
|
|
16206
|
+
reason: "no-applicable-mode",
|
|
16207
|
+
status: "not-required"
|
|
16208
|
+
});
|
|
16209
|
+
if (!findingsFile) {
|
|
16210
|
+
if ((git.recognizeCatchUp ?? recognizeCatchUpCandidate)({
|
|
16211
|
+
base: args.base,
|
|
16212
|
+
cwd
|
|
16213
|
+
}).recognized) return mintNotRequiredProof({
|
|
16214
|
+
reason: "faithful-merge",
|
|
16215
|
+
status: "not-required"
|
|
16216
|
+
});
|
|
16217
|
+
throw new Error("--findings <path> is required when review kinds are enabled.");
|
|
15943
16218
|
}
|
|
15944
|
-
if (!findingsFile) throw new Error("--findings <path> is required when review kinds are enabled.");
|
|
15945
16219
|
if (!evidenceFresh({
|
|
15946
16220
|
candidate: {
|
|
15947
16221
|
headSha,
|
package/dist/schemas.d.ts
CHANGED
|
@@ -268,7 +268,7 @@ interface PrReviewProof {
|
|
|
268
268
|
cleanedPaths: string[];
|
|
269
269
|
ladder?: PrReviewLadderState;
|
|
270
270
|
reviewRequirement?: {
|
|
271
|
-
reason: "no-applicable-mode";
|
|
271
|
+
reason: "no-applicable-mode" | "faithful-merge";
|
|
272
272
|
status: "not-required";
|
|
273
273
|
};
|
|
274
274
|
reviews: PrReviewResult[];
|
|
@@ -443,6 +443,23 @@ type ReadinessRepair = z.infer<typeof readinessRepairSchema>;
|
|
|
443
443
|
type ReadinessStatus = "ready" | "blocked";
|
|
444
444
|
type ManagedReadinessLedger = z.infer<typeof managedReadinessLedgerSchema>;
|
|
445
445
|
//#endregion
|
|
446
|
+
//#region src/catch-up-recognition-record.d.ts
|
|
447
|
+
interface CatchUpRecognition {
|
|
448
|
+
/** The ref the candidate targets, as the recognizing command resolved it. */
|
|
449
|
+
baseRef: string;
|
|
450
|
+
/** The live target tip at recognition time; the head's first parent. */
|
|
451
|
+
baseTipSha: string;
|
|
452
|
+
/** The head's second parent: the admitted base-side content merged in. */
|
|
453
|
+
mergedParentSha: string;
|
|
454
|
+
/**
|
|
455
|
+
* The tree `git merge-tree --write-tree` produced for the two parents. It
|
|
456
|
+
* equals the head's own tree; that equality is the faithfulness proof.
|
|
457
|
+
*/
|
|
458
|
+
mergedTreeSha: string;
|
|
459
|
+
/** The ref that proves the second parent is admitted content. */
|
|
460
|
+
upstreamRef: string;
|
|
461
|
+
}
|
|
462
|
+
//#endregion
|
|
446
463
|
//#region src/pr-verify-proof-contract.d.ts
|
|
447
464
|
declare const SUPPORTED_PR_VERIFY_SCHEMA_VERSIONS: readonly [1, 2, 3, 4];
|
|
448
465
|
type PrVerifySchemaVersion = (typeof SUPPORTED_PR_VERIFY_SCHEMA_VERSIONS)[number];
|
|
@@ -509,6 +526,7 @@ interface PrVerifyProof {
|
|
|
509
526
|
projectKey: string;
|
|
510
527
|
repository: string;
|
|
511
528
|
}[];
|
|
529
|
+
catchUpRecognition?: CatchUpRecognition;
|
|
512
530
|
changedFiles: string[];
|
|
513
531
|
classification: DiffClassification;
|
|
514
532
|
classificationReasons: string[];
|
|
@@ -541,6 +559,7 @@ declare const prVerifyProofSchema: z.ZodObject<{
|
|
|
541
559
|
projectKey: z.ZodString;
|
|
542
560
|
repository: z.ZodString;
|
|
543
561
|
}, z.core.$strip>>>;
|
|
562
|
+
catchUpRecognition: z.ZodOptional<z.ZodType<CatchUpRecognition, unknown, z.core.$ZodTypeInternals<CatchUpRecognition, unknown>>>;
|
|
544
563
|
changedFiles: z.ZodArray<z.ZodString>;
|
|
545
564
|
classification: z.ZodEnum<{
|
|
546
565
|
"docs/process-only": "docs/process-only";
|
|
@@ -827,6 +846,7 @@ declare const prReviewProofSchema: z.ZodObject<{
|
|
|
827
846
|
reviewRequirement: z.ZodOptional<z.ZodObject<{
|
|
828
847
|
reason: z.ZodEnum<{
|
|
829
848
|
"no-applicable-mode": "no-applicable-mode";
|
|
849
|
+
"faithful-merge": "faithful-merge";
|
|
830
850
|
}>;
|
|
831
851
|
status: z.ZodLiteral<"not-required">;
|
|
832
852
|
}, z.core.$strict>>;
|
package/dist/schemas.js
CHANGED
|
@@ -228,6 +228,16 @@ const waivedDemandSchema = z.object({
|
|
|
228
228
|
session: z.string().trim().min(1).optional(),
|
|
229
229
|
unmetReasons: z.array(z.string().min(1)).min(1)
|
|
230
230
|
});
|
|
231
|
+
//#endregion
|
|
232
|
+
//#region src/catch-up-recognition-record.ts
|
|
233
|
+
const objectShaSchema = z.string().regex(/^[0-9a-f]{40}$/u);
|
|
234
|
+
const catchUpRecognitionSchema = z.object({
|
|
235
|
+
baseRef: z.string().min(1),
|
|
236
|
+
baseTipSha: objectShaSchema,
|
|
237
|
+
mergedParentSha: objectShaSchema,
|
|
238
|
+
mergedTreeSha: objectShaSchema,
|
|
239
|
+
upstreamRef: z.string().min(1)
|
|
240
|
+
}).strict();
|
|
231
241
|
const impactStampTargetSchema = z.object({
|
|
232
242
|
basis: z.string().min(1),
|
|
233
243
|
impact: z.enum(["affected", "not-affected"]),
|
|
@@ -515,6 +525,11 @@ const assertPrVerifyProofRules = (proof, context) => {
|
|
|
515
525
|
message: "notRequiredCommands is a schemaVersion>=4 field; a v1–v3 pr:verify proof must not carry it.",
|
|
516
526
|
path: ["notRequiredCommands"]
|
|
517
527
|
});
|
|
528
|
+
if (proof.schemaVersion < 4 && proof.catchUpRecognition !== void 0) context.addIssue({
|
|
529
|
+
code: "custom",
|
|
530
|
+
message: "catchUpRecognition is a schemaVersion>=4 field; a v1–v3 pr:verify proof must not carry it.",
|
|
531
|
+
path: ["catchUpRecognition"]
|
|
532
|
+
});
|
|
518
533
|
if (proof.schemaVersion < 4 && proof.notDemandedRecords !== void 0) context.addIssue({
|
|
519
534
|
code: "custom",
|
|
520
535
|
message: "notDemandedRecords is a schemaVersion>=4 field; a v1–v3 pr:verify proof must not carry it.",
|
|
@@ -573,6 +588,7 @@ const prVerifyProofSchema = z.object({
|
|
|
573
588
|
projectKey: z.string().min(1),
|
|
574
589
|
repository: z.string().min(1)
|
|
575
590
|
})).optional(),
|
|
591
|
+
catchUpRecognition: catchUpRecognitionSchema.optional(),
|
|
576
592
|
changedFiles: z.array(z.string()),
|
|
577
593
|
classification: z.enum([
|
|
578
594
|
"docs/process-only",
|
|
@@ -888,7 +904,7 @@ const prReviewProofSchema = z.object({
|
|
|
888
904
|
patchId: z.string().regex(/^[0-9a-f]{40,64}$/u),
|
|
889
905
|
reviewCycle: z.number().int().positive().optional(),
|
|
890
906
|
reviewRequirement: z.object({
|
|
891
|
-
reason: z.enum(["no-applicable-mode"]),
|
|
907
|
+
reason: z.enum(["no-applicable-mode", "faithful-merge"]),
|
|
892
908
|
status: z.literal("not-required")
|
|
893
909
|
}).strict().optional(),
|
|
894
910
|
reviews: z.array(prReviewResultSchema),
|
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.22",
|
|
4
4
|
"description": "Shared Patronage software factory CLI and project-profile validation tools",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"picomatch": "^4.0.5",
|
|
38
38
|
"yaml": "^2.9.0",
|
|
39
39
|
"zod": "4.4.3",
|
|
40
|
-
"@patronage/factory-ci": "1.0.0-alpha.
|
|
40
|
+
"@patronage/factory-ci": "1.0.0-alpha.22"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@types/node": "24.13.3",
|
|
@@ -46,14 +46,13 @@
|
|
|
46
46
|
"dependency-cruiser": "18.1.0",
|
|
47
47
|
"knip": "6.27.0",
|
|
48
48
|
"oxfmt": "0.59.0",
|
|
49
|
-
"oxlint": "1.
|
|
49
|
+
"oxlint": "1.76.0",
|
|
50
50
|
"oxlint-tsgolint": "7.0.2001",
|
|
51
51
|
"tsdown": "0.21.10",
|
|
52
52
|
"tsx": "4.23.1",
|
|
53
53
|
"typescript": "5.9.3",
|
|
54
|
-
"ultracite": "7.
|
|
55
|
-
"vitest": "4.1.10"
|
|
56
|
-
"vitest-evals": "0.14.0"
|
|
54
|
+
"ultracite": "7.10.5",
|
|
55
|
+
"vitest": "4.1.10"
|
|
57
56
|
},
|
|
58
57
|
"engines": {
|
|
59
58
|
"node": "^24.0.0"
|
|
@@ -70,10 +69,6 @@
|
|
|
70
69
|
"precoverage": "bash ../scripts/ensure-worktree-bootstrap.sh",
|
|
71
70
|
"coverage": "vitest run --coverage",
|
|
72
71
|
"docs:reference": "tsx scripts/update-command-reference.ts",
|
|
73
|
-
"eval:review": "tsx evals/review-prompts/track-b/reviewRunner.ts",
|
|
74
|
-
"eval:review:report": "tsx evals/review-prompts/track-b/reviewReport.ts",
|
|
75
|
-
"eval:review:rescore": "tsx evals/review-prompts/track-b/rescore.ts",
|
|
76
|
-
"eval:review:run": "vitest run --config evals/review-prompts/vitest.eval.config.ts",
|
|
77
72
|
"pack:smoke": "bash scripts/pack-smoke.sh",
|
|
78
73
|
"profile:schema": "tsx scripts/generate-profile-schema.ts",
|
|
79
74
|
"pretest": "bash ../scripts/ensure-worktree-bootstrap.sh",
|