@patronage/software-factory 1.0.0-alpha.3 → 1.0.0-alpha.30

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,12 +2,12 @@ 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. */
@@ -19,6 +19,13 @@ const DEMAND_KEYS = {
19
19
  * happen.
20
20
  */
21
21
  boundaryWave: "boundary-wave",
22
+ /**
23
+ * Closeout pin (#901): a merged interior's declared child issue is still
24
+ * open. The factory does not close issues; the operator closes, or waives
25
+ * this demand for that interior PR. In force at `boundary:check --final`,
26
+ * not at interior `boundary:check` and not at `pr:ready`.
27
+ */
28
+ childIssueOpen: "child-issue-open",
22
29
  /** The PR is still a draft. */
23
30
  draft: "draft",
24
31
  /** GitHub's own check rollup for the candidate head. */
@@ -37,6 +44,13 @@ const DEMAND_KEYS = {
37
44
  * neither a review-rung authority nor a scheduling requirement.
38
45
  */
39
46
  nativeApproval: "native-approval",
47
+ /**
48
+ * The policy identity an earlier stage recorded must be the one readiness
49
+ * resolved (#922 Slice B). In force when a consumed `pr:verify` or
50
+ * `pr:review` proof judged this candidate under a different review policy,
51
+ * or recorded none where the resolved demand includes a security review.
52
+ */
53
+ policyIdentity: "policy-identity",
40
54
  /** A current, head-bound, passing typed `pr:verify` proof. */
41
55
  prVerify: "pr-verify",
42
56
  /** The profile-resolved review ladder policy. */
@@ -116,7 +130,7 @@ const qualifierIsMintable = (family, qualifier) => {
116
130
  if (!QUALIFIER_PATTERN.test(qualifier)) return false;
117
131
  const decoded = decodeQualifier(qualifier);
118
132
  if (decoded === void 0 || demandQualifier(decoded) !== qualifier) return false;
119
- if (family === "review-rung") return EVIDENCE_REVIEW_RUNGS$1.includes(decoded);
133
+ if (family === "review-rung") return EVIDENCE_REVIEW_RUNGS.includes(decoded);
120
134
  if (family === "review-mode") return REVIEW_MODE_DEMAND_VALUES.includes(decoded);
121
135
  return true;
122
136
  };
@@ -200,6 +214,11 @@ const blockedReasonIssues = (proof) => {
200
214
  const drifted = named.findIndex((reason, index) => reason.detail !== blockedReasonDetail(reasons[index]));
201
215
  return drifted === -1 ? [] : blockedReasonIssue(`blockedReasons[${drifted}] does not carry blocking reason ${drifted}; the two projections must tell one story`);
202
216
  };
217
+ const sameHeadSha = (left, right) => {
218
+ if (!left || !right) return false;
219
+ if (Math.min(left.length, right.length) < 7) return false;
220
+ return left.startsWith(right) || right.startsWith(left);
221
+ };
203
222
  //#endregion
204
223
  //#region src/demand-waiver.ts
205
224
  const DEMAND_WAIVER_SCHEMA_VERSION = 1;
@@ -228,10 +247,653 @@ const waivedDemandSchema = z.object({
228
247
  session: z.string().trim().min(1).optional(),
229
248
  unmetReasons: z.array(z.string().min(1)).min(1)
230
249
  });
250
+ //#endregion
251
+ //#region src/diff-classification.ts
252
+ const DIFF_CLASSIFICATIONS = [
253
+ "docs/process-only",
254
+ "trivial",
255
+ "non-trivial"
256
+ ];
257
+ //#endregion
258
+ //#region src/policy-resolution.ts
259
+ const COMMIT_SHA_PATTERN = /^[0-9a-f]{40}$/u;
260
+ const digestSchema = z.string().regex(/^[0-9a-f]{64}$/u);
261
+ /**
262
+ * The authority resolved: the live tip of the PR's own base ref served a
263
+ * parseable profile, and demand was resolved from the union of that policy and
264
+ * the candidate's.
265
+ *
266
+ * Every identity field is REQUIRED. A cross-stage reader compares these digests
267
+ * to decide whether two stages judged the same candidate under the same policy;
268
+ * an `authoritative` record with a digest missing would read to that reader as
269
+ * "nothing disagrees", which is a silent downgrade of exactly the demand this
270
+ * record exists to protect. `reason` is forbidden here — a resolved authority
271
+ * has nothing to explain.
272
+ */
273
+ const authoritativePolicyResolutionSchema = z.object({
274
+ /** sha256 of the canonical base `review` subtree. */
275
+ basePolicyDigest: digestSchema,
276
+ /** The PR's own base ref, whose live tip is the authority. */
277
+ baseRefName: z.string().min(1),
278
+ /** sha256 of the canonical candidate `review` subtree. */
279
+ candidatePolicyDigest: digestSchema,
280
+ /** sha256 of the merged (base ∪ candidate) protected-path set. */
281
+ effectiveDigest: digestSchema,
282
+ /** The base-ref tip the base policy was read at. */
283
+ policyBaseSha: z.string().regex(COMMIT_SHA_PATTERN),
284
+ /**
285
+ * Whether the candidate's `review` subtree differs from the base's. The
286
+ * digests prove which policies were read; this says whether they agreed,
287
+ * which is the fact that explains a demand the candidate's own profile
288
+ * would not have produced.
289
+ */
290
+ reviewPolicyChanged: z.boolean(),
291
+ status: z.literal("authoritative")
292
+ }).strict();
293
+ /**
294
+ * The authority could not be read, and `reason` says why — required, because an
295
+ * unresolved record with no reason is an unactionable refusal. `pr:ready`
296
+ * refuses before it writes a proof, so a Slice A ready proof never carries one;
297
+ * this member is the recorded form of that refusal for the stages that carry it
298
+ * forward.
299
+ */
300
+ const unresolvedPolicyResolutionSchema = z.object({
301
+ /** The base ref whose policy could not be read. */
302
+ baseRefName: z.string().min(1),
303
+ /** Why the authority is unresolved. */
304
+ reason: z.string().min(1),
305
+ status: z.literal("unresolved")
306
+ }).strict();
307
+ /** How a gate resolved the review policy it evaluated a candidate under. */
308
+ const policyResolutionSchema = z.discriminatedUnion("status", [authoritativePolicyResolutionSchema, unresolvedPolicyResolutionSchema]);
309
+ //#endregion
310
+ //#region src/packages/review-prompt-sections/schema.ts
311
+ const REVIEW_PROMPT_SECTION_PROVENANCES = [
312
+ "coding-standards",
313
+ "prior-review-ledger",
314
+ "issue-review-focus"
315
+ ];
316
+ const nonBlankString$1 = z.string().refine((value) => value.trim().length > 0, { message: "must not be blank" });
317
+ const reviewPromptSectionSchema = z.object({
318
+ provenance: z.enum(REVIEW_PROMPT_SECTION_PROVENANCES),
319
+ source: nonBlankString$1,
320
+ text: nonBlankString$1
321
+ }).strict();
322
+ const promptSectionRank = (section) => REVIEW_PROMPT_SECTION_PROVENANCES.indexOf(section.provenance);
323
+ const hasDuplicateProvenance = (sections) => new Set(sections.map((section) => section.provenance)).size !== sections.length;
324
+ const isOutOfAssemblyOrder = (sections) => sections.some((section, index) => index > 0 && promptSectionRank(section) < promptSectionRank(sections[index - 1]));
325
+ const reviewPromptSectionsSchema = z.array(reviewPromptSectionSchema).min(1).refine((sections) => !hasDuplicateProvenance(sections) && !isOutOfAssemblyOrder(sections), { message: "prompt sections must be in assembly order without duplicates" });
326
+ //#endregion
327
+ //#region src/review-ladder-policy.ts
328
+ const REVIEW_CATEGORIES = [
329
+ "correctness",
330
+ "safety",
331
+ "coordination",
332
+ "maintainability",
333
+ "unknown"
334
+ ];
335
+ //#endregion
336
+ //#region src/evidence-freshness.ts
337
+ const patchIdMatches = (recorded, candidate) => Boolean(recorded.patchId) && recorded.patchId === candidate.patchId;
338
+ const headShaMatches = (recorded, candidate) => Boolean(recorded.headSha && candidate.headSha && sameHeadSha(candidate.headSha, recorded.headSha));
339
+ /**
340
+ * The one freshness decision. `true` means the recorded evidence still
341
+ * describes the candidate.
342
+ *
343
+ * Time is not an input and never will be: adding one would contradict the
344
+ * `freshness` definition in root `CONTEXT.md`.
345
+ */
346
+ function evidenceFresh({ candidate, recorded, rule }) {
347
+ if (patchIdMatches(recorded, candidate)) return true;
348
+ return rule === "patch-id-or-head-sha" && headShaMatches(recorded, candidate);
349
+ }
350
+ //#endregion
351
+ //#region src/sha256-hex.ts
352
+ const ROUND_CONSTANTS = new Uint32Array([
353
+ 1116352408,
354
+ 1899447441,
355
+ 3049323471,
356
+ 3921009573,
357
+ 961987163,
358
+ 1508970993,
359
+ 2453635748,
360
+ 2870763221,
361
+ 3624381080,
362
+ 310598401,
363
+ 607225278,
364
+ 1426881987,
365
+ 1925078388,
366
+ 2162078206,
367
+ 2614888103,
368
+ 3248222580,
369
+ 3835390401,
370
+ 4022224774,
371
+ 264347078,
372
+ 604807628,
373
+ 770255983,
374
+ 1249150122,
375
+ 1555081692,
376
+ 1996064986,
377
+ 2554220882,
378
+ 2821834349,
379
+ 2952996808,
380
+ 3210313671,
381
+ 3336571891,
382
+ 3584528711,
383
+ 113926993,
384
+ 338241895,
385
+ 666307205,
386
+ 773529912,
387
+ 1294757372,
388
+ 1396182291,
389
+ 1695183700,
390
+ 1986661051,
391
+ 2177026350,
392
+ 2456956037,
393
+ 2730485921,
394
+ 2820302411,
395
+ 3259730800,
396
+ 3345764771,
397
+ 3516065817,
398
+ 3600352804,
399
+ 4094571909,
400
+ 275423344,
401
+ 430227734,
402
+ 506948616,
403
+ 659060556,
404
+ 883997877,
405
+ 958139571,
406
+ 1322822218,
407
+ 1537002063,
408
+ 1747873779,
409
+ 1955562222,
410
+ 2024104815,
411
+ 2227730452,
412
+ 2361852424,
413
+ 2428436474,
414
+ 2756734187,
415
+ 3204031479,
416
+ 3329325298
417
+ ]);
418
+ const INITIAL_STATE = new Uint32Array([
419
+ 1779033703,
420
+ 3144134277,
421
+ 1013904242,
422
+ 2773480762,
423
+ 1359893119,
424
+ 2600822924,
425
+ 528734635,
426
+ 1541459225
427
+ ]);
428
+ const BLOCK_BYTES = 64;
429
+ const LENGTH_FIELD_BYTES = 8;
430
+ const WORD_BITS = 32;
431
+ const rotateRight = (value, bits) => (value >>> bits | value << WORD_BITS - bits) >>> 0;
432
+ /** Message bytes plus the FIPS 180-4 padding and 64-bit length suffix. */
433
+ const padMessage = (message) => {
434
+ const paddedLength = Math.ceil((message.length + 1 + LENGTH_FIELD_BYTES) / BLOCK_BYTES) * BLOCK_BYTES;
435
+ const padded = new Uint8Array(paddedLength);
436
+ padded.set(message);
437
+ padded[message.length] = 128;
438
+ const bitLength = BigInt(message.length) * 8n;
439
+ new DataView(padded.buffer).setBigUint64(paddedLength - LENGTH_FIELD_BYTES, bitLength);
440
+ return padded;
441
+ };
442
+ const ROUNDS = 64;
443
+ const SCHEDULE_SEED_WORDS = 16;
444
+ const STATE_WORDS = 8;
445
+ const wordAt = (words, index) => words[index];
446
+ /** One 64-byte block folded into the running state, in place. */
447
+ const compress = (state, view, offset) => {
448
+ const schedule = new Uint32Array(ROUNDS);
449
+ for (let index = 0; index < SCHEDULE_SEED_WORDS; index += 1) schedule[index] = view.getUint32(offset + index * 4);
450
+ for (let index = SCHEDULE_SEED_WORDS; index < ROUNDS; index += 1) {
451
+ const previous = wordAt(schedule, index - 15);
452
+ const recent = wordAt(schedule, index - 2);
453
+ const sigma0 = rotateRight(previous, 7) ^ rotateRight(previous, 18) ^ previous >>> 3;
454
+ const sigma1 = rotateRight(recent, 17) ^ rotateRight(recent, 19) ^ recent >>> 10;
455
+ schedule[index] = wordAt(schedule, index - 16) + sigma0 + wordAt(schedule, index - 7) + sigma1 >>> 0;
456
+ }
457
+ const working = new Uint32Array(state);
458
+ for (let index = 0; index < ROUNDS; index += 1) {
459
+ const a = wordAt(working, 0);
460
+ const b = wordAt(working, 1);
461
+ const c = wordAt(working, 2);
462
+ const d = wordAt(working, 3);
463
+ const e = wordAt(working, 4);
464
+ const f = wordAt(working, 5);
465
+ const g = wordAt(working, 6);
466
+ const h = wordAt(working, 7);
467
+ const sum1 = rotateRight(e, 6) ^ rotateRight(e, 11) ^ rotateRight(e, 25);
468
+ const choose = e & f ^ ~e & g;
469
+ const temp1 = h + sum1 + choose + wordAt(ROUND_CONSTANTS, index) + wordAt(schedule, index) >>> 0;
470
+ const sum0 = rotateRight(a, 2) ^ rotateRight(a, 13) ^ rotateRight(a, 22);
471
+ const majority = a & b ^ a & c ^ b & c;
472
+ working[7] = g;
473
+ working[6] = f;
474
+ working[5] = e;
475
+ working[4] = d + temp1 >>> 0;
476
+ working[3] = c;
477
+ working[2] = b;
478
+ working[1] = a;
479
+ working[0] = temp1 + (sum0 + majority >>> 0) >>> 0;
480
+ }
481
+ for (let index = 0; index < STATE_WORDS; index += 1) state[index] = wordAt(state, index) + wordAt(working, index) >>> 0;
482
+ };
483
+ const toHex = (state) => {
484
+ let hex = "";
485
+ for (const word of state) hex += word.toString(16).padStart(8, "0");
486
+ return hex;
487
+ };
488
+ /** The lowercase hex SHA-256 digest of a UTF-8 string. */
489
+ const sha256Hex = (input) => {
490
+ const padded = padMessage(new TextEncoder().encode(input));
491
+ const view = new DataView(padded.buffer);
492
+ const state = new Uint32Array(INITIAL_STATE);
493
+ for (let offset = 0; offset < padded.length; offset += BLOCK_BYTES) compress(state, view, offset);
494
+ return toHex(state);
495
+ };
496
+ //#endregion
497
+ //#region src/review-ladder-ledger.ts
498
+ const normalizeCitedSpan = (text) => text.split(/\r?\n/u).map((line) => line.trim().replaceAll(/\s+/gu, " ")).filter((line) => line.length > 0).join("\n");
499
+ const PRESCRIBED_ACTION_STOPWORDS = new Set([
500
+ "a",
501
+ "an",
502
+ "the",
503
+ "to",
504
+ "of",
505
+ "in",
506
+ "on",
507
+ "and",
508
+ "or",
509
+ "is",
510
+ "are",
511
+ "be",
512
+ "this",
513
+ "that",
514
+ "it",
515
+ "for",
516
+ "with",
517
+ "as",
518
+ "by",
519
+ "from",
520
+ "at",
521
+ "into",
522
+ "should",
523
+ "must",
524
+ "will",
525
+ "would",
526
+ "can",
527
+ "could",
528
+ "not",
529
+ "but",
530
+ "so",
531
+ "if",
532
+ "then",
533
+ "than",
534
+ "when",
535
+ "which",
536
+ "who",
537
+ "whom",
538
+ "its",
539
+ "their",
540
+ "there",
541
+ "here",
542
+ "also",
543
+ "use",
544
+ "using",
545
+ "instead",
546
+ "rather",
547
+ "than",
548
+ "over",
549
+ "up",
550
+ "out",
551
+ "have",
552
+ "has",
553
+ "had",
554
+ "been"
555
+ ]);
556
+ const tokenizePrescribedAction = (text) => {
557
+ const words = text.toLowerCase().replaceAll(/[^a-z0-9\s]/gu, " ").split(/\s+/u).filter((word) => word.length > 0 && !PRESCRIBED_ACTION_STOPWORDS.has(word));
558
+ return [...new Set(words)].toSorted();
559
+ };
560
+ const normalizeTitle = (title) => title.trim().toLowerCase().replaceAll(/\s+/gu, " ");
561
+ const normalizeFile = (file) => file.trim();
562
+ const findingKey = (finding) => {
563
+ const path = finding.file ?? "";
564
+ const normalizedSpan = normalizeCitedSpan(finding.citedSpan ?? "");
565
+ const actionTokens = tokenizePrescribedAction(finding.prescribedAction ?? "");
566
+ const legacyDiscriminator = normalizedSpan.length > 0 || actionTokens.length > 0 ? "" : normalizeTitle(finding.title ?? "");
567
+ return sha256Hex(`${path}\0${sha256Hex(normalizedSpan)}\0${actionTokens.join(" ")}\0${legacyDiscriminator}`);
568
+ };
569
+ const declarationMatchesEntry = (entry, target) => normalizeTitle(entry.title) === normalizeTitle(target.title) && (target.file === void 0 || entry.file !== void 0 && normalizeFile(entry.file) === normalizeFile(target.file));
570
+ const applyDisposition = (ledger, declaration) => {
571
+ const entry = declaration.findingReport ? resolveEntryForFinding([...ledger.values()], declaration.findingReport) : [...ledger.values()].find((candidate) => declarationMatchesEntry(candidate, declaration.finding));
572
+ if (!entry) return false;
573
+ entry.disposition = declaration.disposition;
574
+ entry.priorDisposition = void 0;
575
+ if (declaration.reference !== void 0) entry.reference = declaration.reference;
576
+ return true;
577
+ };
578
+ const definedAttributes = (finding) => Object.fromEntries([
579
+ "blockingAfterCap",
580
+ "category",
581
+ "citedSpan",
582
+ "file",
583
+ "prescribedAction",
584
+ "priority"
585
+ ].flatMap((attribute) => finding[attribute] === void 0 ? [] : [[attribute, finding[attribute]]]));
586
+ const MIN_MEANINGFUL_OVERLAP_LINE_LENGTH = 20;
587
+ const spansOverlap = (a, b) => {
588
+ if (a.length === 0 || b.length === 0) return false;
589
+ if (a === b || a.includes(b) || b.includes(a)) return true;
590
+ const linesA = new Set(a.split("\n").filter((line) => line.length >= MIN_MEANINGFUL_OVERLAP_LINE_LENGTH));
591
+ return b.split("\n").some((line) => line.length >= MIN_MEANINGFUL_OVERLAP_LINE_LENGTH && linesA.has(line));
592
+ };
593
+ const resolveEntryForFinding = (entries, finding) => {
594
+ const primaryKey = findingKey(finding);
595
+ const exact = entries.find((entry) => entry.key === primaryKey);
596
+ if (exact) return exact;
597
+ const { supersedes } = finding;
598
+ if (supersedes) {
599
+ const superseded = entries.find((entry) => declarationMatchesEntry(entry, supersedes));
600
+ if (superseded) return superseded;
601
+ }
602
+ if (finding.file !== void 0 && finding.citedSpan !== void 0) {
603
+ const candidateSpan = normalizeCitedSpan(finding.citedSpan);
604
+ if (candidateSpan.length > 0) {
605
+ const overlapping = entries.find((entry) => entry.disposition === "open" && entry.file === finding.file && entry.citedSpan !== void 0 && spansOverlap(normalizeCitedSpan(entry.citedSpan), candidateSpan));
606
+ if (overlapping) return overlapping;
607
+ }
608
+ }
609
+ };
610
+ const REFLAG_TRANSITIONS = {
611
+ "fixed-in-thread": (entry) => {
612
+ entry.disposition = "open";
613
+ entry.reopenedCount += 1;
614
+ },
615
+ "follow-up-filed": (entry) => {
616
+ entry.priorDisposition = entry.disposition;
617
+ entry.disposition = "stale-repeat";
618
+ entry.staleRepeatCount += 1;
619
+ },
620
+ open: () => {},
621
+ "rerun-noise": (entry) => {
622
+ entry.staleRepeatCount += 1;
623
+ },
624
+ "stale-repeat": (entry) => {
625
+ entry.staleRepeatCount += 1;
626
+ },
627
+ waived: (entry) => {
628
+ entry.priorDisposition = entry.disposition;
629
+ entry.disposition = "stale-repeat";
630
+ entry.staleRepeatCount += 1;
631
+ }
632
+ };
633
+ const applyFlag = (ledger, finding, ref, demoteNew) => {
634
+ const matched = resolveEntryForFinding([...ledger.values()], finding);
635
+ if (!matched) {
636
+ const key = findingKey(finding);
637
+ ledger.set(key, {
638
+ ...definedAttributes(finding),
639
+ disposition: demoteNew ? "rerun-noise" : "open",
640
+ firstFlagged: ref,
641
+ flagCount: 1,
642
+ key,
643
+ lastFlagged: ref,
644
+ reopenedCount: 0,
645
+ staleRepeatCount: 0,
646
+ title: finding.title.trim()
647
+ });
648
+ return;
649
+ }
650
+ matched.flagCount += 1;
651
+ matched.lastFlagged = ref;
652
+ Object.assign(matched, definedAttributes(finding));
653
+ REFLAG_TRANSITIONS[matched.disposition](matched);
654
+ };
655
+ const findingKeySet = (findings) => new Set(findings.map((finding) => findingKey(finding)));
656
+ const isSameHeadRerun = (previous, current) => previous?.identity !== void 0 && current.identity !== void 0 && evidenceFresh({
657
+ candidate: current.identity,
658
+ recorded: previous.identity,
659
+ rule: "patch-id"
660
+ });
661
+ const isDisjointSameHeadRerun = (previous, current) => {
662
+ if (current.correlatedRerun) return false;
663
+ if (!isSameHeadRerun(previous, current) || previous?.findings.length === 0 || current.findings.length === 0) return false;
664
+ const previousKeys = findingKeySet(previous?.findings ?? []);
665
+ return current.findings.every((finding) => !previousKeys.has(findingKey(finding)));
666
+ };
667
+ const foldLedger = (cycles, refs) => {
668
+ const ledger = /* @__PURE__ */ new Map();
669
+ const unmatchedDispositions = [];
670
+ for (const [index, entry] of cycles.entries()) {
671
+ const dispositions = entry.dispositions ?? [];
672
+ const severityFloorDispositions = dispositions.filter((declaration) => declaration.findingReport !== void 0);
673
+ for (const declaration of dispositions.filter((candidate) => candidate.findingReport === void 0)) if (!applyDisposition(ledger, declaration)) unmatchedDispositions.push(declaration);
674
+ const demoteNew = isDisjointSameHeadRerun(cycles[index - 1], entry);
675
+ for (const finding of entry.findings) applyFlag(ledger, finding, refs[index], demoteNew);
676
+ for (const declaration of severityFloorDispositions) if (!applyDisposition(ledger, declaration)) unmatchedDispositions.push(declaration);
677
+ }
678
+ return {
679
+ ledger,
680
+ unmatchedDispositions
681
+ };
682
+ };
683
+ z.array(z.object({
684
+ disposition: z.enum([
685
+ "fixed-in-thread",
686
+ "follow-up-filed",
687
+ "waived"
688
+ ]),
689
+ finding: z.object({
690
+ file: z.string().min(1).optional(),
691
+ title: z.string().min(1)
692
+ }).strict(),
693
+ reference: z.string().min(1).optional()
694
+ }).strict());
695
+ //#endregion
696
+ //#region src/review-ladder.ts
697
+ const LADDER_NEXT_ACTION_KINDS = [
698
+ "run-interior-cycle",
699
+ "advance-to-gate",
700
+ "run-gate-cycle",
701
+ "accept-nonblocking-findings",
702
+ "escalate-to-triage",
703
+ "ready-for-human"
704
+ ];
705
+ const LADDER_STAGES = [
706
+ "interior",
707
+ "interior-complete",
708
+ "gate"
709
+ ];
710
+ const LADDER_FORCED_TRANSITION_KINDS = ["gate-cap-exhausted", "interior-cap-reached"];
711
+ const ladderCycleRefsFor = (cycles) => {
712
+ let interior = 0;
713
+ let gate = 0;
714
+ return cycles.map((entry) => {
715
+ if (entry.stage === "interior") {
716
+ if (gate > 0) throw new TypeError("Invalid ladder history: interior cycle recorded after the gate stage began.");
717
+ interior += 1;
718
+ return {
719
+ cycle: interior,
720
+ stage: "interior"
721
+ };
722
+ }
723
+ gate += 1;
724
+ return {
725
+ cycle: gate,
726
+ stage: "gate"
727
+ };
728
+ });
729
+ };
730
+ //#endregion
731
+ //#region src/pr-readiness/ladder-proof-state.ts
732
+ const prReviewLadderStateSchema = z.object({ cycles: z.array(z.custom()) }).superRefine((ladder, context) => {
733
+ try {
734
+ ladderLedgerFor(ladder);
735
+ } catch {
736
+ context.addIssue({
737
+ code: "custom",
738
+ message: "ladder cycles do not fold into a valid disposition ledger"
739
+ });
740
+ }
741
+ });
742
+ const ladderLedgerFor = (ladder) => [...foldLedger(ladder.cycles, ladderCycleRefsFor(ladder.cycles)).ledger.values()];
743
+ //#endregion
744
+ //#region src/pr-readiness/review-proof.ts
745
+ const PR_REVIEW_SCHEMA_VERSION = 2;
746
+ const nonBlankString = z.string().refine((value) => value.trim().length > 0, { message: "must not be blank" });
747
+ const parseableDateString = z.string().refine((value) => Number.isFinite(Date.parse(value)), { message: "must be a parseable date string" });
748
+ const findingSupersedesSchema = z.object({
749
+ file: z.string().optional(),
750
+ title: nonBlankString
751
+ });
752
+ const prReviewFindingSchema = z.object({
753
+ blockingAfterCap: z.boolean().optional(),
754
+ body: nonBlankString,
755
+ category: z.enum(REVIEW_CATEGORIES).optional(),
756
+ citedSpan: z.string().optional(),
757
+ file: z.string().optional(),
758
+ line: z.number().int().positive().optional(),
759
+ prescribedAction: z.string().optional(),
760
+ priority: z.string().optional(),
761
+ protocolFinding: z.literal(true).optional(),
762
+ supersedes: findingSupersedesSchema.optional(),
763
+ title: nonBlankString
764
+ });
765
+ const prReviewResultSchema = z.object({
766
+ durationMs: z.number().refine((value) => Number.isFinite(value) && value >= 0, { message: "durationMs must be a finite number >= 0" }),
767
+ endedAt: parseableDateString,
768
+ findings: z.array(prReviewFindingSchema),
769
+ issuesFlagged: z.number().int().nonnegative(),
770
+ kind: z.enum(["correctness", "security"]),
771
+ model: nonBlankString.optional(),
772
+ outcome: z.enum([
773
+ "passed",
774
+ "failed",
775
+ "error"
776
+ ]),
777
+ producer: nonBlankString.optional(),
778
+ promptSections: reviewPromptSectionsSchema.optional(),
779
+ rung: z.enum(EVIDENCE_REVIEW_RUNGS).optional(),
780
+ sessionId: nonBlankString.optional(),
781
+ startedAt: parseableDateString,
782
+ summary: z.string().refine((value) => value.trim().length > 0 && value !== "<one sentence>", { message: "summary must not be blank or a placeholder" }),
783
+ typedVerdict: z.boolean().optional(),
784
+ verdictSource: z.enum(["footer", "recovered"]).optional()
785
+ }).superRefine((review, context) => {
786
+ if (review.outcome === "passed") {
787
+ if (review.issuesFlagged !== 0) context.addIssue({
788
+ code: "custom",
789
+ message: "passed reviews must report zero issues flagged",
790
+ path: ["issuesFlagged"]
791
+ });
792
+ if (review.findings.length !== 0) context.addIssue({
793
+ code: "custom",
794
+ message: "passed reviews must not include findings",
795
+ path: ["findings"]
796
+ });
797
+ }
798
+ if (review.outcome !== "passed") {
799
+ if (review.findings.length === 0) context.addIssue({
800
+ code: "custom",
801
+ message: "non-passing reviews must include findings",
802
+ path: ["findings"]
803
+ });
804
+ if (review.findings.length !== review.issuesFlagged) context.addIssue({
805
+ code: "custom",
806
+ message: "issuesFlagged must match findings length",
807
+ path: ["issuesFlagged"]
808
+ });
809
+ }
810
+ if (review.producer === void 0 !== (review.rung === void 0)) context.addIssue({
811
+ code: "custom",
812
+ message: "producer and rung must be recorded together",
813
+ path: [review.producer === void 0 ? "producer" : "rung"]
814
+ });
815
+ });
816
+ const prReviewProofSchema = z.object({
817
+ base: z.string(),
818
+ changedFiles: z.array(z.string()),
819
+ cleanedPaths: z.array(z.string()),
820
+ findingProvenanceVersion: z.literal(1),
821
+ headSha: z.string().regex(/^[0-9a-f]{40}$/u),
822
+ ladder: prReviewLadderStateSchema.optional(),
823
+ maxReviewCycles: z.number().int().positive().optional(),
824
+ patchId: z.string().regex(/^[0-9a-f]{40,64}$/u),
825
+ policyResolution: policyResolutionSchema.optional(),
826
+ reviewCycle: z.number().int().positive().optional(),
827
+ reviewRequirement: z.object({
828
+ reason: z.enum(["no-applicable-mode", "faithful-merge"]),
829
+ status: z.literal("not-required")
830
+ }).strict().optional(),
831
+ reviews: z.array(prReviewResultSchema),
832
+ schemaVersion: z.literal(2)
833
+ }).passthrough().superRefine((proof, context) => {
834
+ if (proof.reviewRequirement && proof.reviews.length > 0) context.addIssue({
835
+ code: "custom",
836
+ message: "not-required review proofs must not contain review runs",
837
+ path: ["reviews"]
838
+ });
839
+ if (proof.reviewCycle !== void 0 && proof.maxReviewCycles !== void 0 && proof.reviewCycle > proof.maxReviewCycles) context.addIssue({
840
+ code: "custom",
841
+ message: "reviewCycle must not exceed maxReviewCycles",
842
+ path: ["reviewCycle"]
843
+ });
844
+ });
845
+ //#endregion
846
+ //#region src/required-check-policy.ts
847
+ const EVIDENCE_CHECK_TYPES = ["review", "verify"];
848
+ const requiredCheckScopeSchema = z.object({ classifications: z.array(z.enum(DIFF_CLASSIFICATIONS)).min(1) }).strict();
849
+ const reviewStatusSchema = z.enum([
850
+ "not-required",
851
+ "current",
852
+ "stale",
853
+ "missing",
854
+ "blocked"
855
+ ]);
856
+ //#endregion
857
+ //#region src/review-state.ts
858
+ const reviewStateIssue = (message, field) => [{
859
+ code: "custom",
860
+ message,
861
+ path: [field]
862
+ }];
863
+ /**
864
+ * The invariant both ledger schemas enforce, written once so the emitting
865
+ * schema and the wire schema cannot drift. It is the producer's state machine:
866
+ *
867
+ * - a review that is not required is never `current` — `reviewStatus` returns
868
+ * `not-required` before it ever looks at the recorded proof;
869
+ * - `current` means a proof was compared against the candidate delta, so the
870
+ * reviewed identity must be recorded;
871
+ * - `missing` means no proof was found, so no reviewed identity can exist.
872
+ */
873
+ const reviewStateIssues = (state) => {
874
+ const hasIdentity = Boolean(state.reviewedHeadSha || state.reviewedPatchId);
875
+ if (state.status === "current") {
876
+ if (!state.required) return reviewStateIssue("a review state that is not required cannot be current", "status");
877
+ if (!hasIdentity) return reviewStateIssue("a current review state must record the reviewed head sha or patch id", "reviewedPatchId");
878
+ }
879
+ if (state.status === "missing" && hasIdentity) return reviewStateIssue("a missing review state must record no reviewed head sha or patch id", "status");
880
+ return [];
881
+ };
882
+ //#endregion
883
+ //#region src/catch-up-recognition-record.ts
884
+ const objectShaSchema = z.string().regex(/^[0-9a-f]{40}$/u);
885
+ const catchUpRecognitionSchema = z.object({
886
+ baseRef: z.string().min(1),
887
+ baseTipSha: objectShaSchema,
888
+ mergedParentSha: objectShaSchema,
889
+ mergedTreeSha: objectShaSchema,
890
+ upstreamRef: z.string().min(1)
891
+ }).strict();
231
892
  const impactStampTargetSchema = z.object({
232
893
  basis: z.string().min(1),
233
894
  impact: z.enum(["affected", "not-affected"]),
234
- name: z.string().min(1)
895
+ name: z.string().min(1),
896
+ subscribedPaths: z.array(z.string())
235
897
  });
236
898
  /**
237
899
  * The recorded stamp. `targets` satisfies the completeness invariant: every
@@ -241,15 +903,17 @@ const impactStampTargetSchema = z.object({
241
903
  */
242
904
  const impactStampSchema = z.object({
243
905
  basis: z.enum(["target-scoped", "conservative"]),
906
+ inertPaths: z.array(z.string()).default([]),
244
907
  reasons: z.array(z.string()),
245
- stampVersion: z.literal(2),
908
+ stampVersion: z.literal(4),
246
909
  targets: z.array(impactStampTargetSchema).superRefine((targets, context) => {
247
910
  const names = targets.map((target) => target.name);
248
911
  if (new Set(names).size !== names.length) context.addIssue({
249
912
  code: "custom",
250
913
  message: "impact stamp targets must have distinct names."
251
914
  });
252
- })
915
+ }),
916
+ unsubscribedPaths: z.array(z.string())
253
917
  });
254
918
  /**
255
919
  * The surfaces that consume the stamp. One classification, consumed
@@ -287,8 +951,8 @@ const impactStampScopeDecision = ({ stamp, surface, targetName }) => {
287
951
  reason: `no trusted identity-bound impact stamp covers this candidate; ${floor}`,
288
952
  scoped: false
289
953
  };
290
- if (stamp.stampVersion !== 2) return {
291
- reason: `impact stamp version ${String(stamp.stampVersion)} is not this build's version 2; ${floor}`,
954
+ if (stamp.stampVersion !== 4) return {
955
+ reason: `impact stamp version ${String(stamp.stampVersion)} is not this build's version 4; ${floor}`,
292
956
  scoped: false
293
957
  };
294
958
  if (stamp.basis !== "target-scoped") return {
@@ -350,9 +1014,7 @@ z.looseObject({
350
1014
  * skip this epic forbids.
351
1015
  *
352
1016
  * 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.
1017
+ * records partial execution — the run stopped at the failing command.
356
1018
  *
357
1019
  * The check is one-directional on purpose. Every SELECTED command must be
358
1020
  * accounted for, but `executedCommands` may legitimately carry MORE than the
@@ -395,7 +1057,7 @@ const assertBatteryCompleteness = (proof, context) => {
395
1057
  });
396
1058
  seen.add(entry.name);
397
1059
  }
398
- if (proof.outcome !== "passed" || proof.schemaVersion < 4) return;
1060
+ if (proof.outcome !== "passed") return;
399
1061
  const disposed = new Set([...executed, ...seen]);
400
1062
  const undisposed = selected.filter((name) => !disposed.has(name));
401
1063
  if (undisposed.length > 0) context.addIssue({
@@ -405,7 +1067,7 @@ const assertBatteryCompleteness = (proof, context) => {
405
1067
  });
406
1068
  };
407
1069
  /**
408
- * The stamp-authorization control: a passed v4 proof may only claim a command
1070
+ * The stamp-authorization control: a passed proof may only claim a command
409
1071
  * was NOT REQUIRED if its own recorded stamp says so.
410
1072
  *
411
1073
  * `assertBatteryCompleteness` proves the two lists partition the selected
@@ -428,8 +1090,8 @@ const assertBatteryCompleteness = (proof, context) => {
428
1090
  * provably released.
429
1091
  *
430
1092
  * 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.
1093
+ * is internal-coherence belt-and-braces in the same trust domain as the rest
1094
+ * of the proof, and write-time truth stays `pr:verify`'s job.
433
1095
  *
434
1096
  * The authorizing evidence is the proof's OWN `impactStamp` — the same stamp
435
1097
  * `pr:verify` computed for this candidate's identity triple and recorded here,
@@ -442,14 +1104,13 @@ const assertBatteryCompleteness = (proof, context) => {
442
1104
  * self-contradictory stamp, an unclassified name, or an `affected` verdict all
443
1105
  * fail the same way scoping itself would.
444
1106
  *
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
1107
+ * Scoped to `outcome: "passed"` for the same reason as the completeness rule:
1108
+ * an aborted run records partial execution, so it makes no false completeness
448
1109
  * claim.
449
1110
  */
450
1111
  const assertNotRequiredStampAuthorization = (proof, context) => {
451
1112
  const notRequired = proof.notRequiredCommands ?? [];
452
- if (proof.outcome !== "passed" || proof.schemaVersion < 4 || notRequired.length === 0) return;
1113
+ if (proof.outcome !== "passed" || notRequired.length === 0) return;
453
1114
  const { impactStamp } = proof;
454
1115
  if (impactStamp === void 0) {
455
1116
  context.addIssue({
@@ -493,489 +1154,32 @@ const assertNotRequiredStampAuthorization = (proof, context) => {
493
1154
  //#endregion
494
1155
  //#region src/pr-verify-proof-rules.ts
495
1156
  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({
1157
+ if (proof.notDemandedRecords !== void 0 && proof.impactStamp === void 0) context.addIssue({
512
1158
  code: "custom",
513
- message: "notRequiredCommands is a schemaVersion>=4 field; a v1–v3 pr:verify proof must not carry it.",
514
- path: ["notRequiredCommands"]
1159
+ message: "notDemandedRecords requires impactStamp; package-level not-demanded records are bound to the stamp identity.",
1160
+ path: ["notDemandedRecords"]
515
1161
  });
516
1162
  assertBatteryCompleteness(proof, context);
517
1163
  assertNotRequiredStampAuthorization(proof, context);
518
1164
  };
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"
1165
+ /**
1166
+ * The pr:verify proof versions a reader accepts: the current one only (#920
1167
+ * item 1). #917 narrowed the pr:ready reader the same way. A pr:verify proof
1168
+ * is per-candidate and short-lived — it is rewritten on every head — so no
1169
+ * stored v1–v3 proof can outlive the release that drops it. Pre-1.0 posture
1170
+ * applies: one current contract, no compat reader. A consumer on an older
1171
+ * factory must adopt this release before HQ ingests its pr:verify proofs.
1172
+ */
1173
+ const SUPPORTED_PR_VERIFY_SCHEMA_VERSIONS = [4];
1174
+ const prVerifyTestFailureSchema = z.object({
1175
+ file: z.string().min(1),
1176
+ kind: z.enum([
1177
+ "assertion",
1178
+ "error",
1179
+ "timeout"
595
1180
  ]),
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
- /**
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.
841
- */
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"
857
- ]),
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
- }
1181
+ title: z.string().min(1)
972
1182
  });
973
- const SUPPORTED_PR_VERIFY_SCHEMA_VERSIONS = [
974
- 1,
975
- 2,
976
- 3,
977
- 4
978
- ];
979
1183
  const executedCommandSchema = z.object({
980
1184
  command: z.string().min(1),
981
1185
  counts: z.object({
@@ -984,9 +1188,9 @@ const executedCommandSchema = z.object({
984
1188
  }).optional(),
985
1189
  durationMs: z.number().nonnegative(),
986
1190
  exitCode: z.number(),
1191
+ failures: z.array(prVerifyTestFailureSchema).min(1).optional(),
987
1192
  name: z.string().min(1),
988
1193
  scope: z.enum([
989
- "always",
990
1194
  "docs-only",
991
1195
  "trivial",
992
1196
  "full"
@@ -997,9 +1201,13 @@ const notRequiredCommandSchema = z.object({
997
1201
  impactTarget: z.string().min(1),
998
1202
  name: z.string().min(1)
999
1203
  });
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(", ")}` });
1204
+ const notDemandedRecordSchema = z.object({
1205
+ basis: z.string().min(1),
1206
+ name: z.string().min(1)
1207
+ });
1208
+ const prVerifySchemaVersionSchema = z.literal(4);
1001
1209
  const prVerifyProofSchema = z.object({
1002
- authoringSession: z.string().trim().min(1).optional(),
1210
+ authoringSession: z.string().trim().min(1),
1003
1211
  base: z.string().min(1),
1004
1212
  baselineFullProofs: z.array(z.object({
1005
1213
  base: z.string().min(1),
@@ -1009,8 +1217,13 @@ const prVerifyProofSchema = z.object({
1009
1217
  projectKey: z.string().min(1),
1010
1218
  repository: z.string().min(1)
1011
1219
  })).optional(),
1220
+ catchUpRecognition: catchUpRecognitionSchema.optional(),
1012
1221
  changedFiles: z.array(z.string()),
1013
- classification: z.enum(DIFF_CLASSIFICATIONS),
1222
+ classification: z.enum([
1223
+ "docs/process-only",
1224
+ "trivial",
1225
+ "non-trivial"
1226
+ ]),
1014
1227
  classificationReasons: z.array(z.string()),
1015
1228
  command: z.literal("patronage-factory pr:verify"),
1016
1229
  durationMs: z.number().nonnegative(),
@@ -1024,9 +1237,11 @@ const prVerifyProofSchema = z.object({
1024
1237
  "trivial",
1025
1238
  "full"
1026
1239
  ]),
1240
+ notDemandedRecords: z.array(notDemandedRecordSchema).min(1).optional(),
1027
1241
  notRequiredCommands: z.array(notRequiredCommandSchema).min(1).optional(),
1028
1242
  outcome: z.enum(["aborted", "passed"]).optional(),
1029
1243
  patchId: z.string().regex(/^[0-9a-f]{40}$/u).optional(),
1244
+ policyResolution: policyResolutionSchema.optional(),
1030
1245
  profilePath: z.string().min(1),
1031
1246
  projectKey: z.string().min(1),
1032
1247
  repository: z.string().min(1),
@@ -1038,186 +1253,108 @@ const prVerifyProofSchema = z.object({
1038
1253
  impactTarget: z.string().min(1).optional(),
1039
1254
  name: z.string().min(1),
1040
1255
  scope: z.enum([
1041
- "always",
1042
1256
  "docs-only",
1043
1257
  "trivial",
1044
1258
  "full"
1045
1259
  ])
1046
1260
  }))
1047
1261
  }).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
1262
+ /** Artifact versions accepted at ingest. */
1263
+ const SUPPORTED_CLOSEOUT_SCHEMA_VERSIONS = [5];
1264
+ const MAX_CLOSEOUT_ROWS = 1e3;
1265
+ const IdentifierSchema = z.string().min(1).max(500);
1266
+ const NarrativeSchema = z.string().min(1).max(1e4);
1267
+ const RepoSchema = z.string().min(1).max(200);
1268
+ const CloseoutSourceSchema = z.object({
1269
+ detail: NarrativeSchema,
1270
+ kind: z.literal("token-harvest")
1057
1271
  }).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"
1272
+ const CloseoutBudgetSchema = z.object({
1273
+ note: NarrativeSchema,
1274
+ tier: z.literal("overseer")
1275
+ }).strict();
1276
+ const CloseoutMetricRowSchema = z.object({
1277
+ label: IdentifierSchema,
1278
+ metric: IdentifierSchema,
1279
+ source: CloseoutSourceSchema,
1280
+ unit: z.enum([
1281
+ "boolean",
1282
+ "count",
1283
+ "text",
1284
+ "tokens",
1285
+ "usd"
1089
1286
  ]),
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({
1287
+ value: z.union([
1288
+ z.number(),
1289
+ NarrativeSchema,
1290
+ z.boolean(),
1291
+ z.null()
1292
+ ])
1293
+ }).strict();
1294
+ const CloseoutBlindspotSchema = z.object({
1295
+ id: IdentifierSchema,
1296
+ note: NarrativeSchema,
1297
+ reason: NarrativeSchema.optional()
1298
+ }).strict();
1299
+ /** Strict, bounded runtime transport contract emitted by factory:closeout. */
1300
+ const closeoutArtifactSchema = z.object({
1301
+ budget: CloseoutBudgetSchema,
1302
+ couldNotSee: z.array(CloseoutBlindspotSchema).min(1).max(MAX_CLOSEOUT_ROWS),
1303
+ epic: IdentifierSchema,
1304
+ generatedAt: z.iso.datetime(),
1305
+ metrics: z.array(CloseoutMetricRowSchema).max(MAX_CLOSEOUT_ROWS),
1306
+ repo: RepoSchema,
1307
+ schemaVersion: z.literal(5)
1308
+ }).strict();
1309
+ //#endregion
1310
+ //#region src/schemas.ts
1311
+ const evidenceCheckTypeSchema = z.enum(EVIDENCE_CHECK_TYPES);
1312
+ const evidenceReviewRungSchema = z.enum(EVIDENCE_REVIEW_RUNGS);
1313
+ const diffClassificationSchema = z.enum(DIFF_CLASSIFICATIONS);
1314
+ const ladderStageSchema = z.enum(LADDER_STAGES);
1315
+ const ladderNextActionSchema = z.enum(LADDER_NEXT_ACTION_KINDS);
1316
+ const ladderForcedTransitionSchema = z.enum(LADDER_FORCED_TRANSITION_KINDS);
1317
+ const followUpActionSchema = z.object({
1318
+ argv: z.array(z.string()),
1319
+ command: z.string()
1320
+ }).strict();
1321
+ const EVIDENCE_ENVELOPE_SCHEMA_VERSION = 1;
1322
+ const evidenceShaSchema = z.string().regex(/^[0-9a-f]{7,40}$/u, { message: "must be a 7-40 char lowercase hex git SHA" });
1323
+ const evidenceEnvelopeSchema = z.object({
1324
+ check: z.string().min(1),
1325
+ checkType: evidenceCheckTypeSchema,
1326
+ findingsPointer: z.string().min(1).optional(),
1327
+ headSha: evidenceShaSchema,
1328
+ mergeBaseSha: evidenceShaSchema,
1329
+ model: z.string().min(1).optional(),
1330
+ outcome: z.enum(["pass", "fail"]),
1331
+ patchId: evidenceShaSchema,
1332
+ policyVersion: z.string().min(1).optional(),
1333
+ producer: z.string().min(1),
1334
+ requestId: z.string().min(1).optional(),
1335
+ rung: evidenceReviewRungSchema.optional(),
1336
+ schemaVersion: z.literal(1),
1337
+ sessionId: z.string().trim().min(1).optional(),
1338
+ timestamp: z.iso.datetime().optional()
1339
+ }).passthrough().superRefine((envelope, context) => {
1340
+ if (envelope.checkType === "review") {
1341
+ if (envelope.rung === void 0) context.addIssue({
1125
1342
  code: "custom",
1126
- message: "passed reviews must not include findings",
1127
- path: ["findings"]
1343
+ message: "review-type evidence envelope requires a rung (ADR 0014 §3).",
1344
+ path: ["rung"]
1128
1345
  });
1129
- }
1130
- if (review.outcome !== "passed") {
1131
- if (review.findings.length === 0) context.addIssue({
1346
+ } else {
1347
+ if (envelope.rung !== void 0) context.addIssue({
1132
1348
  code: "custom",
1133
- message: "non-passing reviews must include findings",
1134
- path: ["findings"]
1349
+ message: "verify-type evidence envelope must not carry rung (review-type-only field, ADR 0014 §3).",
1350
+ path: ["rung"]
1135
1351
  });
1136
- if (review.findings.length !== review.issuesFlagged) context.addIssue({
1352
+ if (envelope.model !== void 0) context.addIssue({
1137
1353
  code: "custom",
1138
- message: "issuesFlagged must match findings length",
1139
- path: ["issuesFlagged"]
1354
+ message: "verify-type evidence envelope must not carry model (review-type-only field, ADR 0014 §3).",
1355
+ path: ["model"]
1140
1356
  });
1141
1357
  }
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
1358
  });
1222
1359
  const BOUNDARY_REVIEW_PROOF_KIND = "boundary-review-proof";
1223
1360
  const coveredSetEntrySchema = z.object({
@@ -1258,7 +1395,7 @@ const boundaryReviewProofSchema = z.object({
1258
1395
  }).passthrough(),
1259
1396
  outcome: z.enum(["pass", "fail"]),
1260
1397
  producer: z.string().min(1),
1261
- rung: z.enum(EVIDENCE_REVIEW_RUNGS),
1398
+ rung: evidenceReviewRungSchema,
1262
1399
  schemaVersion: z.literal(1),
1263
1400
  sessionId: z.string().min(1).optional(),
1264
1401
  specHash: z.object({
@@ -1279,7 +1416,7 @@ const boundaryCheckProofSchema = z.object({
1279
1416
  state: z.enum(["merged", "open"]),
1280
1417
  wave: z.string().min(1)
1281
1418
  })),
1282
- demandedRung: z.enum(EVIDENCE_REVIEW_RUNGS).optional(),
1419
+ demandedRung: evidenceReviewRungSchema.optional(),
1283
1420
  epicIssue: z.number().int().positive(),
1284
1421
  manifestHash: z.object({
1285
1422
  algo: z.literal("sha256"),
@@ -1291,7 +1428,7 @@ const boundaryCheckProofSchema = z.object({
1291
1428
  commentId: z.union([z.string(), z.number()]).optional(),
1292
1429
  commentUrl: z.string().optional(),
1293
1430
  producer: z.string().min(1),
1294
- rung: z.enum(EVIDENCE_REVIEW_RUNGS),
1431
+ rung: evidenceReviewRungSchema,
1295
1432
  sessionId: z.string().min(1).optional(),
1296
1433
  specHash: z.object({
1297
1434
  algo: z.literal("sha256"),
@@ -1306,7 +1443,6 @@ const boundaryCheckProofSchema = z.object({
1306
1443
  }).optional(),
1307
1444
  status: z.enum(["ready", "blocked"])
1308
1445
  });
1309
- const requiredCheckScopeSchema = z.object({ classifications: z.array(z.enum(DIFF_CLASSIFICATIONS)).min(1) }).strict();
1310
1446
  const readinessRepairSchema = z.object({
1311
1447
  action: z.string().min(1),
1312
1448
  code: z.enum([
@@ -1317,12 +1453,15 @@ const readinessRepairSchema = z.object({
1317
1453
  command: z.string().min(1)
1318
1454
  });
1319
1455
  const reviewStateSchema = z.object({
1320
- docsOnlyDeltaAccepted: z.boolean().optional(),
1321
1456
  required: z.boolean(),
1322
1457
  reviewedHeadSha: z.string().optional(),
1323
1458
  reviewedPatchId: z.string().optional(),
1324
- status: z.enum(REVIEW_STATUS_VALUES)
1459
+ status: reviewStatusSchema
1460
+ }).strict().superRefine((state, context) => {
1461
+ for (const issue of reviewStateIssues(state)) context.addIssue(issue);
1325
1462
  });
1463
+ const verifyCommandSchema = z.literal("patronage-factory pr:verify");
1464
+ const verifiedShaSchema = z.string().min(1);
1326
1465
  const historicalPostReadinessCommentSchema = z.object({
1327
1466
  author: z.string().min(1),
1328
1467
  createdAt: z.string().min(1),
@@ -1338,10 +1477,9 @@ const historicalHandledHumanCommentSchema = z.object({
1338
1477
  const managedReadinessLedgerSchema = z.object({
1339
1478
  baseSha: z.string().regex(/^[0-9a-f]{40}$/u),
1340
1479
  blockingReasons: z.array(z.string()),
1341
- classification: z.enum(DIFF_CLASSIFICATIONS),
1480
+ classification: diffClassificationSchema,
1342
1481
  externalChecks: z.array(z.object({
1343
- checkType: z.enum(EVIDENCE_CHECK_TYPES),
1344
- inScope: z.boolean(),
1482
+ checkType: evidenceCheckTypeSchema,
1345
1483
  name: z.string().min(1),
1346
1484
  reason: z.string().optional(),
1347
1485
  scope: requiredCheckScopeSchema.optional(),
@@ -1414,25 +1552,9 @@ const managedReadinessLedgerSchema = z.object({
1414
1552
  gate: z.number().int().nonnegative(),
1415
1553
  interior: z.number().int().nonnegative()
1416
1554
  }),
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
- ])
1555
+ forcedTransition: ladderForcedTransitionSchema.optional(),
1556
+ nextAction: ladderNextActionSchema,
1557
+ stage: ladderStageSchema
1436
1558
  }).optional(),
1437
1559
  reviewRuns: z.array(prReviewResultSchema).optional(),
1438
1560
  reviewTerminalState: z.enum([
@@ -1444,40 +1566,54 @@ const managedReadinessLedgerSchema = z.object({
1444
1566
  correctness: reviewStateSchema,
1445
1567
  security: reviewStateSchema.optional()
1446
1568
  }),
1447
- schemaVersion: z.literal(1),
1569
+ schemaVersion: z.literal(2),
1448
1570
  stackRole: z.enum([
1449
1571
  "single",
1450
1572
  "slice",
1451
1573
  "rollup",
1452
1574
  "merge-gate prerequisite"
1453
1575
  ]).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
- })
1576
+ verification: z.discriminatedUnion("prVerify", [
1577
+ z.object({
1578
+ command: verifyCommandSchema,
1579
+ prVerify: z.literal("missing")
1580
+ }).strict(),
1581
+ z.object({
1582
+ command: verifyCommandSchema,
1583
+ prVerify: z.literal("stale"),
1584
+ verifiedHeadSha: verifiedShaSchema
1585
+ }).strict(),
1586
+ z.object({
1587
+ command: verifyCommandSchema,
1588
+ prVerify: z.literal("passed-via-head"),
1589
+ verifiedHeadSha: verifiedShaSchema
1590
+ }).strict(),
1591
+ z.object({
1592
+ command: verifyCommandSchema,
1593
+ docsOnlyVerifiedHeadSha: verifiedShaSchema,
1594
+ prVerify: z.literal("docs-only-delta"),
1595
+ verifiedHeadSha: verifiedShaSchema.optional()
1596
+ }).strict(),
1597
+ z.object({
1598
+ command: verifyCommandSchema,
1599
+ prVerify: z.literal("trivial-delta"),
1600
+ trivialVerifiedHeadSha: verifiedShaSchema,
1601
+ verifiedHeadSha: verifiedShaSchema.optional()
1602
+ }).strict()
1603
+ ])
1467
1604
  });
1468
1605
  const PR_READY_SCHEMA_VERSION = 3;
1469
1606
  /**
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.
1607
+ * The pr:ready proof versions a reader accepts: the current one only (#917
1608
+ * item 1). #894 narrowed the nested readiness ledger to `schemaVersion: 2` and
1609
+ * the `prVerify` tagged union, which no v1 or v2 pr:ready proof can satisfy.
1610
+ * Advertising 1 and 2 here therefore promised an acceptance the nested schema
1611
+ * already refused. Pre-1.0 posture applies: one current contract, no compat
1612
+ * reader. An alpha.22 consumer must adopt alpha.23 before HQ ingests its
1613
+ * pr:ready proofs.
1474
1614
  */
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(", ")}` });
1615
+ const SUPPORTED_PR_READY_SCHEMA_VERSIONS = [3];
1616
+ const prReadySchemaVersionSchema = z.literal(3);
1481
1617
  const prReadyProofSchema = z.object({
1482
1618
  arming: z.object({
1483
1619
  detail: z.string().min(1).optional(),
@@ -1488,6 +1624,7 @@ const prReadyProofSchema = z.object({
1488
1624
  "not-armed"
1489
1625
  ])
1490
1626
  }).optional(),
1627
+ authoringSession: z.string().min(1).optional(),
1491
1628
  blockedReasons: blockedReasonsSchema.optional(),
1492
1629
  blockingReasons: z.array(z.string()),
1493
1630
  command: z.literal("patronage-factory pr:ready"),
@@ -1495,6 +1632,7 @@ const prReadyProofSchema = z.object({
1495
1632
  humanBlockingReasons: z.array(z.string()).default([]),
1496
1633
  ledger: managedReadinessLedgerSchema,
1497
1634
  notices: z.array(z.string().min(1)).optional(),
1635
+ policyResolution: policyResolutionSchema.optional(),
1498
1636
  profileBlobSha: z.string().regex(/^[0-9a-f]{40}$/u).optional(),
1499
1637
  profilePath: z.string().min(1).optional(),
1500
1638
  repairs: z.array(readinessRepairSchema).default([]),
@@ -1507,15 +1645,7 @@ const prReadyProofSchema = z.object({
1507
1645
  ]),
1508
1646
  waivedDemands: z.array(waivedDemandSchema).optional()
1509
1647
  }).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
1648
  for (const issue of blockedReasonIssues({ ...proof })) context.addIssue(issue);
1519
1649
  });
1520
1650
  //#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 };
1651
+ 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, prReadyProofSchema, prReviewFindingSchema, prReviewProofSchema, prReviewResultSchema, prVerifyProofSchema, reviewPromptSectionSchema, reviewPromptSectionsSchema, waivedDemandSchema };