@patronage/software-factory 1.0.0-alpha.21 → 1.0.0-alpha.23

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
@@ -200,6 +200,11 @@ const blockedReasonIssues = (proof) => {
200
200
  const drifted = named.findIndex((reason, index) => reason.detail !== blockedReasonDetail(reasons[index]));
201
201
  return drifted === -1 ? [] : blockedReasonIssue(`blockedReasons[${drifted}] does not carry blocking reason ${drifted}; the two projections must tell one story`);
202
202
  };
203
+ const sameHeadSha = (left, right) => {
204
+ if (!left || !right) return false;
205
+ if (Math.min(left.length, right.length) < 7) return false;
206
+ return left.startsWith(right) || right.startsWith(left);
207
+ };
203
208
  //#endregion
204
209
  //#region src/demand-waiver.ts
205
210
  const DEMAND_WAIVER_SCHEMA_VERSION = 1;
@@ -229,6 +234,549 @@ const waivedDemandSchema = z.object({
229
234
  unmetReasons: z.array(z.string().min(1)).min(1)
230
235
  });
231
236
  //#endregion
237
+ //#region src/packages/review-prompt-sections/schema.ts
238
+ const REVIEW_PROMPT_SECTION_PROVENANCES = ["prior-review-ledger", "issue-review-focus"];
239
+ const nonBlankString$1 = z.string().refine((value) => value.trim().length > 0, { message: "must not be blank" });
240
+ const reviewPromptSectionSchema = z.object({
241
+ provenance: z.enum(REVIEW_PROMPT_SECTION_PROVENANCES),
242
+ source: nonBlankString$1,
243
+ text: nonBlankString$1
244
+ }).strict();
245
+ const promptSectionRank = (section) => REVIEW_PROMPT_SECTION_PROVENANCES.indexOf(section.provenance);
246
+ const hasDuplicateProvenance = (sections) => new Set(sections.map((section) => section.provenance)).size !== sections.length;
247
+ const isOutOfAssemblyOrder = (sections) => sections.some((section, index) => index > 0 && promptSectionRank(section) < promptSectionRank(sections[index - 1]));
248
+ const reviewPromptSectionsSchema = z.array(reviewPromptSectionSchema).min(1).refine((sections) => !hasDuplicateProvenance(sections) && !isOutOfAssemblyOrder(sections), { message: "prompt sections must be in assembly order without duplicates" });
249
+ //#endregion
250
+ //#region src/review-ladder-policy.ts
251
+ const REVIEW_CATEGORIES = [
252
+ "correctness",
253
+ "safety",
254
+ "coordination",
255
+ "maintainability",
256
+ "unknown"
257
+ ];
258
+ //#endregion
259
+ //#region src/evidence-freshness.ts
260
+ const patchIdMatches = (recorded, candidate) => Boolean(recorded.patchId) && recorded.patchId === candidate.patchId;
261
+ const headShaMatches = (recorded, candidate) => Boolean(recorded.headSha && candidate.headSha && sameHeadSha(candidate.headSha, recorded.headSha));
262
+ /**
263
+ * The one freshness decision. `true` means the recorded evidence still
264
+ * describes the candidate.
265
+ *
266
+ * Time is not an input and never will be: adding one would contradict the
267
+ * `freshness` definition in root `CONTEXT.md`.
268
+ */
269
+ function evidenceFresh({ candidate, recorded, rule }) {
270
+ if (patchIdMatches(recorded, candidate)) return true;
271
+ return rule === "patch-id-or-head-sha" && headShaMatches(recorded, candidate);
272
+ }
273
+ //#endregion
274
+ //#region src/sha256-hex.ts
275
+ const ROUND_CONSTANTS = new Uint32Array([
276
+ 1116352408,
277
+ 1899447441,
278
+ 3049323471,
279
+ 3921009573,
280
+ 961987163,
281
+ 1508970993,
282
+ 2453635748,
283
+ 2870763221,
284
+ 3624381080,
285
+ 310598401,
286
+ 607225278,
287
+ 1426881987,
288
+ 1925078388,
289
+ 2162078206,
290
+ 2614888103,
291
+ 3248222580,
292
+ 3835390401,
293
+ 4022224774,
294
+ 264347078,
295
+ 604807628,
296
+ 770255983,
297
+ 1249150122,
298
+ 1555081692,
299
+ 1996064986,
300
+ 2554220882,
301
+ 2821834349,
302
+ 2952996808,
303
+ 3210313671,
304
+ 3336571891,
305
+ 3584528711,
306
+ 113926993,
307
+ 338241895,
308
+ 666307205,
309
+ 773529912,
310
+ 1294757372,
311
+ 1396182291,
312
+ 1695183700,
313
+ 1986661051,
314
+ 2177026350,
315
+ 2456956037,
316
+ 2730485921,
317
+ 2820302411,
318
+ 3259730800,
319
+ 3345764771,
320
+ 3516065817,
321
+ 3600352804,
322
+ 4094571909,
323
+ 275423344,
324
+ 430227734,
325
+ 506948616,
326
+ 659060556,
327
+ 883997877,
328
+ 958139571,
329
+ 1322822218,
330
+ 1537002063,
331
+ 1747873779,
332
+ 1955562222,
333
+ 2024104815,
334
+ 2227730452,
335
+ 2361852424,
336
+ 2428436474,
337
+ 2756734187,
338
+ 3204031479,
339
+ 3329325298
340
+ ]);
341
+ const INITIAL_STATE = new Uint32Array([
342
+ 1779033703,
343
+ 3144134277,
344
+ 1013904242,
345
+ 2773480762,
346
+ 1359893119,
347
+ 2600822924,
348
+ 528734635,
349
+ 1541459225
350
+ ]);
351
+ const BLOCK_BYTES = 64;
352
+ const LENGTH_FIELD_BYTES = 8;
353
+ const WORD_BITS = 32;
354
+ const rotateRight = (value, bits) => (value >>> bits | value << WORD_BITS - bits) >>> 0;
355
+ /** Message bytes plus the FIPS 180-4 padding and 64-bit length suffix. */
356
+ const padMessage = (message) => {
357
+ const paddedLength = Math.ceil((message.length + 1 + LENGTH_FIELD_BYTES) / BLOCK_BYTES) * BLOCK_BYTES;
358
+ const padded = new Uint8Array(paddedLength);
359
+ padded.set(message);
360
+ padded[message.length] = 128;
361
+ const bitLength = BigInt(message.length) * 8n;
362
+ new DataView(padded.buffer).setBigUint64(paddedLength - LENGTH_FIELD_BYTES, bitLength);
363
+ return padded;
364
+ };
365
+ const ROUNDS = 64;
366
+ const SCHEDULE_SEED_WORDS = 16;
367
+ const STATE_WORDS = 8;
368
+ const wordAt = (words, index) => words[index];
369
+ /** One 64-byte block folded into the running state, in place. */
370
+ const compress = (state, view, offset) => {
371
+ const schedule = new Uint32Array(ROUNDS);
372
+ for (let index = 0; index < SCHEDULE_SEED_WORDS; index += 1) schedule[index] = view.getUint32(offset + index * 4);
373
+ for (let index = SCHEDULE_SEED_WORDS; index < ROUNDS; index += 1) {
374
+ const previous = wordAt(schedule, index - 15);
375
+ const recent = wordAt(schedule, index - 2);
376
+ const sigma0 = rotateRight(previous, 7) ^ rotateRight(previous, 18) ^ previous >>> 3;
377
+ const sigma1 = rotateRight(recent, 17) ^ rotateRight(recent, 19) ^ recent >>> 10;
378
+ schedule[index] = wordAt(schedule, index - 16) + sigma0 + wordAt(schedule, index - 7) + sigma1 >>> 0;
379
+ }
380
+ const working = new Uint32Array(state);
381
+ for (let index = 0; index < ROUNDS; index += 1) {
382
+ const a = wordAt(working, 0);
383
+ const b = wordAt(working, 1);
384
+ const c = wordAt(working, 2);
385
+ const d = wordAt(working, 3);
386
+ const e = wordAt(working, 4);
387
+ const f = wordAt(working, 5);
388
+ const g = wordAt(working, 6);
389
+ const h = wordAt(working, 7);
390
+ const sum1 = rotateRight(e, 6) ^ rotateRight(e, 11) ^ rotateRight(e, 25);
391
+ const choose = e & f ^ ~e & g;
392
+ const temp1 = h + sum1 + choose + wordAt(ROUND_CONSTANTS, index) + wordAt(schedule, index) >>> 0;
393
+ const sum0 = rotateRight(a, 2) ^ rotateRight(a, 13) ^ rotateRight(a, 22);
394
+ const majority = a & b ^ a & c ^ b & c;
395
+ working[7] = g;
396
+ working[6] = f;
397
+ working[5] = e;
398
+ working[4] = d + temp1 >>> 0;
399
+ working[3] = c;
400
+ working[2] = b;
401
+ working[1] = a;
402
+ working[0] = temp1 + (sum0 + majority >>> 0) >>> 0;
403
+ }
404
+ for (let index = 0; index < STATE_WORDS; index += 1) state[index] = wordAt(state, index) + wordAt(working, index) >>> 0;
405
+ };
406
+ const toHex = (state) => {
407
+ let hex = "";
408
+ for (const word of state) hex += word.toString(16).padStart(8, "0");
409
+ return hex;
410
+ };
411
+ /** The lowercase hex SHA-256 digest of a UTF-8 string. */
412
+ const sha256Hex = (input) => {
413
+ const padded = padMessage(new TextEncoder().encode(input));
414
+ const view = new DataView(padded.buffer);
415
+ const state = new Uint32Array(INITIAL_STATE);
416
+ for (let offset = 0; offset < padded.length; offset += BLOCK_BYTES) compress(state, view, offset);
417
+ return toHex(state);
418
+ };
419
+ //#endregion
420
+ //#region src/review-ladder-ledger.ts
421
+ const normalizeCitedSpan = (text) => text.split(/\r?\n/u).map((line) => line.trim().replaceAll(/\s+/gu, " ")).filter((line) => line.length > 0).join("\n");
422
+ const PRESCRIBED_ACTION_STOPWORDS = new Set([
423
+ "a",
424
+ "an",
425
+ "the",
426
+ "to",
427
+ "of",
428
+ "in",
429
+ "on",
430
+ "and",
431
+ "or",
432
+ "is",
433
+ "are",
434
+ "be",
435
+ "this",
436
+ "that",
437
+ "it",
438
+ "for",
439
+ "with",
440
+ "as",
441
+ "by",
442
+ "from",
443
+ "at",
444
+ "into",
445
+ "should",
446
+ "must",
447
+ "will",
448
+ "would",
449
+ "can",
450
+ "could",
451
+ "not",
452
+ "but",
453
+ "so",
454
+ "if",
455
+ "then",
456
+ "than",
457
+ "when",
458
+ "which",
459
+ "who",
460
+ "whom",
461
+ "its",
462
+ "their",
463
+ "there",
464
+ "here",
465
+ "also",
466
+ "use",
467
+ "using",
468
+ "instead",
469
+ "rather",
470
+ "than",
471
+ "over",
472
+ "up",
473
+ "out",
474
+ "have",
475
+ "has",
476
+ "had",
477
+ "been"
478
+ ]);
479
+ const tokenizePrescribedAction = (text) => {
480
+ const words = text.toLowerCase().replaceAll(/[^a-z0-9\s]/gu, " ").split(/\s+/u).filter((word) => word.length > 0 && !PRESCRIBED_ACTION_STOPWORDS.has(word));
481
+ return [...new Set(words)].toSorted();
482
+ };
483
+ const normalizeTitle = (title) => title.trim().toLowerCase().replaceAll(/\s+/gu, " ");
484
+ const normalizeFile = (file) => file.trim();
485
+ const findingKey = (finding) => {
486
+ const path = finding.file ?? "";
487
+ const normalizedSpan = normalizeCitedSpan(finding.citedSpan ?? "");
488
+ const actionTokens = tokenizePrescribedAction(finding.prescribedAction ?? "");
489
+ const legacyDiscriminator = normalizedSpan.length > 0 || actionTokens.length > 0 ? "" : normalizeTitle(finding.title ?? "");
490
+ return sha256Hex(`${path}\0${sha256Hex(normalizedSpan)}\0${actionTokens.join(" ")}\0${legacyDiscriminator}`);
491
+ };
492
+ const declarationMatchesEntry = (entry, target) => normalizeTitle(entry.title) === normalizeTitle(target.title) && (target.file === void 0 || entry.file !== void 0 && normalizeFile(entry.file) === normalizeFile(target.file));
493
+ const applyDisposition = (ledger, declaration) => {
494
+ const entry = declaration.findingReport ? resolveEntryForFinding([...ledger.values()], declaration.findingReport) : [...ledger.values()].find((candidate) => declarationMatchesEntry(candidate, declaration.finding));
495
+ if (!entry) return false;
496
+ entry.disposition = declaration.disposition;
497
+ entry.priorDisposition = void 0;
498
+ if (declaration.reference !== void 0) entry.reference = declaration.reference;
499
+ return true;
500
+ };
501
+ const definedAttributes = (finding) => Object.fromEntries([
502
+ "blockingAfterCap",
503
+ "category",
504
+ "citedSpan",
505
+ "file",
506
+ "prescribedAction",
507
+ "priority"
508
+ ].flatMap((attribute) => finding[attribute] === void 0 ? [] : [[attribute, finding[attribute]]]));
509
+ const MIN_MEANINGFUL_OVERLAP_LINE_LENGTH = 20;
510
+ const spansOverlap = (a, b) => {
511
+ if (a.length === 0 || b.length === 0) return false;
512
+ if (a === b || a.includes(b) || b.includes(a)) return true;
513
+ const linesA = new Set(a.split("\n").filter((line) => line.length >= MIN_MEANINGFUL_OVERLAP_LINE_LENGTH));
514
+ return b.split("\n").some((line) => line.length >= MIN_MEANINGFUL_OVERLAP_LINE_LENGTH && linesA.has(line));
515
+ };
516
+ const resolveEntryForFinding = (entries, finding) => {
517
+ const primaryKey = findingKey(finding);
518
+ const exact = entries.find((entry) => entry.key === primaryKey);
519
+ if (exact) return exact;
520
+ const { supersedes } = finding;
521
+ if (supersedes) {
522
+ const superseded = entries.find((entry) => declarationMatchesEntry(entry, supersedes));
523
+ if (superseded) return superseded;
524
+ }
525
+ if (finding.file !== void 0 && finding.citedSpan !== void 0) {
526
+ const candidateSpan = normalizeCitedSpan(finding.citedSpan);
527
+ if (candidateSpan.length > 0) {
528
+ const overlapping = entries.find((entry) => entry.disposition === "open" && entry.file === finding.file && entry.citedSpan !== void 0 && spansOverlap(normalizeCitedSpan(entry.citedSpan), candidateSpan));
529
+ if (overlapping) return overlapping;
530
+ }
531
+ }
532
+ };
533
+ const REFLAG_TRANSITIONS = {
534
+ "fixed-in-thread": (entry) => {
535
+ entry.disposition = "open";
536
+ entry.reopenedCount += 1;
537
+ },
538
+ "follow-up-filed": (entry) => {
539
+ entry.priorDisposition = entry.disposition;
540
+ entry.disposition = "stale-repeat";
541
+ entry.staleRepeatCount += 1;
542
+ },
543
+ open: () => {},
544
+ "rerun-noise": (entry) => {
545
+ entry.staleRepeatCount += 1;
546
+ },
547
+ "stale-repeat": (entry) => {
548
+ entry.staleRepeatCount += 1;
549
+ },
550
+ waived: (entry) => {
551
+ entry.priorDisposition = entry.disposition;
552
+ entry.disposition = "stale-repeat";
553
+ entry.staleRepeatCount += 1;
554
+ }
555
+ };
556
+ const applyFlag = (ledger, finding, ref, demoteNew) => {
557
+ const matched = resolveEntryForFinding([...ledger.values()], finding);
558
+ if (!matched) {
559
+ const key = findingKey(finding);
560
+ ledger.set(key, {
561
+ ...definedAttributes(finding),
562
+ disposition: demoteNew ? "rerun-noise" : "open",
563
+ firstFlagged: ref,
564
+ flagCount: 1,
565
+ key,
566
+ lastFlagged: ref,
567
+ reopenedCount: 0,
568
+ staleRepeatCount: 0,
569
+ title: finding.title.trim()
570
+ });
571
+ return;
572
+ }
573
+ matched.flagCount += 1;
574
+ matched.lastFlagged = ref;
575
+ Object.assign(matched, definedAttributes(finding));
576
+ REFLAG_TRANSITIONS[matched.disposition](matched);
577
+ };
578
+ const findingKeySet = (findings) => new Set(findings.map((finding) => findingKey(finding)));
579
+ const isSameHeadRerun = (previous, current) => previous?.identity !== void 0 && current.identity !== void 0 && evidenceFresh({
580
+ candidate: current.identity,
581
+ recorded: previous.identity,
582
+ rule: "patch-id"
583
+ });
584
+ const isDisjointSameHeadRerun = (previous, current) => {
585
+ if (current.correlatedRerun) return false;
586
+ if (!isSameHeadRerun(previous, current) || previous?.findings.length === 0 || current.findings.length === 0) return false;
587
+ const previousKeys = findingKeySet(previous?.findings ?? []);
588
+ return current.findings.every((finding) => !previousKeys.has(findingKey(finding)));
589
+ };
590
+ const foldLedger = (cycles, refs) => {
591
+ const ledger = /* @__PURE__ */ new Map();
592
+ const unmatchedDispositions = [];
593
+ for (const [index, entry] of cycles.entries()) {
594
+ const dispositions = entry.dispositions ?? [];
595
+ const severityFloorDispositions = dispositions.filter((declaration) => declaration.findingReport !== void 0);
596
+ for (const declaration of dispositions.filter((candidate) => candidate.findingReport === void 0)) if (!applyDisposition(ledger, declaration)) unmatchedDispositions.push(declaration);
597
+ const demoteNew = isDisjointSameHeadRerun(cycles[index - 1], entry);
598
+ for (const finding of entry.findings) applyFlag(ledger, finding, refs[index], demoteNew);
599
+ for (const declaration of severityFloorDispositions) if (!applyDisposition(ledger, declaration)) unmatchedDispositions.push(declaration);
600
+ }
601
+ return {
602
+ ledger,
603
+ unmatchedDispositions
604
+ };
605
+ };
606
+ z.array(z.object({
607
+ disposition: z.enum([
608
+ "fixed-in-thread",
609
+ "follow-up-filed",
610
+ "waived"
611
+ ]),
612
+ finding: z.object({
613
+ file: z.string().min(1).optional(),
614
+ title: z.string().min(1)
615
+ }).strict(),
616
+ reference: z.string().min(1).optional()
617
+ }).strict());
618
+ //#endregion
619
+ //#region src/review-ladder.ts
620
+ const ladderCycleRefsFor = (cycles) => {
621
+ let interior = 0;
622
+ let gate = 0;
623
+ return cycles.map((entry) => {
624
+ if (entry.stage === "interior") {
625
+ if (gate > 0) throw new TypeError("Invalid ladder history: interior cycle recorded after the gate stage began.");
626
+ interior += 1;
627
+ return {
628
+ cycle: interior,
629
+ stage: "interior"
630
+ };
631
+ }
632
+ gate += 1;
633
+ return {
634
+ cycle: gate,
635
+ stage: "gate"
636
+ };
637
+ });
638
+ };
639
+ //#endregion
640
+ //#region src/pr-readiness/ladder-proof-state.ts
641
+ const prReviewLadderStateSchema = z.object({ cycles: z.array(z.custom()) }).superRefine((ladder, context) => {
642
+ try {
643
+ ladderLedgerFor(ladder);
644
+ } catch {
645
+ context.addIssue({
646
+ code: "custom",
647
+ message: "ladder cycles do not fold into a valid disposition ledger"
648
+ });
649
+ }
650
+ });
651
+ const ladderLedgerFor = (ladder) => [...foldLedger(ladder.cycles, ladderCycleRefsFor(ladder.cycles)).ledger.values()];
652
+ //#endregion
653
+ //#region src/pr-readiness/review-proof.ts
654
+ const PR_REVIEW_SCHEMA_VERSION = 2;
655
+ const nonBlankString = z.string().refine((value) => value.trim().length > 0, { message: "must not be blank" });
656
+ const parseableDateString = z.string().refine((value) => Number.isFinite(Date.parse(value)), { message: "must be a parseable date string" });
657
+ const findingSupersedesSchema = z.object({
658
+ file: z.string().optional(),
659
+ title: nonBlankString
660
+ });
661
+ const prReviewFindingSchema = z.object({
662
+ blockingAfterCap: z.boolean().optional(),
663
+ body: nonBlankString,
664
+ category: z.enum(REVIEW_CATEGORIES).optional(),
665
+ citedSpan: z.string().optional(),
666
+ file: z.string().optional(),
667
+ line: z.number().int().positive().optional(),
668
+ prescribedAction: z.string().optional(),
669
+ priority: z.string().optional(),
670
+ protocolFinding: z.literal(true).optional(),
671
+ supersedes: findingSupersedesSchema.optional(),
672
+ title: nonBlankString
673
+ });
674
+ const prReviewResultSchema = z.object({
675
+ durationMs: z.number().refine((value) => Number.isFinite(value) && value >= 0, { message: "durationMs must be a finite number >= 0" }),
676
+ endedAt: parseableDateString,
677
+ findings: z.array(prReviewFindingSchema),
678
+ issuesFlagged: z.number().int().nonnegative(),
679
+ kind: z.enum(["correctness", "security"]),
680
+ model: nonBlankString.optional(),
681
+ outcome: z.enum([
682
+ "passed",
683
+ "failed",
684
+ "error"
685
+ ]),
686
+ producer: nonBlankString.optional(),
687
+ promptSections: reviewPromptSectionsSchema.optional(),
688
+ rung: z.enum(EVIDENCE_REVIEW_RUNGS$1).optional(),
689
+ sessionId: nonBlankString.optional(),
690
+ startedAt: parseableDateString,
691
+ summary: z.string().refine((value) => value.trim().length > 0 && value !== "<one sentence>", { message: "summary must not be blank or a placeholder" }),
692
+ typedVerdict: z.boolean().optional(),
693
+ verdictSource: z.enum(["footer", "recovered"]).optional()
694
+ }).superRefine((review, context) => {
695
+ if (review.outcome === "passed") {
696
+ if (review.issuesFlagged !== 0) context.addIssue({
697
+ code: "custom",
698
+ message: "passed reviews must report zero issues flagged",
699
+ path: ["issuesFlagged"]
700
+ });
701
+ if (review.findings.length !== 0) context.addIssue({
702
+ code: "custom",
703
+ message: "passed reviews must not include findings",
704
+ path: ["findings"]
705
+ });
706
+ }
707
+ if (review.outcome !== "passed") {
708
+ if (review.findings.length === 0) context.addIssue({
709
+ code: "custom",
710
+ message: "non-passing reviews must include findings",
711
+ path: ["findings"]
712
+ });
713
+ if (review.findings.length !== review.issuesFlagged) context.addIssue({
714
+ code: "custom",
715
+ message: "issuesFlagged must match findings length",
716
+ path: ["issuesFlagged"]
717
+ });
718
+ }
719
+ if (review.producer === void 0 !== (review.rung === void 0)) context.addIssue({
720
+ code: "custom",
721
+ message: "producer and rung must be recorded together",
722
+ path: [review.producer === void 0 ? "producer" : "rung"]
723
+ });
724
+ });
725
+ const prReviewProofSchema = z.object({
726
+ base: z.string(),
727
+ changedFiles: z.array(z.string()),
728
+ cleanedPaths: z.array(z.string()),
729
+ findingProvenanceVersion: z.literal(1),
730
+ headSha: z.string().regex(/^[0-9a-f]{40}$/u),
731
+ ladder: prReviewLadderStateSchema.optional(),
732
+ maxReviewCycles: z.number().int().positive().optional(),
733
+ patchId: z.string().regex(/^[0-9a-f]{40,64}$/u),
734
+ reviewCycle: z.number().int().positive().optional(),
735
+ reviewRequirement: z.object({
736
+ reason: z.enum(["no-applicable-mode", "faithful-merge"]),
737
+ status: z.literal("not-required")
738
+ }).strict().optional(),
739
+ reviews: z.array(prReviewResultSchema),
740
+ schemaVersion: z.literal(2)
741
+ }).passthrough().superRefine((proof, context) => {
742
+ if (proof.reviewRequirement && proof.reviews.length > 0) context.addIssue({
743
+ code: "custom",
744
+ message: "not-required review proofs must not contain review runs",
745
+ path: ["reviews"]
746
+ });
747
+ if (proof.reviewCycle !== void 0 && proof.maxReviewCycles !== void 0 && proof.reviewCycle > proof.maxReviewCycles) context.addIssue({
748
+ code: "custom",
749
+ message: "reviewCycle must not exceed maxReviewCycles",
750
+ path: ["reviewCycle"]
751
+ });
752
+ });
753
+ //#endregion
754
+ //#region src/review-state.ts
755
+ const reviewStateIssue = (message, field) => [{
756
+ code: "custom",
757
+ message,
758
+ path: [field]
759
+ }];
760
+ /**
761
+ * The invariant both ledger schemas enforce, written once so the emitting
762
+ * schema and the wire schema cannot drift. It is the producer's state machine:
763
+ *
764
+ * - a review that is not required is never `current` — `reviewStatus` returns
765
+ * `not-required` before it ever looks at the recorded proof;
766
+ * - `current` means a proof was compared against the candidate delta, so the
767
+ * reviewed identity must be recorded;
768
+ * - `missing` means no proof was found, so no reviewed identity can exist.
769
+ */
770
+ const reviewStateIssues = (state) => {
771
+ const hasIdentity = Boolean(state.reviewedHeadSha || state.reviewedPatchId);
772
+ if (state.status === "current") {
773
+ if (!state.required) return reviewStateIssue("a review state that is not required cannot be current", "status");
774
+ if (!hasIdentity) return reviewStateIssue("a current review state must record the reviewed head sha or patch id", "reviewedPatchId");
775
+ }
776
+ if (state.status === "missing" && hasIdentity) return reviewStateIssue("a missing review state must record no reviewed head sha or patch id", "status");
777
+ return [];
778
+ };
779
+ //#endregion
232
780
  //#region src/catch-up-recognition-record.ts
233
781
  const objectShaSchema = z.string().regex(/^[0-9a-f]{40}$/u);
234
782
  const catchUpRecognitionSchema = z.object({
@@ -241,7 +789,8 @@ const catchUpRecognitionSchema = z.object({
241
789
  const impactStampTargetSchema = z.object({
242
790
  basis: z.string().min(1),
243
791
  impact: z.enum(["affected", "not-affected"]),
244
- name: z.string().min(1)
792
+ name: z.string().min(1),
793
+ subscribedPaths: z.array(z.string())
245
794
  });
246
795
  /**
247
796
  * The recorded stamp. `targets` satisfies the completeness invariant: every
@@ -253,7 +802,7 @@ const impactStampSchema = z.object({
253
802
  basis: z.enum(["target-scoped", "conservative"]),
254
803
  inertPaths: z.array(z.string()).default([]),
255
804
  reasons: z.array(z.string()),
256
- stampVersion: z.literal(3),
805
+ stampVersion: z.literal(4),
257
806
  targets: z.array(impactStampTargetSchema).superRefine((targets, context) => {
258
807
  const names = targets.map((target) => target.name);
259
808
  if (new Set(names).size !== names.length) context.addIssue({
@@ -299,8 +848,8 @@ const impactStampScopeDecision = ({ stamp, surface, targetName }) => {
299
848
  reason: `no trusted identity-bound impact stamp covers this candidate; ${floor}`,
300
849
  scoped: false
301
850
  };
302
- if (stamp.stampVersion !== 3) return {
303
- reason: `impact stamp version ${String(stamp.stampVersion)} is not this build's version 3; ${floor}`,
851
+ if (stamp.stampVersion !== 4) return {
852
+ reason: `impact stamp version ${String(stamp.stampVersion)} is not this build's version 4; ${floor}`,
304
853
  scoped: false
305
854
  };
306
855
  if (stamp.basis !== "target-scoped") return {
@@ -690,14 +1239,6 @@ const DIFF_CLASSIFICATIONS = [
690
1239
  "trivial",
691
1240
  "non-trivial"
692
1241
  ];
693
- const REVIEW_CATEGORIES = [
694
- "correctness",
695
- "safety",
696
- "coordination",
697
- "maintainability",
698
- "unknown"
699
- ];
700
- const REVIEW_RUNGS = ["independentModel", "oracle"];
701
1242
  const REVIEW_STATUS_VALUES = [
702
1243
  "not-required",
703
1244
  "current",
@@ -747,180 +1288,6 @@ const evidenceEnvelopeSchema = z.object({
747
1288
  });
748
1289
  }
749
1290
  });
750
- const PR_REVIEW_SCHEMA_VERSION = 2;
751
- const PR_REVIEW_FINDING_PROVENANCE_VERSION = 1;
752
- const nonBlankString = z.string().refine((value) => value.trim().length > 0, { message: "must not be blank" });
753
- const parseableDateString = z.string().refine((value) => Number.isFinite(Date.parse(value)), { message: "must be a parseable date string" });
754
- const reviewPromptSectionProvenances = ["prior-review-ledger", "issue-review-focus"];
755
- const reviewPromptSectionSchema = z.object({
756
- provenance: z.enum(reviewPromptSectionProvenances),
757
- source: nonBlankString,
758
- text: nonBlankString
759
- }).strict();
760
- 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" });
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
- command: z.string().refine((value) => value.trim().length > 0, { message: "command must not be blank" }).optional(),
780
- durationMs: z.number().refine((value) => Number.isFinite(value) && value >= 0, { message: "durationMs must be a finite number >= 0" }),
781
- endedAt: parseableDateString,
782
- exitCode: z.number().nullable().optional(),
783
- findings: z.array(prReviewFindingSchema),
784
- issuesFlagged: z.number().int().nonnegative(),
785
- kind: z.enum(["correctness", "security"]),
786
- model: nonBlankString.optional(),
787
- outcome: z.enum([
788
- "passed",
789
- "failed",
790
- "error"
791
- ]),
792
- producer: z.string().min(1).optional(),
793
- promptSections: reviewPromptSectionsSchema.optional(),
794
- rung: z.enum(EVIDENCE_REVIEW_RUNGS).optional(),
795
- sessionId: nonBlankString.optional(),
796
- stageResolution: z.object({
797
- effort: z.enum([
798
- "low",
799
- "medium",
800
- "high",
801
- "xhigh"
802
- ]).optional(),
803
- engine: z.string().min(1),
804
- layerSource: z.enum([
805
- "cli",
806
- "operatorSessionGlobal",
807
- "operatorSessionKind",
808
- "profilePin",
809
- "stageConfig",
810
- "userConfig"
811
- ]),
812
- model: z.string().min(1),
813
- rung: z.enum(REVIEW_RUNGS).optional()
814
- }).optional(),
815
- startedAt: parseableDateString,
816
- summary: z.string().refine((value) => value.trim().length > 0 && value !== "<one sentence>", { message: "summary must not be blank or a placeholder" }),
817
- typedVerdict: z.boolean().optional(),
818
- verdictSource: z.enum(["footer", "recovered"]).optional()
819
- }).superRefine((review, context) => {
820
- if (review.outcome === "passed") {
821
- if (review.issuesFlagged !== 0) context.addIssue({
822
- code: "custom",
823
- message: "passed reviews must report zero issues flagged",
824
- path: ["issuesFlagged"]
825
- });
826
- if (review.findings.length !== 0) context.addIssue({
827
- code: "custom",
828
- message: "passed reviews must not include findings",
829
- path: ["findings"]
830
- });
831
- }
832
- if (review.outcome !== "passed") {
833
- if (review.findings.length === 0) context.addIssue({
834
- code: "custom",
835
- message: "non-passing reviews must include findings",
836
- path: ["findings"]
837
- });
838
- if (review.findings.length !== review.issuesFlagged) context.addIssue({
839
- code: "custom",
840
- message: "issuesFlagged must match findings length",
841
- path: ["issuesFlagged"]
842
- });
843
- }
844
- if (review.outcome === "failed" && review.exitCode !== void 0 && review.exitCode !== 0) context.addIssue({
845
- code: "custom",
846
- message: "failed reviews must exit with code 0",
847
- path: ["exitCode"]
848
- });
849
- if (review.producer === void 0 !== (review.rung === void 0)) context.addIssue({
850
- code: "custom",
851
- message: "producer and rung must be recorded together",
852
- path: [review.producer === void 0 ? "producer" : "rung"]
853
- });
854
- });
855
- const ladderFindingSchema = z.object({
856
- citedSpan: z.string().optional(),
857
- file: z.string().optional(),
858
- prescribedAction: z.string().optional(),
859
- supersedes: z.object({
860
- file: z.string().optional(),
861
- title: z.string()
862
- }).optional(),
863
- title: z.string()
864
- }).passthrough();
865
- const ladderDispositionSchema = z.object({
866
- disposition: z.enum([
867
- "fixed-in-thread",
868
- "follow-up-filed",
869
- "waived"
870
- ]),
871
- finding: z.object({
872
- file: z.string().optional(),
873
- title: z.string()
874
- }),
875
- reference: z.string().optional()
876
- }).passthrough();
877
- const ladderCycleSchema = z.object({
878
- dispositions: z.array(ladderDispositionSchema).optional(),
879
- findings: z.array(ladderFindingSchema)
880
- }).passthrough();
881
- const prReviewProofSchema = z.object({
882
- base: z.string(),
883
- changedFiles: z.array(z.string()),
884
- cleanedPaths: z.array(z.string()),
885
- findingProvenanceVersion: z.literal(PR_REVIEW_FINDING_PROVENANCE_VERSION),
886
- headSha: z.string().regex(/^[0-9a-f]{40}$/u),
887
- ladder: z.object({ cycles: z.array(ladderCycleSchema) }).superRefine((ladder, context) => {
888
- let gateStarted = false;
889
- try {
890
- for (const cycle of ladder.cycles) {
891
- const { stage } = cycle;
892
- if (stage === "interior") {
893
- if (gateStarted) throw new TypeError("interior cycle after gate");
894
- } else gateStarted = true;
895
- }
896
- } catch {
897
- context.addIssue({
898
- code: "custom",
899
- message: "ladder cycles do not fold into a valid disposition ledger"
900
- });
901
- }
902
- }).optional(),
903
- maxReviewCycles: z.number().int().positive().optional(),
904
- patchId: z.string().regex(/^[0-9a-f]{40,64}$/u),
905
- reviewCycle: z.number().int().positive().optional(),
906
- reviewRequirement: z.object({
907
- reason: z.enum(["no-applicable-mode", "faithful-merge"]),
908
- status: z.literal("not-required")
909
- }).strict().optional(),
910
- reviews: z.array(prReviewResultSchema),
911
- schemaVersion: z.literal(2)
912
- }).passthrough().superRefine((proof, context) => {
913
- if (proof.reviewRequirement && proof.reviews.length > 0) context.addIssue({
914
- code: "custom",
915
- message: "not-required review proofs must not contain review runs",
916
- path: ["reviews"]
917
- });
918
- if (proof.reviewCycle !== void 0 && proof.maxReviewCycles !== void 0 && proof.reviewCycle > proof.maxReviewCycles) context.addIssue({
919
- code: "custom",
920
- message: "reviewCycle must not exceed maxReviewCycles",
921
- path: ["reviewCycle"]
922
- });
923
- });
924
1291
  const BOUNDARY_REVIEW_PROOF_KIND = "boundary-review-proof";
925
1292
  const coveredSetEntrySchema = z.object({
926
1293
  headSha: evidenceShaSchema.optional(),
@@ -1019,12 +1386,15 @@ const readinessRepairSchema = z.object({
1019
1386
  command: z.string().min(1)
1020
1387
  });
1021
1388
  const reviewStateSchema = z.object({
1022
- docsOnlyDeltaAccepted: z.boolean().optional(),
1023
1389
  required: z.boolean(),
1024
1390
  reviewedHeadSha: z.string().optional(),
1025
1391
  reviewedPatchId: z.string().optional(),
1026
1392
  status: z.enum(REVIEW_STATUS_VALUES)
1393
+ }).strict().superRefine((state, context) => {
1394
+ for (const issue of reviewStateIssues(state)) context.addIssue(issue);
1027
1395
  });
1396
+ const verifyCommandSchema = z.literal("patronage-factory pr:verify");
1397
+ const verifiedShaSchema = z.string().min(1);
1028
1398
  const historicalPostReadinessCommentSchema = z.object({
1029
1399
  author: z.string().min(1),
1030
1400
  createdAt: z.string().min(1),
@@ -1043,7 +1413,6 @@ const managedReadinessLedgerSchema = z.object({
1043
1413
  classification: z.enum(DIFF_CLASSIFICATIONS),
1044
1414
  externalChecks: z.array(z.object({
1045
1415
  checkType: z.enum(EVIDENCE_CHECK_TYPES),
1046
- inScope: z.boolean(),
1047
1416
  name: z.string().min(1),
1048
1417
  reason: z.string().optional(),
1049
1418
  scope: requiredCheckScopeSchema.optional(),
@@ -1146,40 +1515,54 @@ const managedReadinessLedgerSchema = z.object({
1146
1515
  correctness: reviewStateSchema,
1147
1516
  security: reviewStateSchema.optional()
1148
1517
  }),
1149
- schemaVersion: z.literal(1),
1518
+ schemaVersion: z.literal(2),
1150
1519
  stackRole: z.enum([
1151
1520
  "single",
1152
1521
  "slice",
1153
1522
  "rollup",
1154
1523
  "merge-gate prerequisite"
1155
1524
  ]).optional(),
1156
- verification: z.object({
1157
- command: z.literal("patronage-factory pr:verify"),
1158
- docsOnlyDeltaAccepted: z.boolean().optional(),
1159
- docsOnlyVerifiedHeadSha: z.string().optional(),
1160
- prVerify: z.enum([
1161
- "passed",
1162
- "missing",
1163
- "stale"
1164
- ]),
1165
- trivialDeltaAccepted: z.boolean().optional(),
1166
- trivialVerifiedHeadSha: z.string().optional(),
1167
- verifiedHeadSha: z.string().optional()
1168
- })
1525
+ verification: z.discriminatedUnion("prVerify", [
1526
+ z.object({
1527
+ command: verifyCommandSchema,
1528
+ prVerify: z.literal("missing")
1529
+ }).strict(),
1530
+ z.object({
1531
+ command: verifyCommandSchema,
1532
+ prVerify: z.literal("stale"),
1533
+ verifiedHeadSha: verifiedShaSchema
1534
+ }).strict(),
1535
+ z.object({
1536
+ command: verifyCommandSchema,
1537
+ prVerify: z.literal("passed-via-head"),
1538
+ verifiedHeadSha: verifiedShaSchema
1539
+ }).strict(),
1540
+ z.object({
1541
+ command: verifyCommandSchema,
1542
+ docsOnlyVerifiedHeadSha: verifiedShaSchema,
1543
+ prVerify: z.literal("docs-only-delta"),
1544
+ verifiedHeadSha: verifiedShaSchema.optional()
1545
+ }).strict(),
1546
+ z.object({
1547
+ command: verifyCommandSchema,
1548
+ prVerify: z.literal("trivial-delta"),
1549
+ trivialVerifiedHeadSha: verifiedShaSchema,
1550
+ verifiedHeadSha: verifiedShaSchema.optional()
1551
+ }).strict()
1552
+ ])
1169
1553
  });
1170
1554
  const PR_READY_SCHEMA_VERSION = 3;
1171
1555
  /**
1172
- * The pr:ready proof versions a reader still accepts. `pr:ready` emits v3 only
1173
- * (#477) one current contract but v1 and v2 events were spooled before the
1174
- * bumps and HQ must ingest them without degrading, so the wire schema parses
1175
- * all three.
1556
+ * The pr:ready proof versions a reader accepts: the current one only (#917
1557
+ * item 1). #894 narrowed the nested readiness ledger to `schemaVersion: 2` and
1558
+ * the `prVerify` tagged union, which no v1 or v2 pr:ready proof can satisfy.
1559
+ * Advertising 1 and 2 here therefore promised an acceptance the nested schema
1560
+ * already refused. Pre-1.0 posture applies: one current contract, no compat
1561
+ * reader. An alpha.22 consumer must adopt alpha.23 before HQ ingests its
1562
+ * pr:ready proofs.
1176
1563
  */
1177
- const SUPPORTED_PR_READY_SCHEMA_VERSIONS = [
1178
- 1,
1179
- 2,
1180
- 3
1181
- ];
1182
- const prReadySchemaVersionSchema = z.number().refine((value) => SUPPORTED_PR_READY_SCHEMA_VERSIONS.includes(value), { message: `schemaVersion must be one of: ${SUPPORTED_PR_READY_SCHEMA_VERSIONS.join(", ")}` });
1564
+ const SUPPORTED_PR_READY_SCHEMA_VERSIONS = [3];
1565
+ const prReadySchemaVersionSchema = z.literal(3);
1183
1566
  const prReadyProofSchema = z.object({
1184
1567
  arming: z.object({
1185
1568
  detail: z.string().min(1).optional(),
@@ -1209,15 +1592,7 @@ const prReadyProofSchema = z.object({
1209
1592
  ]),
1210
1593
  waivedDemands: z.array(waivedDemandSchema).optional()
1211
1594
  }).superRefine((proof, context) => {
1212
- if (proof.schemaVersion < 2) {
1213
- if (proof.blockedReasons !== void 0) context.addIssue({
1214
- code: "custom",
1215
- message: "blockedReasons is a schemaVersion 2 field; a v1 pr:ready proof must not carry it.",
1216
- path: ["blockedReasons"]
1217
- });
1218
- return;
1219
- }
1220
1595
  for (const issue of blockedReasonIssues({ ...proof })) context.addIssue(issue);
1221
1596
  });
1222
1597
  //#endregion
1223
- 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, prReviewProofSchema, prVerifyProofSchema, waivedDemandSchema };
1598
+ 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 };