@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/CONTEXT.md +10 -0
- package/README.md +15 -5
- package/dist/cli.js +12 -0
- package/dist/index.d.ts +2122 -1762
- package/dist/index.js +15721 -11998
- package/dist/review-prompts/security/SKILL.md +1 -0
- package/dist/review-prompts/security/references/javascript-typescript.md +1 -0
- package/dist/schemas.d.ts +659 -1313
- package/dist/schemas.js +855 -725
- package/package.json +16 -16
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
|
|
5
|
+
const EVIDENCE_REVIEW_RUNGS = [
|
|
6
6
|
"independent-model",
|
|
7
7
|
"oracle",
|
|
8
8
|
"human"
|
|
9
9
|
];
|
|
10
|
-
Object.fromEntries(EVIDENCE_REVIEW_RUNGS
|
|
10
|
+
Object.fromEntries(EVIDENCE_REVIEW_RUNGS.map((rung, index) => [rung, index]));
|
|
11
11
|
//#endregion
|
|
12
12
|
//#region src/demand-keys.ts
|
|
13
13
|
/** The demands that exist at most once per candidate. */
|
|
@@ -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
|
|
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(
|
|
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 !==
|
|
291
|
-
reason: `impact stamp version ${String(stamp.stampVersion)} is not this build's version
|
|
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.
|
|
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"
|
|
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
|
|
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
|
-
*
|
|
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"`
|
|
446
|
-
*
|
|
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" ||
|
|
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.
|
|
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: "
|
|
514
|
-
path: ["
|
|
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
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
const
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
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
|
-
|
|
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
|
|
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)
|
|
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(
|
|
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
|
-
|
|
1049
|
-
const
|
|
1050
|
-
const
|
|
1051
|
-
const
|
|
1052
|
-
const
|
|
1053
|
-
const
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
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
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
protocolFinding: z.literal(true).optional(),
|
|
1073
|
-
supersedes: findingSupersedesSchema.optional(),
|
|
1074
|
-
title: nonBlankString
|
|
1075
|
-
});
|
|
1076
|
-
const prReviewResultSchema = z.object({
|
|
1077
|
-
command: z.string().refine((value) => value.trim().length > 0, { message: "command must not be blank" }).optional(),
|
|
1078
|
-
durationMs: z.number().refine((value) => Number.isFinite(value) && value >= 0, { message: "durationMs must be a finite number >= 0" }),
|
|
1079
|
-
endedAt: parseableDateString,
|
|
1080
|
-
exitCode: z.number().nullable().optional(),
|
|
1081
|
-
findings: z.array(prReviewFindingSchema),
|
|
1082
|
-
issuesFlagged: z.number().int().nonnegative(),
|
|
1083
|
-
kind: z.enum(["correctness", "security"]),
|
|
1084
|
-
model: nonBlankString.optional(),
|
|
1085
|
-
outcome: z.enum([
|
|
1086
|
-
"passed",
|
|
1087
|
-
"failed",
|
|
1088
|
-
"error"
|
|
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
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
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: "
|
|
1127
|
-
path: ["
|
|
1343
|
+
message: "review-type evidence envelope requires a rung (ADR 0014 §3).",
|
|
1344
|
+
path: ["rung"]
|
|
1128
1345
|
});
|
|
1129
|
-
}
|
|
1130
|
-
|
|
1131
|
-
if (review.findings.length === 0) context.addIssue({
|
|
1346
|
+
} else {
|
|
1347
|
+
if (envelope.rung !== void 0) context.addIssue({
|
|
1132
1348
|
code: "custom",
|
|
1133
|
-
message: "
|
|
1134
|
-
path: ["
|
|
1349
|
+
message: "verify-type evidence envelope must not carry rung (review-type-only field, ADR 0014 §3).",
|
|
1350
|
+
path: ["rung"]
|
|
1135
1351
|
});
|
|
1136
|
-
if (
|
|
1352
|
+
if (envelope.model !== void 0) context.addIssue({
|
|
1137
1353
|
code: "custom",
|
|
1138
|
-
message: "
|
|
1139
|
-
path: ["
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
1480
|
+
classification: diffClassificationSchema,
|
|
1342
1481
|
externalChecks: z.array(z.object({
|
|
1343
|
-
checkType:
|
|
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:
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
"interior-takeover"
|
|
1421
|
-
]).optional(),
|
|
1422
|
-
nextAction: z.enum([
|
|
1423
|
-
"run-interior-cycle",
|
|
1424
|
-
"advance-to-gate",
|
|
1425
|
-
"run-gate-cycle",
|
|
1426
|
-
"accept-nonblocking-findings",
|
|
1427
|
-
"escalate-to-triage",
|
|
1428
|
-
"ready-for-human"
|
|
1429
|
-
]),
|
|
1430
|
-
stage: z.enum([
|
|
1431
|
-
"interior",
|
|
1432
|
-
"interior-takeover",
|
|
1433
|
-
"interior-complete",
|
|
1434
|
-
"gate"
|
|
1435
|
-
])
|
|
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(
|
|
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.
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
"stale"
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
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
|
|
1471
|
-
*
|
|
1472
|
-
*
|
|
1473
|
-
*
|
|
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
|
-
|
|
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,
|
|
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 };
|