@patronage/software-factory 0.23.0 → 0.25.0
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 +321 -15
- package/dist/index.js +2775 -1075
- package/dist/schemas.d.ts +48 -4
- package/dist/schemas.js +230 -13
- package/package.json +1 -1
package/dist/schemas.d.ts
CHANGED
|
@@ -19,6 +19,32 @@ interface WaivedDemand {
|
|
|
19
19
|
}
|
|
20
20
|
declare const waivedDemandSchema: z.ZodType<WaivedDemand>;
|
|
21
21
|
//#endregion
|
|
22
|
+
//#region src/blocked-reasons.d.ts
|
|
23
|
+
/**
|
|
24
|
+
* The wire bound on one `detail`. A blocked proof carries one short sentence
|
|
25
|
+
* per demand, never a transcript: HQ's ingest cap is 256 KB and this field
|
|
26
|
+
* must stay far under it even when a candidate blocks on every gate at once.
|
|
27
|
+
*/
|
|
28
|
+
declare const BLOCKED_REASON_DETAIL_MAX = 280;
|
|
29
|
+
/**
|
|
30
|
+
* The wire bound on how many demands one proof may name. Well past any real
|
|
31
|
+
* candidate (there are a dozen fixed demands plus the profile's checks and the
|
|
32
|
+
* live human blockers) and, with the detail bound, keeps the whole field two
|
|
33
|
+
* orders of magnitude under the ingest cap. Bounded here rather than trimmed
|
|
34
|
+
* downstream: HQ rejects an over-long list loudly instead of storing a quietly
|
|
35
|
+
* truncated one.
|
|
36
|
+
*/
|
|
37
|
+
declare const BLOCKED_REASONS_MAX = 100;
|
|
38
|
+
declare const blockedReasonSchema: z.ZodObject<{
|
|
39
|
+
code: z.ZodString;
|
|
40
|
+
detail: z.ZodString;
|
|
41
|
+
}, z.core.$strip>;
|
|
42
|
+
declare const blockedReasonsSchema: z.ZodArray<z.ZodObject<{
|
|
43
|
+
code: z.ZodString;
|
|
44
|
+
detail: z.ZodString;
|
|
45
|
+
}, z.core.$strip>>;
|
|
46
|
+
type BlockedReason = z.infer<typeof blockedReasonSchema>;
|
|
47
|
+
//#endregion
|
|
22
48
|
//#region src/boundary-review-proof.d.ts
|
|
23
49
|
declare const boundaryReviewProofBaseSchema: z.ZodObject<{
|
|
24
50
|
boundary: z.ZodString;
|
|
@@ -551,7 +577,15 @@ type ManagedReadinessLedger = z.infer<typeof managedReadinessLedgerSchema>;
|
|
|
551
577
|
//#endregion
|
|
552
578
|
//#region src/pr-ready.d.ts
|
|
553
579
|
interface PrReadyProof {
|
|
554
|
-
schemaVersion:
|
|
580
|
+
schemaVersion: 2;
|
|
581
|
+
/**
|
|
582
|
+
* Why this run blocked, one entry per refusing demand (#391): `code` is the
|
|
583
|
+
* demand key from the resolver's vocabulary, `detail` the one-sentence
|
|
584
|
+
* refusal. Same refusals as `blockingReasons`, in the same order — the
|
|
585
|
+
* analyzable projection of a flat string list, so a wall of blocked proofs
|
|
586
|
+
* on one PR can be counted by cause. Absent when nothing blocked.
|
|
587
|
+
*/
|
|
588
|
+
blockedReasons?: BlockedReason[];
|
|
555
589
|
blockingReasons: string[];
|
|
556
590
|
humanBlockingReasons: string[];
|
|
557
591
|
command: "patronage-factory pr:ready";
|
|
@@ -1312,8 +1346,18 @@ declare const prMergeCheckProofSchema: z.ZodObject<{
|
|
|
1312
1346
|
worktreePath: z.ZodString;
|
|
1313
1347
|
}, z.core.$strip>>;
|
|
1314
1348
|
}, z.core.$strip>;
|
|
1315
|
-
declare const PR_READY_SCHEMA_VERSION =
|
|
1349
|
+
declare const PR_READY_SCHEMA_VERSION = 2;
|
|
1350
|
+
/**
|
|
1351
|
+
* The pr:ready proof versions a reader still accepts. `pr:ready` emits v2 only
|
|
1352
|
+
* (#391) — one current contract — but v1 events were spooled before the bump
|
|
1353
|
+
* and HQ must ingest them without degrading, so the wire schema parses both.
|
|
1354
|
+
*/
|
|
1355
|
+
declare const SUPPORTED_PR_READY_SCHEMA_VERSIONS: readonly [1, 2];
|
|
1316
1356
|
declare const prReadyProofSchema: z.ZodObject<{
|
|
1357
|
+
blockedReasons: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
1358
|
+
code: z.ZodString;
|
|
1359
|
+
detail: z.ZodString;
|
|
1360
|
+
}, z.core.$strip>>>;
|
|
1317
1361
|
blockingReasons: z.ZodArray<z.ZodString>;
|
|
1318
1362
|
command: z.ZodLiteral<"patronage-factory pr:ready">;
|
|
1319
1363
|
followUp: z.ZodOptional<z.ZodObject<{
|
|
@@ -1629,7 +1673,7 @@ declare const prReadyProofSchema: z.ZodObject<{
|
|
|
1629
1673
|
command: z.ZodString;
|
|
1630
1674
|
}, z.core.$strip>>>;
|
|
1631
1675
|
repository: z.ZodOptional<z.ZodString>;
|
|
1632
|
-
schemaVersion: z.
|
|
1676
|
+
schemaVersion: z.ZodNumber & z.ZodType<1 | 2, number, z.core.$ZodTypeInternals<1 | 2, number>>;
|
|
1633
1677
|
status: z.ZodEnum<{
|
|
1634
1678
|
ready: "ready";
|
|
1635
1679
|
blocked: "blocked";
|
|
@@ -1637,4 +1681,4 @@ declare const prReadyProofSchema: z.ZodObject<{
|
|
|
1637
1681
|
}>;
|
|
1638
1682
|
}, z.core.$strip>;
|
|
1639
1683
|
//#endregion
|
|
1640
|
-
export { BOUNDARY_CHECK_SCHEMA_VERSION, BOUNDARY_REVIEW_PROOF_KIND, BoundaryCheckProofRecord, type BoundaryReviewProof, type CloseoutArtifact, EVIDENCE_ENVELOPE_SCHEMA_VERSION, type EvidenceEnvelope, PR_MERGE_CHECK_SCHEMA_VERSION, PR_READY_SCHEMA_VERSION, PR_REVIEW_SCHEMA_VERSION, type ParsedRetroEnvelope, type PrMergeCheckProof, type PrReadyProof, type PrReviewProof, type PrVerifyProof, RETRO_ENVELOPE_SCHEMA_VERSION, RETRO_ENVELOPE_VALIDATORS, RETRO_ENVELOPE_WIRE_BOUNDS, type RetroEnvelope, SUPPORTED_PR_VERIFY_SCHEMA_VERSIONS, SUPPORTED_RETRO_ENVELOPE_SCHEMA_VERSIONS, type WaivedDemand, boundaryCheckProofSchema, boundaryReviewProofSchema, closeoutArtifactSchema, evidenceEnvelopeSchema, prMergeCheckProofSchema, prReadyProofSchema, prReviewProofSchema, prVerifyProofSchema, retroEnvelopeV1Schema, retroEpicReference, waivedDemandSchema };
|
|
1684
|
+
export { BLOCKED_REASONS_MAX, BLOCKED_REASON_DETAIL_MAX, BOUNDARY_CHECK_SCHEMA_VERSION, BOUNDARY_REVIEW_PROOF_KIND, type BlockedReason, BoundaryCheckProofRecord, type BoundaryReviewProof, type CloseoutArtifact, EVIDENCE_ENVELOPE_SCHEMA_VERSION, type EvidenceEnvelope, PR_MERGE_CHECK_SCHEMA_VERSION, PR_READY_SCHEMA_VERSION, PR_REVIEW_SCHEMA_VERSION, type ParsedRetroEnvelope, type PrMergeCheckProof, type PrReadyProof, type PrReviewProof, type PrVerifyProof, RETRO_ENVELOPE_SCHEMA_VERSION, RETRO_ENVELOPE_VALIDATORS, RETRO_ENVELOPE_WIRE_BOUNDS, type RetroEnvelope, SUPPORTED_PR_READY_SCHEMA_VERSIONS, SUPPORTED_PR_VERIFY_SCHEMA_VERSIONS, SUPPORTED_RETRO_ENVELOPE_SCHEMA_VERSIONS, type WaivedDemand, blockedReasonSchema, blockedReasonsSchema, boundaryCheckProofSchema, boundaryReviewProofSchema, closeoutArtifactSchema, evidenceEnvelopeSchema, prMergeCheckProofSchema, prReadyProofSchema, prReviewProofSchema, prVerifyProofSchema, retroEnvelopeV1Schema, retroEpicReference, waivedDemandSchema };
|
package/dist/schemas.js
CHANGED
|
@@ -1,21 +1,217 @@
|
|
|
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
|
+
/** The PR is still a draft. */
|
|
14
|
+
draft: "draft",
|
|
15
|
+
/** The candidate is not the intended final human review point. */
|
|
16
|
+
finalReviewPoint: "final-review-point",
|
|
17
|
+
/** GitHub's own check rollup for the candidate head. */
|
|
18
|
+
githubChecks: "github-checks",
|
|
19
|
+
/** Local HEAD and the GitHub PR head must be the same commit. */
|
|
20
|
+
headIdentity: "head-identity",
|
|
21
|
+
/** An unhandled post-readiness human comment or review submission. */
|
|
22
|
+
humanBlocker: "human-blocker",
|
|
23
|
+
/** The repository-wide merge freeze. */
|
|
24
|
+
mergeFreeze: "merge-freeze",
|
|
25
|
+
/** GitHub's mergeability / merge-state rollup. */
|
|
26
|
+
mergeState: "merge-state",
|
|
27
|
+
/** The PR body's required rendered sections. */
|
|
28
|
+
prBodySections: "pr-body-sections",
|
|
29
|
+
/** A current, head-bound, passing typed `pr:verify` proof. */
|
|
30
|
+
prVerify: "pr-verify",
|
|
31
|
+
/** The profile-resolved review ladder policy. */
|
|
32
|
+
reviewLadder: "review-ladder",
|
|
33
|
+
/** Unresolved GitHub review threads. */
|
|
34
|
+
reviewThreads: "review-threads",
|
|
35
|
+
/** An explicit trivial waiver that the diff does not support. */
|
|
36
|
+
trivialWaiver: "trivial-waiver"
|
|
37
|
+
};
|
|
38
|
+
/** The families whose instances are named by a resolved value. */
|
|
39
|
+
const QUALIFIED_DEMAND_FAMILIES = [
|
|
40
|
+
"required-check",
|
|
41
|
+
"review-mode",
|
|
42
|
+
"review-rung"
|
|
43
|
+
];
|
|
44
|
+
const QUALIFIER_PATTERN = /^[A-Za-z0-9._%/-]{1,64}$/u;
|
|
45
|
+
const percentEncode = (character) => {
|
|
46
|
+
const code = character.codePointAt(0) ?? 0;
|
|
47
|
+
if (code >= 55296 && code <= 57343) return `%u${code.toString(16).toUpperCase().padStart(4, "0")}`;
|
|
48
|
+
return [...new TextEncoder().encode(character)].map((byte) => `%${byte.toString(16).toUpperCase().padStart(2, "0")}`).join("");
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* Express a resolved value as a qualifier. Percent-encoding, not scrubbing:
|
|
52
|
+
* the mapping is injective, so two differently named required checks can never
|
|
53
|
+
* collapse into one key — which would let one operator waiver silently cover a
|
|
54
|
+
* demand nobody waived, and make HQ count two causes as one.
|
|
55
|
+
*
|
|
56
|
+
* Deterministic in both directions of use: the key a `pr:ready` proof records
|
|
57
|
+
* is the key a `pr:merge-check` waiver matches.
|
|
58
|
+
*/
|
|
59
|
+
const demandQualifier = (value) => value.replaceAll(/[^A-Za-z0-9._/-]/gu, percentEncode);
|
|
60
|
+
const FIXED_DEMAND_KEYS = Object.values(DEMAND_KEYS);
|
|
61
|
+
/** The review modes a profile can resolve, and therefore demand. */
|
|
62
|
+
const REVIEW_MODE_DEMAND_VALUES = ["correctness", "security"];
|
|
63
|
+
const escapePattern = /%(?:u[0-9A-F]{4}|[0-9A-F]{2})/gu;
|
|
64
|
+
/**
|
|
65
|
+
* Reverse `demandQualifier`. A qualifier is canonical exactly when encoding
|
|
66
|
+
* its decoded form reproduces it — which rejects both malformed escapes and
|
|
67
|
+
* noncanonical aliases like `%41` for `A`. Returns undefined when the
|
|
68
|
+
* qualifier cannot have been minted here.
|
|
69
|
+
*/
|
|
70
|
+
const decodeQualifier = (qualifier) => {
|
|
71
|
+
const bytes = [];
|
|
72
|
+
let decoded = "";
|
|
73
|
+
let index = 0;
|
|
74
|
+
const flush = () => {
|
|
75
|
+
if (bytes.length === 0) return true;
|
|
76
|
+
try {
|
|
77
|
+
decoded += new TextDecoder("utf-8", {
|
|
78
|
+
fatal: true,
|
|
79
|
+
ignoreBOM: true
|
|
80
|
+
}).decode(Uint8Array.from(bytes));
|
|
81
|
+
} catch {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
bytes.length = 0;
|
|
85
|
+
return true;
|
|
86
|
+
};
|
|
87
|
+
while (index < qualifier.length) {
|
|
88
|
+
const character = qualifier[index];
|
|
89
|
+
if (character !== "%") {
|
|
90
|
+
if (!flush()) return;
|
|
91
|
+
decoded += character;
|
|
92
|
+
index += 1;
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
escapePattern.lastIndex = index;
|
|
96
|
+
const escape = escapePattern.exec(qualifier);
|
|
97
|
+
if (!escape || escape.index !== index) return;
|
|
98
|
+
if (escape[0][1] === "u") {
|
|
99
|
+
if (!flush()) return;
|
|
100
|
+
decoded += String.fromCodePoint(Number.parseInt(escape[0].slice(2), 16));
|
|
101
|
+
} else bytes.push(Number.parseInt(escape[0].slice(1), 16));
|
|
102
|
+
index += escape[0].length;
|
|
103
|
+
}
|
|
104
|
+
return flush() ? decoded : void 0;
|
|
105
|
+
};
|
|
106
|
+
const qualifierIsMintable = (family, qualifier) => {
|
|
107
|
+
if (!QUALIFIER_PATTERN.test(qualifier)) return false;
|
|
108
|
+
const decoded = decodeQualifier(qualifier);
|
|
109
|
+
if (decoded === void 0 || demandQualifier(decoded) !== qualifier) return false;
|
|
110
|
+
if (family === "review-rung") return EVIDENCE_REVIEW_RUNGS$1.includes(decoded);
|
|
111
|
+
if (family === "review-mode") return REVIEW_MODE_DEMAND_VALUES.includes(decoded);
|
|
112
|
+
return true;
|
|
113
|
+
};
|
|
114
|
+
/**
|
|
115
|
+
* A demand key from the closed vocabulary: a known unqualified family, or a
|
|
116
|
+
* known qualified family with a qualifier this module could actually have
|
|
117
|
+
* minted. Closed on purpose — an invented family, an unresolvable rung, or a
|
|
118
|
+
* noncanonical encoding would be indistinguishable from a typo to anyone
|
|
119
|
+
* counting causes, which is the whole point of recording codes.
|
|
120
|
+
*/
|
|
121
|
+
const demandKeySchema = z.string().refine((value) => {
|
|
122
|
+
if (FIXED_DEMAND_KEYS.includes(value)) return true;
|
|
123
|
+
const separator = value.indexOf(":");
|
|
124
|
+
if (separator === -1) return false;
|
|
125
|
+
const family = value.slice(0, separator);
|
|
126
|
+
return QUALIFIED_DEMAND_FAMILIES.includes(family) && qualifierIsMintable(family, value.slice(separator + 1));
|
|
127
|
+
}, { message: `a demand key is one of ${FIXED_DEMAND_KEYS.join(", ")} or a resolvable ${QUALIFIED_DEMAND_FAMILIES.join(" / ")} key` });
|
|
128
|
+
/**
|
|
129
|
+
* The key shape an operator may *name* in a waiver: syntactic, not closed.
|
|
130
|
+
*
|
|
131
|
+
* Deliberately distinct from `demandKeySchema` above, which is what a proof's
|
|
132
|
+
* reason codes must come from. A waiver store is durable operator evidence
|
|
133
|
+
* written before today's vocabulary existed, so tightening its validation
|
|
134
|
+
* would make an existing record parse as no waiver and let the next write drop
|
|
135
|
+
* it. A waiver naming a key nobody resolves is already inert — `pr:merge-check`
|
|
136
|
+
* reports it as an unapplied waiver — so nothing is admitted by accepting it.
|
|
137
|
+
*/
|
|
138
|
+
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).");
|
|
139
|
+
//#endregion
|
|
140
|
+
//#region src/blocked-reasons.ts
|
|
141
|
+
/**
|
|
142
|
+
* The wire bound on one `detail`. A blocked proof carries one short sentence
|
|
143
|
+
* per demand, never a transcript: HQ's ingest cap is 256 KB and this field
|
|
144
|
+
* must stay far under it even when a candidate blocks on every gate at once.
|
|
145
|
+
*/
|
|
146
|
+
const BLOCKED_REASON_DETAIL_MAX = 280;
|
|
147
|
+
/**
|
|
148
|
+
* The wire bound on how many demands one proof may name. Well past any real
|
|
149
|
+
* candidate (there are a dozen fixed demands plus the profile's checks and the
|
|
150
|
+
* live human blockers) and, with the detail bound, keeps the whole field two
|
|
151
|
+
* orders of magnitude under the ingest cap. Bounded here rather than trimmed
|
|
152
|
+
* downstream: HQ rejects an over-long list loudly instead of storing a quietly
|
|
153
|
+
* truncated one.
|
|
154
|
+
*/
|
|
155
|
+
const BLOCKED_REASONS_MAX = 100;
|
|
156
|
+
/** One refusal sentence: single line, trimmed, bounded. */
|
|
157
|
+
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" });
|
|
158
|
+
const blockedReasonSchema = z.object({
|
|
159
|
+
code: demandKeySchema,
|
|
160
|
+
detail: blockedReasonDetailSchema
|
|
161
|
+
});
|
|
162
|
+
const blockedReasonsSchema = z.array(blockedReasonSchema).max(100);
|
|
163
|
+
/** Fit one refusal sentence to the wire bound without losing its head. */
|
|
164
|
+
const blockedReasonDetail = (reason) => {
|
|
165
|
+
const line = reason.replaceAll(/\s+/gu, " ").trim();
|
|
166
|
+
return line.length <= 280 ? line : `${line.slice(0, 279)}…`;
|
|
167
|
+
};
|
|
168
|
+
const blockedReasonIssue = (message) => [{
|
|
169
|
+
code: "custom",
|
|
170
|
+
message,
|
|
171
|
+
path: ["blockedReasons"]
|
|
172
|
+
}];
|
|
173
|
+
/**
|
|
174
|
+
* The invariant every reader enforces, shared by the emitting schema and the
|
|
175
|
+
* wire schema so it is one rule rather than two that can drift. It is the
|
|
176
|
+
* producer's state machine, written down:
|
|
177
|
+
*
|
|
178
|
+
* - `ready` refused nothing, so it carries neither projection, and it is the
|
|
179
|
+
* final review point;
|
|
180
|
+
* - `slice-ready/not-final` was held back by exactly one demand — being a
|
|
181
|
+
* slice — and names it;
|
|
182
|
+
* - `blocked` refused something other than being a slice, and says so;
|
|
183
|
+
* - whichever it is, `blockedReasons` names every refusal listed in
|
|
184
|
+
* `blockingReasons`, in the same order, as the bounded form of that sentence.
|
|
185
|
+
*
|
|
186
|
+
* Naming the slice demand and the ledger's `finalReviewPoint` are the same
|
|
187
|
+
* fact, so a proof that says one and not the other is refused.
|
|
188
|
+
*/
|
|
189
|
+
const blockedReasonIssues = (proof) => {
|
|
190
|
+
const named = proof.blockedReasons ?? [];
|
|
191
|
+
const reasons = proof.blockingReasons ?? [];
|
|
192
|
+
const slice = DEMAND_KEYS.finalReviewPoint;
|
|
193
|
+
const namesSlice = named.some((reason) => reason.code === slice);
|
|
194
|
+
if (proof.status === "ready") {
|
|
195
|
+
if (named.length > 0 || reasons.length > 0) return blockedReasonIssue("a ready pr:ready proof must carry no blocking reasons");
|
|
196
|
+
} else if (proof.status === "slice-ready/not-final") {
|
|
197
|
+
if (named.length !== 1 || !namesSlice) return blockedReasonIssue(`a slice-ready/not-final pr:ready proof is held back by exactly one demand, ${slice}`);
|
|
198
|
+
} else if (reasons.length === 0) return blockedReasonIssue("a blocked pr:ready proof must record what blocked it");
|
|
199
|
+
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}`);
|
|
200
|
+
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`);
|
|
201
|
+
if (named.length !== reasons.length) return blockedReasonIssue(`blockedReasons must name every blocking reason: ${reasons.length} reason(s), ${named.length} named`);
|
|
202
|
+
const drifted = named.findIndex((reason, index) => reason.detail !== blockedReasonDetail(reasons[index]));
|
|
203
|
+
return drifted === -1 ? [] : blockedReasonIssue(`blockedReasons[${drifted}] does not carry blocking reason ${drifted}; the two projections must tell one story`);
|
|
204
|
+
};
|
|
205
|
+
//#endregion
|
|
2
206
|
//#region src/demand-waiver.ts
|
|
3
207
|
const DEMAND_WAIVER_SCHEMA_VERSION = 1;
|
|
4
208
|
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
209
|
const demandWaiverSchema = z.object({
|
|
14
210
|
candidate: z.object({
|
|
15
211
|
headSha: shaSchema,
|
|
16
212
|
pr: z.number().int().positive()
|
|
17
213
|
}),
|
|
18
|
-
demand:
|
|
214
|
+
demand: waiverDemandKeySchema,
|
|
19
215
|
operator: z.string().trim().min(1),
|
|
20
216
|
rationale: z.string().trim().min(1),
|
|
21
217
|
recordedAt: z.iso.datetime(),
|
|
@@ -27,7 +223,7 @@ z.object({
|
|
|
27
223
|
waivers: z.array(demandWaiverSchema)
|
|
28
224
|
});
|
|
29
225
|
const waivedDemandSchema = z.object({
|
|
30
|
-
demand:
|
|
226
|
+
demand: waiverDemandKeySchema,
|
|
31
227
|
operator: z.string().trim().min(1),
|
|
32
228
|
rationale: z.string().trim().min(1),
|
|
33
229
|
recordedAt: z.iso.datetime(),
|
|
@@ -1018,8 +1214,16 @@ const managedReadinessLedgerSchema = z.object({
|
|
|
1018
1214
|
verifiedHeadSha: z.string().optional()
|
|
1019
1215
|
})
|
|
1020
1216
|
});
|
|
1021
|
-
const PR_READY_SCHEMA_VERSION =
|
|
1217
|
+
const PR_READY_SCHEMA_VERSION = 2;
|
|
1218
|
+
/**
|
|
1219
|
+
* The pr:ready proof versions a reader still accepts. `pr:ready` emits v2 only
|
|
1220
|
+
* (#391) — one current contract — but v1 events were spooled before the bump
|
|
1221
|
+
* and HQ must ingest them without degrading, so the wire schema parses both.
|
|
1222
|
+
*/
|
|
1223
|
+
const SUPPORTED_PR_READY_SCHEMA_VERSIONS = [1, 2];
|
|
1224
|
+
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
1225
|
const prReadyProofSchema = z.object({
|
|
1226
|
+
blockedReasons: blockedReasonsSchema.optional(),
|
|
1023
1227
|
blockingReasons: z.array(z.string()),
|
|
1024
1228
|
command: z.literal("patronage-factory pr:ready"),
|
|
1025
1229
|
followUp: followUpActionSchema.optional(),
|
|
@@ -1029,12 +1233,25 @@ const prReadyProofSchema = z.object({
|
|
|
1029
1233
|
profilePath: z.string().min(1).optional(),
|
|
1030
1234
|
repairs: z.array(readinessRepairSchema).default([]),
|
|
1031
1235
|
repository: z.string().regex(/^[^/\s]+\/[^/\s]+$/u).optional(),
|
|
1032
|
-
schemaVersion:
|
|
1236
|
+
schemaVersion: prReadySchemaVersionSchema,
|
|
1033
1237
|
status: z.enum([
|
|
1034
1238
|
"ready",
|
|
1035
1239
|
"blocked",
|
|
1036
1240
|
"slice-ready/not-final"
|
|
1037
1241
|
])
|
|
1242
|
+
}).superRefine((proof, context) => {
|
|
1243
|
+
if (proof.schemaVersion < 2) {
|
|
1244
|
+
if (proof.blockedReasons !== void 0) context.addIssue({
|
|
1245
|
+
code: "custom",
|
|
1246
|
+
message: "blockedReasons is a schemaVersion 2 field; a v1 pr:ready proof must not carry it.",
|
|
1247
|
+
path: ["blockedReasons"]
|
|
1248
|
+
});
|
|
1249
|
+
return;
|
|
1250
|
+
}
|
|
1251
|
+
for (const issue of blockedReasonIssues({
|
|
1252
|
+
...proof,
|
|
1253
|
+
finalReviewPoint: proof.ledger.finalReviewPoint
|
|
1254
|
+
})) context.addIssue(issue);
|
|
1038
1255
|
});
|
|
1039
1256
|
//#endregion
|
|
1040
|
-
export { BOUNDARY_CHECK_SCHEMA_VERSION, BOUNDARY_REVIEW_PROOF_KIND, EVIDENCE_ENVELOPE_SCHEMA_VERSION, PR_MERGE_CHECK_SCHEMA_VERSION, PR_READY_SCHEMA_VERSION, PR_REVIEW_SCHEMA_VERSION, RETRO_ENVELOPE_SCHEMA_VERSION, RETRO_ENVELOPE_VALIDATORS, RETRO_ENVELOPE_WIRE_BOUNDS, SUPPORTED_PR_VERIFY_SCHEMA_VERSIONS, SUPPORTED_RETRO_ENVELOPE_SCHEMA_VERSIONS, boundaryCheckProofSchema, boundaryReviewProofSchema, closeoutArtifactSchema, evidenceEnvelopeSchema, prMergeCheckProofSchema, prReadyProofSchema, prReviewProofSchema, prVerifyProofSchema, retroEnvelopeV1Schema, retroEpicReference, waivedDemandSchema };
|
|
1257
|
+
export { BLOCKED_REASONS_MAX, BLOCKED_REASON_DETAIL_MAX, BOUNDARY_CHECK_SCHEMA_VERSION, BOUNDARY_REVIEW_PROOF_KIND, EVIDENCE_ENVELOPE_SCHEMA_VERSION, PR_MERGE_CHECK_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, prMergeCheckProofSchema, prReadyProofSchema, prReviewProofSchema, prVerifyProofSchema, retroEnvelopeV1Schema, retroEpicReference, waivedDemandSchema };
|