@patronage/software-factory 0.30.0 → 1.0.0-alpha.0
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 +1 -1
- package/dist/index.d.ts +317 -7
- package/dist/index.js +919 -52
- package/dist/schemas.d.ts +73 -0
- package/dist/schemas.js +298 -12
- package/package.json +1 -1
package/dist/schemas.d.ts
CHANGED
|
@@ -484,6 +484,31 @@ interface PrReadyProof {
|
|
|
484
484
|
waivedDemands?: WaivedDemand[];
|
|
485
485
|
}
|
|
486
486
|
//#endregion
|
|
487
|
+
//#region src/impact-stamp.d.ts
|
|
488
|
+
/**
|
|
489
|
+
* The recorded stamp. `targets` satisfies the completeness invariant: every
|
|
490
|
+
* target the profile declares is present, each with its exact basis — never
|
|
491
|
+
* silently absent. `basis: "conservative"` means an unmodelled or unprovable
|
|
492
|
+
* input widened every target to full impact.
|
|
493
|
+
*/
|
|
494
|
+
declare const impactStampSchema: z.ZodObject<{
|
|
495
|
+
basis: z.ZodEnum<{
|
|
496
|
+
"lockfile-scoped": "lockfile-scoped";
|
|
497
|
+
conservative: "conservative";
|
|
498
|
+
}>;
|
|
499
|
+
reasons: z.ZodArray<z.ZodString>;
|
|
500
|
+
stampVersion: z.ZodLiteral<1>;
|
|
501
|
+
targets: z.ZodArray<z.ZodObject<{
|
|
502
|
+
basis: z.ZodString;
|
|
503
|
+
impact: z.ZodEnum<{
|
|
504
|
+
affected: "affected";
|
|
505
|
+
"not-affected": "not-affected";
|
|
506
|
+
}>;
|
|
507
|
+
name: z.ZodString;
|
|
508
|
+
}, z.core.$strip>>;
|
|
509
|
+
}, z.core.$strip>;
|
|
510
|
+
type ImpactStamp = z.infer<typeof impactStampSchema>;
|
|
511
|
+
//#endregion
|
|
487
512
|
//#region src/pr-readiness/external-evidence.d.ts
|
|
488
513
|
declare const evidenceEnvelopeBaseSchema: z.ZodObject<{
|
|
489
514
|
check: z.ZodString;
|
|
@@ -520,6 +545,15 @@ type PrVerifySchemaVersion = (typeof SUPPORTED_PR_VERIFY_SCHEMA_VERSIONS$1)[numb
|
|
|
520
545
|
interface PrVerifyCommandResult {
|
|
521
546
|
command: string;
|
|
522
547
|
description: string;
|
|
548
|
+
/**
|
|
549
|
+
* Epic #550 wave 3 (#430): the profile's declared impact target for this
|
|
550
|
+
* command, recorded from v4 on. It is the proof's own command→target
|
|
551
|
+
* mapping: a `notRequiredCommands` entry must name the target recorded HERE
|
|
552
|
+
* for the same command, so a withholding cannot point at whichever released
|
|
553
|
+
* target happens to be convenient. Absent for a command the profile does
|
|
554
|
+
* not scope, and for v1–v3 proofs, which predate the field.
|
|
555
|
+
*/
|
|
556
|
+
impactTarget?: string;
|
|
523
557
|
name: string;
|
|
524
558
|
scope: "always" | "docs-only" | "trivial" | "full";
|
|
525
559
|
}
|
|
@@ -534,6 +568,21 @@ interface PrVerifyExecutedCommand {
|
|
|
534
568
|
name: string;
|
|
535
569
|
scope: "always" | "docs-only" | "trivial" | "full";
|
|
536
570
|
}
|
|
571
|
+
/**
|
|
572
|
+
* Epic #550 wave 3 (#430): a verification command the impact stamp proved this
|
|
573
|
+
* candidate cannot reach, so the battery did not run it.
|
|
574
|
+
*
|
|
575
|
+
* The completeness invariant lives in the pairing: `verificationCommands`
|
|
576
|
+
* records every command the resolved mode selected, `executedCommands` records
|
|
577
|
+
* what ran, and this list records the rest WITH the stamp's own basis. A
|
|
578
|
+
* withheld stack is therefore never silently absent from the proof — it is
|
|
579
|
+
* present, named, and explained.
|
|
580
|
+
*/
|
|
581
|
+
interface PrVerifyNotRequiredCommand {
|
|
582
|
+
basis: string;
|
|
583
|
+
impactTarget: string;
|
|
584
|
+
name: string;
|
|
585
|
+
}
|
|
537
586
|
interface PrVerifyProof {
|
|
538
587
|
schemaVersion: PrVerifySchemaVersion;
|
|
539
588
|
authoringSession?: string;
|
|
@@ -554,8 +603,10 @@ interface PrVerifyProof {
|
|
|
554
603
|
endedAt: string;
|
|
555
604
|
executedCommands?: PrVerifyExecutedCommand[];
|
|
556
605
|
headSha: string;
|
|
606
|
+
impactStamp?: ImpactStamp;
|
|
557
607
|
mode: ResolvedPrVerifyMode;
|
|
558
608
|
mergeBaseSha?: string;
|
|
609
|
+
notRequiredCommands?: PrVerifyNotRequiredCommand[];
|
|
559
610
|
outcome?: "aborted" | "passed";
|
|
560
611
|
patchId?: string;
|
|
561
612
|
profilePath: string;
|
|
@@ -1309,12 +1360,33 @@ declare const prVerifyProofSchema: z.ZodObject<{
|
|
|
1309
1360
|
}>;
|
|
1310
1361
|
}, z.core.$strip>>>;
|
|
1311
1362
|
headSha: z.ZodString;
|
|
1363
|
+
impactStamp: z.ZodOptional<z.ZodObject<{
|
|
1364
|
+
basis: z.ZodEnum<{
|
|
1365
|
+
"lockfile-scoped": "lockfile-scoped";
|
|
1366
|
+
conservative: "conservative";
|
|
1367
|
+
}>;
|
|
1368
|
+
reasons: z.ZodArray<z.ZodString>;
|
|
1369
|
+
stampVersion: z.ZodLiteral<1>;
|
|
1370
|
+
targets: z.ZodArray<z.ZodObject<{
|
|
1371
|
+
basis: z.ZodString;
|
|
1372
|
+
impact: z.ZodEnum<{
|
|
1373
|
+
affected: "affected";
|
|
1374
|
+
"not-affected": "not-affected";
|
|
1375
|
+
}>;
|
|
1376
|
+
name: z.ZodString;
|
|
1377
|
+
}, z.core.$strip>>;
|
|
1378
|
+
}, z.core.$strip>>;
|
|
1312
1379
|
mergeBaseSha: z.ZodOptional<z.ZodString>;
|
|
1313
1380
|
mode: z.ZodEnum<{
|
|
1314
1381
|
trivial: "trivial";
|
|
1315
1382
|
"docs-only": "docs-only";
|
|
1316
1383
|
full: "full";
|
|
1317
1384
|
}>;
|
|
1385
|
+
notRequiredCommands: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
1386
|
+
basis: z.ZodString;
|
|
1387
|
+
impactTarget: z.ZodString;
|
|
1388
|
+
name: z.ZodString;
|
|
1389
|
+
}, z.core.$strip>>>;
|
|
1318
1390
|
outcome: z.ZodOptional<z.ZodEnum<{
|
|
1319
1391
|
aborted: "aborted";
|
|
1320
1392
|
passed: "passed";
|
|
@@ -1328,6 +1400,7 @@ declare const prVerifyProofSchema: z.ZodObject<{
|
|
|
1328
1400
|
verificationCommands: z.ZodArray<z.ZodObject<{
|
|
1329
1401
|
command: z.ZodString;
|
|
1330
1402
|
description: z.ZodString;
|
|
1403
|
+
impactTarget: z.ZodOptional<z.ZodString>;
|
|
1331
1404
|
name: z.ZodString;
|
|
1332
1405
|
scope: z.ZodEnum<{
|
|
1333
1406
|
trivial: "trivial";
|
package/dist/schemas.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import "yaml";
|
|
2
3
|
//#region src/review-rungs.ts
|
|
3
4
|
const EVIDENCE_REVIEW_RUNGS$1 = [
|
|
4
5
|
"independent-model",
|
|
@@ -226,6 +227,294 @@ const waivedDemandSchema = z.object({
|
|
|
226
227
|
session: z.string().trim().min(1).optional(),
|
|
227
228
|
unmetReasons: z.array(z.string().min(1)).min(1)
|
|
228
229
|
});
|
|
230
|
+
const impactStampTargetSchema = z.object({
|
|
231
|
+
basis: z.string().min(1),
|
|
232
|
+
impact: z.enum(["affected", "not-affected"]),
|
|
233
|
+
name: z.string().min(1)
|
|
234
|
+
});
|
|
235
|
+
/**
|
|
236
|
+
* The recorded stamp. `targets` satisfies the completeness invariant: every
|
|
237
|
+
* target the profile declares is present, each with its exact basis — never
|
|
238
|
+
* silently absent. `basis: "conservative"` means an unmodelled or unprovable
|
|
239
|
+
* input widened every target to full impact.
|
|
240
|
+
*/
|
|
241
|
+
const impactStampSchema = z.object({
|
|
242
|
+
basis: z.enum(["lockfile-scoped", "conservative"]),
|
|
243
|
+
reasons: z.array(z.string()),
|
|
244
|
+
stampVersion: z.literal(1),
|
|
245
|
+
targets: z.array(impactStampTargetSchema).superRefine((targets, context) => {
|
|
246
|
+
const names = targets.map((target) => target.name);
|
|
247
|
+
if (new Set(names).size !== names.length) context.addIssue({
|
|
248
|
+
code: "custom",
|
|
249
|
+
message: "impact stamp targets must have distinct names."
|
|
250
|
+
});
|
|
251
|
+
})
|
|
252
|
+
});
|
|
253
|
+
/**
|
|
254
|
+
* The surfaces that consume the stamp. One classification, consumed
|
|
255
|
+
* everywhere (#430): every surface routes through
|
|
256
|
+
* {@link impactStampScopeDecision}, so no surface can be more trusting than
|
|
257
|
+
* another, and no surface re-derives path rules of its own. The value only
|
|
258
|
+
* selects the wording of the conservative floor a refusal falls back to.
|
|
259
|
+
*/
|
|
260
|
+
const IMPACT_SCOPE_SURFACES = {
|
|
261
|
+
demand: "full demand floor",
|
|
262
|
+
"preview-lifecycle": "full preview-lifecycle floor",
|
|
263
|
+
"verification-battery": "full verification-battery floor"
|
|
264
|
+
};
|
|
265
|
+
/**
|
|
266
|
+
* Whether a trusted stamp RELEASES one target-scoped unit of work — an
|
|
267
|
+
* external proof demand (#542 wave 2), a verification-battery command, or a
|
|
268
|
+
* preview (Alchemy) lifecycle stack (#430 wave 3).
|
|
269
|
+
*
|
|
270
|
+
* The only release path is a `lockfile-scoped` stamp of exactly this build's
|
|
271
|
+
* stamp version whose target entry for exactly this name is provably
|
|
272
|
+
* `not-affected`. Every other input — no trusted stamp, an unknown stamp
|
|
273
|
+
* version, a conservative stamp, a name the stamp does not classify, or an
|
|
274
|
+
* `affected` verdict — keeps today's full-work floor. Work only ever gets
|
|
275
|
+
* released, never added or satisfied: this function can withdraw a demand, it
|
|
276
|
+
* can never certify one.
|
|
277
|
+
*
|
|
278
|
+
* Identity binding is the CALLER's precondition: pass only a stamp computed
|
|
279
|
+
* for, or read from a pr:verify proof bound to, the current candidate (see
|
|
280
|
+
* `trustedImpactStamp` in `pr-readiness/verification-proof.ts`); pass
|
|
281
|
+
* `undefined` otherwise.
|
|
282
|
+
*/
|
|
283
|
+
const impactStampScopeDecision = ({ stamp, surface, targetName }) => {
|
|
284
|
+
const floor = IMPACT_SCOPE_SURFACES[surface];
|
|
285
|
+
if (stamp === void 0) return {
|
|
286
|
+
reason: `no trusted identity-bound impact stamp covers this candidate; ${floor}`,
|
|
287
|
+
scoped: false
|
|
288
|
+
};
|
|
289
|
+
if (stamp.stampVersion !== 1) return {
|
|
290
|
+
reason: `impact stamp version ${String(stamp.stampVersion)} is not this build's version 1; ${floor}`,
|
|
291
|
+
scoped: false
|
|
292
|
+
};
|
|
293
|
+
if (stamp.basis !== "lockfile-scoped") return {
|
|
294
|
+
reason: `impact stamp is conservative (full impact assumed); ${floor}`,
|
|
295
|
+
scoped: false
|
|
296
|
+
};
|
|
297
|
+
const matches = stamp.targets.filter((candidate) => candidate.name === targetName);
|
|
298
|
+
if (matches.length > 1) return {
|
|
299
|
+
reason: `impact stamp is self-contradictory: ${matches.length} rows classify a target named "${targetName}"; ${floor}`,
|
|
300
|
+
scoped: false
|
|
301
|
+
};
|
|
302
|
+
const [target] = matches;
|
|
303
|
+
if (target === void 0) return {
|
|
304
|
+
reason: `impact stamp does not classify a target named "${targetName}"; ${floor}`,
|
|
305
|
+
scoped: false
|
|
306
|
+
};
|
|
307
|
+
if (target.impact !== "not-affected") return {
|
|
308
|
+
reason: `impact stamp records target "${targetName}" as affected (${target.basis})`,
|
|
309
|
+
scoped: false
|
|
310
|
+
};
|
|
311
|
+
return {
|
|
312
|
+
reason: `impact stamp released: target "${targetName}" is provably not affected by this candidate's delta (${target.basis})`,
|
|
313
|
+
scoped: true
|
|
314
|
+
};
|
|
315
|
+
};
|
|
316
|
+
const importerDependencySchema = z.looseObject({
|
|
317
|
+
specifier: z.string().optional(),
|
|
318
|
+
version: z.string()
|
|
319
|
+
});
|
|
320
|
+
const importerSectionSchema = z.looseObject({
|
|
321
|
+
dependencies: z.record(z.string(), importerDependencySchema).optional(),
|
|
322
|
+
devDependencies: z.record(z.string(), importerDependencySchema).optional(),
|
|
323
|
+
optionalDependencies: z.record(z.string(), importerDependencySchema).optional()
|
|
324
|
+
});
|
|
325
|
+
const snapshotSectionSchema = z.looseObject({
|
|
326
|
+
dependencies: z.record(z.string(), z.string()).optional(),
|
|
327
|
+
devDependencies: z.record(z.string(), z.string()).optional(),
|
|
328
|
+
optionalDependencies: z.record(z.string(), z.string()).optional()
|
|
329
|
+
});
|
|
330
|
+
z.looseObject({
|
|
331
|
+
importers: z.record(z.string(), importerSectionSchema),
|
|
332
|
+
lockfileVersion: z.unknown().optional(),
|
|
333
|
+
packages: z.record(z.string(), z.unknown()).optional(),
|
|
334
|
+
snapshots: z.record(z.string(), snapshotSectionSchema).optional()
|
|
335
|
+
});
|
|
336
|
+
//#endregion
|
|
337
|
+
//#region src/verification-battery.ts
|
|
338
|
+
/**
|
|
339
|
+
* The completeness invariant as an enforceable control (ADR 0024: controls
|
|
340
|
+
* fail loudly), shared by both pr:verify proof schemas so they cannot drift.
|
|
341
|
+
*
|
|
342
|
+
* Four rules:
|
|
343
|
+
* 1. A withheld command names one the resolved mode actually selected.
|
|
344
|
+
* 2. A command is never recorded as both executed and not-required.
|
|
345
|
+
* 3. Withheld commands are distinct — a duplicated disposition would let one
|
|
346
|
+
* name carry two different bases.
|
|
347
|
+
* 4. On a PASSED proof, every selected command has a disposition: silence is
|
|
348
|
+
* not a disposition, and "absent from both lists" is exactly the silent
|
|
349
|
+
* skip this epic forbids.
|
|
350
|
+
*
|
|
351
|
+
* Rule 4 is scoped to `outcome: "passed"` because an ABORTED proof legitimately
|
|
352
|
+
* records partial execution — the run stopped at the failing command. It is
|
|
353
|
+
* also scoped to schemaVersion >= 4: `executedCommands` postdates v1–v3, so a
|
|
354
|
+
* legacy proof that never recorded it is not making a false completeness claim.
|
|
355
|
+
*
|
|
356
|
+
* The check is one-directional on purpose. Every SELECTED command must be
|
|
357
|
+
* accounted for, but `executedCommands` may legitimately carry MORE than the
|
|
358
|
+
* profile selected — full mode appends the `workspace:install-resolves` probe,
|
|
359
|
+
* which is real work no profile declares.
|
|
360
|
+
*/
|
|
361
|
+
const assertBatteryCompleteness = (proof, context) => {
|
|
362
|
+
const selected = proof.verificationCommands.map((command) => command.name);
|
|
363
|
+
const selectedSet = new Set(selected);
|
|
364
|
+
const executed = new Set((proof.executedCommands ?? []).map((command) => command.name));
|
|
365
|
+
const notRequired = proof.notRequiredCommands ?? [];
|
|
366
|
+
const seen = /* @__PURE__ */ new Set();
|
|
367
|
+
for (const [index, entry] of notRequired.entries()) {
|
|
368
|
+
if (!selectedSet.has(entry.name)) context.addIssue({
|
|
369
|
+
code: "custom",
|
|
370
|
+
message: `notRequiredCommands entry "${entry.name}" is not one of this proof's verificationCommands.`,
|
|
371
|
+
path: [
|
|
372
|
+
"notRequiredCommands",
|
|
373
|
+
index,
|
|
374
|
+
"name"
|
|
375
|
+
]
|
|
376
|
+
});
|
|
377
|
+
if (executed.has(entry.name)) context.addIssue({
|
|
378
|
+
code: "custom",
|
|
379
|
+
message: `Command "${entry.name}" is recorded both as executed and as not-required.`,
|
|
380
|
+
path: [
|
|
381
|
+
"notRequiredCommands",
|
|
382
|
+
index,
|
|
383
|
+
"name"
|
|
384
|
+
]
|
|
385
|
+
});
|
|
386
|
+
if (seen.has(entry.name)) context.addIssue({
|
|
387
|
+
code: "custom",
|
|
388
|
+
message: `notRequiredCommands records "${entry.name}" more than once.`,
|
|
389
|
+
path: [
|
|
390
|
+
"notRequiredCommands",
|
|
391
|
+
index,
|
|
392
|
+
"name"
|
|
393
|
+
]
|
|
394
|
+
});
|
|
395
|
+
seen.add(entry.name);
|
|
396
|
+
}
|
|
397
|
+
if (proof.outcome !== "passed" || proof.schemaVersion < 4) return;
|
|
398
|
+
const disposed = new Set([...executed, ...seen]);
|
|
399
|
+
const undisposed = selected.filter((name) => !disposed.has(name));
|
|
400
|
+
if (undisposed.length > 0) context.addIssue({
|
|
401
|
+
code: "custom",
|
|
402
|
+
message: `A passed pr:verify proof must give every selected verification command a disposition; [${undisposed.join(", ")}] appear in verificationCommands but in neither executedCommands nor notRequiredCommands.`,
|
|
403
|
+
path: ["executedCommands"]
|
|
404
|
+
});
|
|
405
|
+
};
|
|
406
|
+
/**
|
|
407
|
+
* The stamp-authorization control: a passed v4 proof may only claim a command
|
|
408
|
+
* was NOT REQUIRED if its own recorded stamp says so.
|
|
409
|
+
*
|
|
410
|
+
* `assertBatteryCompleteness` proves the two lists partition the selected
|
|
411
|
+
* commands — that no command is silently absent. It does not prove the
|
|
412
|
+
* withholding was EARNED. Without this rule a proof could name every expensive
|
|
413
|
+
* command in `notRequiredCommands`, carry no stamp at all (or a conservative
|
|
414
|
+
* one), and still read as full verification: the omission would be recorded,
|
|
415
|
+
* accounted for, and completely unauthorized.
|
|
416
|
+
*
|
|
417
|
+
* Two things have to hold, and the first is what keeps the second honest:
|
|
418
|
+
*
|
|
419
|
+
* 1. COHERENCE. The entry's `impactTarget` must equal the `impactTarget` the
|
|
420
|
+
* proof records for that same command in `verificationCommands`. The entry
|
|
421
|
+
* does not get to nominate its own target; the proof's command→target
|
|
422
|
+
* mapping (written by `pr:verify` from the loaded profile) does. Without
|
|
423
|
+
* this, authorization validates a self-reported field and a crafted proof
|
|
424
|
+
* can withhold an affected command while pointing at an unrelated released
|
|
425
|
+
* target.
|
|
426
|
+
* 2. AUTHORIZATION. That recorded target must be one the proof's own stamp
|
|
427
|
+
* provably released.
|
|
428
|
+
*
|
|
429
|
+
* The proof is one artifact and a determined forger controls all of it; this
|
|
430
|
+
* is internal-coherence belt-and-braces in the same trust domain as the
|
|
431
|
+
* version guards, and write-time truth stays `pr:verify`'s job.
|
|
432
|
+
*
|
|
433
|
+
* The authorizing evidence is the proof's OWN `impactStamp` — the same stamp
|
|
434
|
+
* `pr:verify` computed for this candidate's identity triple and recorded here,
|
|
435
|
+
* so authorization is bound to the same identities the proof binds. The
|
|
436
|
+
* predicate is the shared {@link impactStampScopeDecision}, not a second
|
|
437
|
+
* reading of the stamp: an entry is authorized exactly when the stamp would
|
|
438
|
+
* have released that target's command in the first place (lockfile-scoped
|
|
439
|
+
* basis, this build's stamp version, exactly one row for the target,
|
|
440
|
+
* `not-affected`). A conservative stamp, an unknown version, a
|
|
441
|
+
* self-contradictory stamp, an unclassified name, or an `affected` verdict all
|
|
442
|
+
* fail the same way scoping itself would.
|
|
443
|
+
*
|
|
444
|
+
* Scoped to `outcome: "passed"` and schemaVersion >= 4 for the same reasons as
|
|
445
|
+
* the completeness rule: an aborted run records partial execution, and a
|
|
446
|
+
* legacy proof predates both fields, so neither is making a false completeness
|
|
447
|
+
* claim.
|
|
448
|
+
*/
|
|
449
|
+
const assertNotRequiredStampAuthorization = (proof, context) => {
|
|
450
|
+
const notRequired = proof.notRequiredCommands ?? [];
|
|
451
|
+
if (proof.outcome !== "passed" || proof.schemaVersion < 4 || notRequired.length === 0) return;
|
|
452
|
+
const { impactStamp } = proof;
|
|
453
|
+
if (impactStamp === void 0) {
|
|
454
|
+
context.addIssue({
|
|
455
|
+
code: "custom",
|
|
456
|
+
message: `A passed pr:verify proof that withholds verification commands must record the impact stamp that authorized the withholding; [${notRequired.map((entry) => entry.name).join(", ")}] are recorded not-required by a proof carrying no impactStamp.`,
|
|
457
|
+
path: ["impactStamp"]
|
|
458
|
+
});
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
const recordedTargets = new Map(proof.verificationCommands.map((command) => [command.name, command.impactTarget]));
|
|
462
|
+
for (const [index, entry] of notRequired.entries()) {
|
|
463
|
+
const recorded = recordedTargets.get(entry.name);
|
|
464
|
+
if (recorded !== entry.impactTarget) {
|
|
465
|
+
context.addIssue({
|
|
466
|
+
code: "custom",
|
|
467
|
+
message: recorded === void 0 ? `notRequiredCommands entry "${entry.name}" claims impactTarget "${entry.impactTarget}", but this proof records no impactTarget for that command; only a command the profile scopes can be withheld.` : `notRequiredCommands entry "${entry.name}" claims impactTarget "${entry.impactTarget}", but this proof records impactTarget "${recorded}" for that command.`,
|
|
468
|
+
path: [
|
|
469
|
+
"notRequiredCommands",
|
|
470
|
+
index,
|
|
471
|
+
"impactTarget"
|
|
472
|
+
]
|
|
473
|
+
});
|
|
474
|
+
continue;
|
|
475
|
+
}
|
|
476
|
+
const decision = impactStampScopeDecision({
|
|
477
|
+
stamp: impactStamp,
|
|
478
|
+
surface: "verification-battery",
|
|
479
|
+
targetName: entry.impactTarget
|
|
480
|
+
});
|
|
481
|
+
if (!decision.scoped) context.addIssue({
|
|
482
|
+
code: "custom",
|
|
483
|
+
message: `notRequiredCommands entry "${entry.name}" is not authorized by this proof's impactStamp: ${decision.reason}.`,
|
|
484
|
+
path: [
|
|
485
|
+
"notRequiredCommands",
|
|
486
|
+
index,
|
|
487
|
+
"impactTarget"
|
|
488
|
+
]
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
};
|
|
492
|
+
//#endregion
|
|
493
|
+
//#region src/pr-verify-proof-rules.ts
|
|
494
|
+
const assertPrVerifyProofRules = (proof, context) => {
|
|
495
|
+
if (proof.schemaVersion >= 4 && proof.authoringSession === void 0) context.addIssue({
|
|
496
|
+
code: "custom",
|
|
497
|
+
message: "schemaVersion 4 pr:verify proof requires authoringSession (the recorded authoring identity, or the `unknown` sentinel).",
|
|
498
|
+
path: ["authoringSession"]
|
|
499
|
+
});
|
|
500
|
+
if (proof.schemaVersion < 4 && proof.authoringSession !== void 0) context.addIssue({
|
|
501
|
+
code: "custom",
|
|
502
|
+
message: "authoringSession is a schemaVersion>=4 field; a v1–v3 pr:verify proof must not carry it.",
|
|
503
|
+
path: ["authoringSession"]
|
|
504
|
+
});
|
|
505
|
+
if (proof.schemaVersion < 4 && proof.impactStamp !== void 0) context.addIssue({
|
|
506
|
+
code: "custom",
|
|
507
|
+
message: "impactStamp is a schemaVersion>=4 field; a v1–v3 pr:verify proof must not carry it.",
|
|
508
|
+
path: ["impactStamp"]
|
|
509
|
+
});
|
|
510
|
+
if (proof.schemaVersion < 4 && proof.notRequiredCommands !== void 0) context.addIssue({
|
|
511
|
+
code: "custom",
|
|
512
|
+
message: "notRequiredCommands is a schemaVersion>=4 field; a v1–v3 pr:verify proof must not carry it.",
|
|
513
|
+
path: ["notRequiredCommands"]
|
|
514
|
+
});
|
|
515
|
+
assertBatteryCompleteness(proof, context);
|
|
516
|
+
assertNotRequiredStampAuthorization(proof, context);
|
|
517
|
+
};
|
|
229
518
|
const LEGACY_CLOSEOUT_SCHEMA_VERSION = 3;
|
|
230
519
|
/** Current writer plus retained reader versions accepted at ingest. */
|
|
231
520
|
const SUPPORTED_CLOSEOUT_SCHEMA_VERSIONS = [LEGACY_CLOSEOUT_SCHEMA_VERSION, 4];
|
|
@@ -702,6 +991,11 @@ const executedCommandSchema = z.object({
|
|
|
702
991
|
"full"
|
|
703
992
|
])
|
|
704
993
|
});
|
|
994
|
+
const notRequiredCommandSchema = z.object({
|
|
995
|
+
basis: z.string().min(1),
|
|
996
|
+
impactTarget: z.string().min(1),
|
|
997
|
+
name: z.string().min(1)
|
|
998
|
+
});
|
|
705
999
|
const prVerifySchemaVersionSchema = z.number().refine((value) => SUPPORTED_PR_VERIFY_SCHEMA_VERSIONS.includes(value), { message: `schemaVersion must be one of: ${SUPPORTED_PR_VERIFY_SCHEMA_VERSIONS.join(", ")}` });
|
|
706
1000
|
const prVerifyProofSchema = z.object({
|
|
707
1001
|
authoringSession: z.string().trim().min(1).optional(),
|
|
@@ -722,12 +1016,14 @@ const prVerifyProofSchema = z.object({
|
|
|
722
1016
|
endedAt: z.iso.datetime(),
|
|
723
1017
|
executedCommands: z.array(executedCommandSchema).optional(),
|
|
724
1018
|
headSha: z.string().regex(/^[0-9a-f]{40}$/u),
|
|
1019
|
+
impactStamp: impactStampSchema.optional(),
|
|
725
1020
|
mergeBaseSha: z.string().regex(/^[0-9a-f]{40}$/u).optional(),
|
|
726
1021
|
mode: z.enum([
|
|
727
1022
|
"docs-only",
|
|
728
1023
|
"trivial",
|
|
729
1024
|
"full"
|
|
730
1025
|
]),
|
|
1026
|
+
notRequiredCommands: z.array(notRequiredCommandSchema).min(1).optional(),
|
|
731
1027
|
outcome: z.enum(["aborted", "passed"]).optional(),
|
|
732
1028
|
patchId: z.string().regex(/^[0-9a-f]{40}$/u).optional(),
|
|
733
1029
|
profilePath: z.string().min(1),
|
|
@@ -738,6 +1034,7 @@ const prVerifyProofSchema = z.object({
|
|
|
738
1034
|
verificationCommands: z.array(z.object({
|
|
739
1035
|
command: z.string().min(1),
|
|
740
1036
|
description: z.string().min(1),
|
|
1037
|
+
impactTarget: z.string().min(1).optional(),
|
|
741
1038
|
name: z.string().min(1),
|
|
742
1039
|
scope: z.enum([
|
|
743
1040
|
"always",
|
|
@@ -746,18 +1043,7 @@ const prVerifyProofSchema = z.object({
|
|
|
746
1043
|
"full"
|
|
747
1044
|
])
|
|
748
1045
|
}))
|
|
749
|
-
}).superRefine(
|
|
750
|
-
if (proof.schemaVersion >= 4 && proof.authoringSession === void 0) context.addIssue({
|
|
751
|
-
code: "custom",
|
|
752
|
-
message: "schemaVersion 4 pr:verify proof requires authoringSession (the recorded authoring identity, or the `unknown` sentinel).",
|
|
753
|
-
path: ["authoringSession"]
|
|
754
|
-
});
|
|
755
|
-
if (proof.schemaVersion < 4 && proof.authoringSession !== void 0) context.addIssue({
|
|
756
|
-
code: "custom",
|
|
757
|
-
message: "authoringSession is a schemaVersion>=4 field; a v1–v3 pr:verify proof must not carry it.",
|
|
758
|
-
path: ["authoringSession"]
|
|
759
|
-
});
|
|
760
|
-
});
|
|
1046
|
+
}).superRefine(assertPrVerifyProofRules);
|
|
761
1047
|
const PR_REVIEW_SCHEMA_VERSION = 2;
|
|
762
1048
|
const PR_REVIEW_FINDING_PROVENANCE_VERSION = 1;
|
|
763
1049
|
const nonBlankString = z.string().refine((value) => value.trim().length > 0, { message: "must not be blank" });
|