@patronage/software-factory 0.23.0 → 0.30.0-alpha.1
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/CONTEXT.md +1 -1
- package/README.md +5 -5
- package/dist/index.d.ts +602 -404
- package/dist/index.js +6330 -4580
- package/dist/schemas.d.ts +99 -163
- package/dist/schemas.js +261 -75
- package/package.json +1 -1
package/dist/schemas.js
CHANGED
|
@@ -1,21 +1,230 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
//#region src/review-rungs.ts
|
|
3
|
+
const EVIDENCE_REVIEW_RUNGS$1 = [
|
|
4
|
+
"independent-model",
|
|
5
|
+
"oracle",
|
|
6
|
+
"human"
|
|
7
|
+
];
|
|
8
|
+
Object.fromEntries(EVIDENCE_REVIEW_RUNGS$1.map((rung, index) => [rung, index]));
|
|
9
|
+
//#endregion
|
|
10
|
+
//#region src/demand-keys.ts
|
|
11
|
+
/** The demands that exist at most once per candidate. */
|
|
12
|
+
const DEMAND_KEYS = {
|
|
13
|
+
/**
|
|
14
|
+
* The boundary wave that authorizes this candidate's merge schedule (#477).
|
|
15
|
+
* In force exactly when GitHub already has auto-merge enabled: a live
|
|
16
|
+
* schedule with no wave demand resolved is an unauthorized merge waiting to
|
|
17
|
+
* happen.
|
|
18
|
+
*/
|
|
19
|
+
boundaryWave: "boundary-wave",
|
|
20
|
+
/** The PR is still a draft. */
|
|
21
|
+
draft: "draft",
|
|
22
|
+
/** The candidate is not the intended final human review point. */
|
|
23
|
+
finalReviewPoint: "final-review-point",
|
|
24
|
+
/** GitHub's own check rollup for the candidate head. */
|
|
25
|
+
githubChecks: "github-checks",
|
|
26
|
+
/** Local HEAD and the GitHub PR head must be the same commit. */
|
|
27
|
+
headIdentity: "head-identity",
|
|
28
|
+
/** An unhandled post-readiness human comment or review submission. */
|
|
29
|
+
humanBlocker: "human-blocker",
|
|
30
|
+
/** The repository-wide merge freeze. */
|
|
31
|
+
mergeFreeze: "merge-freeze",
|
|
32
|
+
/** GitHub's mergeability / merge-state rollup. */
|
|
33
|
+
mergeState: "merge-state",
|
|
34
|
+
/**
|
|
35
|
+
* A native GitHub approval, demanded when the boundary wave in force does
|
|
36
|
+
* not authorize machine merge (#477). "Green = authorized to merge" is then
|
|
37
|
+
* a property of the branded required check itself.
|
|
38
|
+
*/
|
|
39
|
+
nativeApproval: "native-approval",
|
|
40
|
+
/** The PR body's required rendered sections. */
|
|
41
|
+
prBodySections: "pr-body-sections",
|
|
42
|
+
/** A current, head-bound, passing typed `pr:verify` proof. */
|
|
43
|
+
prVerify: "pr-verify",
|
|
44
|
+
/** The profile-resolved review ladder policy. */
|
|
45
|
+
reviewLadder: "review-ladder",
|
|
46
|
+
/** Unresolved GitHub review threads. */
|
|
47
|
+
reviewThreads: "review-threads",
|
|
48
|
+
/** An explicit trivial waiver that the diff does not support. */
|
|
49
|
+
trivialWaiver: "trivial-waiver"
|
|
50
|
+
};
|
|
51
|
+
/** The families whose instances are named by a resolved value. */
|
|
52
|
+
const QUALIFIED_DEMAND_FAMILIES = [
|
|
53
|
+
"required-check",
|
|
54
|
+
"review-mode",
|
|
55
|
+
"review-rung"
|
|
56
|
+
];
|
|
57
|
+
const QUALIFIER_PATTERN = /^[A-Za-z0-9._%/-]{1,64}$/u;
|
|
58
|
+
const percentEncode = (character) => {
|
|
59
|
+
const code = character.codePointAt(0) ?? 0;
|
|
60
|
+
if (code >= 55296 && code <= 57343) return `%u${code.toString(16).toUpperCase().padStart(4, "0")}`;
|
|
61
|
+
return [...new TextEncoder().encode(character)].map((byte) => `%${byte.toString(16).toUpperCase().padStart(2, "0")}`).join("");
|
|
62
|
+
};
|
|
63
|
+
/**
|
|
64
|
+
* Express a resolved value as a qualifier. Percent-encoding, not scrubbing:
|
|
65
|
+
* the mapping is injective, so two differently named required checks can never
|
|
66
|
+
* collapse into one key — which would let one operator waiver silently cover a
|
|
67
|
+
* demand nobody waived, and make HQ count two causes as one.
|
|
68
|
+
*
|
|
69
|
+
* Deterministic in both directions of use: the key a `pr:ready` proof records
|
|
70
|
+
* is the key a `pr:ready` waiver matches.
|
|
71
|
+
*/
|
|
72
|
+
const demandQualifier = (value) => value.replaceAll(/[^A-Za-z0-9._/-]/gu, percentEncode);
|
|
73
|
+
const FIXED_DEMAND_KEYS = Object.values(DEMAND_KEYS);
|
|
74
|
+
/** The review modes a profile can resolve, and therefore demand. */
|
|
75
|
+
const REVIEW_MODE_DEMAND_VALUES = ["correctness", "security"];
|
|
76
|
+
const escapePattern = /%(?:u[0-9A-F]{4}|[0-9A-F]{2})/gu;
|
|
77
|
+
/**
|
|
78
|
+
* Reverse `demandQualifier`. A qualifier is canonical exactly when encoding
|
|
79
|
+
* its decoded form reproduces it — which rejects both malformed escapes and
|
|
80
|
+
* noncanonical aliases like `%41` for `A`. Returns undefined when the
|
|
81
|
+
* qualifier cannot have been minted here.
|
|
82
|
+
*/
|
|
83
|
+
const decodeQualifier = (qualifier) => {
|
|
84
|
+
const bytes = [];
|
|
85
|
+
let decoded = "";
|
|
86
|
+
let index = 0;
|
|
87
|
+
const flush = () => {
|
|
88
|
+
if (bytes.length === 0) return true;
|
|
89
|
+
try {
|
|
90
|
+
decoded += new TextDecoder("utf-8", {
|
|
91
|
+
fatal: true,
|
|
92
|
+
ignoreBOM: true
|
|
93
|
+
}).decode(Uint8Array.from(bytes));
|
|
94
|
+
} catch {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
bytes.length = 0;
|
|
98
|
+
return true;
|
|
99
|
+
};
|
|
100
|
+
while (index < qualifier.length) {
|
|
101
|
+
const character = qualifier[index];
|
|
102
|
+
if (character !== "%") {
|
|
103
|
+
if (!flush()) return;
|
|
104
|
+
decoded += character;
|
|
105
|
+
index += 1;
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
escapePattern.lastIndex = index;
|
|
109
|
+
const escape = escapePattern.exec(qualifier);
|
|
110
|
+
if (!escape || escape.index !== index) return;
|
|
111
|
+
if (escape[0][1] === "u") {
|
|
112
|
+
if (!flush()) return;
|
|
113
|
+
decoded += String.fromCodePoint(Number.parseInt(escape[0].slice(2), 16));
|
|
114
|
+
} else bytes.push(Number.parseInt(escape[0].slice(1), 16));
|
|
115
|
+
index += escape[0].length;
|
|
116
|
+
}
|
|
117
|
+
return flush() ? decoded : void 0;
|
|
118
|
+
};
|
|
119
|
+
const qualifierIsMintable = (family, qualifier) => {
|
|
120
|
+
if (!QUALIFIER_PATTERN.test(qualifier)) return false;
|
|
121
|
+
const decoded = decodeQualifier(qualifier);
|
|
122
|
+
if (decoded === void 0 || demandQualifier(decoded) !== qualifier) return false;
|
|
123
|
+
if (family === "review-rung") return EVIDENCE_REVIEW_RUNGS$1.includes(decoded);
|
|
124
|
+
if (family === "review-mode") return REVIEW_MODE_DEMAND_VALUES.includes(decoded);
|
|
125
|
+
return true;
|
|
126
|
+
};
|
|
127
|
+
/**
|
|
128
|
+
* A demand key from the closed vocabulary: a known unqualified family, or a
|
|
129
|
+
* known qualified family with a qualifier this module could actually have
|
|
130
|
+
* minted. Closed on purpose — an invented family, an unresolvable rung, or a
|
|
131
|
+
* noncanonical encoding would be indistinguishable from a typo to anyone
|
|
132
|
+
* counting causes, which is the whole point of recording codes.
|
|
133
|
+
*/
|
|
134
|
+
const demandKeySchema = z.string().refine((value) => {
|
|
135
|
+
if (FIXED_DEMAND_KEYS.includes(value)) return true;
|
|
136
|
+
const separator = value.indexOf(":");
|
|
137
|
+
if (separator === -1) return false;
|
|
138
|
+
const family = value.slice(0, separator);
|
|
139
|
+
return QUALIFIED_DEMAND_FAMILIES.includes(family) && qualifierIsMintable(family, value.slice(separator + 1));
|
|
140
|
+
}, { message: `a demand key is one of ${FIXED_DEMAND_KEYS.join(", ")} or a resolvable ${QUALIFIED_DEMAND_FAMILIES.join(" / ")} key` });
|
|
141
|
+
/**
|
|
142
|
+
* The key shape an operator may *name* in a waiver: syntactic, not closed.
|
|
143
|
+
*
|
|
144
|
+
* Deliberately distinct from `demandKeySchema` above, which is what a proof's
|
|
145
|
+
* reason codes must come from. A waiver store is durable operator evidence
|
|
146
|
+
* written before today's vocabulary existed, so tightening its validation
|
|
147
|
+
* would make an existing record parse as no waiver and let the next write drop
|
|
148
|
+
* it. A waiver naming a key nobody resolves is already inert — `pr:ready`
|
|
149
|
+
* reports it as an unapplied waiver — so nothing is admitted by accepting it.
|
|
150
|
+
*/
|
|
151
|
+
const waiverDemandKeySchema = z.string().regex(/^[a-z][a-z0-9-]*(?::[A-Za-z0-9._%/-]+)?$/u, "A demand key is a lowercase family, optionally qualified by its resolved value (e.g. review-rung:human).");
|
|
152
|
+
//#endregion
|
|
153
|
+
//#region src/blocked-reasons.ts
|
|
154
|
+
/**
|
|
155
|
+
* The wire bound on one `detail`. A blocked proof carries one short sentence
|
|
156
|
+
* per demand, never a transcript: HQ's ingest cap is 256 KB and this field
|
|
157
|
+
* must stay far under it even when a candidate blocks on every gate at once.
|
|
158
|
+
*/
|
|
159
|
+
const BLOCKED_REASON_DETAIL_MAX = 280;
|
|
160
|
+
/**
|
|
161
|
+
* The wire bound on how many demands one proof may name. Well past any real
|
|
162
|
+
* candidate (there are a dozen fixed demands plus the profile's checks and the
|
|
163
|
+
* live human blockers) and, with the detail bound, keeps the whole field two
|
|
164
|
+
* orders of magnitude under the ingest cap. Bounded here rather than trimmed
|
|
165
|
+
* downstream: HQ rejects an over-long list loudly instead of storing a quietly
|
|
166
|
+
* truncated one.
|
|
167
|
+
*/
|
|
168
|
+
const BLOCKED_REASONS_MAX = 100;
|
|
169
|
+
/** One refusal sentence: single line, trimmed, bounded. */
|
|
170
|
+
const blockedReasonDetailSchema = z.string().min(1).max(280).refine((value) => value.trim().length > 0, { message: "must not be blank" }).refine((value) => !/[\r\n]/u.test(value), { message: "must be one line, not a multi-line payload" });
|
|
171
|
+
const blockedReasonSchema = z.object({
|
|
172
|
+
code: demandKeySchema,
|
|
173
|
+
detail: blockedReasonDetailSchema
|
|
174
|
+
});
|
|
175
|
+
const blockedReasonsSchema = z.array(blockedReasonSchema).max(100);
|
|
176
|
+
/** Fit one refusal sentence to the wire bound without losing its head. */
|
|
177
|
+
const blockedReasonDetail = (reason) => {
|
|
178
|
+
const line = reason.replaceAll(/\s+/gu, " ").trim();
|
|
179
|
+
return line.length <= 280 ? line : `${line.slice(0, 279)}…`;
|
|
180
|
+
};
|
|
181
|
+
const blockedReasonIssue = (message) => [{
|
|
182
|
+
code: "custom",
|
|
183
|
+
message,
|
|
184
|
+
path: ["blockedReasons"]
|
|
185
|
+
}];
|
|
186
|
+
/**
|
|
187
|
+
* The invariant every reader enforces, shared by the emitting schema and the
|
|
188
|
+
* wire schema so it is one rule rather than two that can drift. It is the
|
|
189
|
+
* producer's state machine, written down:
|
|
190
|
+
*
|
|
191
|
+
* - `ready` refused nothing, so it carries neither projection, and it is the
|
|
192
|
+
* final review point;
|
|
193
|
+
* - `slice-ready/not-final` was held back by exactly one demand — being a
|
|
194
|
+
* slice — and names it;
|
|
195
|
+
* - `blocked` refused something other than being a slice, and says so;
|
|
196
|
+
* - whichever it is, `blockedReasons` names every refusal listed in
|
|
197
|
+
* `blockingReasons`, in the same order, as the bounded form of that sentence.
|
|
198
|
+
*
|
|
199
|
+
* Naming the slice demand and the ledger's `finalReviewPoint` are the same
|
|
200
|
+
* fact, so a proof that says one and not the other is refused.
|
|
201
|
+
*/
|
|
202
|
+
const blockedReasonIssues = (proof) => {
|
|
203
|
+
const named = proof.blockedReasons ?? [];
|
|
204
|
+
const reasons = proof.blockingReasons ?? [];
|
|
205
|
+
const slice = DEMAND_KEYS.finalReviewPoint;
|
|
206
|
+
const namesSlice = named.some((reason) => reason.code === slice);
|
|
207
|
+
if (proof.status === "ready") {
|
|
208
|
+
if (named.length > 0 || reasons.length > 0) return blockedReasonIssue("a ready pr:ready proof must carry no blocking reasons");
|
|
209
|
+
} else if (proof.status === "slice-ready/not-final") {
|
|
210
|
+
if (named.length !== 1 || !namesSlice) return blockedReasonIssue(`a slice-ready/not-final pr:ready proof is held back by exactly one demand, ${slice}`);
|
|
211
|
+
} else if (reasons.length === 0) return blockedReasonIssue("a blocked pr:ready proof must record what blocked it");
|
|
212
|
+
else if (named.length > 0 && !named.some((r) => r.code !== slice)) return blockedReasonIssue(`a blocked pr:ready proof must name a demand other than ${slice}`);
|
|
213
|
+
if (proof.finalReviewPoint !== void 0 && namesSlice === proof.finalReviewPoint) return blockedReasonIssue(`naming ${slice} and the ledger's finalReviewPoint are the same fact; this proof says both`);
|
|
214
|
+
if (named.length !== reasons.length) return blockedReasonIssue(`blockedReasons must name every blocking reason: ${reasons.length} reason(s), ${named.length} named`);
|
|
215
|
+
const drifted = named.findIndex((reason, index) => reason.detail !== blockedReasonDetail(reasons[index]));
|
|
216
|
+
return drifted === -1 ? [] : blockedReasonIssue(`blockedReasons[${drifted}] does not carry blocking reason ${drifted}; the two projections must tell one story`);
|
|
217
|
+
};
|
|
218
|
+
//#endregion
|
|
2
219
|
//#region src/demand-waiver.ts
|
|
3
220
|
const DEMAND_WAIVER_SCHEMA_VERSION = 1;
|
|
4
221
|
const shaSchema = z.string().regex(/^[0-9a-f]{40}$/u);
|
|
5
|
-
/**
|
|
6
|
-
* A resolved demand's key: a family, optionally qualified by the resolved
|
|
7
|
-
* value that makes it this demand *instance* (`review-rung:human`,
|
|
8
|
-
* `required-check:core`, `merge-freeze`). The consumer that resolved the
|
|
9
|
-
* demand owns the key; this module only matches on it, which is why no
|
|
10
|
-
* control name is enumerated here.
|
|
11
|
-
*/
|
|
12
|
-
const demandKeySchema = z.string().regex(/^[a-z][a-z0-9-]*(?::[A-Za-z0-9._/-]+)?$/u, "A demand key is a lowercase family, optionally qualified by its resolved value (e.g. review-rung:human).");
|
|
13
222
|
const demandWaiverSchema = z.object({
|
|
14
223
|
candidate: z.object({
|
|
15
224
|
headSha: shaSchema,
|
|
16
225
|
pr: z.number().int().positive()
|
|
17
226
|
}),
|
|
18
|
-
demand:
|
|
227
|
+
demand: waiverDemandKeySchema,
|
|
19
228
|
operator: z.string().trim().min(1),
|
|
20
229
|
rationale: z.string().trim().min(1),
|
|
21
230
|
recordedAt: z.iso.datetime(),
|
|
@@ -27,7 +236,7 @@ z.object({
|
|
|
27
236
|
waivers: z.array(demandWaiverSchema)
|
|
28
237
|
});
|
|
29
238
|
const waivedDemandSchema = z.object({
|
|
30
|
-
demand:
|
|
239
|
+
demand: waiverDemandKeySchema,
|
|
31
240
|
operator: z.string().trim().min(1),
|
|
32
241
|
rationale: z.string().trim().min(1),
|
|
33
242
|
recordedAt: z.iso.datetime(),
|
|
@@ -783,67 +992,7 @@ const boundaryCheckProofSchema = z.object({
|
|
|
783
992
|
}).optional(),
|
|
784
993
|
status: z.enum(["ready", "blocked"])
|
|
785
994
|
});
|
|
786
|
-
const
|
|
787
|
-
z.object({
|
|
788
|
-
headSha: z.string().regex(/^[0-9a-f]{40}$/u),
|
|
789
|
-
kind: z.literal("match")
|
|
790
|
-
}),
|
|
791
|
-
z.object({
|
|
792
|
-
kind: z.literal("diverged"),
|
|
793
|
-
liveHeadSha: z.string().regex(/^[0-9a-f]{40}$/u),
|
|
794
|
-
postProofCommits: z.array(z.object({
|
|
795
|
-
sha: z.string().min(1),
|
|
796
|
-
subject: z.string()
|
|
797
|
-
})).optional(),
|
|
798
|
-
proofHeadSha: z.string().regex(/^[0-9a-f]{40}$/u)
|
|
799
|
-
}),
|
|
800
|
-
z.object({
|
|
801
|
-
kind: z.literal("live-head-invalid"),
|
|
802
|
-
pr: z.number().int().positive(),
|
|
803
|
-
received: z.string()
|
|
804
|
-
}),
|
|
805
|
-
z.object({
|
|
806
|
-
errorDetail: z.string().optional(),
|
|
807
|
-
kind: z.literal("ready-proof-missing"),
|
|
808
|
-
readyProofPath: z.string().min(1)
|
|
809
|
-
}),
|
|
810
|
-
z.object({
|
|
811
|
-
kind: z.literal("ready-proof-pr-mismatch"),
|
|
812
|
-
proofPr: z.number().int().positive(),
|
|
813
|
-
requestedPr: z.number().int().positive()
|
|
814
|
-
}),
|
|
815
|
-
z.object({
|
|
816
|
-
blockingReasons: z.array(z.string()),
|
|
817
|
-
kind: z.literal("ready-proof-not-ready"),
|
|
818
|
-
status: z.string().min(1)
|
|
819
|
-
})
|
|
820
|
-
]);
|
|
821
|
-
const PR_MERGE_CHECK_SCHEMA_VERSION = 1;
|
|
822
|
-
const prMergeCheckProofSchema = z.object({
|
|
823
|
-
blockingReasons: z.array(z.string()),
|
|
824
|
-
command: z.literal("patronage-factory pr:merge-check"),
|
|
825
|
-
followUp: followUpActionSchema.optional(),
|
|
826
|
-
identity: mergeGuardIdentitySchema,
|
|
827
|
-
liveHeadSha: z.string().regex(/^[0-9a-f]{40}$/u).optional(),
|
|
828
|
-
notices: z.array(z.string()).optional(),
|
|
829
|
-
pr: z.number().int().positive(),
|
|
830
|
-
schemaVersion: z.literal(1),
|
|
831
|
-
status: z.enum(["pass", "fail"]),
|
|
832
|
-
waivedDemands: z.array(waivedDemandSchema).optional(),
|
|
833
|
-
worktreeHeldBranch: z.object({
|
|
834
|
-
branch: z.string().min(1),
|
|
835
|
-
worktreePath: z.string().min(1)
|
|
836
|
-
}).optional()
|
|
837
|
-
});
|
|
838
|
-
const requiredCheckScopeSchema = z.object({
|
|
839
|
-
classifications: z.array(z.enum(DIFF_CLASSIFICATIONS)).min(1).optional(),
|
|
840
|
-
labels: z.array(z.string().min(1)).min(1).optional()
|
|
841
|
-
}).strict().superRefine((scope, context) => {
|
|
842
|
-
if (scope.labels === void 0 && scope.classifications === void 0) context.addIssue({
|
|
843
|
-
code: "custom",
|
|
844
|
-
message: "requiredChecks scope must declare at least one condition (labels and/or classifications)."
|
|
845
|
-
});
|
|
846
|
-
});
|
|
995
|
+
const requiredCheckScopeSchema = z.object({ classifications: z.array(z.enum(DIFF_CLASSIFICATIONS)).min(1) }).strict();
|
|
847
996
|
const readinessRepairSchema = z.object({
|
|
848
997
|
action: z.string().min(1),
|
|
849
998
|
code: z.enum([
|
|
@@ -1018,23 +1167,60 @@ const managedReadinessLedgerSchema = z.object({
|
|
|
1018
1167
|
verifiedHeadSha: z.string().optional()
|
|
1019
1168
|
})
|
|
1020
1169
|
});
|
|
1021
|
-
const PR_READY_SCHEMA_VERSION =
|
|
1170
|
+
const PR_READY_SCHEMA_VERSION = 3;
|
|
1171
|
+
/**
|
|
1172
|
+
* The pr:ready proof versions a reader still accepts. `pr:ready` emits v3 only
|
|
1173
|
+
* (#477) — one current contract — but v1 and v2 events were spooled before the
|
|
1174
|
+
* bumps and HQ must ingest them without degrading, so the wire schema parses
|
|
1175
|
+
* all three.
|
|
1176
|
+
*/
|
|
1177
|
+
const SUPPORTED_PR_READY_SCHEMA_VERSIONS = [
|
|
1178
|
+
1,
|
|
1179
|
+
2,
|
|
1180
|
+
3
|
|
1181
|
+
];
|
|
1182
|
+
const prReadySchemaVersionSchema = z.number().refine((value) => SUPPORTED_PR_READY_SCHEMA_VERSIONS.includes(value), { message: `schemaVersion must be one of: ${SUPPORTED_PR_READY_SCHEMA_VERSIONS.join(", ")}` });
|
|
1022
1183
|
const prReadyProofSchema = z.object({
|
|
1184
|
+
arming: z.object({
|
|
1185
|
+
detail: z.string().min(1).optional(),
|
|
1186
|
+
headSha: z.string().regex(/^[0-9a-f]{40}$/u),
|
|
1187
|
+
outcome: z.enum([
|
|
1188
|
+
"armed",
|
|
1189
|
+
"merged",
|
|
1190
|
+
"not-armed"
|
|
1191
|
+
])
|
|
1192
|
+
}).optional(),
|
|
1193
|
+
blockedReasons: blockedReasonsSchema.optional(),
|
|
1023
1194
|
blockingReasons: z.array(z.string()),
|
|
1024
1195
|
command: z.literal("patronage-factory pr:ready"),
|
|
1025
1196
|
followUp: followUpActionSchema.optional(),
|
|
1026
1197
|
humanBlockingReasons: z.array(z.string()).default([]),
|
|
1027
1198
|
ledger: managedReadinessLedgerSchema,
|
|
1199
|
+
notices: z.array(z.string().min(1)).optional(),
|
|
1028
1200
|
profileBlobSha: z.string().regex(/^[0-9a-f]{40}$/u).optional(),
|
|
1029
1201
|
profilePath: z.string().min(1).optional(),
|
|
1030
1202
|
repairs: z.array(readinessRepairSchema).default([]),
|
|
1031
1203
|
repository: z.string().regex(/^[^/\s]+\/[^/\s]+$/u).optional(),
|
|
1032
|
-
schemaVersion:
|
|
1204
|
+
schemaVersion: prReadySchemaVersionSchema,
|
|
1033
1205
|
status: z.enum([
|
|
1034
1206
|
"ready",
|
|
1035
1207
|
"blocked",
|
|
1036
1208
|
"slice-ready/not-final"
|
|
1037
|
-
])
|
|
1209
|
+
]),
|
|
1210
|
+
waivedDemands: z.array(waivedDemandSchema).optional()
|
|
1211
|
+
}).superRefine((proof, context) => {
|
|
1212
|
+
if (proof.schemaVersion < 2) {
|
|
1213
|
+
if (proof.blockedReasons !== void 0) context.addIssue({
|
|
1214
|
+
code: "custom",
|
|
1215
|
+
message: "blockedReasons is a schemaVersion 2 field; a v1 pr:ready proof must not carry it.",
|
|
1216
|
+
path: ["blockedReasons"]
|
|
1217
|
+
});
|
|
1218
|
+
return;
|
|
1219
|
+
}
|
|
1220
|
+
for (const issue of blockedReasonIssues({
|
|
1221
|
+
...proof,
|
|
1222
|
+
finalReviewPoint: proof.ledger.finalReviewPoint
|
|
1223
|
+
})) context.addIssue(issue);
|
|
1038
1224
|
});
|
|
1039
1225
|
//#endregion
|
|
1040
|
-
export { BOUNDARY_CHECK_SCHEMA_VERSION, BOUNDARY_REVIEW_PROOF_KIND, EVIDENCE_ENVELOPE_SCHEMA_VERSION,
|
|
1226
|
+
export { BLOCKED_REASONS_MAX, BLOCKED_REASON_DETAIL_MAX, BOUNDARY_CHECK_SCHEMA_VERSION, BOUNDARY_REVIEW_PROOF_KIND, EVIDENCE_ENVELOPE_SCHEMA_VERSION, PR_READY_SCHEMA_VERSION, PR_REVIEW_SCHEMA_VERSION, RETRO_ENVELOPE_SCHEMA_VERSION, RETRO_ENVELOPE_VALIDATORS, RETRO_ENVELOPE_WIRE_BOUNDS, SUPPORTED_PR_READY_SCHEMA_VERSIONS, SUPPORTED_PR_VERIFY_SCHEMA_VERSIONS, SUPPORTED_RETRO_ENVELOPE_SCHEMA_VERSIONS, blockedReasonSchema, blockedReasonsSchema, boundaryCheckProofSchema, boundaryReviewProofSchema, closeoutArtifactSchema, evidenceEnvelopeSchema, prReadyProofSchema, prReviewProofSchema, prVerifyProofSchema, retroEnvelopeV1Schema, retroEpicReference, waivedDemandSchema };
|