@patronage/software-factory 1.0.0-alpha.4 → 1.0.0-alpha.41
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 → GLOSSARY.md} +10 -0
- package/README.md +17 -7
- package/dist/cli.js +12 -0
- package/dist/index.d.ts +3733 -3084
- package/dist/index.js +22696 -13794
- package/dist/review-prompts/security/SKILL.md +1 -0
- package/dist/review-prompts/security/references/javascript-typescript.md +1 -0
- package/dist/schemas.d.ts +664 -1265
- package/dist/schemas.js +911 -762
- package/package.json +16 -17
package/dist/schemas.js
CHANGED
|
@@ -2,23 +2,31 @@ import { z } from "zod";
|
|
|
2
2
|
import "yaml";
|
|
3
3
|
import "picomatch";
|
|
4
4
|
//#region src/review-rungs.ts
|
|
5
|
-
const EVIDENCE_REVIEW_RUNGS
|
|
5
|
+
const EVIDENCE_REVIEW_RUNGS = [
|
|
6
6
|
"independent-model",
|
|
7
7
|
"oracle",
|
|
8
8
|
"human"
|
|
9
9
|
];
|
|
10
|
-
Object.fromEntries(EVIDENCE_REVIEW_RUNGS
|
|
10
|
+
Object.fromEntries(EVIDENCE_REVIEW_RUNGS.map((rung, index) => [rung, index]));
|
|
11
11
|
//#endregion
|
|
12
12
|
//#region src/demand-keys.ts
|
|
13
13
|
/** The demands that exist at most once per candidate. */
|
|
14
14
|
const DEMAND_KEYS = {
|
|
15
15
|
/**
|
|
16
16
|
* The boundary wave that authorizes this candidate's merge schedule (#477).
|
|
17
|
-
* In force exactly when GitHub already has auto-merge enabled
|
|
18
|
-
*
|
|
19
|
-
*
|
|
17
|
+
* In force exactly when GitHub already has auto-merge enabled on a candidate
|
|
18
|
+
* that the standalone default does not authorize (#1299): a live schedule on
|
|
19
|
+
* an epic member with no wave demand resolved is an unauthorized merge
|
|
20
|
+
* waiting to happen.
|
|
20
21
|
*/
|
|
21
22
|
boundaryWave: "boundary-wave",
|
|
23
|
+
/**
|
|
24
|
+
* Closeout pin (#901): a merged interior's declared child issue is still
|
|
25
|
+
* open. The factory does not close issues; the operator closes, or waives
|
|
26
|
+
* this demand for that interior PR. In force at `boundary:check --final`,
|
|
27
|
+
* not at interior `boundary:check` and not at `pr:ready`.
|
|
28
|
+
*/
|
|
29
|
+
childIssueOpen: "child-issue-open",
|
|
22
30
|
/** The PR is still a draft. */
|
|
23
31
|
draft: "draft",
|
|
24
32
|
/** GitHub's own check rollup for the candidate head. */
|
|
@@ -37,10 +45,26 @@ const DEMAND_KEYS = {
|
|
|
37
45
|
* neither a review-rung authority nor a scheduling requirement.
|
|
38
46
|
*/
|
|
39
47
|
nativeApproval: "native-approval",
|
|
48
|
+
/**
|
|
49
|
+
* The policy identity an earlier stage recorded must be the one readiness
|
|
50
|
+
* resolved (#922 Slice B). In force when a consumed `pr:verify` or
|
|
51
|
+
* `pr:review` proof judged this candidate under a different review policy,
|
|
52
|
+
* or recorded none where the resolved demand includes a security review.
|
|
53
|
+
*/
|
|
54
|
+
policyIdentity: "policy-identity",
|
|
40
55
|
/** A current, head-bound, passing typed `pr:verify` proof. */
|
|
41
56
|
prVerify: "pr-verify",
|
|
42
57
|
/** The profile-resolved review ladder policy. */
|
|
43
58
|
reviewLadder: "review-ladder",
|
|
59
|
+
/**
|
|
60
|
+
* A review round past the second on one pull request (#1280). A round is
|
|
61
|
+
* every review recorded at one patch, so a pair of clean sessions at one
|
|
62
|
+
* head is one round. Only an operator waiver clears it: `pr:review
|
|
63
|
+
* --assemble` refuses to start a third round without one, and `pr:ready`
|
|
64
|
+
* refuses a ladder that holds more than two. The waiver rationale carries
|
|
65
|
+
* the operator's disposition.
|
|
66
|
+
*/
|
|
67
|
+
reviewRoundLimit: "review-round-limit",
|
|
44
68
|
/** Unresolved GitHub review threads. */
|
|
45
69
|
reviewThreads: "review-threads"
|
|
46
70
|
};
|
|
@@ -116,7 +140,7 @@ const qualifierIsMintable = (family, qualifier) => {
|
|
|
116
140
|
if (!QUALIFIER_PATTERN.test(qualifier)) return false;
|
|
117
141
|
const decoded = decodeQualifier(qualifier);
|
|
118
142
|
if (decoded === void 0 || demandQualifier(decoded) !== qualifier) return false;
|
|
119
|
-
if (family === "review-rung") return EVIDENCE_REVIEW_RUNGS
|
|
143
|
+
if (family === "review-rung") return EVIDENCE_REVIEW_RUNGS.includes(decoded);
|
|
120
144
|
if (family === "review-mode") return REVIEW_MODE_DEMAND_VALUES.includes(decoded);
|
|
121
145
|
return true;
|
|
122
146
|
};
|
|
@@ -228,10 +252,675 @@ const waivedDemandSchema = z.object({
|
|
|
228
252
|
session: z.string().trim().min(1).optional(),
|
|
229
253
|
unmetReasons: z.array(z.string().min(1)).min(1)
|
|
230
254
|
});
|
|
255
|
+
//#endregion
|
|
256
|
+
//#region src/diff-classification.ts
|
|
257
|
+
const DIFF_CLASSIFICATIONS = [
|
|
258
|
+
"docs/process-only",
|
|
259
|
+
"trivial",
|
|
260
|
+
"non-trivial"
|
|
261
|
+
];
|
|
262
|
+
//#endregion
|
|
263
|
+
//#region src/policy-resolution.ts
|
|
264
|
+
const COMMIT_SHA_PATTERN = /^[0-9a-f]{40}$/u;
|
|
265
|
+
const digestSchema = z.string().regex(/^[0-9a-f]{64}$/u);
|
|
266
|
+
/**
|
|
267
|
+
* The authority resolved: the live tip of the PR's own base ref served a
|
|
268
|
+
* parseable profile, and demand was resolved from the union of that policy and
|
|
269
|
+
* the candidate's.
|
|
270
|
+
*
|
|
271
|
+
* Every identity field is REQUIRED. A cross-stage reader compares these digests
|
|
272
|
+
* to decide whether two stages judged the same candidate under the same policy;
|
|
273
|
+
* an `authoritative` record with a digest missing would read to that reader as
|
|
274
|
+
* "nothing disagrees", which is a silent downgrade of exactly the demand this
|
|
275
|
+
* record exists to protect. `reason` is forbidden here — a resolved authority
|
|
276
|
+
* has nothing to explain.
|
|
277
|
+
*/
|
|
278
|
+
const authoritativePolicyResolutionSchema = z.object({
|
|
279
|
+
/** sha256 of the canonical base `review` subtree. */
|
|
280
|
+
basePolicyDigest: digestSchema,
|
|
281
|
+
/** The PR's own base ref, whose live tip is the authority. */
|
|
282
|
+
baseRefName: z.string().min(1),
|
|
283
|
+
/** sha256 of the canonical candidate `review` subtree. */
|
|
284
|
+
candidatePolicyDigest: digestSchema,
|
|
285
|
+
/** sha256 of the merged (base ∪ candidate) protected-path set. */
|
|
286
|
+
effectiveDigest: digestSchema,
|
|
287
|
+
/** The base-ref tip the base policy was read at. */
|
|
288
|
+
policyBaseSha: z.string().regex(COMMIT_SHA_PATTERN),
|
|
289
|
+
/**
|
|
290
|
+
* Whether the candidate's `review` subtree differs from the base's. The
|
|
291
|
+
* digests prove which policies were read; this says whether they agreed,
|
|
292
|
+
* which is the fact that explains a demand the candidate's own profile
|
|
293
|
+
* would not have produced.
|
|
294
|
+
*/
|
|
295
|
+
reviewPolicyChanged: z.boolean(),
|
|
296
|
+
status: z.literal("authoritative")
|
|
297
|
+
}).strict();
|
|
298
|
+
/**
|
|
299
|
+
* The authority could not be read, and `reason` says why — required, because an
|
|
300
|
+
* unresolved record with no reason is an unactionable refusal. `pr:ready`
|
|
301
|
+
* refuses before it writes a proof, so a Slice A ready proof never carries one;
|
|
302
|
+
* this member is the recorded form of that refusal for the stages that carry it
|
|
303
|
+
* forward.
|
|
304
|
+
*/
|
|
305
|
+
const unresolvedPolicyResolutionSchema = z.object({
|
|
306
|
+
/** The base ref whose policy could not be read. */
|
|
307
|
+
baseRefName: z.string().min(1),
|
|
308
|
+
cause: z.enum([
|
|
309
|
+
"no-pull-request",
|
|
310
|
+
"lookup-failed",
|
|
311
|
+
"policy-unreadable"
|
|
312
|
+
]).optional(),
|
|
313
|
+
/** Why the authority is unresolved. */
|
|
314
|
+
reason: z.string().min(1),
|
|
315
|
+
status: z.literal("unresolved")
|
|
316
|
+
}).strict();
|
|
317
|
+
/** How a gate resolved the review policy it evaluated a candidate under. */
|
|
318
|
+
const policyResolutionSchema = z.discriminatedUnion("status", [authoritativePolicyResolutionSchema, unresolvedPolicyResolutionSchema]);
|
|
319
|
+
//#endregion
|
|
320
|
+
//#region src/packages/review-prompt-sections/schema.ts
|
|
321
|
+
const REVIEW_PROMPT_SECTION_PROVENANCES = [
|
|
322
|
+
"coding-standards",
|
|
323
|
+
"prior-review-ledger",
|
|
324
|
+
"issue-review-focus"
|
|
325
|
+
];
|
|
326
|
+
const nonBlankString$2 = z.string().refine((value) => value.trim().length > 0, { message: "must not be blank" });
|
|
327
|
+
const reviewPromptSectionSchema = z.object({
|
|
328
|
+
provenance: z.enum(REVIEW_PROMPT_SECTION_PROVENANCES),
|
|
329
|
+
source: nonBlankString$2,
|
|
330
|
+
text: nonBlankString$2
|
|
331
|
+
}).strict();
|
|
332
|
+
const promptSectionRank = (section) => REVIEW_PROMPT_SECTION_PROVENANCES.indexOf(section.provenance);
|
|
333
|
+
const hasDuplicateProvenance = (sections) => new Set(sections.map((section) => section.provenance)).size !== sections.length;
|
|
334
|
+
const isOutOfAssemblyOrder = (sections) => sections.some((section, index) => index > 0 && promptSectionRank(section) < promptSectionRank(sections[index - 1]));
|
|
335
|
+
const reviewPromptSectionsSchema = z.array(reviewPromptSectionSchema).min(1).refine((sections) => !hasDuplicateProvenance(sections) && !isOutOfAssemblyOrder(sections), { message: "prompt sections must be in assembly order without duplicates" });
|
|
336
|
+
new Uint32Array([
|
|
337
|
+
1116352408,
|
|
338
|
+
1899447441,
|
|
339
|
+
3049323471,
|
|
340
|
+
3921009573,
|
|
341
|
+
961987163,
|
|
342
|
+
1508970993,
|
|
343
|
+
2453635748,
|
|
344
|
+
2870763221,
|
|
345
|
+
3624381080,
|
|
346
|
+
310598401,
|
|
347
|
+
607225278,
|
|
348
|
+
1426881987,
|
|
349
|
+
1925078388,
|
|
350
|
+
2162078206,
|
|
351
|
+
2614888103,
|
|
352
|
+
3248222580,
|
|
353
|
+
3835390401,
|
|
354
|
+
4022224774,
|
|
355
|
+
264347078,
|
|
356
|
+
604807628,
|
|
357
|
+
770255983,
|
|
358
|
+
1249150122,
|
|
359
|
+
1555081692,
|
|
360
|
+
1996064986,
|
|
361
|
+
2554220882,
|
|
362
|
+
2821834349,
|
|
363
|
+
2952996808,
|
|
364
|
+
3210313671,
|
|
365
|
+
3336571891,
|
|
366
|
+
3584528711,
|
|
367
|
+
113926993,
|
|
368
|
+
338241895,
|
|
369
|
+
666307205,
|
|
370
|
+
773529912,
|
|
371
|
+
1294757372,
|
|
372
|
+
1396182291,
|
|
373
|
+
1695183700,
|
|
374
|
+
1986661051,
|
|
375
|
+
2177026350,
|
|
376
|
+
2456956037,
|
|
377
|
+
2730485921,
|
|
378
|
+
2820302411,
|
|
379
|
+
3259730800,
|
|
380
|
+
3345764771,
|
|
381
|
+
3516065817,
|
|
382
|
+
3600352804,
|
|
383
|
+
4094571909,
|
|
384
|
+
275423344,
|
|
385
|
+
430227734,
|
|
386
|
+
506948616,
|
|
387
|
+
659060556,
|
|
388
|
+
883997877,
|
|
389
|
+
958139571,
|
|
390
|
+
1322822218,
|
|
391
|
+
1537002063,
|
|
392
|
+
1747873779,
|
|
393
|
+
1955562222,
|
|
394
|
+
2024104815,
|
|
395
|
+
2227730452,
|
|
396
|
+
2361852424,
|
|
397
|
+
2428436474,
|
|
398
|
+
2756734187,
|
|
399
|
+
3204031479,
|
|
400
|
+
3329325298
|
|
401
|
+
]);
|
|
402
|
+
new Uint32Array([
|
|
403
|
+
1779033703,
|
|
404
|
+
3144134277,
|
|
405
|
+
1013904242,
|
|
406
|
+
2773480762,
|
|
407
|
+
1359893119,
|
|
408
|
+
2600822924,
|
|
409
|
+
528734635,
|
|
410
|
+
1541459225
|
|
411
|
+
]);
|
|
412
|
+
//#endregion
|
|
413
|
+
//#region src/review-ladder-policy.ts
|
|
414
|
+
const REVIEW_CATEGORIES = [
|
|
415
|
+
"correctness",
|
|
416
|
+
"safety",
|
|
417
|
+
"coordination",
|
|
418
|
+
"maintainability",
|
|
419
|
+
"unknown"
|
|
420
|
+
];
|
|
421
|
+
//#endregion
|
|
422
|
+
//#region src/review-ladder-ledger.ts
|
|
423
|
+
const OCCURRENCE_ADDRESS_PATTERN = /^c[1-9][0-9]*f[1-9][0-9]*$/u;
|
|
424
|
+
const occurrenceAddress = (cycle, finding) => `c${cycle}f${finding}`;
|
|
425
|
+
/** The 1-based recording position an address names. */
|
|
426
|
+
const cycleOfAddress = (address) => Number(/^c(?<cycle>\d+)f/u.exec(address)?.groups?.cycle);
|
|
427
|
+
const POLICY_DECLARATION_RULES = ["cycle-2-severity-floor", "identity-repeat"];
|
|
428
|
+
const definedAttributes = (finding) => Object.fromEntries([
|
|
429
|
+
"blockingAfterCap",
|
|
430
|
+
"category",
|
|
431
|
+
"citedSpan",
|
|
432
|
+
"file",
|
|
433
|
+
"prescribedAction",
|
|
434
|
+
"priority"
|
|
435
|
+
].flatMap((attribute) => finding[attribute] === void 0 ? [] : [[attribute, finding[attribute]]]));
|
|
436
|
+
const AUTHORITY_RANK = {
|
|
437
|
+
operator: 3,
|
|
438
|
+
policy: 1,
|
|
439
|
+
reviewer: 2
|
|
440
|
+
};
|
|
441
|
+
const outranked = (occurrence, declaration) => occurrence.authority !== void 0 && AUTHORITY_RANK[occurrence.authority] > AUTHORITY_RANK[declaration.authority];
|
|
442
|
+
const projectedGroupDisposition = (members) => members.some((member) => member.disposition === "open") ? "open" : members.at(-1)?.disposition ?? "open";
|
|
443
|
+
const invalid = (message) => {
|
|
444
|
+
throw new TypeError(`Invalid ladder history: ${message}`);
|
|
445
|
+
};
|
|
446
|
+
const entryFor = (members, reopenedCount) => {
|
|
447
|
+
const [root] = members;
|
|
448
|
+
const latest = members.at(-1);
|
|
449
|
+
if (!root || !latest) return invalid("a finding group has no members.");
|
|
450
|
+
const disposition = projectedGroupDisposition(members);
|
|
451
|
+
const settled = members.findLast((member) => member.disposition !== "stale-repeat" && member.disposition !== "open");
|
|
452
|
+
const referenced = disposition === "open" ? void 0 : settled ?? latest;
|
|
453
|
+
return {
|
|
454
|
+
...definedAttributes(latest.finding),
|
|
455
|
+
address: root.address,
|
|
456
|
+
disposition,
|
|
457
|
+
firstFlagged: root.cycle,
|
|
458
|
+
flagCount: members.length,
|
|
459
|
+
lastFlagged: latest.cycle,
|
|
460
|
+
occurrences: [...members],
|
|
461
|
+
...disposition === "stale-repeat" && settled ? { priorDisposition: settled.disposition } : {},
|
|
462
|
+
...referenced === void 0 ? {} : { reference: referenced.reference ?? referenced.rule },
|
|
463
|
+
reopenedCount,
|
|
464
|
+
staleRepeatCount: members.filter((member) => member.disposition === "stale-repeat").length,
|
|
465
|
+
title: root.finding.title.trim()
|
|
466
|
+
};
|
|
467
|
+
};
|
|
468
|
+
const recordOccurrences = (cycle, cycleIndex, ref, occurrences, recordedIn, groups, reopened) => {
|
|
469
|
+
const { recording } = cycle;
|
|
470
|
+
if (!recording) return invalid(`cycle ${cycleIndex + 1} carries no recording; the ledger reads recorded occurrences and declarations only.`);
|
|
471
|
+
if (recording.occurrences.length !== cycle.findings.length) return invalid(`cycle ${cycleIndex + 1} records ${recording.occurrences.length} occurrence(s) for ${cycle.findings.length} finding(s).`);
|
|
472
|
+
for (const [findingIndex, record] of recording.occurrences.entries()) {
|
|
473
|
+
const address = occurrenceAddress(cycleIndex + 1, findingIndex + 1);
|
|
474
|
+
const finding = cycle.findings[findingIndex];
|
|
475
|
+
if (record.address !== address || finding === void 0) return invalid(`cycle ${cycleIndex + 1} finding ${findingIndex + 1} is recorded at ${record.address}, not ${address}.`);
|
|
476
|
+
let root = address;
|
|
477
|
+
if (record.repeatOf !== void 0) {
|
|
478
|
+
const target = occurrences.get(record.repeatOf);
|
|
479
|
+
if (!target || (recordedIn.get(record.repeatOf) ?? cycleIndex) >= cycleIndex) return invalid(`${address} repeats ${record.repeatOf}, which names no occurrence recorded in an earlier cycle.`);
|
|
480
|
+
({root} = target);
|
|
481
|
+
}
|
|
482
|
+
const members = groups.get(root) ?? [];
|
|
483
|
+
if (members.length > 0 && projectedGroupDisposition(members) === "fixed-in-thread") reopened.set(root, (reopened.get(root) ?? 0) + 1);
|
|
484
|
+
const occurrence = {
|
|
485
|
+
address,
|
|
486
|
+
cycle: ref,
|
|
487
|
+
disposition: "open",
|
|
488
|
+
finding,
|
|
489
|
+
historical: cycle.historical === true,
|
|
490
|
+
root
|
|
491
|
+
};
|
|
492
|
+
occurrences.set(address, occurrence);
|
|
493
|
+
recordedIn.set(address, cycleIndex);
|
|
494
|
+
groups.set(root, [...members, occurrence]);
|
|
495
|
+
}
|
|
496
|
+
};
|
|
497
|
+
const applyDeclaration = (declaration, cycleIndex, occurrences) => {
|
|
498
|
+
const target = occurrences.get(declaration.target);
|
|
499
|
+
if (!target) return invalid(`cycle ${cycleIndex + 1} declares ${declaration.disposition} on ${declaration.target}, which names no occurrence recorded at or before that cycle.`);
|
|
500
|
+
if (outranked(target, declaration)) return;
|
|
501
|
+
target.disposition = declaration.disposition;
|
|
502
|
+
target.authority = declaration.authority;
|
|
503
|
+
if (declaration.authority === "policy") {
|
|
504
|
+
target.rule = declaration.rule;
|
|
505
|
+
delete target.reference;
|
|
506
|
+
} else {
|
|
507
|
+
delete target.rule;
|
|
508
|
+
if (declaration.reference === void 0) delete target.reference;
|
|
509
|
+
else target.reference = declaration.reference;
|
|
510
|
+
}
|
|
511
|
+
};
|
|
512
|
+
const foldLedger = (cycles, refs) => {
|
|
513
|
+
const occurrences = /* @__PURE__ */ new Map();
|
|
514
|
+
const recordedIn = /* @__PURE__ */ new Map();
|
|
515
|
+
const groups = /* @__PURE__ */ new Map();
|
|
516
|
+
const reopened = /* @__PURE__ */ new Map();
|
|
517
|
+
for (const [cycleIndex, cycle] of cycles.entries()) {
|
|
518
|
+
const ref = refs[cycleIndex];
|
|
519
|
+
if (!ref) return invalid(`cycle ${cycleIndex + 1} has no ladder position.`);
|
|
520
|
+
const declarations = cycle.recording?.declarations ?? [];
|
|
521
|
+
for (const declaration of declarations) if (cycleOfAddress(declaration.target) <= cycleIndex) applyDeclaration(declaration, cycleIndex, occurrences);
|
|
522
|
+
recordOccurrences(cycle, cycleIndex, ref, occurrences, recordedIn, groups, reopened);
|
|
523
|
+
for (const declaration of declarations) if (cycleOfAddress(declaration.target) > cycleIndex) applyDeclaration(declaration, cycleIndex, occurrences);
|
|
524
|
+
}
|
|
525
|
+
const ledger = /* @__PURE__ */ new Map();
|
|
526
|
+
for (const [root, members] of groups) ledger.set(root, entryFor(members, reopened.get(root) ?? 0));
|
|
527
|
+
return {
|
|
528
|
+
ledger,
|
|
529
|
+
occurrences: [...occurrences.values()]
|
|
530
|
+
};
|
|
531
|
+
};
|
|
532
|
+
//#endregion
|
|
533
|
+
//#region src/review-recording-schema.ts
|
|
534
|
+
const ABSENT_PROOF_DIGEST = "absent";
|
|
535
|
+
const sha256Pattern = /^[0-9a-f]{64}$/u;
|
|
536
|
+
const occurrenceAddressSchema$1 = z.string().regex(OCCURRENCE_ADDRESS_PATTERN);
|
|
537
|
+
const declarationRecordSchema = z.union([z.object({
|
|
538
|
+
authority: z.enum(["operator", "reviewer"]),
|
|
539
|
+
disposition: z.enum([
|
|
540
|
+
"fixed-in-thread",
|
|
541
|
+
"follow-up-filed",
|
|
542
|
+
"waived"
|
|
543
|
+
]),
|
|
544
|
+
reference: z.string().min(1).optional(),
|
|
545
|
+
target: occurrenceAddressSchema$1
|
|
546
|
+
}).strict(), z.object({
|
|
547
|
+
authority: z.literal("policy"),
|
|
548
|
+
disposition: z.enum(["policy-accepted", "stale-repeat"]),
|
|
549
|
+
rule: z.enum(POLICY_DECLARATION_RULES),
|
|
550
|
+
target: occurrenceAddressSchema$1
|
|
551
|
+
}).strict()]);
|
|
552
|
+
const recordsShape = {
|
|
553
|
+
declarations: z.array(declarationRecordSchema),
|
|
554
|
+
occurrences: z.array(z.object({
|
|
555
|
+
address: occurrenceAddressSchema$1,
|
|
556
|
+
declaredCycle: z.number().int().positive(),
|
|
557
|
+
repeatOf: occurrenceAddressSchema$1.optional()
|
|
558
|
+
}).strict())
|
|
559
|
+
};
|
|
560
|
+
const reviewCycleRecordingSchema = z.object({
|
|
561
|
+
...recordsShape,
|
|
562
|
+
appliedTo: z.union([z.string().regex(sha256Pattern), z.literal(ABSENT_PROOF_DIGEST)]),
|
|
563
|
+
base: z.object({
|
|
564
|
+
ref: z.string().min(1),
|
|
565
|
+
sha: z.string().regex(/^[0-9a-f]{40}$/u)
|
|
566
|
+
}).strict(),
|
|
567
|
+
dispositionsDigest: z.string().regex(sha256Pattern).optional(),
|
|
568
|
+
findingsDigest: z.string().regex(sha256Pattern),
|
|
569
|
+
recordingId: z.string().regex(sha256Pattern)
|
|
570
|
+
}).strict();
|
|
571
|
+
//#endregion
|
|
572
|
+
//#region src/review-recording.ts
|
|
573
|
+
const occurrenceAddressSchema = z.string().regex(OCCURRENCE_ADDRESS_PATTERN);
|
|
574
|
+
const declarableDisposition = z.enum([
|
|
575
|
+
"fixed-in-thread",
|
|
576
|
+
"follow-up-filed",
|
|
577
|
+
"waived"
|
|
578
|
+
]);
|
|
579
|
+
z.array(z.union([z.object({
|
|
580
|
+
disposition: declarableDisposition,
|
|
581
|
+
reference: z.string().min(1).optional(),
|
|
582
|
+
target: occurrenceAddressSchema
|
|
583
|
+
}).strict(), z.object({
|
|
584
|
+
disposition: declarableDisposition,
|
|
585
|
+
finding: z.object({
|
|
586
|
+
file: z.string().min(1).optional(),
|
|
587
|
+
title: z.string().min(1)
|
|
588
|
+
}).strict(),
|
|
589
|
+
reference: z.string().min(1).optional()
|
|
590
|
+
}).strict()]));
|
|
591
|
+
const REVIEW_LOWER_FINDING_PRIORITIES = [
|
|
592
|
+
"P2",
|
|
593
|
+
"P3",
|
|
594
|
+
"P4"
|
|
595
|
+
];
|
|
596
|
+
const nonBlankString$1 = z.string().trim().min(1);
|
|
597
|
+
const reviewFindingSchema = z.object({
|
|
598
|
+
blockingAfterCap: z.boolean().optional(),
|
|
599
|
+
body: nonBlankString$1,
|
|
600
|
+
category: nonBlankString$1.optional(),
|
|
601
|
+
citedSpan: nonBlankString$1.optional(),
|
|
602
|
+
file: nonBlankString$1.optional(),
|
|
603
|
+
line: z.number().int().positive().optional(),
|
|
604
|
+
prescribedAction: nonBlankString$1.optional(),
|
|
605
|
+
priority: nonBlankString$1.optional(),
|
|
606
|
+
supersedes: z.object({
|
|
607
|
+
file: nonBlankString$1.optional(),
|
|
608
|
+
title: nonBlankString$1
|
|
609
|
+
}).strict().optional(),
|
|
610
|
+
title: nonBlankString$1
|
|
611
|
+
}).strict();
|
|
612
|
+
const findingsReviewSchema = z.object({
|
|
613
|
+
findings: z.array(reviewFindingSchema),
|
|
614
|
+
issuesFlagged: z.number().int().nonnegative(),
|
|
615
|
+
kind: z.enum(["correctness", "security"]),
|
|
616
|
+
lowerFindings: z.array(reviewFindingSchema.extend({ priority: z.enum(REVIEW_LOWER_FINDING_PRIORITIES) })).optional(),
|
|
617
|
+
outcome: z.enum(["passed", "failed"]),
|
|
618
|
+
summary: nonBlankString$1
|
|
619
|
+
}).strict().superRefine((review, context) => {
|
|
620
|
+
for (const [index, finding] of review.findings.entries()) if (REVIEW_LOWER_FINDING_PRIORITIES.some((priority) => priority === finding.priority?.trim().toUpperCase())) context.addIssue({
|
|
621
|
+
code: "custom",
|
|
622
|
+
message: "Below-floor finding: move it to lowerFindings",
|
|
623
|
+
path: [
|
|
624
|
+
"findings",
|
|
625
|
+
index,
|
|
626
|
+
"priority"
|
|
627
|
+
]
|
|
628
|
+
});
|
|
629
|
+
if (review.issuesFlagged !== review.findings.length) context.addIssue({
|
|
630
|
+
code: "custom",
|
|
631
|
+
message: "issuesFlagged must match findings length",
|
|
632
|
+
path: ["issuesFlagged"]
|
|
633
|
+
});
|
|
634
|
+
if (review.outcome === "passed" && review.findings.length > 0) context.addIssue({
|
|
635
|
+
code: "custom",
|
|
636
|
+
message: "passed reviews must not include findings",
|
|
637
|
+
path: ["findings"]
|
|
638
|
+
});
|
|
639
|
+
if (review.outcome === "failed" && review.findings.length === 0) context.addIssue({
|
|
640
|
+
code: "custom",
|
|
641
|
+
message: "failed reviews must include findings",
|
|
642
|
+
path: ["findings"]
|
|
643
|
+
});
|
|
644
|
+
});
|
|
645
|
+
const reviewResolutionSchema = z.object({
|
|
646
|
+
reference: nonBlankString$1,
|
|
647
|
+
target: z.string().regex(OCCURRENCE_ADDRESS_PATTERN)
|
|
648
|
+
}).strict();
|
|
649
|
+
z.object({
|
|
650
|
+
model: nonBlankString$1,
|
|
651
|
+
patchId: nonBlankString$1,
|
|
652
|
+
producer: nonBlankString$1.optional(),
|
|
653
|
+
resolutions: z.array(reviewResolutionSchema).optional(),
|
|
654
|
+
reviews: z.array(findingsReviewSchema),
|
|
655
|
+
rung: z.enum(EVIDENCE_REVIEW_RUNGS).optional(),
|
|
656
|
+
schemaVersion: z.union([
|
|
657
|
+
z.literal(1),
|
|
658
|
+
z.literal(2),
|
|
659
|
+
z.literal(3)
|
|
660
|
+
]),
|
|
661
|
+
sessionId: nonBlankString$1
|
|
662
|
+
}).strict().superRefine((file, context) => {
|
|
663
|
+
if (file.schemaVersion === 1) {
|
|
664
|
+
if (file.producer !== void 0) context.addIssue({
|
|
665
|
+
code: "custom",
|
|
666
|
+
message: "producer is a schemaVersion 2 field",
|
|
667
|
+
path: ["producer"]
|
|
668
|
+
});
|
|
669
|
+
if (file.rung !== void 0) context.addIssue({
|
|
670
|
+
code: "custom",
|
|
671
|
+
message: "rung is a schemaVersion 2 field",
|
|
672
|
+
path: ["rung"]
|
|
673
|
+
});
|
|
674
|
+
} else {
|
|
675
|
+
if (file.producer === void 0) context.addIssue({
|
|
676
|
+
code: "custom",
|
|
677
|
+
message: `schemaVersion ${file.schemaVersion} findings require producer`,
|
|
678
|
+
path: ["producer"]
|
|
679
|
+
});
|
|
680
|
+
if (file.rung === void 0) context.addIssue({
|
|
681
|
+
code: "custom",
|
|
682
|
+
message: `schemaVersion ${file.schemaVersion} findings require rung`,
|
|
683
|
+
path: ["rung"]
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
const seen = /* @__PURE__ */ new Set();
|
|
687
|
+
for (const [index, review] of file.reviews.entries()) {
|
|
688
|
+
if (seen.has(review.kind)) context.addIssue({
|
|
689
|
+
code: "custom",
|
|
690
|
+
message: `duplicate review kind: ${review.kind}`,
|
|
691
|
+
path: [
|
|
692
|
+
"reviews",
|
|
693
|
+
index,
|
|
694
|
+
"kind"
|
|
695
|
+
]
|
|
696
|
+
});
|
|
697
|
+
seen.add(review.kind);
|
|
698
|
+
}
|
|
699
|
+
});
|
|
700
|
+
//#endregion
|
|
701
|
+
//#region src/review-ladder.ts
|
|
702
|
+
const LADDER_NEXT_ACTION_KINDS = [
|
|
703
|
+
"run-interior-cycle",
|
|
704
|
+
"advance-to-gate",
|
|
705
|
+
"run-gate-cycle",
|
|
706
|
+
"accept-nonblocking-findings",
|
|
707
|
+
"escalate-to-triage",
|
|
708
|
+
"ready-for-human"
|
|
709
|
+
];
|
|
710
|
+
const LADDER_STAGES = [
|
|
711
|
+
"interior",
|
|
712
|
+
"interior-complete",
|
|
713
|
+
"gate"
|
|
714
|
+
];
|
|
715
|
+
const LADDER_FORCED_TRANSITION_KINDS = ["gate-cap-exhausted", "interior-cap-reached"];
|
|
716
|
+
const ladderCycleRefsFor = (cycles) => {
|
|
717
|
+
let interior = 0;
|
|
718
|
+
let gate = 0;
|
|
719
|
+
return cycles.map((entry) => {
|
|
720
|
+
if (entry.stage === "interior") {
|
|
721
|
+
if (gate > 0) throw new TypeError("Invalid ladder history: interior cycle recorded after the gate stage began.");
|
|
722
|
+
interior += 1;
|
|
723
|
+
return {
|
|
724
|
+
cycle: interior,
|
|
725
|
+
stage: "interior"
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
gate += 1;
|
|
729
|
+
return {
|
|
730
|
+
cycle: gate,
|
|
731
|
+
stage: "gate"
|
|
732
|
+
};
|
|
733
|
+
});
|
|
734
|
+
};
|
|
735
|
+
//#endregion
|
|
736
|
+
//#region src/pr-readiness/ladder-proof-state.ts
|
|
737
|
+
const persistedCycleSchema = z.custom((cycle) => typeof cycle === "object" && cycle !== null, "ladder cycle must be an object").superRefine((cycle, context) => {
|
|
738
|
+
const parsed = reviewCycleRecordingSchema.safeParse(cycle.recording);
|
|
739
|
+
if (!parsed.success) context.addIssue({
|
|
740
|
+
code: "custom",
|
|
741
|
+
message: `ladder cycle carries no valid recording (schema 3): ${parsed.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ")}`,
|
|
742
|
+
path: ["recording"]
|
|
743
|
+
});
|
|
744
|
+
});
|
|
745
|
+
const prReviewLadderStateSchema = z.object({ cycles: z.array(persistedCycleSchema) }).superRefine((ladder, context) => {
|
|
746
|
+
try {
|
|
747
|
+
ladderLedgerFor(ladder);
|
|
748
|
+
} catch (error) {
|
|
749
|
+
context.addIssue({
|
|
750
|
+
code: "custom",
|
|
751
|
+
message: `ladder cycles do not fold into a valid disposition ledger: ${error instanceof Error ? error.message : String(error)}`
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
});
|
|
755
|
+
const ladderLedgerFor = (ladder) => [...foldLedger(ladder.cycles, ladderCycleRefsFor(ladder.cycles)).ledger.values()];
|
|
756
|
+
//#endregion
|
|
757
|
+
//#region src/pr-readiness/review-proof.ts
|
|
758
|
+
const PR_REVIEW_SCHEMA_VERSION = 3;
|
|
759
|
+
const nonBlankString = z.string().refine((value) => value.trim().length > 0, { message: "must not be blank" });
|
|
760
|
+
const parseableDateString = z.string().refine((value) => Number.isFinite(Date.parse(value)), { message: "must be a parseable date string" });
|
|
761
|
+
const findingSupersedesSchema = z.object({
|
|
762
|
+
file: z.string().optional(),
|
|
763
|
+
title: nonBlankString
|
|
764
|
+
});
|
|
765
|
+
const prReviewFindingSchema = z.object({
|
|
766
|
+
blockingAfterCap: z.boolean().optional(),
|
|
767
|
+
body: nonBlankString,
|
|
768
|
+
category: z.enum(REVIEW_CATEGORIES).optional(),
|
|
769
|
+
citedSpan: z.string().optional(),
|
|
770
|
+
file: z.string().optional(),
|
|
771
|
+
line: z.number().int().positive().optional(),
|
|
772
|
+
prescribedAction: z.string().optional(),
|
|
773
|
+
priority: z.string().optional(),
|
|
774
|
+
protocolFinding: z.literal(true).optional(),
|
|
775
|
+
supersedes: findingSupersedesSchema.optional(),
|
|
776
|
+
title: nonBlankString
|
|
777
|
+
});
|
|
778
|
+
const prReviewResultSchema = z.object({
|
|
779
|
+
durationMs: z.number().refine((value) => Number.isFinite(value) && value >= 0, { message: "durationMs must be a finite number >= 0" }),
|
|
780
|
+
endedAt: parseableDateString,
|
|
781
|
+
findings: z.array(prReviewFindingSchema),
|
|
782
|
+
issuesFlagged: z.number().int().nonnegative(),
|
|
783
|
+
kind: z.enum(["correctness", "security"]),
|
|
784
|
+
lowerFindings: z.array(z.intersection(prReviewFindingSchema, z.object({ priority: z.enum(REVIEW_LOWER_FINDING_PRIORITIES) }))).optional(),
|
|
785
|
+
model: nonBlankString.optional(),
|
|
786
|
+
outcome: z.enum([
|
|
787
|
+
"passed",
|
|
788
|
+
"failed",
|
|
789
|
+
"error"
|
|
790
|
+
]),
|
|
791
|
+
producer: nonBlankString.optional(),
|
|
792
|
+
promptSections: reviewPromptSectionsSchema.optional(),
|
|
793
|
+
rung: z.enum(EVIDENCE_REVIEW_RUNGS).optional(),
|
|
794
|
+
sessionId: nonBlankString.optional(),
|
|
795
|
+
startedAt: parseableDateString,
|
|
796
|
+
summary: z.string().refine((value) => value.trim().length > 0 && value !== "<one sentence>", { message: "summary must not be blank or a placeholder" }),
|
|
797
|
+
typedVerdict: z.boolean().optional(),
|
|
798
|
+
verdictSource: z.enum(["footer", "recovered"]).optional()
|
|
799
|
+
}).superRefine((review, context) => {
|
|
800
|
+
if (review.outcome === "passed") {
|
|
801
|
+
if (review.issuesFlagged !== 0) context.addIssue({
|
|
802
|
+
code: "custom",
|
|
803
|
+
message: "passed reviews must report zero issues flagged",
|
|
804
|
+
path: ["issuesFlagged"]
|
|
805
|
+
});
|
|
806
|
+
if (review.findings.length !== 0) context.addIssue({
|
|
807
|
+
code: "custom",
|
|
808
|
+
message: "passed reviews must not include findings",
|
|
809
|
+
path: ["findings"]
|
|
810
|
+
});
|
|
811
|
+
}
|
|
812
|
+
if (review.outcome !== "passed") {
|
|
813
|
+
if (review.findings.length === 0) context.addIssue({
|
|
814
|
+
code: "custom",
|
|
815
|
+
message: "non-passing reviews must include findings",
|
|
816
|
+
path: ["findings"]
|
|
817
|
+
});
|
|
818
|
+
if (review.findings.length !== review.issuesFlagged) context.addIssue({
|
|
819
|
+
code: "custom",
|
|
820
|
+
message: "issuesFlagged must match findings length",
|
|
821
|
+
path: ["issuesFlagged"]
|
|
822
|
+
});
|
|
823
|
+
}
|
|
824
|
+
if (review.producer === void 0 !== (review.rung === void 0)) context.addIssue({
|
|
825
|
+
code: "custom",
|
|
826
|
+
message: "producer and rung must be recorded together",
|
|
827
|
+
path: [review.producer === void 0 ? "producer" : "rung"]
|
|
828
|
+
});
|
|
829
|
+
});
|
|
830
|
+
const convertedProofMessage = (value) => {
|
|
831
|
+
if (typeof value !== "object" || value === null) return;
|
|
832
|
+
const { importedFrom, recoveryRequired } = value;
|
|
833
|
+
if (importedFrom === void 0 && recoveryRequired === void 0) return;
|
|
834
|
+
const proofDigest = importedFrom?.proofDigest;
|
|
835
|
+
return `Unsupported pr:review proof: it was converted from schema 2 by the retired import, and its schema-2 recovery is no longer read. The original schema-2 bytes are retained at .factory-memory/review-proof-imports/${typeof proofDigest === "string" && /^[0-9a-f]{64}$/u.test(proofDigest) ? proofDigest : "<digest>"}.json; move this proof aside unedited and record a fresh review at the current head.`;
|
|
836
|
+
};
|
|
837
|
+
const prReviewProofSchema = z.object({
|
|
838
|
+
base: z.string(),
|
|
839
|
+
changedFiles: z.array(z.string()),
|
|
840
|
+
cleanedPaths: z.array(z.string()),
|
|
841
|
+
findingProvenanceVersion: z.literal(1),
|
|
842
|
+
headSha: z.string().regex(/^[0-9a-f]{40}$/u),
|
|
843
|
+
ladder: prReviewLadderStateSchema.optional(),
|
|
844
|
+
maxReviewCycles: z.number().int().positive().optional(),
|
|
845
|
+
patchId: z.string().regex(/^[0-9a-f]{40,64}$/u),
|
|
846
|
+
policyResolution: policyResolutionSchema.optional(),
|
|
847
|
+
reviewCycle: z.number().int().positive().optional(),
|
|
848
|
+
reviewRequirement: z.object({
|
|
849
|
+
reason: z.enum(["no-applicable-mode", "faithful-merge"]),
|
|
850
|
+
status: z.literal("not-required")
|
|
851
|
+
}).strict().optional(),
|
|
852
|
+
reviews: z.array(prReviewResultSchema),
|
|
853
|
+
schemaVersion: z.literal(3)
|
|
854
|
+
}).passthrough().superRefine((proof, context) => {
|
|
855
|
+
const converted = convertedProofMessage(proof);
|
|
856
|
+
if (converted !== void 0) context.addIssue({
|
|
857
|
+
code: "custom",
|
|
858
|
+
message: converted,
|
|
859
|
+
path: ["importedFrom" in proof ? "importedFrom" : "recoveryRequired"]
|
|
860
|
+
});
|
|
861
|
+
if (proof.reviewRequirement && proof.reviews.length > 0) context.addIssue({
|
|
862
|
+
code: "custom",
|
|
863
|
+
message: "not-required review proofs must not contain review runs",
|
|
864
|
+
path: ["reviews"]
|
|
865
|
+
});
|
|
866
|
+
if (proof.reviewCycle !== void 0 && proof.maxReviewCycles !== void 0 && proof.reviewCycle > proof.maxReviewCycles) context.addIssue({
|
|
867
|
+
code: "custom",
|
|
868
|
+
message: "reviewCycle must not exceed maxReviewCycles",
|
|
869
|
+
path: ["reviewCycle"]
|
|
870
|
+
});
|
|
871
|
+
});
|
|
872
|
+
//#endregion
|
|
873
|
+
//#region src/required-check-policy.ts
|
|
874
|
+
const EVIDENCE_CHECK_TYPES = ["review", "verify"];
|
|
875
|
+
const requiredCheckScopeSchema = z.object({ classifications: z.array(z.enum(DIFF_CLASSIFICATIONS)).min(1) }).strict();
|
|
876
|
+
const reviewStatusSchema = z.enum([
|
|
877
|
+
"not-required",
|
|
878
|
+
"current",
|
|
879
|
+
"stale",
|
|
880
|
+
"missing",
|
|
881
|
+
"blocked"
|
|
882
|
+
]);
|
|
883
|
+
//#endregion
|
|
884
|
+
//#region src/review-state.ts
|
|
885
|
+
const reviewStateIssue = (message, field) => [{
|
|
886
|
+
code: "custom",
|
|
887
|
+
message,
|
|
888
|
+
path: [field]
|
|
889
|
+
}];
|
|
890
|
+
/**
|
|
891
|
+
* The invariant both ledger schemas enforce, written once so the emitting
|
|
892
|
+
* schema and the wire schema cannot drift. It is the producer's state machine:
|
|
893
|
+
*
|
|
894
|
+
* - a review that is not required is never `current` — `reviewStatus` returns
|
|
895
|
+
* `not-required` before it ever looks at the recorded proof;
|
|
896
|
+
* - `current` means a proof was compared against the candidate delta, so the
|
|
897
|
+
* reviewed identity must be recorded;
|
|
898
|
+
* - `missing` means no proof was found, so no reviewed identity can exist.
|
|
899
|
+
*/
|
|
900
|
+
const reviewStateIssues = (state) => {
|
|
901
|
+
const hasIdentity = Boolean(state.reviewedHeadSha || state.reviewedPatchId);
|
|
902
|
+
if (state.status === "current") {
|
|
903
|
+
if (!state.required) return reviewStateIssue("a review state that is not required cannot be current", "status");
|
|
904
|
+
if (!hasIdentity) return reviewStateIssue("a current review state must record the reviewed head sha or patch id", "reviewedPatchId");
|
|
905
|
+
}
|
|
906
|
+
if (state.status === "missing" && hasIdentity) return reviewStateIssue("a missing review state must record no reviewed head sha or patch id", "status");
|
|
907
|
+
return [];
|
|
908
|
+
};
|
|
909
|
+
//#endregion
|
|
910
|
+
//#region src/catch-up-recognition-record.ts
|
|
911
|
+
const objectShaSchema = z.string().regex(/^[0-9a-f]{40}$/u);
|
|
912
|
+
const catchUpRecognitionSchema = z.object({
|
|
913
|
+
baseRef: z.string().min(1),
|
|
914
|
+
baseTipSha: objectShaSchema,
|
|
915
|
+
mergedParentSha: objectShaSchema,
|
|
916
|
+
mergedTreeSha: objectShaSchema,
|
|
917
|
+
upstreamRef: z.string().min(1)
|
|
918
|
+
}).strict();
|
|
231
919
|
const impactStampTargetSchema = z.object({
|
|
232
920
|
basis: z.string().min(1),
|
|
233
921
|
impact: z.enum(["affected", "not-affected"]),
|
|
234
|
-
name: z.string().min(1)
|
|
922
|
+
name: z.string().min(1),
|
|
923
|
+
subscribedPaths: z.array(z.string())
|
|
235
924
|
});
|
|
236
925
|
/**
|
|
237
926
|
* The recorded stamp. `targets` satisfies the completeness invariant: every
|
|
@@ -241,15 +930,17 @@ const impactStampTargetSchema = z.object({
|
|
|
241
930
|
*/
|
|
242
931
|
const impactStampSchema = z.object({
|
|
243
932
|
basis: z.enum(["target-scoped", "conservative"]),
|
|
933
|
+
inertPaths: z.array(z.string()).default([]),
|
|
244
934
|
reasons: z.array(z.string()),
|
|
245
|
-
stampVersion: z.literal(
|
|
935
|
+
stampVersion: z.literal(4),
|
|
246
936
|
targets: z.array(impactStampTargetSchema).superRefine((targets, context) => {
|
|
247
937
|
const names = targets.map((target) => target.name);
|
|
248
938
|
if (new Set(names).size !== names.length) context.addIssue({
|
|
249
939
|
code: "custom",
|
|
250
940
|
message: "impact stamp targets must have distinct names."
|
|
251
941
|
});
|
|
252
|
-
})
|
|
942
|
+
}),
|
|
943
|
+
unsubscribedPaths: z.array(z.string())
|
|
253
944
|
});
|
|
254
945
|
/**
|
|
255
946
|
* The surfaces that consume the stamp. One classification, consumed
|
|
@@ -287,8 +978,8 @@ const impactStampScopeDecision = ({ stamp, surface, targetName }) => {
|
|
|
287
978
|
reason: `no trusted identity-bound impact stamp covers this candidate; ${floor}`,
|
|
288
979
|
scoped: false
|
|
289
980
|
};
|
|
290
|
-
if (stamp.stampVersion !==
|
|
291
|
-
reason: `impact stamp version ${String(stamp.stampVersion)} is not this build's version
|
|
981
|
+
if (stamp.stampVersion !== 4) return {
|
|
982
|
+
reason: `impact stamp version ${String(stamp.stampVersion)} is not this build's version 4; ${floor}`,
|
|
292
983
|
scoped: false
|
|
293
984
|
};
|
|
294
985
|
if (stamp.basis !== "target-scoped") return {
|
|
@@ -350,9 +1041,7 @@ z.looseObject({
|
|
|
350
1041
|
* skip this epic forbids.
|
|
351
1042
|
*
|
|
352
1043
|
* Rule 4 is scoped to `outcome: "passed"` because an ABORTED proof legitimately
|
|
353
|
-
* records partial execution — the run stopped at the failing command.
|
|
354
|
-
* also scoped to schemaVersion >= 4: `executedCommands` postdates v1–v3, so a
|
|
355
|
-
* legacy proof that never recorded it is not making a false completeness claim.
|
|
1044
|
+
* records partial execution — the run stopped at the failing command.
|
|
356
1045
|
*
|
|
357
1046
|
* The check is one-directional on purpose. Every SELECTED command must be
|
|
358
1047
|
* accounted for, but `executedCommands` may legitimately carry MORE than the
|
|
@@ -395,7 +1084,7 @@ const assertBatteryCompleteness = (proof, context) => {
|
|
|
395
1084
|
});
|
|
396
1085
|
seen.add(entry.name);
|
|
397
1086
|
}
|
|
398
|
-
if (proof.outcome !== "passed"
|
|
1087
|
+
if (proof.outcome !== "passed") return;
|
|
399
1088
|
const disposed = new Set([...executed, ...seen]);
|
|
400
1089
|
const undisposed = selected.filter((name) => !disposed.has(name));
|
|
401
1090
|
if (undisposed.length > 0) context.addIssue({
|
|
@@ -405,7 +1094,7 @@ const assertBatteryCompleteness = (proof, context) => {
|
|
|
405
1094
|
});
|
|
406
1095
|
};
|
|
407
1096
|
/**
|
|
408
|
-
* The stamp-authorization control: a passed
|
|
1097
|
+
* The stamp-authorization control: a passed proof may only claim a command
|
|
409
1098
|
* was NOT REQUIRED if its own recorded stamp says so.
|
|
410
1099
|
*
|
|
411
1100
|
* `assertBatteryCompleteness` proves the two lists partition the selected
|
|
@@ -428,8 +1117,8 @@ const assertBatteryCompleteness = (proof, context) => {
|
|
|
428
1117
|
* provably released.
|
|
429
1118
|
*
|
|
430
1119
|
* The proof is one artifact and a determined forger controls all of it; this
|
|
431
|
-
* is internal-coherence belt-and-braces in the same trust domain as the
|
|
432
|
-
*
|
|
1120
|
+
* is internal-coherence belt-and-braces in the same trust domain as the rest
|
|
1121
|
+
* of the proof, and write-time truth stays `pr:verify`'s job.
|
|
433
1122
|
*
|
|
434
1123
|
* The authorizing evidence is the proof's OWN `impactStamp` — the same stamp
|
|
435
1124
|
* `pr:verify` computed for this candidate's identity triple and recorded here,
|
|
@@ -442,14 +1131,13 @@ const assertBatteryCompleteness = (proof, context) => {
|
|
|
442
1131
|
* self-contradictory stamp, an unclassified name, or an `affected` verdict all
|
|
443
1132
|
* fail the same way scoping itself would.
|
|
444
1133
|
*
|
|
445
|
-
* Scoped to `outcome: "passed"`
|
|
446
|
-
*
|
|
447
|
-
* legacy proof predates both fields, so neither is making a false completeness
|
|
1134
|
+
* Scoped to `outcome: "passed"` for the same reason as the completeness rule:
|
|
1135
|
+
* an aborted run records partial execution, so it makes no false completeness
|
|
448
1136
|
* claim.
|
|
449
1137
|
*/
|
|
450
1138
|
const assertNotRequiredStampAuthorization = (proof, context) => {
|
|
451
1139
|
const notRequired = proof.notRequiredCommands ?? [];
|
|
452
|
-
if (proof.outcome !== "passed" ||
|
|
1140
|
+
if (proof.outcome !== "passed" || notRequired.length === 0) return;
|
|
453
1141
|
const { impactStamp } = proof;
|
|
454
1142
|
if (impactStamp === void 0) {
|
|
455
1143
|
context.addIssue({
|
|
@@ -493,489 +1181,32 @@ const assertNotRequiredStampAuthorization = (proof, context) => {
|
|
|
493
1181
|
//#endregion
|
|
494
1182
|
//#region src/pr-verify-proof-rules.ts
|
|
495
1183
|
const assertPrVerifyProofRules = (proof, context) => {
|
|
496
|
-
if (proof.
|
|
497
|
-
code: "custom",
|
|
498
|
-
message: "schemaVersion 4 pr:verify proof requires authoringSession (the recorded authoring identity, or the `unknown` sentinel).",
|
|
499
|
-
path: ["authoringSession"]
|
|
500
|
-
});
|
|
501
|
-
if (proof.schemaVersion < 4 && proof.authoringSession !== void 0) context.addIssue({
|
|
502
|
-
code: "custom",
|
|
503
|
-
message: "authoringSession is a schemaVersion>=4 field; a v1–v3 pr:verify proof must not carry it.",
|
|
504
|
-
path: ["authoringSession"]
|
|
505
|
-
});
|
|
506
|
-
if (proof.schemaVersion < 4 && proof.impactStamp !== void 0) context.addIssue({
|
|
507
|
-
code: "custom",
|
|
508
|
-
message: "impactStamp is a schemaVersion>=4 field; a v1–v3 pr:verify proof must not carry it.",
|
|
509
|
-
path: ["impactStamp"]
|
|
510
|
-
});
|
|
511
|
-
if (proof.schemaVersion < 4 && proof.notRequiredCommands !== void 0) context.addIssue({
|
|
1184
|
+
if (proof.notDemandedRecords !== void 0 && proof.impactStamp === void 0) context.addIssue({
|
|
512
1185
|
code: "custom",
|
|
513
|
-
message: "
|
|
514
|
-
path: ["
|
|
1186
|
+
message: "notDemandedRecords requires impactStamp; package-level not-demanded records are bound to the stamp identity.",
|
|
1187
|
+
path: ["notDemandedRecords"]
|
|
515
1188
|
});
|
|
516
1189
|
assertBatteryCompleteness(proof, context);
|
|
517
1190
|
assertNotRequiredStampAuthorization(proof, context);
|
|
518
1191
|
};
|
|
519
|
-
const LEGACY_CLOSEOUT_SCHEMA_VERSION = 3;
|
|
520
|
-
/** Current writer plus retained reader versions accepted at ingest. */
|
|
521
|
-
const SUPPORTED_CLOSEOUT_SCHEMA_VERSIONS = [LEGACY_CLOSEOUT_SCHEMA_VERSION, 4];
|
|
522
|
-
const MAX_CLOSEOUT_ROWS = 1e3;
|
|
523
|
-
const IdentifierSchema = z.string().min(1).max(500);
|
|
524
|
-
const NarrativeSchema = z.string().min(1).max(1e4);
|
|
525
|
-
const RepoSchema = z.string().min(1).max(200);
|
|
526
|
-
const IsoDateSchema = z.string().regex(/^\d{4}-\d{2}-\d{2}$/u);
|
|
527
|
-
const CloseoutLandingSpotSchema = z.enum([
|
|
528
|
-
"CLI",
|
|
529
|
-
"profile",
|
|
530
|
-
"skill",
|
|
531
|
-
"ADR"
|
|
532
|
-
]);
|
|
533
|
-
const CurrentCloseoutSourceSchema = z.object({
|
|
534
|
-
detail: NarrativeSchema,
|
|
535
|
-
kind: z.literal("boundary-thermo-attestation")
|
|
536
|
-
}).strict();
|
|
537
|
-
const LegacyCloseoutSourceSchema = z.object({
|
|
538
|
-
detail: NarrativeSchema,
|
|
539
|
-
kind: z.enum(["interior-telemetry", "boundary-thermo-attestation"])
|
|
540
|
-
}).strict();
|
|
541
|
-
const LegacyCloseoutScopingSchema = z.object({
|
|
542
|
-
epic: IdentifierSchema,
|
|
543
|
-
issueFilter: z.number().int().positive().optional(),
|
|
544
|
-
prFilter: z.number().int().positive().optional(),
|
|
545
|
-
repo: RepoSchema,
|
|
546
|
-
scopedByEpicIssueSet: z.boolean(),
|
|
547
|
-
window: z.object({
|
|
548
|
-
from: IsoDateSchema,
|
|
549
|
-
to: IsoDateSchema
|
|
550
|
-
}).strict()
|
|
551
|
-
}).strict();
|
|
552
|
-
const CloseoutBudgetSchema = z.object({
|
|
553
|
-
note: NarrativeSchema,
|
|
554
|
-
tier: z.literal("overseer")
|
|
555
|
-
}).strict();
|
|
556
|
-
const LegacyCloseoutBudgetSchema = z.object({
|
|
557
|
-
interiorSourcesConsumed: z.number().int().nonnegative(),
|
|
558
|
-
note: NarrativeSchema,
|
|
559
|
-
scoping: LegacyCloseoutScopingSchema.optional(),
|
|
560
|
-
tier: z.literal("overseer")
|
|
561
|
-
}).strict();
|
|
562
|
-
const BoundaryThermoAttestationSchema = z.object({
|
|
563
|
-
owner: z.string().min(1).max(200),
|
|
564
|
-
runs: z.number().int().nonnegative(),
|
|
565
|
-
waived: z.boolean(),
|
|
566
|
-
waiverRationale: NarrativeSchema.optional()
|
|
567
|
-
}).strict();
|
|
568
|
-
const CloseoutMetricRowSchema = z.object({
|
|
569
|
-
label: IdentifierSchema,
|
|
570
|
-
metric: IdentifierSchema,
|
|
571
|
-
source: CurrentCloseoutSourceSchema,
|
|
572
|
-
unit: z.enum([
|
|
573
|
-
"count",
|
|
574
|
-
"boolean",
|
|
575
|
-
"text"
|
|
576
|
-
]),
|
|
577
|
-
value: z.union([
|
|
578
|
-
z.number(),
|
|
579
|
-
NarrativeSchema,
|
|
580
|
-
z.boolean(),
|
|
581
|
-
z.null()
|
|
582
|
-
])
|
|
583
|
-
}).strict();
|
|
584
|
-
const LegacyCloseoutMetricRowSchema = z.object({
|
|
585
|
-
label: IdentifierSchema,
|
|
586
|
-
metric: IdentifierSchema,
|
|
587
|
-
source: LegacyCloseoutSourceSchema,
|
|
588
|
-
unit: z.enum([
|
|
589
|
-
"tokens",
|
|
590
|
-
"usd",
|
|
591
|
-
"count",
|
|
592
|
-
"ratio",
|
|
593
|
-
"boolean",
|
|
594
|
-
"text"
|
|
595
|
-
]),
|
|
596
|
-
value: z.union([
|
|
597
|
-
z.number(),
|
|
598
|
-
NarrativeSchema,
|
|
599
|
-
z.boolean(),
|
|
600
|
-
z.null()
|
|
601
|
-
])
|
|
602
|
-
}).strict();
|
|
603
|
-
const CloseoutLessonSchema = z.object({
|
|
604
|
-
id: IdentifierSchema,
|
|
605
|
-
landingSpot: CloseoutLandingSpotSchema,
|
|
606
|
-
lesson: NarrativeSchema,
|
|
607
|
-
rationale: NarrativeSchema.optional(),
|
|
608
|
-
source: CurrentCloseoutSourceSchema.optional()
|
|
609
|
-
}).strict();
|
|
610
|
-
const LegacyCloseoutLessonSchema = z.object({
|
|
611
|
-
id: IdentifierSchema,
|
|
612
|
-
landingSpot: CloseoutLandingSpotSchema,
|
|
613
|
-
lesson: NarrativeSchema,
|
|
614
|
-
rationale: NarrativeSchema.optional(),
|
|
615
|
-
source: LegacyCloseoutSourceSchema.optional()
|
|
616
|
-
}).strict();
|
|
617
|
-
const CloseoutBlindspotSchema = z.object({
|
|
618
|
-
id: IdentifierSchema,
|
|
619
|
-
note: NarrativeSchema,
|
|
620
|
-
reason: NarrativeSchema.optional()
|
|
621
|
-
}).strict();
|
|
622
|
-
const closeoutLedgerRowSchema = (schemaVersion) => z.object({
|
|
623
|
-
detail: NarrativeSchema.optional(),
|
|
624
|
-
epic: IdentifierSchema,
|
|
625
|
-
generatedAt: z.iso.datetime(),
|
|
626
|
-
key: IdentifierSchema,
|
|
627
|
-
landingSpot: CloseoutLandingSpotSchema.optional(),
|
|
628
|
-
repo: RepoSchema,
|
|
629
|
-
rowType: z.enum([
|
|
630
|
-
"metric",
|
|
631
|
-
"lesson",
|
|
632
|
-
"blindspot"
|
|
633
|
-
]),
|
|
634
|
-
schemaVersion: z.literal(schemaVersion),
|
|
635
|
-
source: NarrativeSchema,
|
|
636
|
-
unit: IdentifierSchema.optional(),
|
|
637
|
-
value: z.union([
|
|
638
|
-
NarrativeSchema,
|
|
639
|
-
z.number(),
|
|
640
|
-
z.null()
|
|
641
|
-
])
|
|
642
|
-
}).strict();
|
|
643
|
-
const CloseoutLedgerRowSchema = closeoutLedgerRowSchema(4);
|
|
644
|
-
const LegacyCloseoutLedgerRowSchema = closeoutLedgerRowSchema(LEGACY_CLOSEOUT_SCHEMA_VERSION);
|
|
645
|
-
const closeoutArtifactShape = (input) => z.object({
|
|
646
|
-
boundaryThermo: BoundaryThermoAttestationSchema,
|
|
647
|
-
budget: input.budget,
|
|
648
|
-
couldNotSee: z.array(CloseoutBlindspotSchema).min(1).max(MAX_CLOSEOUT_ROWS),
|
|
649
|
-
epic: IdentifierSchema,
|
|
650
|
-
generatedAt: z.iso.datetime(),
|
|
651
|
-
ledgerRows: z.array(input.ledgerRows).max(MAX_CLOSEOUT_ROWS),
|
|
652
|
-
lessons: z.array(input.lessons).max(MAX_CLOSEOUT_ROWS),
|
|
653
|
-
metrics: z.array(input.metrics).max(MAX_CLOSEOUT_ROWS),
|
|
654
|
-
repo: RepoSchema,
|
|
655
|
-
schemaVersion: z.literal(input.schemaVersion)
|
|
656
|
-
}).strict();
|
|
657
|
-
/** Strict, bounded runtime transport contract emitted by factory:closeout. */
|
|
658
|
-
const closeoutArtifactSchema = closeoutArtifactShape({
|
|
659
|
-
budget: CloseoutBudgetSchema,
|
|
660
|
-
ledgerRows: CloseoutLedgerRowSchema,
|
|
661
|
-
lessons: CloseoutLessonSchema,
|
|
662
|
-
metrics: CloseoutMetricRowSchema,
|
|
663
|
-
schemaVersion: 4
|
|
664
|
-
});
|
|
665
|
-
/** Reader-only compatibility for closeout artifacts written before #554. */
|
|
666
|
-
const legacyCloseoutArtifactSchema = closeoutArtifactShape({
|
|
667
|
-
budget: LegacyCloseoutBudgetSchema,
|
|
668
|
-
ledgerRows: LegacyCloseoutLedgerRowSchema,
|
|
669
|
-
lessons: LegacyCloseoutLessonSchema,
|
|
670
|
-
metrics: LegacyCloseoutMetricRowSchema,
|
|
671
|
-
schemaVersion: LEGACY_CLOSEOUT_SCHEMA_VERSION
|
|
672
|
-
});
|
|
673
|
-
/** Accept current output and retained v3 artifacts when reading or replaying. */
|
|
674
|
-
const closeoutArtifactReadSchema = z.union([closeoutArtifactSchema, legacyCloseoutArtifactSchema]);
|
|
675
|
-
//#endregion
|
|
676
|
-
//#region src/retro-envelope.ts
|
|
677
|
-
/**
|
|
678
|
-
* Versioned retro envelope schema (epic #27 wave 2, issue #34).
|
|
679
|
-
*
|
|
680
|
-
* One envelope per lane, built at `factory:closeout` and delivered through the
|
|
681
|
-
* typed gate-sink as the `retro-envelope` ingest kind. Re-derived in TypeScript
|
|
682
|
-
* from the `spike/telemetry-layer2` S5 scratch schema (reference-only, never
|
|
683
|
-
* merged). This module is the single source of truth for the v1 wire shape
|
|
684
|
-
* and its bounds ({@link RETRO_ENVELOPE_WIRE_BOUNDS}) — producer and consumer
|
|
685
|
-
* alike. HQ imports these exports directly from the Worker-safe
|
|
686
|
-
* `@patronage/software-factory/schemas` subpath
|
|
687
|
-
* (`software-factory-hq/src/contracts/retro-schemas.ts`) instead of
|
|
688
|
-
* maintaining a parallel hand-written copy, so there is exactly one wire
|
|
689
|
-
* contract and no drift-detection machinery is needed (issue #350; formerly
|
|
690
|
-
* a hand-written twin plus a 767-line parity test, #46).
|
|
691
|
-
*
|
|
692
|
-
* DESIGN INVARIANT: cross-family token sums must be UNREPRESENTABLE.
|
|
693
|
-
*
|
|
694
|
-
* The two model families use different tokenizers, prices, and accounting
|
|
695
|
-
* conventions, so any token total that spans Claude and GPT is a lie:
|
|
696
|
-
*
|
|
697
|
-
* 1. There is no combined/total token field anywhere in the envelope.
|
|
698
|
-
* 2. `tokenFamilies` is strict — its only keys are `claude` and `gpt`; data
|
|
699
|
-
* cannot smuggle in a third "all"/"combined" slot.
|
|
700
|
-
* 3. The family BLOCKS are structurally different shapes with DIFFERENT keys
|
|
701
|
-
* (claude is a single flat block keyed on freshInput/cacheReadInput/
|
|
702
|
-
* cacheCreationInput; gpt is `{ roles: [...] }`). The exclusive input-tier
|
|
703
|
-
* COUNTS share no key name across families. The residual names shared
|
|
704
|
-
* between the claude block and a gpt ROLE entry are pinned to exactly
|
|
705
|
-
* {costUsd, model, output} (PR #32 advisory): `costUsd` is deliberate —
|
|
706
|
-
* USD is the one cross-family summable unit (rule 4); `model` is an
|
|
707
|
-
* unsummable label; `output` is the same name at DIFFERENT depths (lane
|
|
708
|
-
* block vs per-role entry), frozen by a tripwire test in
|
|
709
|
-
* `retro-envelope.test.ts` so the overlap cannot grow. Renaming `output`
|
|
710
|
-
* is a schemaVersion-2 wire change, deliberately not spent in v1.
|
|
711
|
-
* 4. Cost is per-family USD and nullable. Combined totals are allowed in USD
|
|
712
|
-
* only, and only as a projection-time sum of per-family USD.
|
|
713
|
-
*
|
|
714
|
-
* Field names also encode the S2/S3 reader lessons: Claude `freshInput` alone
|
|
715
|
-
* is not prompt size (true input context = freshInput + cacheReadInput +
|
|
716
|
-
* cacheCreationInput, requestId-deduped), and codex `inputInclusiveOfCache`
|
|
717
|
-
* already includes `cachedInput`, so `freshInputDerived` (inclusive − cached)
|
|
718
|
-
* is the only value safe to feed a per-token pricer.
|
|
719
|
-
*
|
|
720
|
-
* COMPLETENESS POSTURE: harvest may have no usable native log for a lane, so
|
|
721
|
-
* `tokenFamilies` may legitimately be absent. The closeout build gate demands
|
|
722
|
-
* a valid envelope, not available telemetry. A families-absent envelope keeps
|
|
723
|
-
* its operator-visible data gaps and is a replayable advisory HQ event, so it
|
|
724
|
-
* never substitutes unavailable usage with zero. The wire shape (field names,
|
|
725
|
-
* types, structure) stays byte-parity with HQ v1.
|
|
726
|
-
*/
|
|
727
|
-
const RETRO_ENVELOPE_SCHEMA_VERSION = 1;
|
|
728
|
-
/**
|
|
729
|
-
* v1 wire bounds — the single source the schemas below are built from and the
|
|
730
|
-
* builder's sanitization seam clamps to (`retro-envelope-builder.ts` imports
|
|
731
|
-
* these; it keeps no bound constants of its own). HQ imports this module
|
|
732
|
-
* directly (issue #350), so there is one set of bounds, not a second copy to
|
|
733
|
-
* keep in sync.
|
|
734
|
-
*/
|
|
735
|
-
const RETRO_ENVELOPE_WIRE_BOUNDS = {
|
|
736
|
-
/** archiveRef pointer (key/URL) max characters. */
|
|
737
|
-
archiveRefMaxChars: 2048,
|
|
738
|
-
/** refs.branch max characters (git ref length ceiling). */
|
|
739
|
-
branchMaxChars: 255,
|
|
740
|
-
/** gates[] wire cap — the builder keeps the most recent records. */
|
|
741
|
-
gatesMax: 500,
|
|
742
|
-
/** gpt roles[] cap (delegated roles per lane). */
|
|
743
|
-
gptRolesMax: 20,
|
|
744
|
-
/** Cap for list fields: joinKeys id arrays, phases, dataGaps. */
|
|
745
|
-
listMax: 100,
|
|
746
|
-
/** Bounded name fields: agentRunId, gate, phase name, refs, join-key ids. */
|
|
747
|
-
nameMaxChars: 200,
|
|
748
|
-
/** Short identifier fields: repo name/owner, model, role. */
|
|
749
|
-
shortMaxChars: 100,
|
|
750
|
-
/** Free-text fields: dataGaps entries, outcome.verdict. */
|
|
751
|
-
textMaxChars: 500
|
|
752
|
-
};
|
|
753
|
-
const nonNegInt = z.number().int().nonnegative();
|
|
754
|
-
const isoTimestamp = z.iso.datetime();
|
|
755
|
-
const boundedName = z.string().min(1).max(RETRO_ENVELOPE_WIRE_BOUNDS.nameMaxChars);
|
|
756
|
-
const boundedShortName = z.string().min(1).max(RETRO_ENVELOPE_WIRE_BOUNDS.shortMaxChars);
|
|
757
|
-
const boundedText = z.string().min(1).max(RETRO_ENVELOPE_WIRE_BOUNDS.textMaxChars);
|
|
758
|
-
const RetroRepoSchema = z.object({
|
|
759
|
-
name: boundedShortName,
|
|
760
|
-
owner: boundedShortName
|
|
761
|
-
}).strict();
|
|
762
|
-
const RetroRefsSchema = z.object({
|
|
763
|
-
branch: z.string().min(1).max(RETRO_ENVELOPE_WIRE_BOUNDS.branchMaxChars).optional(),
|
|
764
|
-
epic: boundedName.optional(),
|
|
765
|
-
headSha: z.string().regex(/^[0-9a-f]{7,40}$/u).optional(),
|
|
766
|
-
issue: boundedName.optional(),
|
|
767
|
-
prNumber: z.number().int().positive().optional()
|
|
768
|
-
}).strict();
|
|
769
|
-
/**
|
|
770
|
-
* Per-lane join keys: how the envelope re-joins raw per-session sources.
|
|
771
|
-
* `agentRunId` lives at the envelope root; these carry the per-family session
|
|
772
|
-
* identities (Claude `session.id`, codex `threadId`).
|
|
773
|
-
*/
|
|
774
|
-
const RetroJoinKeysSchema = z.object({
|
|
775
|
-
claudeSessionIds: z.array(boundedName).max(RETRO_ENVELOPE_WIRE_BOUNDS.listMax).default([]),
|
|
776
|
-
codexThreadIds: z.array(boundedName).max(RETRO_ENVELOPE_WIRE_BOUNDS.listMax).default([])
|
|
777
|
-
}).strict();
|
|
778
|
-
const RetroWallClockSchema = z.object({
|
|
779
|
-
endTs: isoTimestamp,
|
|
780
|
-
startTs: isoTimestamp,
|
|
781
|
-
totalSec: z.number().nonnegative()
|
|
782
|
-
}).strict();
|
|
783
|
-
const RetroPhaseMarkSchema = z.object({
|
|
784
|
-
at: isoTimestamp,
|
|
785
|
-
deltaSec: z.number().nonnegative().optional(),
|
|
786
|
-
name: boundedName
|
|
787
|
-
}).strict();
|
|
788
|
-
/** Gate-timing ledger entry: `{gate, startedAt, duration, outcome, cycle}`. */
|
|
789
|
-
const RetroGateLedgerEntrySchema = z.object({
|
|
790
|
-
cycle: nonNegInt,
|
|
791
|
-
duration: z.number().nonnegative(),
|
|
792
|
-
gate: boundedName,
|
|
793
|
-
outcome: z.enum([
|
|
794
|
-
"pass",
|
|
795
|
-
"fail",
|
|
796
|
-
"skip"
|
|
797
|
-
]),
|
|
798
|
-
startedAt: isoTimestamp
|
|
799
|
-
}).strict();
|
|
800
|
-
/**
|
|
801
|
-
* Claude family: Anthropic-style EXCLUSIVE input tiers. True input context is
|
|
802
|
-
* freshInput + cacheReadInput + cacheCreationInput; values are
|
|
803
|
-
* requestId-deduped (S2: naive row sums overcount input ~1.94x).
|
|
804
|
-
*/
|
|
805
|
-
const claudeTokenBlockSchema = z.object({
|
|
806
|
-
cacheCreationInput: nonNegInt,
|
|
807
|
-
cacheReadInput: nonNegInt,
|
|
808
|
-
costUsd: z.number().nonnegative().nullable(),
|
|
809
|
-
family: z.literal("claude"),
|
|
810
|
-
freshInput: nonNegInt,
|
|
811
|
-
model: boundedShortName,
|
|
812
|
-
output: nonNegInt,
|
|
813
|
-
requests: nonNegInt
|
|
814
|
-
}).strict();
|
|
815
|
-
/**
|
|
816
|
-
* GPT/codex family, per delegated role. codex reports `input_tokens`
|
|
817
|
-
* INCLUSIVE of cached tokens; `freshInputDerived` = inclusive − cached is the
|
|
818
|
-
* only value safe for a per-token pricer (S3: skipping this overprices ~3.9x).
|
|
819
|
-
*/
|
|
820
|
-
const gptRoleUsageSchema = z.object({
|
|
821
|
-
cachedInput: nonNegInt,
|
|
822
|
-
costUsd: z.number().nonnegative().nullable(),
|
|
823
|
-
freshInputDerived: nonNegInt,
|
|
824
|
-
inputInclusiveOfCache: nonNegInt,
|
|
825
|
-
model: boundedShortName,
|
|
826
|
-
output: nonNegInt,
|
|
827
|
-
reasoningOutput: nonNegInt,
|
|
828
|
-
role: boundedShortName,
|
|
829
|
-
threadId: boundedName.optional()
|
|
830
|
-
}).strict().refine((usage) => usage.freshInputDerived === usage.inputInclusiveOfCache - usage.cachedInput, { message: "freshInputDerived must equal inputInclusiveOfCache - cachedInput (S3 accounting rule)" });
|
|
831
|
-
const gptTokenBlockSchema = z.object({
|
|
832
|
-
family: z.literal("gpt"),
|
|
833
|
-
roles: z.array(gptRoleUsageSchema).min(1).max(RETRO_ENVELOPE_WIRE_BOUNDS.gptRolesMax)
|
|
834
|
-
}).strict();
|
|
835
1192
|
/**
|
|
836
|
-
*
|
|
837
|
-
*
|
|
838
|
-
*
|
|
839
|
-
*
|
|
840
|
-
*
|
|
1193
|
+
* The pr:verify proof versions a reader accepts: the current one only (#920
|
|
1194
|
+
* item 1). #917 narrowed the pr:ready reader the same way. A pr:verify proof
|
|
1195
|
+
* is per-candidate and short-lived — it is rewritten on every head — so no
|
|
1196
|
+
* stored v1–v3 proof can outlive the release that drops it. Pre-1.0 posture
|
|
1197
|
+
* applies: one current contract, no compat reader. A consumer on an older
|
|
1198
|
+
* factory must adopt this release before HQ ingests its pr:verify proofs.
|
|
841
1199
|
*/
|
|
842
|
-
const
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
thermoFixRounds: nonNegInt
|
|
850
|
-
}).strict();
|
|
851
|
-
const RetroOutcomeSchema = z.object({
|
|
852
|
-
status: z.enum([
|
|
853
|
-
"success",
|
|
854
|
-
"blocked",
|
|
855
|
-
"fail",
|
|
856
|
-
"ship-with-followups"
|
|
1200
|
+
const SUPPORTED_PR_VERIFY_SCHEMA_VERSIONS = [4];
|
|
1201
|
+
const prVerifyTestFailureSchema = z.object({
|
|
1202
|
+
file: z.string().min(1),
|
|
1203
|
+
kind: z.enum([
|
|
1204
|
+
"assertion",
|
|
1205
|
+
"error",
|
|
1206
|
+
"timeout"
|
|
857
1207
|
]),
|
|
858
|
-
|
|
859
|
-
}).strict();
|
|
860
|
-
const retroEnvelopeV1Schema = z.object({
|
|
861
|
-
agentRunId: boundedName,
|
|
862
|
-
/** Pointer (key/URL) to #8's durable proof archive — never the payload. */
|
|
863
|
-
archiveRef: z.string().min(1).max(RETRO_ENVELOPE_WIRE_BOUNDS.archiveRefMaxChars).optional(),
|
|
864
|
-
cycles: RetroCycleCountersSchema,
|
|
865
|
-
dataGaps: z.array(boundedText).max(RETRO_ENVELOPE_WIRE_BOUNDS.listMax).default([]),
|
|
866
|
-
gates: z.array(RetroGateLedgerEntrySchema).max(RETRO_ENVELOPE_WIRE_BOUNDS.gatesMax),
|
|
867
|
-
generatedAt: isoTimestamp,
|
|
868
|
-
interventions: z.object({ count: nonNegInt }).strict(),
|
|
869
|
-
joinKeys: RetroJoinKeysSchema,
|
|
870
|
-
kind: z.literal("retro-envelope"),
|
|
871
|
-
outcome: RetroOutcomeSchema.optional(),
|
|
872
|
-
phases: z.array(RetroPhaseMarkSchema).max(RETRO_ENVELOPE_WIRE_BOUNDS.listMax),
|
|
873
|
-
refs: RetroRefsSchema,
|
|
874
|
-
repo: RetroRepoSchema,
|
|
875
|
-
schemaVersion: z.literal(1),
|
|
876
|
-
tokenFamilies: retroTokenFamiliesSchema,
|
|
877
|
-
wallClock: RetroWallClockSchema
|
|
878
|
-
}).strict().refine((envelope) => envelope.refs.epic !== void 0 || envelope.refs.issue !== void 0 || envelope.refs.prNumber !== void 0, { message: "refs must anchor to an epic, issue, or PR" });
|
|
879
|
-
/**
|
|
880
|
-
* Version 1 envelopes written before #170 carried an inert `harness` label.
|
|
881
|
-
* Accept that exact historical field and normalize it away. The current strict
|
|
882
|
-
* schema above neither emits nor exposes it, and all other unknown fields still
|
|
883
|
-
* fail validation.
|
|
884
|
-
*/
|
|
885
|
-
const retroEnvelopeV1ReaderSchema = z.preprocess((candidate) => {
|
|
886
|
-
if (candidate === null || typeof candidate !== "object" || Array.isArray(candidate) || !("harness" in candidate)) return candidate;
|
|
887
|
-
const record = candidate;
|
|
888
|
-
if (!boundedShortName.safeParse(record.harness).success) return candidate;
|
|
889
|
-
const { harness: _historicalHarness, ...current } = record;
|
|
890
|
-
return current;
|
|
891
|
-
}, retroEnvelopeV1Schema);
|
|
892
|
-
/**
|
|
893
|
-
* Versioned payload validators, keyed by schema major. Unknown majors never
|
|
894
|
-
* reach these — {@link parseRetroEnvelope} returns them raw and marked
|
|
895
|
-
* degraded, mirroring HQ's ingest skew posture (stored raw, never dropped).
|
|
896
|
-
*/
|
|
897
|
-
const RETRO_ENVELOPE_VALIDATORS = { [1]: retroEnvelopeV1ReaderSchema };
|
|
898
|
-
const SUPPORTED_RETRO_ENVELOPE_SCHEMA_VERSIONS = Object.keys(RETRO_ENVELOPE_VALIDATORS).map(Number);
|
|
899
|
-
/** Epic anchor for an envelope: refs.epic, else refs.issue, else the PR. */
|
|
900
|
-
const retroEpicReference = (refs) => {
|
|
901
|
-
return (refs.epic ?? refs.issue ?? (refs.prNumber === void 0 ? void 0 : `pr#${refs.prNumber}`))?.replace(/^#/u, "");
|
|
902
|
-
};
|
|
903
|
-
//#endregion
|
|
904
|
-
//#region src/schemas.ts
|
|
905
|
-
const EVIDENCE_CHECK_TYPES = ["review", "verify"];
|
|
906
|
-
const EVIDENCE_REVIEW_RUNGS = [
|
|
907
|
-
"independent-model",
|
|
908
|
-
"oracle",
|
|
909
|
-
"human"
|
|
910
|
-
];
|
|
911
|
-
const DIFF_CLASSIFICATIONS = [
|
|
912
|
-
"docs/process-only",
|
|
913
|
-
"trivial",
|
|
914
|
-
"non-trivial"
|
|
915
|
-
];
|
|
916
|
-
const REVIEW_CATEGORIES = [
|
|
917
|
-
"correctness",
|
|
918
|
-
"safety",
|
|
919
|
-
"coordination",
|
|
920
|
-
"maintainability",
|
|
921
|
-
"unknown"
|
|
922
|
-
];
|
|
923
|
-
const REVIEW_RUNGS = ["independentModel", "oracle"];
|
|
924
|
-
const REVIEW_STATUS_VALUES = [
|
|
925
|
-
"not-required",
|
|
926
|
-
"current",
|
|
927
|
-
"stale",
|
|
928
|
-
"missing",
|
|
929
|
-
"blocked"
|
|
930
|
-
];
|
|
931
|
-
const followUpActionSchema = z.object({
|
|
932
|
-
argv: z.array(z.string()),
|
|
933
|
-
command: z.string()
|
|
934
|
-
}).strict();
|
|
935
|
-
const EVIDENCE_ENVELOPE_SCHEMA_VERSION = 1;
|
|
936
|
-
const evidenceShaSchema = z.string().regex(/^[0-9a-f]{7,40}$/u, { message: "must be a 7-40 char lowercase hex git SHA" });
|
|
937
|
-
const evidenceEnvelopeSchema = z.object({
|
|
938
|
-
check: z.string().min(1),
|
|
939
|
-
checkType: z.enum(EVIDENCE_CHECK_TYPES),
|
|
940
|
-
findingsPointer: z.string().min(1).optional(),
|
|
941
|
-
headSha: evidenceShaSchema,
|
|
942
|
-
mergeBaseSha: evidenceShaSchema,
|
|
943
|
-
model: z.string().min(1).optional(),
|
|
944
|
-
outcome: z.enum(["pass", "fail"]),
|
|
945
|
-
patchId: evidenceShaSchema,
|
|
946
|
-
policyVersion: z.string().min(1).optional(),
|
|
947
|
-
producer: z.string().min(1),
|
|
948
|
-
requestId: z.string().min(1).optional(),
|
|
949
|
-
rung: z.enum(EVIDENCE_REVIEW_RUNGS).optional(),
|
|
950
|
-
schemaVersion: z.literal(1),
|
|
951
|
-
sessionId: z.string().trim().min(1).optional(),
|
|
952
|
-
timestamp: z.iso.datetime().optional()
|
|
953
|
-
}).passthrough().superRefine((envelope, context) => {
|
|
954
|
-
if (envelope.checkType === "review") {
|
|
955
|
-
if (envelope.rung === void 0) context.addIssue({
|
|
956
|
-
code: "custom",
|
|
957
|
-
message: "review-type evidence envelope requires a rung (ADR 0014 §3).",
|
|
958
|
-
path: ["rung"]
|
|
959
|
-
});
|
|
960
|
-
} else {
|
|
961
|
-
if (envelope.rung !== void 0) context.addIssue({
|
|
962
|
-
code: "custom",
|
|
963
|
-
message: "verify-type evidence envelope must not carry rung (review-type-only field, ADR 0014 §3).",
|
|
964
|
-
path: ["rung"]
|
|
965
|
-
});
|
|
966
|
-
if (envelope.model !== void 0) context.addIssue({
|
|
967
|
-
code: "custom",
|
|
968
|
-
message: "verify-type evidence envelope must not carry model (review-type-only field, ADR 0014 §3).",
|
|
969
|
-
path: ["model"]
|
|
970
|
-
});
|
|
971
|
-
}
|
|
1208
|
+
title: z.string().min(1)
|
|
972
1209
|
});
|
|
973
|
-
const SUPPORTED_PR_VERIFY_SCHEMA_VERSIONS = [
|
|
974
|
-
1,
|
|
975
|
-
2,
|
|
976
|
-
3,
|
|
977
|
-
4
|
|
978
|
-
];
|
|
979
1210
|
const executedCommandSchema = z.object({
|
|
980
1211
|
command: z.string().min(1),
|
|
981
1212
|
counts: z.object({
|
|
@@ -984,9 +1215,9 @@ const executedCommandSchema = z.object({
|
|
|
984
1215
|
}).optional(),
|
|
985
1216
|
durationMs: z.number().nonnegative(),
|
|
986
1217
|
exitCode: z.number(),
|
|
1218
|
+
failures: z.array(prVerifyTestFailureSchema).min(1).optional(),
|
|
987
1219
|
name: z.string().min(1),
|
|
988
1220
|
scope: z.enum([
|
|
989
|
-
"always",
|
|
990
1221
|
"docs-only",
|
|
991
1222
|
"trivial",
|
|
992
1223
|
"full"
|
|
@@ -997,10 +1228,15 @@ const notRequiredCommandSchema = z.object({
|
|
|
997
1228
|
impactTarget: z.string().min(1),
|
|
998
1229
|
name: z.string().min(1)
|
|
999
1230
|
});
|
|
1000
|
-
const
|
|
1231
|
+
const notDemandedRecordSchema = z.object({
|
|
1232
|
+
basis: z.string().min(1),
|
|
1233
|
+
name: z.string().min(1)
|
|
1234
|
+
});
|
|
1235
|
+
const prVerifySchemaVersionSchema = z.literal(4);
|
|
1001
1236
|
const prVerifyProofSchema = z.object({
|
|
1002
|
-
authoringSession: z.string().trim().min(1)
|
|
1237
|
+
authoringSession: z.string().trim().min(1),
|
|
1003
1238
|
base: z.string().min(1),
|
|
1239
|
+
baseSha: z.string().regex(/^[0-9a-f]{40}$/u).optional(),
|
|
1004
1240
|
baselineFullProofs: z.array(z.object({
|
|
1005
1241
|
base: z.string().min(1),
|
|
1006
1242
|
changedFiles: z.array(z.string()),
|
|
@@ -1009,8 +1245,13 @@ const prVerifyProofSchema = z.object({
|
|
|
1009
1245
|
projectKey: z.string().min(1),
|
|
1010
1246
|
repository: z.string().min(1)
|
|
1011
1247
|
})).optional(),
|
|
1248
|
+
catchUpRecognition: catchUpRecognitionSchema.optional(),
|
|
1012
1249
|
changedFiles: z.array(z.string()),
|
|
1013
|
-
classification: z.enum(
|
|
1250
|
+
classification: z.enum([
|
|
1251
|
+
"docs/process-only",
|
|
1252
|
+
"trivial",
|
|
1253
|
+
"non-trivial"
|
|
1254
|
+
]),
|
|
1014
1255
|
classificationReasons: z.array(z.string()),
|
|
1015
1256
|
command: z.literal("patronage-factory pr:verify"),
|
|
1016
1257
|
durationMs: z.number().nonnegative(),
|
|
@@ -1024,9 +1265,11 @@ const prVerifyProofSchema = z.object({
|
|
|
1024
1265
|
"trivial",
|
|
1025
1266
|
"full"
|
|
1026
1267
|
]),
|
|
1268
|
+
notDemandedRecords: z.array(notDemandedRecordSchema).min(1).optional(),
|
|
1027
1269
|
notRequiredCommands: z.array(notRequiredCommandSchema).min(1).optional(),
|
|
1028
1270
|
outcome: z.enum(["aborted", "passed"]).optional(),
|
|
1029
1271
|
patchId: z.string().regex(/^[0-9a-f]{40}$/u).optional(),
|
|
1272
|
+
policyResolution: policyResolutionSchema.optional(),
|
|
1030
1273
|
profilePath: z.string().min(1),
|
|
1031
1274
|
projectKey: z.string().min(1),
|
|
1032
1275
|
repository: z.string().min(1),
|
|
@@ -1038,186 +1281,133 @@ const prVerifyProofSchema = z.object({
|
|
|
1038
1281
|
impactTarget: z.string().min(1).optional(),
|
|
1039
1282
|
name: z.string().min(1),
|
|
1040
1283
|
scope: z.enum([
|
|
1041
|
-
"always",
|
|
1042
1284
|
"docs-only",
|
|
1043
1285
|
"trivial",
|
|
1044
1286
|
"full"
|
|
1045
1287
|
])
|
|
1046
1288
|
}))
|
|
1047
1289
|
}).superRefine(assertPrVerifyProofRules);
|
|
1048
|
-
|
|
1049
|
-
const
|
|
1050
|
-
const
|
|
1051
|
-
const
|
|
1052
|
-
const
|
|
1053
|
-
const
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1290
|
+
/** Artifact versions accepted at ingest. */
|
|
1291
|
+
const SUPPORTED_CLOSEOUT_SCHEMA_VERSIONS = [5];
|
|
1292
|
+
const MAX_CLOSEOUT_ROWS = 1e3;
|
|
1293
|
+
const IdentifierSchema = z.string().min(1).max(500);
|
|
1294
|
+
const NarrativeSchema = z.string().min(1).max(1e4);
|
|
1295
|
+
const RepoSchema = z.string().min(1).max(200);
|
|
1296
|
+
const CloseoutSourceSchema = z.object({
|
|
1297
|
+
detail: NarrativeSchema,
|
|
1298
|
+
kind: z.literal("token-harvest")
|
|
1057
1299
|
}).strict();
|
|
1058
|
-
const
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
protocolFinding: z.literal(true).optional(),
|
|
1073
|
-
supersedes: findingSupersedesSchema.optional(),
|
|
1074
|
-
title: nonBlankString
|
|
1075
|
-
});
|
|
1076
|
-
const prReviewResultSchema = z.object({
|
|
1077
|
-
command: z.string().refine((value) => value.trim().length > 0, { message: "command must not be blank" }).optional(),
|
|
1078
|
-
durationMs: z.number().refine((value) => Number.isFinite(value) && value >= 0, { message: "durationMs must be a finite number >= 0" }),
|
|
1079
|
-
endedAt: parseableDateString,
|
|
1080
|
-
exitCode: z.number().nullable().optional(),
|
|
1081
|
-
findings: z.array(prReviewFindingSchema),
|
|
1082
|
-
issuesFlagged: z.number().int().nonnegative(),
|
|
1083
|
-
kind: z.enum(["correctness", "security"]),
|
|
1084
|
-
model: nonBlankString.optional(),
|
|
1085
|
-
outcome: z.enum([
|
|
1086
|
-
"passed",
|
|
1087
|
-
"failed",
|
|
1088
|
-
"error"
|
|
1300
|
+
const CloseoutBudgetSchema = z.object({
|
|
1301
|
+
note: NarrativeSchema,
|
|
1302
|
+
tier: z.literal("overseer")
|
|
1303
|
+
}).strict();
|
|
1304
|
+
const CloseoutMetricRowSchema = z.object({
|
|
1305
|
+
label: IdentifierSchema,
|
|
1306
|
+
metric: IdentifierSchema,
|
|
1307
|
+
source: CloseoutSourceSchema,
|
|
1308
|
+
unit: z.enum([
|
|
1309
|
+
"boolean",
|
|
1310
|
+
"count",
|
|
1311
|
+
"text",
|
|
1312
|
+
"tokens",
|
|
1313
|
+
"usd"
|
|
1089
1314
|
]),
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1315
|
+
value: z.union([
|
|
1316
|
+
z.number(),
|
|
1317
|
+
NarrativeSchema,
|
|
1318
|
+
z.boolean(),
|
|
1319
|
+
z.null()
|
|
1320
|
+
])
|
|
1321
|
+
}).strict();
|
|
1322
|
+
const CloseoutBlindspotSchema = z.object({
|
|
1323
|
+
id: IdentifierSchema,
|
|
1324
|
+
note: NarrativeSchema,
|
|
1325
|
+
reason: NarrativeSchema.optional()
|
|
1326
|
+
}).strict();
|
|
1327
|
+
/** Strict, bounded runtime transport contract emitted by factory:closeout. */
|
|
1328
|
+
const closeoutArtifactSchema = z.object({
|
|
1329
|
+
budget: CloseoutBudgetSchema,
|
|
1330
|
+
couldNotSee: z.array(CloseoutBlindspotSchema).min(1).max(MAX_CLOSEOUT_ROWS),
|
|
1331
|
+
epic: IdentifierSchema,
|
|
1332
|
+
generatedAt: z.iso.datetime(),
|
|
1333
|
+
metrics: z.array(CloseoutMetricRowSchema).max(MAX_CLOSEOUT_ROWS),
|
|
1334
|
+
repo: RepoSchema,
|
|
1335
|
+
schemaVersion: z.literal(5)
|
|
1336
|
+
}).strict();
|
|
1337
|
+
/** The self-timed gate commands. */
|
|
1338
|
+
const FACTORY_GATE_NAMES = [
|
|
1339
|
+
"boundary:check",
|
|
1340
|
+
"factory:closeout",
|
|
1341
|
+
"pr:ready",
|
|
1342
|
+
"pr:review",
|
|
1343
|
+
"pr:verify"
|
|
1344
|
+
];
|
|
1345
|
+
const nonNegativeInteger = z.number().int().nonnegative();
|
|
1346
|
+
const gateTimingRecordSchema = z.object({
|
|
1347
|
+
/** The lane run identity the ledger is keyed by. */
|
|
1348
|
+
agentRunId: z.string().min(1),
|
|
1349
|
+
cycle: z.number().int().positive(),
|
|
1350
|
+
durationMs: nonNegativeInteger,
|
|
1351
|
+
gate: z.enum(FACTORY_GATE_NAMES),
|
|
1352
|
+
outcome: z.enum(["fail", "pass"]),
|
|
1353
|
+
schemaVersion: z.literal(1),
|
|
1354
|
+
/**
|
|
1355
|
+
* Milliseconds from the lane run's origin (the first record's start) to
|
|
1356
|
+
* this record's start. The first record is offset 0.
|
|
1357
|
+
*/
|
|
1358
|
+
startOffsetMs: nonNegativeInteger,
|
|
1359
|
+
/** Wall-clock start of this gate invocation (ISO 8601). */
|
|
1360
|
+
startedAt: z.iso.datetime()
|
|
1361
|
+
}).strict();
|
|
1362
|
+
//#endregion
|
|
1363
|
+
//#region src/schemas.ts
|
|
1364
|
+
const evidenceCheckTypeSchema = z.enum(EVIDENCE_CHECK_TYPES);
|
|
1365
|
+
const evidenceReviewRungSchema = z.enum(EVIDENCE_REVIEW_RUNGS);
|
|
1366
|
+
const diffClassificationSchema = z.enum(DIFF_CLASSIFICATIONS);
|
|
1367
|
+
const ladderStageSchema = z.enum(LADDER_STAGES);
|
|
1368
|
+
const ladderNextActionSchema = z.enum(LADDER_NEXT_ACTION_KINDS);
|
|
1369
|
+
const ladderForcedTransitionSchema = z.enum(LADDER_FORCED_TRANSITION_KINDS);
|
|
1370
|
+
const followUpActionSchema = z.object({
|
|
1371
|
+
argv: z.array(z.string()),
|
|
1372
|
+
command: z.string()
|
|
1373
|
+
}).strict();
|
|
1374
|
+
const EVIDENCE_ENVELOPE_SCHEMA_VERSION = 1;
|
|
1375
|
+
const evidenceShaSchema = z.string().regex(/^[0-9a-f]{7,40}$/u, { message: "must be a 7-40 char lowercase hex git SHA" });
|
|
1376
|
+
const evidenceEnvelopeSchema = z.object({
|
|
1377
|
+
check: z.string().min(1),
|
|
1378
|
+
checkType: evidenceCheckTypeSchema,
|
|
1379
|
+
findingsPointer: z.string().min(1).optional(),
|
|
1380
|
+
headSha: evidenceShaSchema,
|
|
1381
|
+
mergeBaseSha: evidenceShaSchema,
|
|
1382
|
+
model: z.string().min(1).optional(),
|
|
1383
|
+
outcome: z.enum(["pass", "fail"]),
|
|
1384
|
+
patchId: evidenceShaSchema,
|
|
1385
|
+
policyVersion: z.string().min(1).optional(),
|
|
1386
|
+
producer: z.string().min(1),
|
|
1387
|
+
requestId: z.string().min(1).optional(),
|
|
1388
|
+
rung: evidenceReviewRungSchema.optional(),
|
|
1389
|
+
schemaVersion: z.literal(1),
|
|
1390
|
+
sessionId: z.string().trim().min(1).optional(),
|
|
1391
|
+
timestamp: z.iso.datetime().optional()
|
|
1392
|
+
}).passthrough().superRefine((envelope, context) => {
|
|
1393
|
+
if (envelope.checkType === "review") {
|
|
1394
|
+
if (envelope.rung === void 0) context.addIssue({
|
|
1125
1395
|
code: "custom",
|
|
1126
|
-
message: "
|
|
1127
|
-
path: ["
|
|
1396
|
+
message: "review-type evidence envelope requires a rung (ADR 0014).",
|
|
1397
|
+
path: ["rung"]
|
|
1128
1398
|
});
|
|
1129
|
-
}
|
|
1130
|
-
|
|
1131
|
-
if (review.findings.length === 0) context.addIssue({
|
|
1399
|
+
} else {
|
|
1400
|
+
if (envelope.rung !== void 0) context.addIssue({
|
|
1132
1401
|
code: "custom",
|
|
1133
|
-
message: "
|
|
1134
|
-
path: ["
|
|
1402
|
+
message: "verify-type evidence envelope must not carry rung (review-type-only field, ADR 0014).",
|
|
1403
|
+
path: ["rung"]
|
|
1135
1404
|
});
|
|
1136
|
-
if (
|
|
1405
|
+
if (envelope.model !== void 0) context.addIssue({
|
|
1137
1406
|
code: "custom",
|
|
1138
|
-
message: "
|
|
1139
|
-
path: ["
|
|
1407
|
+
message: "verify-type evidence envelope must not carry model (review-type-only field, ADR 0014).",
|
|
1408
|
+
path: ["model"]
|
|
1140
1409
|
});
|
|
1141
1410
|
}
|
|
1142
|
-
if (review.outcome === "failed" && review.exitCode !== void 0 && review.exitCode !== 0) context.addIssue({
|
|
1143
|
-
code: "custom",
|
|
1144
|
-
message: "failed reviews must exit with code 0",
|
|
1145
|
-
path: ["exitCode"]
|
|
1146
|
-
});
|
|
1147
|
-
if (review.producer === void 0 !== (review.rung === void 0)) context.addIssue({
|
|
1148
|
-
code: "custom",
|
|
1149
|
-
message: "producer and rung must be recorded together",
|
|
1150
|
-
path: [review.producer === void 0 ? "producer" : "rung"]
|
|
1151
|
-
});
|
|
1152
|
-
});
|
|
1153
|
-
const ladderFindingSchema = z.object({
|
|
1154
|
-
citedSpan: z.string().optional(),
|
|
1155
|
-
file: z.string().optional(),
|
|
1156
|
-
prescribedAction: z.string().optional(),
|
|
1157
|
-
supersedes: z.object({
|
|
1158
|
-
file: z.string().optional(),
|
|
1159
|
-
title: z.string()
|
|
1160
|
-
}).optional(),
|
|
1161
|
-
title: z.string()
|
|
1162
|
-
}).passthrough();
|
|
1163
|
-
const ladderDispositionSchema = z.object({
|
|
1164
|
-
disposition: z.enum([
|
|
1165
|
-
"fixed-in-thread",
|
|
1166
|
-
"follow-up-filed",
|
|
1167
|
-
"waived"
|
|
1168
|
-
]),
|
|
1169
|
-
finding: z.object({
|
|
1170
|
-
file: z.string().optional(),
|
|
1171
|
-
title: z.string()
|
|
1172
|
-
}),
|
|
1173
|
-
reference: z.string().optional()
|
|
1174
|
-
}).passthrough();
|
|
1175
|
-
const ladderCycleSchema = z.object({
|
|
1176
|
-
dispositions: z.array(ladderDispositionSchema).optional(),
|
|
1177
|
-
findings: z.array(ladderFindingSchema)
|
|
1178
|
-
}).passthrough();
|
|
1179
|
-
const prReviewProofSchema = z.object({
|
|
1180
|
-
base: z.string(),
|
|
1181
|
-
changedFiles: z.array(z.string()),
|
|
1182
|
-
cleanedPaths: z.array(z.string()),
|
|
1183
|
-
findingProvenanceVersion: z.literal(PR_REVIEW_FINDING_PROVENANCE_VERSION),
|
|
1184
|
-
headSha: z.string().regex(/^[0-9a-f]{40}$/u),
|
|
1185
|
-
ladder: z.object({ cycles: z.array(ladderCycleSchema) }).superRefine((ladder, context) => {
|
|
1186
|
-
let gateStarted = false;
|
|
1187
|
-
try {
|
|
1188
|
-
for (const cycle of ladder.cycles) {
|
|
1189
|
-
const { stage } = cycle;
|
|
1190
|
-
if (stage === "interior") {
|
|
1191
|
-
if (gateStarted) throw new TypeError("interior cycle after gate");
|
|
1192
|
-
} else gateStarted = true;
|
|
1193
|
-
}
|
|
1194
|
-
} catch {
|
|
1195
|
-
context.addIssue({
|
|
1196
|
-
code: "custom",
|
|
1197
|
-
message: "ladder cycles do not fold into a valid disposition ledger"
|
|
1198
|
-
});
|
|
1199
|
-
}
|
|
1200
|
-
}).optional(),
|
|
1201
|
-
maxReviewCycles: z.number().int().positive().optional(),
|
|
1202
|
-
patchId: z.string().regex(/^[0-9a-f]{40,64}$/u),
|
|
1203
|
-
reviewCycle: z.number().int().positive().optional(),
|
|
1204
|
-
reviewRequirement: z.object({
|
|
1205
|
-
reason: z.enum(["no-applicable-mode"]),
|
|
1206
|
-
status: z.literal("not-required")
|
|
1207
|
-
}).strict().optional(),
|
|
1208
|
-
reviews: z.array(prReviewResultSchema),
|
|
1209
|
-
schemaVersion: z.literal(2)
|
|
1210
|
-
}).passthrough().superRefine((proof, context) => {
|
|
1211
|
-
if (proof.reviewRequirement && proof.reviews.length > 0) context.addIssue({
|
|
1212
|
-
code: "custom",
|
|
1213
|
-
message: "not-required review proofs must not contain review runs",
|
|
1214
|
-
path: ["reviews"]
|
|
1215
|
-
});
|
|
1216
|
-
if (proof.reviewCycle !== void 0 && proof.maxReviewCycles !== void 0 && proof.reviewCycle > proof.maxReviewCycles) context.addIssue({
|
|
1217
|
-
code: "custom",
|
|
1218
|
-
message: "reviewCycle must not exceed maxReviewCycles",
|
|
1219
|
-
path: ["reviewCycle"]
|
|
1220
|
-
});
|
|
1221
1411
|
});
|
|
1222
1412
|
const BOUNDARY_REVIEW_PROOF_KIND = "boundary-review-proof";
|
|
1223
1413
|
const coveredSetEntrySchema = z.object({
|
|
@@ -1258,7 +1448,7 @@ const boundaryReviewProofSchema = z.object({
|
|
|
1258
1448
|
}).passthrough(),
|
|
1259
1449
|
outcome: z.enum(["pass", "fail"]),
|
|
1260
1450
|
producer: z.string().min(1),
|
|
1261
|
-
rung:
|
|
1451
|
+
rung: evidenceReviewRungSchema,
|
|
1262
1452
|
schemaVersion: z.literal(1),
|
|
1263
1453
|
sessionId: z.string().min(1).optional(),
|
|
1264
1454
|
specHash: z.object({
|
|
@@ -1279,7 +1469,7 @@ const boundaryCheckProofSchema = z.object({
|
|
|
1279
1469
|
state: z.enum(["merged", "open"]),
|
|
1280
1470
|
wave: z.string().min(1)
|
|
1281
1471
|
})),
|
|
1282
|
-
demandedRung:
|
|
1472
|
+
demandedRung: evidenceReviewRungSchema.optional(),
|
|
1283
1473
|
epicIssue: z.number().int().positive(),
|
|
1284
1474
|
manifestHash: z.object({
|
|
1285
1475
|
algo: z.literal("sha256"),
|
|
@@ -1291,7 +1481,7 @@ const boundaryCheckProofSchema = z.object({
|
|
|
1291
1481
|
commentId: z.union([z.string(), z.number()]).optional(),
|
|
1292
1482
|
commentUrl: z.string().optional(),
|
|
1293
1483
|
producer: z.string().min(1),
|
|
1294
|
-
rung:
|
|
1484
|
+
rung: evidenceReviewRungSchema,
|
|
1295
1485
|
sessionId: z.string().min(1).optional(),
|
|
1296
1486
|
specHash: z.object({
|
|
1297
1487
|
algo: z.literal("sha256"),
|
|
@@ -1306,42 +1496,27 @@ const boundaryCheckProofSchema = z.object({
|
|
|
1306
1496
|
}).optional(),
|
|
1307
1497
|
status: z.enum(["ready", "blocked"])
|
|
1308
1498
|
});
|
|
1309
|
-
const requiredCheckScopeSchema = z.object({ classifications: z.array(z.enum(DIFF_CLASSIFICATIONS)).min(1) }).strict();
|
|
1310
1499
|
const readinessRepairSchema = z.object({
|
|
1311
1500
|
action: z.string().min(1),
|
|
1312
|
-
code: z.enum([
|
|
1313
|
-
"undraft-pr",
|
|
1314
|
-
"render-pr-body-sections",
|
|
1315
|
-
"await-post-undraft-checks"
|
|
1316
|
-
]),
|
|
1501
|
+
code: z.enum(["undraft-pr", "await-post-undraft-checks"]),
|
|
1317
1502
|
command: z.string().min(1)
|
|
1318
1503
|
});
|
|
1319
1504
|
const reviewStateSchema = z.object({
|
|
1320
|
-
docsOnlyDeltaAccepted: z.boolean().optional(),
|
|
1321
1505
|
required: z.boolean(),
|
|
1322
1506
|
reviewedHeadSha: z.string().optional(),
|
|
1323
1507
|
reviewedPatchId: z.string().optional(),
|
|
1324
|
-
status:
|
|
1325
|
-
})
|
|
1326
|
-
const
|
|
1327
|
-
author: z.string().min(1),
|
|
1328
|
-
createdAt: z.string().min(1),
|
|
1329
|
-
summary: z.string().min(1),
|
|
1330
|
-
url: z.string().min(1)
|
|
1331
|
-
});
|
|
1332
|
-
const historicalHandledHumanCommentSchema = z.object({
|
|
1333
|
-
clearedAt: z.string().min(1).optional(),
|
|
1334
|
-
sessionId: z.string().min(1).optional(),
|
|
1335
|
-
source: z.enum(["check-run", "cli"]),
|
|
1336
|
-
url: z.string().min(1)
|
|
1508
|
+
status: reviewStatusSchema
|
|
1509
|
+
}).strict().superRefine((state, context) => {
|
|
1510
|
+
for (const issue of reviewStateIssues(state)) context.addIssue(issue);
|
|
1337
1511
|
});
|
|
1512
|
+
const verifyCommandSchema = z.literal("patronage-factory pr:verify");
|
|
1513
|
+
const verifiedShaSchema = z.string().min(1);
|
|
1338
1514
|
const managedReadinessLedgerSchema = z.object({
|
|
1339
1515
|
baseSha: z.string().regex(/^[0-9a-f]{40}$/u),
|
|
1340
1516
|
blockingReasons: z.array(z.string()),
|
|
1341
|
-
classification:
|
|
1517
|
+
classification: diffClassificationSchema,
|
|
1342
1518
|
externalChecks: z.array(z.object({
|
|
1343
|
-
checkType:
|
|
1344
|
-
inScope: z.boolean(),
|
|
1519
|
+
checkType: evidenceCheckTypeSchema,
|
|
1345
1520
|
name: z.string().min(1),
|
|
1346
1521
|
reason: z.string().optional(),
|
|
1347
1522
|
scope: requiredCheckScopeSchema.optional(),
|
|
@@ -1352,7 +1527,6 @@ const managedReadinessLedgerSchema = z.object({
|
|
|
1352
1527
|
"out-of-scope"
|
|
1353
1528
|
])
|
|
1354
1529
|
})).optional(),
|
|
1355
|
-
finalReviewPoint: z.boolean().optional(),
|
|
1356
1530
|
github: z.object({
|
|
1357
1531
|
draft: z.boolean(),
|
|
1358
1532
|
mergeStateStatus: z.string(),
|
|
@@ -1367,16 +1541,9 @@ const managedReadinessLedgerSchema = z.object({
|
|
|
1367
1541
|
reviewDecision: z.string().nullable().optional(),
|
|
1368
1542
|
unresolvedReviewThreads: z.number().int().nonnegative()
|
|
1369
1543
|
}),
|
|
1370
|
-
handledCommentsProducer: z.object({
|
|
1371
|
-
identity: z.string().min(1),
|
|
1372
|
-
mode: z.enum(["app", "commit-status"])
|
|
1373
|
-
}).optional(),
|
|
1374
|
-
handledHumanComments: z.array(historicalHandledHumanCommentSchema).optional(),
|
|
1375
1544
|
headSha: z.string().regex(/^[0-9a-f]{40}$/u),
|
|
1376
1545
|
mergeBaseSha: z.string().regex(/^[0-9a-f]{40}$/u).optional(),
|
|
1377
1546
|
patchId: z.string().regex(/^[0-9a-f]{40,64}$/u),
|
|
1378
|
-
postReadinessHumanComments: z.array(historicalPostReadinessCommentSchema).optional(),
|
|
1379
|
-
postReadinessHumanReviews: z.array(historicalPostReadinessCommentSchema).optional(),
|
|
1380
1547
|
pr: z.number().int().positive(),
|
|
1381
1548
|
repairs: z.array(readinessRepairSchema).default([]),
|
|
1382
1549
|
reviewCycleState: z.object({
|
|
@@ -1414,25 +1581,9 @@ const managedReadinessLedgerSchema = z.object({
|
|
|
1414
1581
|
gate: z.number().int().nonnegative(),
|
|
1415
1582
|
interior: z.number().int().nonnegative()
|
|
1416
1583
|
}),
|
|
1417
|
-
forcedTransition:
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
"interior-takeover"
|
|
1421
|
-
]).optional(),
|
|
1422
|
-
nextAction: z.enum([
|
|
1423
|
-
"run-interior-cycle",
|
|
1424
|
-
"advance-to-gate",
|
|
1425
|
-
"run-gate-cycle",
|
|
1426
|
-
"accept-nonblocking-findings",
|
|
1427
|
-
"escalate-to-triage",
|
|
1428
|
-
"ready-for-human"
|
|
1429
|
-
]),
|
|
1430
|
-
stage: z.enum([
|
|
1431
|
-
"interior",
|
|
1432
|
-
"interior-takeover",
|
|
1433
|
-
"interior-complete",
|
|
1434
|
-
"gate"
|
|
1435
|
-
])
|
|
1584
|
+
forcedTransition: ladderForcedTransitionSchema.optional(),
|
|
1585
|
+
nextAction: ladderNextActionSchema,
|
|
1586
|
+
stage: ladderStageSchema
|
|
1436
1587
|
}).optional(),
|
|
1437
1588
|
reviewRuns: z.array(prReviewResultSchema).optional(),
|
|
1438
1589
|
reviewTerminalState: z.enum([
|
|
@@ -1444,40 +1595,48 @@ const managedReadinessLedgerSchema = z.object({
|
|
|
1444
1595
|
correctness: reviewStateSchema,
|
|
1445
1596
|
security: reviewStateSchema.optional()
|
|
1446
1597
|
}),
|
|
1447
|
-
schemaVersion: z.literal(
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
"
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1598
|
+
schemaVersion: z.literal(2),
|
|
1599
|
+
verification: z.discriminatedUnion("prVerify", [
|
|
1600
|
+
z.object({
|
|
1601
|
+
command: verifyCommandSchema,
|
|
1602
|
+
prVerify: z.literal("missing")
|
|
1603
|
+
}).strict(),
|
|
1604
|
+
z.object({
|
|
1605
|
+
command: verifyCommandSchema,
|
|
1606
|
+
prVerify: z.literal("stale"),
|
|
1607
|
+
verifiedHeadSha: verifiedShaSchema
|
|
1608
|
+
}).strict(),
|
|
1609
|
+
z.object({
|
|
1610
|
+
command: verifyCommandSchema,
|
|
1611
|
+
prVerify: z.literal("passed-via-head"),
|
|
1612
|
+
verifiedHeadSha: verifiedShaSchema
|
|
1613
|
+
}).strict(),
|
|
1614
|
+
z.object({
|
|
1615
|
+
command: verifyCommandSchema,
|
|
1616
|
+
docsOnlyVerifiedHeadSha: verifiedShaSchema,
|
|
1617
|
+
prVerify: z.literal("docs-only-delta"),
|
|
1618
|
+
verifiedHeadSha: verifiedShaSchema.optional()
|
|
1619
|
+
}).strict(),
|
|
1620
|
+
z.object({
|
|
1621
|
+
command: verifyCommandSchema,
|
|
1622
|
+
prVerify: z.literal("trivial-delta"),
|
|
1623
|
+
trivialVerifiedHeadSha: verifiedShaSchema,
|
|
1624
|
+
verifiedHeadSha: verifiedShaSchema.optional()
|
|
1625
|
+
}).strict()
|
|
1626
|
+
])
|
|
1467
1627
|
});
|
|
1468
1628
|
const PR_READY_SCHEMA_VERSION = 3;
|
|
1469
1629
|
/**
|
|
1470
|
-
* The pr:ready proof versions a reader
|
|
1471
|
-
*
|
|
1472
|
-
*
|
|
1473
|
-
*
|
|
1630
|
+
* The pr:ready proof versions a reader accepts: the current one only (#917
|
|
1631
|
+
* item 1). #894 narrowed the nested readiness ledger to `schemaVersion: 2` and
|
|
1632
|
+
* the `prVerify` tagged union, which no v1 or v2 pr:ready proof can satisfy.
|
|
1633
|
+
* Advertising 1 and 2 here therefore promised an acceptance the nested schema
|
|
1634
|
+
* already refused. Pre-1.0 posture applies: one current contract, no compat
|
|
1635
|
+
* reader. An alpha.22 consumer must adopt alpha.23 before HQ ingests its
|
|
1636
|
+
* pr:ready proofs.
|
|
1474
1637
|
*/
|
|
1475
|
-
const SUPPORTED_PR_READY_SCHEMA_VERSIONS = [
|
|
1476
|
-
|
|
1477
|
-
2,
|
|
1478
|
-
3
|
|
1479
|
-
];
|
|
1480
|
-
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(", ")}` });
|
|
1638
|
+
const SUPPORTED_PR_READY_SCHEMA_VERSIONS = [3];
|
|
1639
|
+
const prReadySchemaVersionSchema = z.literal(3);
|
|
1481
1640
|
const prReadyProofSchema = z.object({
|
|
1482
1641
|
arming: z.object({
|
|
1483
1642
|
detail: z.string().min(1).optional(),
|
|
@@ -1488,6 +1647,7 @@ const prReadyProofSchema = z.object({
|
|
|
1488
1647
|
"not-armed"
|
|
1489
1648
|
])
|
|
1490
1649
|
}).optional(),
|
|
1650
|
+
authoringSession: z.string().min(1).optional(),
|
|
1491
1651
|
blockedReasons: blockedReasonsSchema.optional(),
|
|
1492
1652
|
blockingReasons: z.array(z.string()),
|
|
1493
1653
|
command: z.literal("patronage-factory pr:ready"),
|
|
@@ -1495,27 +1655,16 @@ const prReadyProofSchema = z.object({
|
|
|
1495
1655
|
humanBlockingReasons: z.array(z.string()).default([]),
|
|
1496
1656
|
ledger: managedReadinessLedgerSchema,
|
|
1497
1657
|
notices: z.array(z.string().min(1)).optional(),
|
|
1658
|
+
policyResolution: policyResolutionSchema.optional(),
|
|
1498
1659
|
profileBlobSha: z.string().regex(/^[0-9a-f]{40}$/u).optional(),
|
|
1499
1660
|
profilePath: z.string().min(1).optional(),
|
|
1500
1661
|
repairs: z.array(readinessRepairSchema).default([]),
|
|
1501
1662
|
repository: z.string().regex(/^[^/\s]+\/[^/\s]+$/u).optional(),
|
|
1502
1663
|
schemaVersion: prReadySchemaVersionSchema,
|
|
1503
|
-
status: z.enum([
|
|
1504
|
-
"ready",
|
|
1505
|
-
"blocked",
|
|
1506
|
-
"slice-ready/not-final"
|
|
1507
|
-
]),
|
|
1664
|
+
status: z.enum(["ready", "blocked"]),
|
|
1508
1665
|
waivedDemands: z.array(waivedDemandSchema).optional()
|
|
1509
1666
|
}).superRefine((proof, context) => {
|
|
1510
|
-
if (proof.schemaVersion < 2) {
|
|
1511
|
-
if (proof.blockedReasons !== void 0) context.addIssue({
|
|
1512
|
-
code: "custom",
|
|
1513
|
-
message: "blockedReasons is a schemaVersion 2 field; a v1 pr:ready proof must not carry it.",
|
|
1514
|
-
path: ["blockedReasons"]
|
|
1515
|
-
});
|
|
1516
|
-
return;
|
|
1517
|
-
}
|
|
1518
1667
|
for (const issue of blockedReasonIssues({ ...proof })) context.addIssue(issue);
|
|
1519
1668
|
});
|
|
1520
1669
|
//#endregion
|
|
1521
|
-
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,
|
|
1670
|
+
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, SUPPORTED_CLOSEOUT_SCHEMA_VERSIONS, SUPPORTED_PR_READY_SCHEMA_VERSIONS, SUPPORTED_PR_VERIFY_SCHEMA_VERSIONS, blockedReasonSchema, blockedReasonsSchema, boundaryCheckProofSchema, boundaryReviewProofSchema, closeoutArtifactSchema, evidenceEnvelopeSchema, gateTimingRecordSchema, prReadyProofSchema, prReviewFindingSchema, prReviewProofSchema, prReviewResultSchema, prVerifyProofSchema, reviewPromptSectionSchema, reviewPromptSectionsSchema, waivedDemandSchema };
|