@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/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$1 = [
5
+ const EVIDENCE_REVIEW_RUNGS = [
6
6
  "independent-model",
7
7
  "oracle",
8
8
  "human"
9
9
  ];
10
- Object.fromEntries(EVIDENCE_REVIEW_RUNGS$1.map((rung, index) => [rung, index]));
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: a live
18
- * schedule with no wave demand resolved is an unauthorized merge waiting to
19
- * happen.
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$1.includes(decoded);
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(2),
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 !== 2) return {
291
- reason: `impact stamp version ${String(stamp.stampVersion)} is not this build's version 2; ${floor}`,
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. It is
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" || proof.schemaVersion < 4) return;
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 v4 proof may only claim a command
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
- * version guards, and write-time truth stays `pr:verify`'s job.
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"` and schemaVersion >= 4 for the same reasons as
446
- * the completeness rule: an aborted run records partial execution, and a
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" || proof.schemaVersion < 4 || notRequired.length === 0) return;
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.schemaVersion >= 4 && proof.authoringSession === void 0) context.addIssue({
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: "notRequiredCommands is a schemaVersion>=4 field; a v1–v3 pr:verify proof must not carry it.",
514
- path: ["notRequiredCommands"]
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
- * Strict: only `claude` and `gpt`, both optional, no third slot.
837
- *
838
- * A lane may have no usable native session data. The build gate accepts that
839
- * state and the advisory sink journals the valid envelope with its recorded
840
- * gaps, without inventing usage.
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 retroTokenFamiliesSchema = z.object({
843
- claude: claudeTokenBlockSchema.optional(),
844
- gpt: gptTokenBlockSchema.optional()
845
- }).strict();
846
- const RetroCycleCountersSchema = z.object({
847
- gateRunsToFirstGreen: nonNegInt,
848
- reviewerFixRounds: nonNegInt,
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
- verdict: boundedText.optional()
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 prVerifySchemaVersionSchema = z.number().refine((value) => SUPPORTED_PR_VERIFY_SCHEMA_VERSIONS.includes(value), { message: `schemaVersion must be one of: ${SUPPORTED_PR_VERIFY_SCHEMA_VERSIONS.join(", ")}` });
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).optional(),
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(DIFF_CLASSIFICATIONS),
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
- const PR_REVIEW_SCHEMA_VERSION = 2;
1049
- const PR_REVIEW_FINDING_PROVENANCE_VERSION = 1;
1050
- const nonBlankString = z.string().refine((value) => value.trim().length > 0, { message: "must not be blank" });
1051
- const parseableDateString = z.string().refine((value) => Number.isFinite(Date.parse(value)), { message: "must be a parseable date string" });
1052
- const reviewPromptSectionProvenances = ["prior-review-ledger", "issue-review-focus"];
1053
- const reviewPromptSectionSchema = z.object({
1054
- provenance: z.enum(reviewPromptSectionProvenances),
1055
- source: nonBlankString,
1056
- text: nonBlankString
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 reviewPromptSectionsSchema = z.array(reviewPromptSectionSchema).min(1).refine((sections) => new Set(sections.map((section) => section.provenance)).size === sections.length && !sections.some((section, index) => index > 0 && reviewPromptSectionProvenances.indexOf(section.provenance) < reviewPromptSectionProvenances.indexOf(sections[index - 1].provenance)), { message: "prompt sections must be in assembly order without duplicates" });
1059
- const findingSupersedesSchema = z.object({
1060
- file: z.string().optional(),
1061
- title: nonBlankString
1062
- });
1063
- const prReviewFindingSchema = z.object({
1064
- blockingAfterCap: z.boolean().optional(),
1065
- body: nonBlankString,
1066
- category: z.enum(REVIEW_CATEGORIES).optional(),
1067
- citedSpan: z.string().optional(),
1068
- file: z.string().optional(),
1069
- line: z.number().int().positive().optional(),
1070
- prescribedAction: z.string().optional(),
1071
- priority: z.string().optional(),
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
- producer: z.string().min(1).optional(),
1091
- promptSections: reviewPromptSectionsSchema.optional(),
1092
- rung: z.enum(EVIDENCE_REVIEW_RUNGS).optional(),
1093
- sessionId: nonBlankString.optional(),
1094
- stageResolution: z.object({
1095
- effort: z.enum([
1096
- "low",
1097
- "medium",
1098
- "high",
1099
- "xhigh"
1100
- ]).optional(),
1101
- engine: z.string().min(1),
1102
- layerSource: z.enum([
1103
- "cli",
1104
- "operatorSessionGlobal",
1105
- "operatorSessionKind",
1106
- "profilePin",
1107
- "stageConfig",
1108
- "userConfig"
1109
- ]),
1110
- model: z.string().min(1),
1111
- rung: z.enum(REVIEW_RUNGS).optional()
1112
- }).optional(),
1113
- startedAt: parseableDateString,
1114
- summary: z.string().refine((value) => value.trim().length > 0 && value !== "<one sentence>", { message: "summary must not be blank or a placeholder" }),
1115
- typedVerdict: z.boolean().optional(),
1116
- verdictSource: z.enum(["footer", "recovered"]).optional()
1117
- }).superRefine((review, context) => {
1118
- if (review.outcome === "passed") {
1119
- if (review.issuesFlagged !== 0) context.addIssue({
1120
- code: "custom",
1121
- message: "passed reviews must report zero issues flagged",
1122
- path: ["issuesFlagged"]
1123
- });
1124
- if (review.findings.length !== 0) context.addIssue({
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: "passed reviews must not include findings",
1127
- path: ["findings"]
1396
+ message: "review-type evidence envelope requires a rung (ADR 0014).",
1397
+ path: ["rung"]
1128
1398
  });
1129
- }
1130
- if (review.outcome !== "passed") {
1131
- if (review.findings.length === 0) context.addIssue({
1399
+ } else {
1400
+ if (envelope.rung !== void 0) context.addIssue({
1132
1401
  code: "custom",
1133
- message: "non-passing reviews must include findings",
1134
- path: ["findings"]
1402
+ message: "verify-type evidence envelope must not carry rung (review-type-only field, ADR 0014).",
1403
+ path: ["rung"]
1135
1404
  });
1136
- if (review.findings.length !== review.issuesFlagged) context.addIssue({
1405
+ if (envelope.model !== void 0) context.addIssue({
1137
1406
  code: "custom",
1138
- message: "issuesFlagged must match findings length",
1139
- path: ["issuesFlagged"]
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: z.enum(EVIDENCE_REVIEW_RUNGS),
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: z.enum(EVIDENCE_REVIEW_RUNGS).optional(),
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: z.enum(EVIDENCE_REVIEW_RUNGS),
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: z.enum(REVIEW_STATUS_VALUES)
1325
- });
1326
- const historicalPostReadinessCommentSchema = z.object({
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: z.enum(DIFF_CLASSIFICATIONS),
1517
+ classification: diffClassificationSchema,
1342
1518
  externalChecks: z.array(z.object({
1343
- checkType: z.enum(EVIDENCE_CHECK_TYPES),
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: z.enum([
1418
- "gate-cap-exhausted",
1419
- "interior-cap-reached",
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(1),
1448
- stackRole: z.enum([
1449
- "single",
1450
- "slice",
1451
- "rollup",
1452
- "merge-gate prerequisite"
1453
- ]).optional(),
1454
- verification: z.object({
1455
- command: z.literal("patronage-factory pr:verify"),
1456
- docsOnlyDeltaAccepted: z.boolean().optional(),
1457
- docsOnlyVerifiedHeadSha: z.string().optional(),
1458
- prVerify: z.enum([
1459
- "passed",
1460
- "missing",
1461
- "stale"
1462
- ]),
1463
- trivialDeltaAccepted: z.boolean().optional(),
1464
- trivialVerifiedHeadSha: z.string().optional(),
1465
- verifiedHeadSha: z.string().optional()
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 still accepts. `pr:ready` emits v3 only
1471
- * (#477) — one current contract — but v1 and v2 events were spooled before the
1472
- * bumps and HQ must ingest them without degrading, so the wire schema parses
1473
- * all three.
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
- 1,
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, RETRO_ENVELOPE_SCHEMA_VERSION, RETRO_ENVELOPE_VALIDATORS, RETRO_ENVELOPE_WIRE_BOUNDS, SUPPORTED_CLOSEOUT_SCHEMA_VERSIONS, SUPPORTED_PR_READY_SCHEMA_VERSIONS, SUPPORTED_PR_VERIFY_SCHEMA_VERSIONS, SUPPORTED_RETRO_ENVELOPE_SCHEMA_VERSIONS, blockedReasonSchema, blockedReasonsSchema, boundaryCheckProofSchema, boundaryReviewProofSchema, closeoutArtifactReadSchema, closeoutArtifactSchema, evidenceEnvelopeSchema, legacyCloseoutArtifactSchema, prReadyProofSchema, prReviewProofSchema, prVerifyProofSchema, retroEnvelopeV1Schema, retroEpicReference, waivedDemandSchema };
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 };