@patronage/software-factory 1.0.0-alpha.31 → 1.0.0-alpha.32
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/README.md +2 -2
- package/dist/index.d.ts +253 -49
- package/dist/index.js +2430 -945
- package/dist/schemas.d.ts +109 -14
- package/dist/schemas.js +263 -295
- package/package.json +2 -2
package/dist/schemas.js
CHANGED
|
@@ -214,11 +214,6 @@ const blockedReasonIssues = (proof) => {
|
|
|
214
214
|
const drifted = named.findIndex((reason, index) => reason.detail !== blockedReasonDetail(reasons[index]));
|
|
215
215
|
return drifted === -1 ? [] : blockedReasonIssue(`blockedReasons[${drifted}] does not carry blocking reason ${drifted}; the two projections must tell one story`);
|
|
216
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
|
-
};
|
|
222
217
|
//#endregion
|
|
223
218
|
//#region src/demand-waiver.ts
|
|
224
219
|
const DEMAND_WAIVER_SCHEMA_VERSION = 1;
|
|
@@ -333,23 +328,121 @@ const REVIEW_CATEGORIES = [
|
|
|
333
328
|
"unknown"
|
|
334
329
|
];
|
|
335
330
|
//#endregion
|
|
336
|
-
//#region src/
|
|
337
|
-
const
|
|
338
|
-
const
|
|
339
|
-
/**
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
331
|
+
//#region src/review-ladder-ledger.ts
|
|
332
|
+
const OCCURRENCE_ADDRESS_PATTERN = /^c[1-9][0-9]*f[1-9][0-9]*$/u;
|
|
333
|
+
const occurrenceAddress = (cycle, finding) => `c${cycle}f${finding}`;
|
|
334
|
+
/** The 1-based recording position an address names. */
|
|
335
|
+
const cycleOfAddress = (address) => Number(/^c(?<cycle>\d+)f/u.exec(address)?.groups?.cycle);
|
|
336
|
+
const POLICY_DECLARATION_RULES = [
|
|
337
|
+
"cycle-2-severity-floor",
|
|
338
|
+
"identity-repeat",
|
|
339
|
+
"schema-2-import"
|
|
340
|
+
];
|
|
341
|
+
const definedAttributes = (finding) => Object.fromEntries([
|
|
342
|
+
"blockingAfterCap",
|
|
343
|
+
"category",
|
|
344
|
+
"citedSpan",
|
|
345
|
+
"file",
|
|
346
|
+
"prescribedAction",
|
|
347
|
+
"priority"
|
|
348
|
+
].flatMap((attribute) => finding[attribute] === void 0 ? [] : [[attribute, finding[attribute]]]));
|
|
349
|
+
const AUTHORITY_RANK = {
|
|
350
|
+
operator: 3,
|
|
351
|
+
policy: 1,
|
|
352
|
+
reviewer: 2
|
|
353
|
+
};
|
|
354
|
+
const outranked = (occurrence, declaration) => occurrence.authority !== void 0 && AUTHORITY_RANK[occurrence.authority] > AUTHORITY_RANK[declaration.authority];
|
|
355
|
+
const projectedGroupDisposition = (members) => members.some((member) => member.disposition === "open") ? "open" : members.at(-1)?.disposition ?? "open";
|
|
356
|
+
const invalid = (message) => {
|
|
357
|
+
throw new TypeError(`Invalid ladder history: ${message}`);
|
|
358
|
+
};
|
|
359
|
+
const entryFor = (members, reopenedCount) => {
|
|
360
|
+
const [root] = members;
|
|
361
|
+
const latest = members.at(-1);
|
|
362
|
+
if (!root || !latest) return invalid("a finding group has no members.");
|
|
363
|
+
const disposition = projectedGroupDisposition(members);
|
|
364
|
+
const settled = members.findLast((member) => member.disposition !== "stale-repeat" && member.disposition !== "open");
|
|
365
|
+
const referenced = disposition === "open" ? void 0 : settled ?? latest;
|
|
366
|
+
return {
|
|
367
|
+
...definedAttributes(latest.finding),
|
|
368
|
+
address: root.address,
|
|
369
|
+
disposition,
|
|
370
|
+
firstFlagged: root.cycle,
|
|
371
|
+
flagCount: members.length,
|
|
372
|
+
lastFlagged: latest.cycle,
|
|
373
|
+
occurrences: [...members],
|
|
374
|
+
...disposition === "stale-repeat" && settled ? { priorDisposition: settled.disposition } : {},
|
|
375
|
+
...referenced === void 0 ? {} : { reference: referenced.reference ?? referenced.rule },
|
|
376
|
+
reopenedCount,
|
|
377
|
+
staleRepeatCount: members.filter((member) => member.disposition === "stale-repeat").length,
|
|
378
|
+
title: root.finding.title.trim()
|
|
379
|
+
};
|
|
380
|
+
};
|
|
381
|
+
const recordOccurrences = (cycle, cycleIndex, ref, occurrences, recordedIn, groups, reopened) => {
|
|
382
|
+
const { recording } = cycle;
|
|
383
|
+
if (!recording) return invalid(`cycle ${cycleIndex + 1} carries no recording; the ledger reads recorded occurrences and declarations only.`);
|
|
384
|
+
if (recording.occurrences.length !== cycle.findings.length) return invalid(`cycle ${cycleIndex + 1} records ${recording.occurrences.length} occurrence(s) for ${cycle.findings.length} finding(s).`);
|
|
385
|
+
for (const [findingIndex, record] of recording.occurrences.entries()) {
|
|
386
|
+
const address = occurrenceAddress(cycleIndex + 1, findingIndex + 1);
|
|
387
|
+
const finding = cycle.findings[findingIndex];
|
|
388
|
+
if (record.address !== address || finding === void 0) return invalid(`cycle ${cycleIndex + 1} finding ${findingIndex + 1} is recorded at ${record.address}, not ${address}.`);
|
|
389
|
+
let root = address;
|
|
390
|
+
if (record.repeatOf !== void 0) {
|
|
391
|
+
const target = occurrences.get(record.repeatOf);
|
|
392
|
+
if (!target || (recordedIn.get(record.repeatOf) ?? cycleIndex) >= cycleIndex) return invalid(`${address} repeats ${record.repeatOf}, which names no occurrence recorded in an earlier cycle.`);
|
|
393
|
+
({root} = target);
|
|
394
|
+
}
|
|
395
|
+
const members = groups.get(root) ?? [];
|
|
396
|
+
if (members.length > 0 && projectedGroupDisposition(members) === "fixed-in-thread") reopened.set(root, (reopened.get(root) ?? 0) + 1);
|
|
397
|
+
const occurrence = {
|
|
398
|
+
address,
|
|
399
|
+
cycle: ref,
|
|
400
|
+
disposition: "open",
|
|
401
|
+
finding,
|
|
402
|
+
historical: cycle.historical === true,
|
|
403
|
+
root
|
|
404
|
+
};
|
|
405
|
+
occurrences.set(address, occurrence);
|
|
406
|
+
recordedIn.set(address, cycleIndex);
|
|
407
|
+
groups.set(root, [...members, occurrence]);
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
const applyDeclaration = (declaration, cycleIndex, occurrences) => {
|
|
411
|
+
const target = occurrences.get(declaration.target);
|
|
412
|
+
if (!target) return invalid(`cycle ${cycleIndex + 1} declares ${declaration.disposition} on ${declaration.target}, which names no occurrence recorded at or before that cycle.`);
|
|
413
|
+
if (outranked(target, declaration)) return;
|
|
414
|
+
target.disposition = declaration.disposition;
|
|
415
|
+
target.authority = declaration.authority;
|
|
416
|
+
if (declaration.authority === "policy") {
|
|
417
|
+
target.rule = declaration.rule;
|
|
418
|
+
delete target.reference;
|
|
419
|
+
} else {
|
|
420
|
+
delete target.rule;
|
|
421
|
+
if (declaration.reference === void 0) delete target.reference;
|
|
422
|
+
else target.reference = declaration.reference;
|
|
423
|
+
}
|
|
424
|
+
};
|
|
425
|
+
const foldLedger = (cycles, refs) => {
|
|
426
|
+
const occurrences = /* @__PURE__ */ new Map();
|
|
427
|
+
const recordedIn = /* @__PURE__ */ new Map();
|
|
428
|
+
const groups = /* @__PURE__ */ new Map();
|
|
429
|
+
const reopened = /* @__PURE__ */ new Map();
|
|
430
|
+
for (const [cycleIndex, cycle] of cycles.entries()) {
|
|
431
|
+
const ref = refs[cycleIndex];
|
|
432
|
+
if (!ref) return invalid(`cycle ${cycleIndex + 1} has no ladder position.`);
|
|
433
|
+
const declarations = cycle.recording?.declarations ?? [];
|
|
434
|
+
for (const declaration of declarations) if (cycleOfAddress(declaration.target) <= cycleIndex) applyDeclaration(declaration, cycleIndex, occurrences);
|
|
435
|
+
recordOccurrences(cycle, cycleIndex, ref, occurrences, recordedIn, groups, reopened);
|
|
436
|
+
for (const declaration of declarations) if (cycleOfAddress(declaration.target) > cycleIndex) applyDeclaration(declaration, cycleIndex, occurrences);
|
|
437
|
+
}
|
|
438
|
+
const ledger = /* @__PURE__ */ new Map();
|
|
439
|
+
for (const [root, members] of groups) ledger.set(root, entryFor(members, reopened.get(root) ?? 0));
|
|
440
|
+
return {
|
|
441
|
+
ledger,
|
|
442
|
+
occurrences: [...occurrences.values()]
|
|
443
|
+
};
|
|
444
|
+
};
|
|
445
|
+
new Uint32Array([
|
|
353
446
|
1116352408,
|
|
354
447
|
1899447441,
|
|
355
448
|
3049323471,
|
|
@@ -415,7 +508,7 @@ const ROUND_CONSTANTS = new Uint32Array([
|
|
|
415
508
|
3204031479,
|
|
416
509
|
3329325298
|
|
417
510
|
]);
|
|
418
|
-
|
|
511
|
+
new Uint32Array([
|
|
419
512
|
1779033703,
|
|
420
513
|
3144134277,
|
|
421
514
|
1013904242,
|
|
@@ -425,273 +518,6 @@ const INITIAL_STATE = new Uint32Array([
|
|
|
425
518
|
528734635,
|
|
426
519
|
1541459225
|
|
427
520
|
]);
|
|
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
521
|
//#endregion
|
|
696
522
|
//#region src/review-ladder.ts
|
|
697
523
|
const LADDER_NEXT_ACTION_KINDS = [
|
|
@@ -728,21 +554,138 @@ const ladderCycleRefsFor = (cycles) => {
|
|
|
728
554
|
});
|
|
729
555
|
};
|
|
730
556
|
//#endregion
|
|
557
|
+
//#region src/review-recording-schema.ts
|
|
558
|
+
const ABSENT_PROOF_DIGEST = "absent";
|
|
559
|
+
const sha256Pattern = /^[0-9a-f]{64}$/u;
|
|
560
|
+
const occurrenceAddressSchema$1 = z.string().regex(OCCURRENCE_ADDRESS_PATTERN);
|
|
561
|
+
const declarationRecordSchema = z.union([z.object({
|
|
562
|
+
authority: z.enum(["operator", "reviewer"]),
|
|
563
|
+
disposition: z.enum([
|
|
564
|
+
"fixed-in-thread",
|
|
565
|
+
"follow-up-filed",
|
|
566
|
+
"waived"
|
|
567
|
+
]),
|
|
568
|
+
reference: z.string().min(1).optional(),
|
|
569
|
+
target: occurrenceAddressSchema$1
|
|
570
|
+
}).strict(), z.object({
|
|
571
|
+
authority: z.literal("policy"),
|
|
572
|
+
disposition: z.enum(["policy-accepted", "stale-repeat"]),
|
|
573
|
+
rule: z.enum(POLICY_DECLARATION_RULES),
|
|
574
|
+
target: occurrenceAddressSchema$1
|
|
575
|
+
}).strict()]);
|
|
576
|
+
const recordsShape = {
|
|
577
|
+
declarations: z.array(declarationRecordSchema),
|
|
578
|
+
occurrences: z.array(z.object({
|
|
579
|
+
address: occurrenceAddressSchema$1,
|
|
580
|
+
declaredCycle: z.number().int().positive(),
|
|
581
|
+
repeatOf: occurrenceAddressSchema$1.optional()
|
|
582
|
+
}).strict())
|
|
583
|
+
};
|
|
584
|
+
const reviewCycleRecordingSchema = z.union([z.object({
|
|
585
|
+
...recordsShape,
|
|
586
|
+
appliedTo: z.union([z.string().regex(sha256Pattern), z.literal(ABSENT_PROOF_DIGEST)]),
|
|
587
|
+
base: z.object({
|
|
588
|
+
ref: z.string().min(1),
|
|
589
|
+
sha: z.string().regex(/^[0-9a-f]{40}$/u)
|
|
590
|
+
}).strict(),
|
|
591
|
+
dispositionsDigest: z.string().regex(sha256Pattern).optional(),
|
|
592
|
+
findingsDigest: z.string().regex(sha256Pattern),
|
|
593
|
+
recordingId: z.string().regex(sha256Pattern)
|
|
594
|
+
}).strict(), z.object({
|
|
595
|
+
...recordsShape,
|
|
596
|
+
imported: z.object({
|
|
597
|
+
proofDigest: z.string().regex(sha256Pattern),
|
|
598
|
+
schemaVersion: z.literal(2)
|
|
599
|
+
}).strict()
|
|
600
|
+
}).strict()]);
|
|
601
|
+
//#endregion
|
|
602
|
+
//#region src/review-recording.ts
|
|
603
|
+
const occurrenceAddressSchema = z.string().regex(OCCURRENCE_ADDRESS_PATTERN);
|
|
604
|
+
const declarableDisposition = z.enum([
|
|
605
|
+
"fixed-in-thread",
|
|
606
|
+
"follow-up-filed",
|
|
607
|
+
"waived"
|
|
608
|
+
]);
|
|
609
|
+
z.array(z.union([z.object({
|
|
610
|
+
disposition: declarableDisposition,
|
|
611
|
+
reference: z.string().min(1).optional(),
|
|
612
|
+
target: occurrenceAddressSchema
|
|
613
|
+
}).strict(), z.object({
|
|
614
|
+
disposition: declarableDisposition,
|
|
615
|
+
finding: z.object({
|
|
616
|
+
file: z.string().min(1).optional(),
|
|
617
|
+
title: z.string().min(1)
|
|
618
|
+
}).strict(),
|
|
619
|
+
reference: z.string().min(1).optional()
|
|
620
|
+
}).strict()]));
|
|
621
|
+
//#endregion
|
|
622
|
+
//#region src/review-proof-import.ts
|
|
623
|
+
const legacyFindingSchema = z.object({
|
|
624
|
+
blockingAfterCap: z.boolean().optional(),
|
|
625
|
+
category: z.string().optional(),
|
|
626
|
+
citedSpan: z.string().optional(),
|
|
627
|
+
file: z.string().optional(),
|
|
628
|
+
prescribedAction: z.string().optional(),
|
|
629
|
+
priority: z.string().optional(),
|
|
630
|
+
supersedes: z.object({
|
|
631
|
+
file: z.string().optional(),
|
|
632
|
+
title: z.string()
|
|
633
|
+
}).passthrough().optional(),
|
|
634
|
+
title: z.string()
|
|
635
|
+
}).passthrough();
|
|
636
|
+
const legacyDeclarationSchema = z.object({
|
|
637
|
+
disposition: z.enum([
|
|
638
|
+
"fixed-in-thread",
|
|
639
|
+
"follow-up-filed",
|
|
640
|
+
"waived"
|
|
641
|
+
]),
|
|
642
|
+
finding: z.object({
|
|
643
|
+
file: z.string().optional(),
|
|
644
|
+
title: z.string()
|
|
645
|
+
}).passthrough(),
|
|
646
|
+
findingReport: legacyFindingSchema.optional(),
|
|
647
|
+
reference: z.string().optional()
|
|
648
|
+
}).passthrough();
|
|
649
|
+
const legacyCycleSchema = z.object({
|
|
650
|
+
correlatedRerun: z.boolean().optional(),
|
|
651
|
+
dispositions: z.array(legacyDeclarationSchema).optional(),
|
|
652
|
+
engine: z.string().optional(),
|
|
653
|
+
findings: z.array(legacyFindingSchema),
|
|
654
|
+
historical: z.literal(true).optional(),
|
|
655
|
+
identity: z.object({
|
|
656
|
+
headSha: z.string(),
|
|
657
|
+
patchId: z.string()
|
|
658
|
+
}).passthrough().optional(),
|
|
659
|
+
model: z.string().optional(),
|
|
660
|
+
session: z.record(z.string(), z.unknown()).optional(),
|
|
661
|
+
stage: z.enum(["interior", "gate"]),
|
|
662
|
+
usage: z.record(z.string(), z.unknown()).optional()
|
|
663
|
+
}).passthrough();
|
|
664
|
+
z.object({ cycles: z.array(legacyCycleSchema) }).passthrough();
|
|
665
|
+
//#endregion
|
|
731
666
|
//#region src/pr-readiness/ladder-proof-state.ts
|
|
732
|
-
const
|
|
667
|
+
const persistedCycleSchema = z.custom((cycle) => typeof cycle === "object" && cycle !== null, "ladder cycle must be an object").superRefine((cycle, context) => {
|
|
668
|
+
const parsed = reviewCycleRecordingSchema.safeParse(cycle.recording);
|
|
669
|
+
if (!parsed.success) context.addIssue({
|
|
670
|
+
code: "custom",
|
|
671
|
+
message: `ladder cycle carries no valid recording (schema 3): ${parsed.error.issues.map((issue) => `${issue.path.join(".")}: ${issue.message}`).join("; ")}`,
|
|
672
|
+
path: ["recording"]
|
|
673
|
+
});
|
|
674
|
+
});
|
|
675
|
+
const prReviewLadderStateSchema = z.object({ cycles: z.array(persistedCycleSchema) }).superRefine((ladder, context) => {
|
|
733
676
|
try {
|
|
734
677
|
ladderLedgerFor(ladder);
|
|
735
|
-
} catch {
|
|
678
|
+
} catch (error) {
|
|
736
679
|
context.addIssue({
|
|
737
680
|
code: "custom",
|
|
738
|
-
message:
|
|
681
|
+
message: `ladder cycles do not fold into a valid disposition ledger: ${error instanceof Error ? error.message : String(error)}`
|
|
739
682
|
});
|
|
740
683
|
}
|
|
741
684
|
});
|
|
742
685
|
const ladderLedgerFor = (ladder) => [...foldLedger(ladder.cycles, ladderCycleRefsFor(ladder.cycles)).ledger.values()];
|
|
743
686
|
//#endregion
|
|
744
687
|
//#region src/pr-readiness/review-proof.ts
|
|
745
|
-
const PR_REVIEW_SCHEMA_VERSION =
|
|
688
|
+
const PR_REVIEW_SCHEMA_VERSION = 3;
|
|
746
689
|
const nonBlankString = z.string().refine((value) => value.trim().length > 0, { message: "must not be blank" });
|
|
747
690
|
const parseableDateString = z.string().refine((value) => Number.isFinite(Date.parse(value)), { message: "must be a parseable date string" });
|
|
748
691
|
const findingSupersedesSchema = z.object({
|
|
@@ -819,18 +762,42 @@ const prReviewProofSchema = z.object({
|
|
|
819
762
|
cleanedPaths: z.array(z.string()),
|
|
820
763
|
findingProvenanceVersion: z.literal(1),
|
|
821
764
|
headSha: z.string().regex(/^[0-9a-f]{40}$/u),
|
|
765
|
+
importedFrom: z.object({
|
|
766
|
+
proofDigest: z.string().regex(/^[0-9a-f]{64}$/u),
|
|
767
|
+
schemaVersion: z.literal(2)
|
|
768
|
+
}).strict().optional(),
|
|
822
769
|
ladder: prReviewLadderStateSchema.optional(),
|
|
823
770
|
maxReviewCycles: z.number().int().positive().optional(),
|
|
824
771
|
patchId: z.string().regex(/^[0-9a-f]{40,64}$/u),
|
|
825
772
|
policyResolution: policyResolutionSchema.optional(),
|
|
773
|
+
recoveryRequired: z.array(z.object({
|
|
774
|
+
candidates: z.array(z.string().regex(OCCURRENCE_ADDRESS_PATTERN)),
|
|
775
|
+
cycle: z.number().int().positive(),
|
|
776
|
+
declaration: z.object({
|
|
777
|
+
disposition: z.enum([
|
|
778
|
+
"fixed-in-thread",
|
|
779
|
+
"follow-up-filed",
|
|
780
|
+
"waived"
|
|
781
|
+
]),
|
|
782
|
+
file: z.string().optional(),
|
|
783
|
+
reference: z.string().optional(),
|
|
784
|
+
title: z.string()
|
|
785
|
+
}).strict(),
|
|
786
|
+
reason: z.string().min(1)
|
|
787
|
+
}).strict()).optional(),
|
|
826
788
|
reviewCycle: z.number().int().positive().optional(),
|
|
827
789
|
reviewRequirement: z.object({
|
|
828
790
|
reason: z.enum(["no-applicable-mode", "faithful-merge"]),
|
|
829
791
|
status: z.literal("not-required")
|
|
830
792
|
}).strict().optional(),
|
|
831
793
|
reviews: z.array(prReviewResultSchema),
|
|
832
|
-
schemaVersion: z.literal(
|
|
794
|
+
schemaVersion: z.literal(3)
|
|
833
795
|
}).passthrough().superRefine((proof, context) => {
|
|
796
|
+
if (proof.importedFrom !== void 0 && proof.recoveryRequired === void 0) context.addIssue({
|
|
797
|
+
code: "custom",
|
|
798
|
+
message: "a proof converted from schema 2 (importedFrom) must carry recoveryRequired, an array that may be empty",
|
|
799
|
+
path: ["recoveryRequired"]
|
|
800
|
+
});
|
|
834
801
|
if (proof.reviewRequirement && proof.reviews.length > 0) context.addIssue({
|
|
835
802
|
code: "custom",
|
|
836
803
|
message: "not-required review proofs must not contain review runs",
|
|
@@ -1209,6 +1176,7 @@ const prVerifySchemaVersionSchema = z.literal(4);
|
|
|
1209
1176
|
const prVerifyProofSchema = z.object({
|
|
1210
1177
|
authoringSession: z.string().trim().min(1),
|
|
1211
1178
|
base: z.string().min(1),
|
|
1179
|
+
baseSha: z.string().regex(/^[0-9a-f]{40}$/u).optional(),
|
|
1212
1180
|
baselineFullProofs: z.array(z.object({
|
|
1213
1181
|
base: z.string().min(1),
|
|
1214
1182
|
changedFiles: z.array(z.string()),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@patronage/software-factory",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.32",
|
|
4
4
|
"description": "Shared Patronage software factory CLI and project-profile validation tools",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"picomatch": "^4.0.5",
|
|
38
38
|
"yaml": "^2.9.0",
|
|
39
39
|
"zod": "4.4.3",
|
|
40
|
-
"@patronage/factory-ci": "1.0.0-alpha.
|
|
40
|
+
"@patronage/factory-ci": "1.0.0-alpha.32"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@types/node": "24.13.3",
|