@patronage/software-factory 1.0.0-alpha.15 → 1.0.0-alpha.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/dist/index.d.ts +134 -123
- package/dist/index.js +319 -67
- package/dist/schemas.d.ts +27 -27
- package/dist/schemas.js +1 -1
- package/package.json +4 -3
package/README.md
CHANGED
|
@@ -64,6 +64,8 @@ The repository profile declares its admission policy. Operator-local settings an
|
|
|
64
64
|
|
|
65
65
|
Generated production workflows may call `psf production:impact --before <sha> --after <sha> --github-output <path>`. The command recomputes the profile's opt-in impact subscriptions from those exact real commits and emits stable demanded/withdrawn target outputs, the changed-file decision basis, and unsubscribed paths. It only withdraws work: missing, zero, malformed, unreachable, non-ancestral, or otherwise unclassifiable identities leave every known target demanded. The factory owns this decision seam; each consumer owns its deploy jobs, commands, credentials, topology, and post-deploy convergence/no-op proof. Merge-target Verify remains full and does not consult this command.
|
|
66
66
|
|
|
67
|
+
Generated pull-request workflows may call `psf candidate:impact --base <sha> --head <sha> --github-output <path>` to route Candidate verification surfaces. The command evaluates the same profile impact declarations and pnpm dependency graph from the exact base/head identity, diffed from their merge base; it is the candidate-named sibling of `production:impact` and replaces repository-owned path filters, never adds a second matcher. The `head` commit must be the checked-out `HEAD`, and both commits must be reachable (`fetch-depth: 0`). Every doubt path — unreadable identity, malformed or unsupported graph data, an unowned profile change, a working-tree profile that differs from the head commit, or classifier refusal — fails closed and leaves every declared target demanded. `production:impact` keeps its merge-push `before`/`after` contract unchanged.
|
|
68
|
+
|
|
67
69
|
For factory workflow, install the four canonical skills from the repository:
|
|
68
70
|
|
|
69
71
|
```bash
|
package/dist/index.d.ts
CHANGED
|
@@ -16,8 +16,8 @@ declare const boundaryManifestBaseSchema: z.ZodObject<{
|
|
|
16
16
|
boundary: z.ZodString;
|
|
17
17
|
closeout: z.ZodOptional<z.ZodObject<{
|
|
18
18
|
review: z.ZodEnum<{
|
|
19
|
-
oracle: "oracle";
|
|
20
19
|
"independent-model": "independent-model";
|
|
20
|
+
oracle: "oracle";
|
|
21
21
|
human: "human";
|
|
22
22
|
}>;
|
|
23
23
|
}, z.core.$loose>>;
|
|
@@ -35,8 +35,8 @@ declare const boundaryManifestBaseSchema: z.ZodObject<{
|
|
|
35
35
|
issues: z.ZodArray<z.ZodNumber>;
|
|
36
36
|
name: z.ZodString;
|
|
37
37
|
review: z.ZodEnum<{
|
|
38
|
-
oracle: "oracle";
|
|
39
38
|
"independent-model": "independent-model";
|
|
39
|
+
oracle: "oracle";
|
|
40
40
|
human: "human";
|
|
41
41
|
}>;
|
|
42
42
|
}, z.core.$loose>>;
|
|
@@ -45,8 +45,8 @@ declare const boundaryManifestSchema: z.ZodObject<{
|
|
|
45
45
|
boundary: z.ZodString;
|
|
46
46
|
closeout: z.ZodOptional<z.ZodObject<{
|
|
47
47
|
review: z.ZodEnum<{
|
|
48
|
-
oracle: "oracle";
|
|
49
48
|
"independent-model": "independent-model";
|
|
49
|
+
oracle: "oracle";
|
|
50
50
|
human: "human";
|
|
51
51
|
}>;
|
|
52
52
|
}, z.core.$loose>>;
|
|
@@ -64,8 +64,8 @@ declare const boundaryManifestSchema: z.ZodObject<{
|
|
|
64
64
|
issues: z.ZodArray<z.ZodNumber>;
|
|
65
65
|
name: z.ZodString;
|
|
66
66
|
review: z.ZodEnum<{
|
|
67
|
-
oracle: "oracle";
|
|
68
67
|
"independent-model": "independent-model";
|
|
68
|
+
oracle: "oracle";
|
|
69
69
|
human: "human";
|
|
70
70
|
}>;
|
|
71
71
|
}, z.core.$loose>>;
|
|
@@ -126,8 +126,8 @@ declare const boundaryReviewProofBaseSchema: z.ZodObject<{
|
|
|
126
126
|
mergedSha: z.ZodOptional<z.ZodString>;
|
|
127
127
|
pr: z.ZodNumber;
|
|
128
128
|
state: z.ZodEnum<{
|
|
129
|
-
open: "open";
|
|
130
129
|
merged: "merged";
|
|
130
|
+
open: "open";
|
|
131
131
|
}>;
|
|
132
132
|
wave: z.ZodOptional<z.ZodString>;
|
|
133
133
|
}, z.core.$loose>>;
|
|
@@ -150,8 +150,8 @@ declare const boundaryReviewProofBaseSchema: z.ZodObject<{
|
|
|
150
150
|
}>;
|
|
151
151
|
producer: z.ZodString;
|
|
152
152
|
rung: z.ZodEnum<{
|
|
153
|
-
oracle: "oracle";
|
|
154
153
|
"independent-model": "independent-model";
|
|
154
|
+
oracle: "oracle";
|
|
155
155
|
human: "human";
|
|
156
156
|
}>;
|
|
157
157
|
schemaVersion: z.ZodLiteral<1>;
|
|
@@ -170,8 +170,8 @@ declare const boundaryReviewProofSchema: z.ZodObject<{
|
|
|
170
170
|
mergedSha: z.ZodOptional<z.ZodString>;
|
|
171
171
|
pr: z.ZodNumber;
|
|
172
172
|
state: z.ZodEnum<{
|
|
173
|
-
open: "open";
|
|
174
173
|
merged: "merged";
|
|
174
|
+
open: "open";
|
|
175
175
|
}>;
|
|
176
176
|
wave: z.ZodOptional<z.ZodString>;
|
|
177
177
|
}, z.core.$loose>>;
|
|
@@ -194,8 +194,8 @@ declare const boundaryReviewProofSchema: z.ZodObject<{
|
|
|
194
194
|
}>;
|
|
195
195
|
producer: z.ZodString;
|
|
196
196
|
rung: z.ZodEnum<{
|
|
197
|
-
oracle: "oracle";
|
|
198
197
|
"independent-model": "independent-model";
|
|
198
|
+
oracle: "oracle";
|
|
199
199
|
human: "human";
|
|
200
200
|
}>;
|
|
201
201
|
schemaVersion: z.ZodLiteral<1>;
|
|
@@ -285,6 +285,15 @@ interface HqSpoolEntryOutcome {
|
|
|
285
285
|
detail?: string;
|
|
286
286
|
eventId: string;
|
|
287
287
|
kind: string;
|
|
288
|
+
/**
|
|
289
|
+
* `true` when HQ refused the content as schema-invalid (HTTP 422, #739).
|
|
290
|
+
* The same bytes get the same verdict on every retry under the current
|
|
291
|
+
* ingest schema, so retrying without a schema change cannot succeed.
|
|
292
|
+
* Distinct from `undeliverable`: an HQ schema fix can still admit these
|
|
293
|
+
* bytes, so eviction is an operator decision (`hq:flush --evict-permanent`),
|
|
294
|
+
* never automatic.
|
|
295
|
+
*/
|
|
296
|
+
permanent?: boolean;
|
|
288
297
|
spool: string;
|
|
289
298
|
/**
|
|
290
299
|
* `undeliverable` is the one terminal verdict (#445). Every other status
|
|
@@ -302,6 +311,13 @@ interface HqSpoolFlushInput {
|
|
|
302
311
|
clientSecret: string;
|
|
303
312
|
/** The profile's HQ origin; entries recorded against another are refused. */
|
|
304
313
|
endpoint: string;
|
|
314
|
+
/**
|
|
315
|
+
* Disposition schema-permanent rejections (HTTP 422) in place with the
|
|
316
|
+
* `.undeliverable` suffix instead of retaining them (#739). Operator
|
|
317
|
+
* opt-in: a later HQ schema fix can still admit the same bytes, so the
|
|
318
|
+
* drain never evicts on its own.
|
|
319
|
+
*/
|
|
320
|
+
evictPermanentRejections?: boolean;
|
|
305
321
|
/** Operator-named spool directories; replaces the default two locations. */
|
|
306
322
|
explicitDirectories?: string[];
|
|
307
323
|
repository: HqSpoolRepository;
|
|
@@ -617,10 +633,81 @@ declare const FollowUpActionSchema: z.ZodType<FollowUpAction>;
|
|
|
617
633
|
declare const DIFF_CLASSIFICATIONS: readonly ["docs/process-only", "trivial", "non-trivial"];
|
|
618
634
|
type DiffClassification = (typeof DIFF_CLASSIFICATIONS)[number];
|
|
619
635
|
//#endregion
|
|
636
|
+
//#region src/impact-stamp.d.ts
|
|
637
|
+
/**
|
|
638
|
+
* The recorded stamp. `targets` satisfies the completeness invariant: every
|
|
639
|
+
* target the profile declares is present, each with its exact basis — never
|
|
640
|
+
* silently absent. `basis: "conservative"` means an unmodelled or unprovable
|
|
641
|
+
* input widened every target to full impact.
|
|
642
|
+
*/
|
|
643
|
+
declare const impactStampSchema: z.ZodObject<{
|
|
644
|
+
basis: z.ZodEnum<{
|
|
645
|
+
"target-scoped": "target-scoped";
|
|
646
|
+
conservative: "conservative";
|
|
647
|
+
}>;
|
|
648
|
+
reasons: z.ZodArray<z.ZodString>;
|
|
649
|
+
stampVersion: z.ZodLiteral<3>;
|
|
650
|
+
targets: z.ZodArray<z.ZodObject<{
|
|
651
|
+
basis: z.ZodString;
|
|
652
|
+
impact: z.ZodEnum<{
|
|
653
|
+
affected: "affected";
|
|
654
|
+
"not-affected": "not-affected";
|
|
655
|
+
}>;
|
|
656
|
+
name: z.ZodString;
|
|
657
|
+
}, z.core.$strip>>;
|
|
658
|
+
unsubscribedPaths: z.ZodArray<z.ZodString>;
|
|
659
|
+
}, z.core.$strip>;
|
|
660
|
+
type ImpactStamp = z.infer<typeof impactStampSchema>;
|
|
661
|
+
/**
|
|
662
|
+
* The surfaces that consume the stamp. One classification, consumed
|
|
663
|
+
* everywhere (#430): every surface routes through
|
|
664
|
+
* {@link impactStampScopeDecision}, so no surface can be more trusting than
|
|
665
|
+
* another, and no surface re-derives path rules of its own. The value only
|
|
666
|
+
* selects the wording of the conservative floor a refusal falls back to.
|
|
667
|
+
*/
|
|
668
|
+
declare const IMPACT_SCOPE_SURFACES: {
|
|
669
|
+
readonly demand: "full demand floor";
|
|
670
|
+
readonly "preview-lifecycle": "full preview-lifecycle floor";
|
|
671
|
+
readonly "verification-battery": "full verification-battery floor";
|
|
672
|
+
};
|
|
673
|
+
type ImpactScopeSurface = keyof typeof IMPACT_SCOPE_SURFACES;
|
|
674
|
+
interface ImpactScopeDecision {
|
|
675
|
+
reason: string;
|
|
676
|
+
/** True ⇒ this target's work is provably unreachable and may be released. */
|
|
677
|
+
scoped: boolean;
|
|
678
|
+
}
|
|
679
|
+
/**
|
|
680
|
+
* Whether a trusted stamp RELEASES one target-scoped unit of work — an
|
|
681
|
+
* external proof demand (#542 wave 2), a verification-battery command, or a
|
|
682
|
+
* preview (Alchemy) lifecycle stack (#430 wave 3).
|
|
683
|
+
*
|
|
684
|
+
* The only release path is a `target-scoped` stamp of exactly this build's
|
|
685
|
+
* stamp version whose target entry for exactly this name is provably
|
|
686
|
+
* `not-affected`. Every other input — no trusted stamp, an unknown stamp
|
|
687
|
+
* version, a conservative stamp, a name the stamp does not classify, or an
|
|
688
|
+
* `affected` verdict — keeps today's full-work floor. Work only ever gets
|
|
689
|
+
* released, never added or satisfied: this function can withdraw a demand, it
|
|
690
|
+
* can never certify one.
|
|
691
|
+
*
|
|
692
|
+
* Identity binding is the CALLER's precondition: pass only a stamp computed
|
|
693
|
+
* for, or read from a pr:verify proof bound to, the current candidate (see
|
|
694
|
+
* `trustedImpactStamp` in `pr-readiness/verification-proof.ts`); pass
|
|
695
|
+
* `undefined` otherwise.
|
|
696
|
+
*/
|
|
697
|
+
declare const impactStampScopeDecision: ({
|
|
698
|
+
stamp,
|
|
699
|
+
surface,
|
|
700
|
+
targetName
|
|
701
|
+
}: {
|
|
702
|
+
stamp: ImpactStamp | undefined;
|
|
703
|
+
surface: ImpactScopeSurface;
|
|
704
|
+
targetName: string;
|
|
705
|
+
}) => ImpactScopeDecision;
|
|
706
|
+
//#endregion
|
|
620
707
|
//#region src/profile.d.ts
|
|
621
708
|
declare const DEFAULT_FACTORY_REPOSITORY = "unknown/unknown";
|
|
622
|
-
/** The one profile shape this build accepts (ADR 0023, #318, #351). */
|
|
623
|
-
declare const PROFILE_SCHEMA_VERSION =
|
|
709
|
+
/** The one profile shape this build accepts (ADR 0023, #318, #351, #742). */
|
|
710
|
+
declare const PROFILE_SCHEMA_VERSION = 6;
|
|
624
711
|
declare const factoryProjectProfileSchema: z.ZodObject<{
|
|
625
712
|
$schema: z.ZodOptional<z.ZodString>;
|
|
626
713
|
extensions: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnknown>>;
|
|
@@ -629,18 +716,13 @@ declare const factoryProjectProfileSchema: z.ZodObject<{
|
|
|
629
716
|
endpoint: z.ZodIntersection<z.ZodString, z.ZodString>;
|
|
630
717
|
}, z.core.$strict>>;
|
|
631
718
|
impact: z.ZodOptional<z.ZodObject<{
|
|
719
|
+
inert: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
632
720
|
targets: z.ZodArray<z.ZodObject<{
|
|
633
721
|
importers: z.ZodArray<z.ZodString>;
|
|
634
722
|
name: z.ZodString;
|
|
635
723
|
paths: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
636
724
|
}, z.core.$strict>>;
|
|
637
725
|
}, z.core.$strict>>;
|
|
638
|
-
proof: z.ZodObject<{
|
|
639
|
-
classificationPolicy: z.ZodObject<{
|
|
640
|
-
docsOnly: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
641
|
-
trivial: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
642
|
-
}, z.core.$strict>;
|
|
643
|
-
}, z.core.$strict>;
|
|
644
726
|
repository: z.ZodObject<{
|
|
645
727
|
name: z.ZodString;
|
|
646
728
|
owner: z.ZodString;
|
|
@@ -649,8 +731,8 @@ declare const factoryProjectProfileSchema: z.ZodObject<{
|
|
|
649
731
|
name: z.ZodString;
|
|
650
732
|
scope: z.ZodOptional<z.ZodObject<{
|
|
651
733
|
classifications: z.ZodArray<z.ZodEnum<{
|
|
652
|
-
trivial: "trivial";
|
|
653
734
|
"docs/process-only": "docs/process-only";
|
|
735
|
+
trivial: "trivial";
|
|
654
736
|
"non-trivial": "non-trivial";
|
|
655
737
|
}>>;
|
|
656
738
|
}, z.core.$strict>>;
|
|
@@ -660,7 +742,7 @@ declare const factoryProjectProfileSchema: z.ZodObject<{
|
|
|
660
742
|
paths: z.ZodArray<z.ZodString>;
|
|
661
743
|
}, z.core.$strict>>;
|
|
662
744
|
}, z.core.$strict>>;
|
|
663
|
-
schemaVersion: z.ZodLiteral<
|
|
745
|
+
schemaVersion: z.ZodLiteral<6>;
|
|
664
746
|
verification: z.ZodObject<{
|
|
665
747
|
commands: z.ZodArray<z.ZodObject<{
|
|
666
748
|
command: z.ZodString;
|
|
@@ -1217,8 +1299,8 @@ declare const managedReadinessLedgerSchema: z.ZodObject<{
|
|
|
1217
1299
|
baseSha: z.ZodString;
|
|
1218
1300
|
blockingReasons: z.ZodArray<z.ZodString>;
|
|
1219
1301
|
classification: z.ZodEnum<{
|
|
1220
|
-
trivial: "trivial";
|
|
1221
1302
|
"docs/process-only": "docs/process-only";
|
|
1303
|
+
trivial: "trivial";
|
|
1222
1304
|
"non-trivial": "non-trivial";
|
|
1223
1305
|
}>;
|
|
1224
1306
|
externalChecks: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
@@ -1231,8 +1313,8 @@ declare const managedReadinessLedgerSchema: z.ZodObject<{
|
|
|
1231
1313
|
reason: z.ZodOptional<z.ZodString>;
|
|
1232
1314
|
scope: z.ZodOptional<z.ZodObject<{
|
|
1233
1315
|
classifications: z.ZodArray<z.ZodEnum<{
|
|
1234
|
-
trivial: "trivial";
|
|
1235
1316
|
"docs/process-only": "docs/process-only";
|
|
1317
|
+
trivial: "trivial";
|
|
1236
1318
|
"non-trivial": "non-trivial";
|
|
1237
1319
|
}>>;
|
|
1238
1320
|
}, z.core.$strict>>;
|
|
@@ -1282,15 +1364,15 @@ declare const managedReadinessLedgerSchema: z.ZodObject<{
|
|
|
1282
1364
|
unknown: "unknown";
|
|
1283
1365
|
critical: "critical";
|
|
1284
1366
|
high: "high";
|
|
1285
|
-
medium: "medium";
|
|
1286
1367
|
low: "low";
|
|
1368
|
+
medium: "medium";
|
|
1287
1369
|
}>>;
|
|
1288
1370
|
highestOpenSeverity: z.ZodOptional<z.ZodEnum<{
|
|
1289
1371
|
unknown: "unknown";
|
|
1290
1372
|
critical: "critical";
|
|
1291
1373
|
high: "high";
|
|
1292
|
-
medium: "medium";
|
|
1293
1374
|
low: "low";
|
|
1375
|
+
medium: "medium";
|
|
1294
1376
|
}>>;
|
|
1295
1377
|
maxReviewCycles: z.ZodOptional<z.ZodNumber>;
|
|
1296
1378
|
nonBlockingFindings: z.ZodNumber;
|
|
@@ -1318,8 +1400,8 @@ declare const managedReadinessLedgerSchema: z.ZodObject<{
|
|
|
1318
1400
|
}>;
|
|
1319
1401
|
stage: z.ZodEnum<{
|
|
1320
1402
|
gate: "gate";
|
|
1321
|
-
"interior-complete": "interior-complete";
|
|
1322
1403
|
interior: "interior";
|
|
1404
|
+
"interior-complete": "interior-complete";
|
|
1323
1405
|
}>;
|
|
1324
1406
|
}, z.core.$strip>>;
|
|
1325
1407
|
reviewRuns: z.ZodOptional<z.ZodArray<z.ZodType<PrReviewResult, unknown, z.core.$ZodTypeInternals<PrReviewResult, unknown>>>>;
|
|
@@ -1598,84 +1680,13 @@ declare function validatePrReadyProof(value: unknown): PrReadyProof;
|
|
|
1598
1680
|
declare function readPrReadyProof(filePath: string): PrReadyProof;
|
|
1599
1681
|
declare function runPrReady(args: PrReadyArgs, dependencies?: PrReadyDependencies): Promise<PrReadyProof>;
|
|
1600
1682
|
//#endregion
|
|
1601
|
-
//#region src/impact-stamp.d.ts
|
|
1602
|
-
/**
|
|
1603
|
-
* The recorded stamp. `targets` satisfies the completeness invariant: every
|
|
1604
|
-
* target the profile declares is present, each with its exact basis — never
|
|
1605
|
-
* silently absent. `basis: "conservative"` means an unmodelled or unprovable
|
|
1606
|
-
* input widened every target to full impact.
|
|
1607
|
-
*/
|
|
1608
|
-
declare const impactStampSchema: z.ZodObject<{
|
|
1609
|
-
basis: z.ZodEnum<{
|
|
1610
|
-
"target-scoped": "target-scoped";
|
|
1611
|
-
conservative: "conservative";
|
|
1612
|
-
}>;
|
|
1613
|
-
reasons: z.ZodArray<z.ZodString>;
|
|
1614
|
-
stampVersion: z.ZodLiteral<3>;
|
|
1615
|
-
targets: z.ZodArray<z.ZodObject<{
|
|
1616
|
-
basis: z.ZodString;
|
|
1617
|
-
impact: z.ZodEnum<{
|
|
1618
|
-
affected: "affected";
|
|
1619
|
-
"not-affected": "not-affected";
|
|
1620
|
-
}>;
|
|
1621
|
-
name: z.ZodString;
|
|
1622
|
-
}, z.core.$strip>>;
|
|
1623
|
-
unsubscribedPaths: z.ZodArray<z.ZodString>;
|
|
1624
|
-
}, z.core.$strip>;
|
|
1625
|
-
type ImpactStamp = z.infer<typeof impactStampSchema>;
|
|
1626
|
-
/**
|
|
1627
|
-
* The surfaces that consume the stamp. One classification, consumed
|
|
1628
|
-
* everywhere (#430): every surface routes through
|
|
1629
|
-
* {@link impactStampScopeDecision}, so no surface can be more trusting than
|
|
1630
|
-
* another, and no surface re-derives path rules of its own. The value only
|
|
1631
|
-
* selects the wording of the conservative floor a refusal falls back to.
|
|
1632
|
-
*/
|
|
1633
|
-
declare const IMPACT_SCOPE_SURFACES: {
|
|
1634
|
-
readonly demand: "full demand floor";
|
|
1635
|
-
readonly "preview-lifecycle": "full preview-lifecycle floor";
|
|
1636
|
-
readonly "verification-battery": "full verification-battery floor";
|
|
1637
|
-
};
|
|
1638
|
-
type ImpactScopeSurface = keyof typeof IMPACT_SCOPE_SURFACES;
|
|
1639
|
-
interface ImpactScopeDecision {
|
|
1640
|
-
reason: string;
|
|
1641
|
-
/** True ⇒ this target's work is provably unreachable and may be released. */
|
|
1642
|
-
scoped: boolean;
|
|
1643
|
-
}
|
|
1644
|
-
/**
|
|
1645
|
-
* Whether a trusted stamp RELEASES one target-scoped unit of work — an
|
|
1646
|
-
* external proof demand (#542 wave 2), a verification-battery command, or a
|
|
1647
|
-
* preview (Alchemy) lifecycle stack (#430 wave 3).
|
|
1648
|
-
*
|
|
1649
|
-
* The only release path is a `target-scoped` stamp of exactly this build's
|
|
1650
|
-
* stamp version whose target entry for exactly this name is provably
|
|
1651
|
-
* `not-affected`. Every other input — no trusted stamp, an unknown stamp
|
|
1652
|
-
* version, a conservative stamp, a name the stamp does not classify, or an
|
|
1653
|
-
* `affected` verdict — keeps today's full-work floor. Work only ever gets
|
|
1654
|
-
* released, never added or satisfied: this function can withdraw a demand, it
|
|
1655
|
-
* can never certify one.
|
|
1656
|
-
*
|
|
1657
|
-
* Identity binding is the CALLER's precondition: pass only a stamp computed
|
|
1658
|
-
* for, or read from a pr:verify proof bound to, the current candidate (see
|
|
1659
|
-
* `trustedImpactStamp` in `pr-readiness/verification-proof.ts`); pass
|
|
1660
|
-
* `undefined` otherwise.
|
|
1661
|
-
*/
|
|
1662
|
-
declare const impactStampScopeDecision: ({
|
|
1663
|
-
stamp,
|
|
1664
|
-
surface,
|
|
1665
|
-
targetName
|
|
1666
|
-
}: {
|
|
1667
|
-
stamp: ImpactStamp | undefined;
|
|
1668
|
-
surface: ImpactScopeSurface;
|
|
1669
|
-
targetName: string;
|
|
1670
|
-
}) => ImpactScopeDecision;
|
|
1671
|
-
//#endregion
|
|
1672
1683
|
//#region src/required-check-policy.d.ts
|
|
1673
1684
|
declare const EVIDENCE_CHECK_TYPES: readonly ["review", "verify"];
|
|
1674
1685
|
type EvidenceCheckType = (typeof EVIDENCE_CHECK_TYPES)[number];
|
|
1675
1686
|
declare const requiredCheckScopeSchema: z.ZodObject<{
|
|
1676
1687
|
classifications: z.ZodArray<z.ZodEnum<{
|
|
1677
|
-
trivial: "trivial";
|
|
1678
1688
|
"docs/process-only": "docs/process-only";
|
|
1689
|
+
trivial: "trivial";
|
|
1679
1690
|
"non-trivial": "non-trivial";
|
|
1680
1691
|
}>>;
|
|
1681
1692
|
}, z.core.$strict>;
|
|
@@ -1732,8 +1743,8 @@ declare const evidenceEnvelopeBaseSchema: z.ZodObject<{
|
|
|
1732
1743
|
producer: z.ZodString;
|
|
1733
1744
|
requestId: z.ZodOptional<z.ZodString>;
|
|
1734
1745
|
rung: z.ZodOptional<z.ZodEnum<{
|
|
1735
|
-
oracle: "oracle";
|
|
1736
1746
|
"independent-model": "independent-model";
|
|
1747
|
+
oracle: "oracle";
|
|
1737
1748
|
human: "human";
|
|
1738
1749
|
}>>;
|
|
1739
1750
|
schemaVersion: z.ZodLiteral<1>;
|
|
@@ -1759,8 +1770,8 @@ declare const evidenceEnvelopeSchema: z.ZodObject<{
|
|
|
1759
1770
|
producer: z.ZodString;
|
|
1760
1771
|
requestId: z.ZodOptional<z.ZodString>;
|
|
1761
1772
|
rung: z.ZodOptional<z.ZodEnum<{
|
|
1762
|
-
oracle: "oracle";
|
|
1763
1773
|
"independent-model": "independent-model";
|
|
1774
|
+
oracle: "oracle";
|
|
1764
1775
|
human: "human";
|
|
1765
1776
|
}>>;
|
|
1766
1777
|
schemaVersion: z.ZodLiteral<1>;
|
|
@@ -2050,18 +2061,18 @@ declare const boundaryCheckProofSchema: z.ZodObject<{
|
|
|
2050
2061
|
boundary: z.ZodOptional<z.ZodString>;
|
|
2051
2062
|
command: z.ZodLiteral<"patronage-factory boundary:check">;
|
|
2052
2063
|
coveredSet: z.ZodArray<z.ZodObject<{
|
|
2053
|
-
issue: z.ZodNumber
|
|
2064
|
+
issue: z.ZodOptional<z.ZodNumber>;
|
|
2054
2065
|
pr: z.ZodNumber;
|
|
2055
2066
|
sha: z.ZodString;
|
|
2056
2067
|
state: z.ZodEnum<{
|
|
2057
|
-
open: "open";
|
|
2058
2068
|
merged: "merged";
|
|
2069
|
+
open: "open";
|
|
2059
2070
|
}>;
|
|
2060
2071
|
wave: z.ZodString;
|
|
2061
2072
|
}, z.core.$strip>>;
|
|
2062
2073
|
demandedRung: z.ZodOptional<z.ZodEnum<{
|
|
2063
|
-
oracle: "oracle";
|
|
2064
2074
|
"independent-model": "independent-model";
|
|
2075
|
+
oracle: "oracle";
|
|
2065
2076
|
human: "human";
|
|
2066
2077
|
}>>;
|
|
2067
2078
|
epicIssue: z.ZodNumber;
|
|
@@ -2076,8 +2087,8 @@ declare const boundaryCheckProofSchema: z.ZodObject<{
|
|
|
2076
2087
|
commentUrl: z.ZodOptional<z.ZodString>;
|
|
2077
2088
|
producer: z.ZodString;
|
|
2078
2089
|
rung: z.ZodEnum<{
|
|
2079
|
-
oracle: "oracle";
|
|
2080
2090
|
"independent-model": "independent-model";
|
|
2091
|
+
oracle: "oracle";
|
|
2081
2092
|
human: "human";
|
|
2082
2093
|
}>;
|
|
2083
2094
|
sessionId: z.ZodOptional<z.ZodString>;
|
|
@@ -2093,8 +2104,8 @@ declare const boundaryCheckProofSchema: z.ZodObject<{
|
|
|
2093
2104
|
value: z.ZodString;
|
|
2094
2105
|
}, z.core.$strip>>;
|
|
2095
2106
|
status: z.ZodEnum<{
|
|
2096
|
-
blocked: "blocked";
|
|
2097
2107
|
ready: "ready";
|
|
2108
|
+
blocked: "blocked";
|
|
2098
2109
|
}>;
|
|
2099
2110
|
}, z.core.$strip>;
|
|
2100
2111
|
type BoundaryCheckProofRecord = z.infer<typeof boundaryCheckProofSchema>;
|
|
@@ -3212,14 +3223,14 @@ declare const FACTORY_TRACE_EVENT_DEFINITIONS: readonly [DefinedTraceEvent<"revi
|
|
|
3212
3223
|
interiorCycle: z.ZodOptional<z.ZodNumber>;
|
|
3213
3224
|
observedAt: z.ZodString;
|
|
3214
3225
|
purpose: z.ZodEnum<{
|
|
3215
|
-
review: "review";
|
|
3216
3226
|
code: "code";
|
|
3227
|
+
review: "review";
|
|
3217
3228
|
}>;
|
|
3218
3229
|
slotResolution: z.ZodObject<{
|
|
3219
3230
|
effort: z.ZodEnum<{
|
|
3220
3231
|
high: "high";
|
|
3221
|
-
medium: "medium";
|
|
3222
3232
|
low: "low";
|
|
3233
|
+
medium: "medium";
|
|
3223
3234
|
xhigh: "xhigh";
|
|
3224
3235
|
}>;
|
|
3225
3236
|
engine: z.ZodString;
|
|
@@ -3231,18 +3242,18 @@ declare const FACTORY_TRACE_EVENT_DEFINITIONS: readonly [DefinedTraceEvent<"revi
|
|
|
3231
3242
|
}>;
|
|
3232
3243
|
model: z.ZodString;
|
|
3233
3244
|
tier: z.ZodEnum<{
|
|
3245
|
+
oracle: "oracle";
|
|
3234
3246
|
review: "review";
|
|
3235
3247
|
delegate: "delegate";
|
|
3236
|
-
oracle: "oracle";
|
|
3237
3248
|
workhorse: "workhorse";
|
|
3238
3249
|
scout: "scout";
|
|
3239
3250
|
}>;
|
|
3240
3251
|
}, z.core.$strict>;
|
|
3241
3252
|
startedAt: z.ZodString;
|
|
3242
3253
|
tier: z.ZodEnum<{
|
|
3254
|
+
oracle: "oracle";
|
|
3243
3255
|
review: "review";
|
|
3244
3256
|
delegate: "delegate";
|
|
3245
|
-
oracle: "oracle";
|
|
3246
3257
|
workhorse: "workhorse";
|
|
3247
3258
|
scout: "scout";
|
|
3248
3259
|
}>;
|
|
@@ -3291,14 +3302,14 @@ declare const FACTORY_TRACE_EVENT_DEFINITIONS: readonly [DefinedTraceEvent<"revi
|
|
|
3291
3302
|
interiorCycle: z.ZodOptional<z.ZodNumber>;
|
|
3292
3303
|
observedAt: z.ZodString;
|
|
3293
3304
|
purpose: z.ZodEnum<{
|
|
3294
|
-
review: "review";
|
|
3295
3305
|
code: "code";
|
|
3306
|
+
review: "review";
|
|
3296
3307
|
}>;
|
|
3297
3308
|
slotResolution: z.ZodObject<{
|
|
3298
3309
|
effort: z.ZodEnum<{
|
|
3299
3310
|
high: "high";
|
|
3300
|
-
medium: "medium";
|
|
3301
3311
|
low: "low";
|
|
3312
|
+
medium: "medium";
|
|
3302
3313
|
xhigh: "xhigh";
|
|
3303
3314
|
}>;
|
|
3304
3315
|
engine: z.ZodString;
|
|
@@ -3310,18 +3321,18 @@ declare const FACTORY_TRACE_EVENT_DEFINITIONS: readonly [DefinedTraceEvent<"revi
|
|
|
3310
3321
|
}>;
|
|
3311
3322
|
model: z.ZodString;
|
|
3312
3323
|
tier: z.ZodEnum<{
|
|
3324
|
+
oracle: "oracle";
|
|
3313
3325
|
review: "review";
|
|
3314
3326
|
delegate: "delegate";
|
|
3315
|
-
oracle: "oracle";
|
|
3316
3327
|
workhorse: "workhorse";
|
|
3317
3328
|
scout: "scout";
|
|
3318
3329
|
}>;
|
|
3319
3330
|
}, z.core.$strict>;
|
|
3320
3331
|
startedAt: z.ZodString;
|
|
3321
3332
|
tier: z.ZodEnum<{
|
|
3333
|
+
oracle: "oracle";
|
|
3322
3334
|
review: "review";
|
|
3323
3335
|
delegate: "delegate";
|
|
3324
|
-
oracle: "oracle";
|
|
3325
3336
|
workhorse: "workhorse";
|
|
3326
3337
|
scout: "scout";
|
|
3327
3338
|
}>;
|
|
@@ -3361,14 +3372,14 @@ declare const FACTORY_TRACE_EVENT_DEFINITIONS: readonly [DefinedTraceEvent<"revi
|
|
|
3361
3372
|
interiorCycle: z.ZodOptional<z.ZodNumber>;
|
|
3362
3373
|
observedAt: z.ZodString;
|
|
3363
3374
|
purpose: z.ZodEnum<{
|
|
3364
|
-
review: "review";
|
|
3365
3375
|
code: "code";
|
|
3376
|
+
review: "review";
|
|
3366
3377
|
}>;
|
|
3367
3378
|
slotResolution: z.ZodObject<{
|
|
3368
3379
|
effort: z.ZodEnum<{
|
|
3369
3380
|
high: "high";
|
|
3370
|
-
medium: "medium";
|
|
3371
3381
|
low: "low";
|
|
3382
|
+
medium: "medium";
|
|
3372
3383
|
xhigh: "xhigh";
|
|
3373
3384
|
}>;
|
|
3374
3385
|
engine: z.ZodString;
|
|
@@ -3380,18 +3391,18 @@ declare const FACTORY_TRACE_EVENT_DEFINITIONS: readonly [DefinedTraceEvent<"revi
|
|
|
3380
3391
|
}>;
|
|
3381
3392
|
model: z.ZodString;
|
|
3382
3393
|
tier: z.ZodEnum<{
|
|
3394
|
+
oracle: "oracle";
|
|
3383
3395
|
review: "review";
|
|
3384
3396
|
delegate: "delegate";
|
|
3385
|
-
oracle: "oracle";
|
|
3386
3397
|
workhorse: "workhorse";
|
|
3387
3398
|
scout: "scout";
|
|
3388
3399
|
}>;
|
|
3389
3400
|
}, z.core.$strict>;
|
|
3390
3401
|
startedAt: z.ZodString;
|
|
3391
3402
|
tier: z.ZodEnum<{
|
|
3403
|
+
oracle: "oracle";
|
|
3392
3404
|
review: "review";
|
|
3393
3405
|
delegate: "delegate";
|
|
3394
|
-
oracle: "oracle";
|
|
3395
3406
|
workhorse: "workhorse";
|
|
3396
3407
|
scout: "scout";
|
|
3397
3408
|
}>;
|
|
@@ -3434,17 +3445,17 @@ declare const FACTORY_TRACE_EVENT_DEFINITIONS: readonly [DefinedTraceEvent<"revi
|
|
|
3434
3445
|
exitCode: number | null;
|
|
3435
3446
|
harness: string;
|
|
3436
3447
|
observedAt: string;
|
|
3437
|
-
purpose: "
|
|
3448
|
+
purpose: "code" | "review";
|
|
3438
3449
|
slotResolution: {
|
|
3439
|
-
effort: "high" | "
|
|
3450
|
+
effort: "high" | "low" | "medium" | "xhigh";
|
|
3440
3451
|
engine: string;
|
|
3441
3452
|
layerSource: "committedDefault" | "operatorSession" | "userConfig";
|
|
3442
3453
|
model: string;
|
|
3443
|
-
tier: "
|
|
3454
|
+
tier: "oracle" | "review" | "delegate" | "workhorse" | "scout";
|
|
3444
3455
|
fallbackTier?: "delegate" | undefined;
|
|
3445
3456
|
};
|
|
3446
3457
|
startedAt: string;
|
|
3447
|
-
tier: "
|
|
3458
|
+
tier: "oracle" | "review" | "delegate" | "workhorse" | "scout";
|
|
3448
3459
|
tokenUsage: {
|
|
3449
3460
|
source: "cursor-csv" | "codex-json" | "cursor-json" | "claude-json";
|
|
3450
3461
|
status: "available";
|
|
@@ -3761,7 +3772,7 @@ declare const evaluateReadiness: (input: EvaluationInput) => {
|
|
|
3761
3772
|
ledger: {
|
|
3762
3773
|
baseSha: string;
|
|
3763
3774
|
blockingReasons: string[];
|
|
3764
|
-
classification: "
|
|
3775
|
+
classification: "docs/process-only" | "trivial" | "non-trivial";
|
|
3765
3776
|
github: {
|
|
3766
3777
|
draft: boolean;
|
|
3767
3778
|
mergeStateStatus: string;
|
|
@@ -3810,7 +3821,7 @@ declare const evaluateReadiness: (input: EvaluationInput) => {
|
|
|
3810
3821
|
status: "satisfied" | "unmet" | "out-of-scope";
|
|
3811
3822
|
reason?: string | undefined;
|
|
3812
3823
|
scope?: {
|
|
3813
|
-
classifications: ("
|
|
3824
|
+
classifications: ("docs/process-only" | "trivial" | "non-trivial")[];
|
|
3814
3825
|
} | undefined;
|
|
3815
3826
|
}[] | undefined;
|
|
3816
3827
|
mergeBaseSha?: string | undefined;
|
|
@@ -3826,8 +3837,8 @@ declare const evaluateReadiness: (input: EvaluationInput) => {
|
|
|
3826
3837
|
nonBlockingFindings: number;
|
|
3827
3838
|
openFindings: number;
|
|
3828
3839
|
windowExhausted: boolean;
|
|
3829
|
-
highestBlockingSeverity?: "unknown" | "critical" | "high" | "
|
|
3830
|
-
highestOpenSeverity?: "unknown" | "critical" | "high" | "
|
|
3840
|
+
highestBlockingSeverity?: "unknown" | "critical" | "high" | "low" | "medium" | undefined;
|
|
3841
|
+
highestOpenSeverity?: "unknown" | "critical" | "high" | "low" | "medium" | undefined;
|
|
3831
3842
|
maxReviewCycles?: number | undefined;
|
|
3832
3843
|
reviewCycle?: number | undefined;
|
|
3833
3844
|
staleRepeatFindings?: number | undefined;
|
|
@@ -3838,7 +3849,7 @@ declare const evaluateReadiness: (input: EvaluationInput) => {
|
|
|
3838
3849
|
interior: number;
|
|
3839
3850
|
};
|
|
3840
3851
|
nextAction: "run-gate-cycle" | "accept-nonblocking-findings" | "escalate-to-triage" | "ready-for-human" | "advance-to-gate" | "run-interior-cycle";
|
|
3841
|
-
stage: "gate" | "interior
|
|
3852
|
+
stage: "gate" | "interior" | "interior-complete";
|
|
3842
3853
|
forcedTransition?: "gate-cap-exhausted" | "interior-cap-reached" | undefined;
|
|
3843
3854
|
} | undefined;
|
|
3844
3855
|
reviewRuns?: PrReviewResult[] | undefined;
|
package/dist/index.js
CHANGED
|
@@ -17,7 +17,7 @@ import { parse } from "yaml";
|
|
|
17
17
|
import { FACTORY_PROOF_GATE_STEP_NAME, GitHubApiError, mintInstallationToken, productionImpactTargetOutput, readVitestProfileDocument } from "@patronage/factory-ci";
|
|
18
18
|
import { promisify } from "node:util";
|
|
19
19
|
//#region package.json
|
|
20
|
-
var version = "1.0.0-alpha.
|
|
20
|
+
var version = "1.0.0-alpha.16";
|
|
21
21
|
//#endregion
|
|
22
22
|
//#region src/review-rungs.ts
|
|
23
23
|
const EVIDENCE_REVIEW_RUNGS$1 = [
|
|
@@ -1141,17 +1141,17 @@ const requiredCheckInScope = ({ context, scope }) => {
|
|
|
1141
1141
|
//#region src/profile.ts
|
|
1142
1142
|
const DEFAULT_FACTORY_REPOSITORY = "unknown/unknown";
|
|
1143
1143
|
/**
|
|
1144
|
-
* The shapes
|
|
1144
|
+
* The shapes v6 replaced. Not loadable — {@link legacyProfileMigration} turns
|
|
1145
1145
|
* an encounter with one into a per-field migration error rather than a
|
|
1146
1146
|
* schema complaint about a shape the profile was never written in.
|
|
1147
1147
|
*/
|
|
1148
1148
|
const LEGACY_PROFILE_SCHEMA_VERSIONS = [
|
|
1149
1149
|
2,
|
|
1150
1150
|
3,
|
|
1151
|
-
4
|
|
1151
|
+
4,
|
|
1152
|
+
5
|
|
1152
1153
|
];
|
|
1153
1154
|
const PROFILE_JSON_SCHEMA_DESCRIPTION = "Editor validation covers the profile input shape and straightforward cross-field constraints. Zod remains the load-time enforcement authority: verification command names must be distinct and requiredChecks entries must have distinct names; these invariants are enforced at load time, not by this schema. Impact target path patterns encode repo-relative safety in JSON Schema, while complete picomatch syntax validity remains a runtime-only refinement.";
|
|
1154
|
-
const globSchema = z.string().min(1).refine(isValidGlob, "must be a valid glob");
|
|
1155
1155
|
const repoRelativeGlobSchema = z.string().min(1).regex(/^(?!(?:\/[\s\S]*|[A-Za-z]:\/[\s\S]*|!(?!\()[\s\S]*|[\s\S]*\\[\s\S]*|[\s\S]*\/\/[\s\S]*|[\s\S]*\/$|\.{1,2}(?:\/|$)[\s\S]*|[\s\S]*\/\.{1,2}(?:\/|$)[\s\S]*)).+$/u, "must be a repo-relative positive glob").refine(isValidRepoRelativeGlob, "must be a valid repo-relative glob");
|
|
1156
1156
|
const commandSchema = z.object({
|
|
1157
1157
|
command: z.string().min(1),
|
|
@@ -1187,22 +1187,21 @@ const impactTargetSchema = z.object({
|
|
|
1187
1187
|
name: z.string().min(1),
|
|
1188
1188
|
paths: z.array(repoRelativeGlobSchema).min(1).optional().describe("Opt-in repo-relative path subscriptions. A target is affected only when a valid changed path matches one of its globs; valid unmatched paths affect no target and are recorded as unsubscribed. Shared files must be listed by every subscribing target.")
|
|
1189
1189
|
}).strict();
|
|
1190
|
-
const impactConfigSchema = z.object({
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1190
|
+
const impactConfigSchema = z.object({
|
|
1191
|
+
inert: z.array(repoRelativeGlobSchema).min(1).optional().describe("Opt-in repo-relative globs declaring repository-inert ownership. A candidate qualifies for the docs/process-only verification rung only when no changed path affects a declared impact target and every changed path matches one of these globs. Paths without a declared owner stay non-trivial."),
|
|
1192
|
+
targets: z.array(impactTargetSchema).min(1).superRefine((targets, context) => {
|
|
1193
|
+
const names = targets.map((target) => target.name);
|
|
1194
|
+
if (new Set(names).size !== names.length) context.addIssue({
|
|
1195
|
+
code: "custom",
|
|
1196
|
+
message: "impact.targets entries must have distinct names."
|
|
1197
|
+
});
|
|
1198
|
+
})
|
|
1199
|
+
}).strict();
|
|
1197
1200
|
const factoryProjectProfileSchema = z.object({
|
|
1198
1201
|
$schema: z.string().min(1).optional(),
|
|
1199
1202
|
extensions: z.record(z.string(), z.unknown()).optional(),
|
|
1200
1203
|
hq: hqIngestConfigSchema.optional(),
|
|
1201
1204
|
impact: impactConfigSchema.optional(),
|
|
1202
|
-
proof: z.object({ classificationPolicy: z.object({
|
|
1203
|
-
docsOnly: z.array(globSchema).default([]),
|
|
1204
|
-
trivial: z.array(globSchema).default([])
|
|
1205
|
-
}).strict() }).strict(),
|
|
1206
1205
|
repository: z.object({
|
|
1207
1206
|
name: z.string().min(1),
|
|
1208
1207
|
owner: z.string().min(1)
|
|
@@ -1213,9 +1212,18 @@ const factoryProjectProfileSchema = z.object({
|
|
|
1213
1212
|
code: "custom",
|
|
1214
1213
|
message: "requiredChecks entries must have distinct names."
|
|
1215
1214
|
});
|
|
1215
|
+
for (const [index, check] of checks.entries()) if (check.scope?.classifications.includes("trivial")) context.addIssue({
|
|
1216
|
+
code: "custom",
|
|
1217
|
+
message: `requiredChecks entry "${check.name}" scopes on classification "trivial", which the classifier no longer produces (#742); scope on "docs/process-only" or "non-trivial".`,
|
|
1218
|
+
path: [
|
|
1219
|
+
index,
|
|
1220
|
+
"scope",
|
|
1221
|
+
"classifications"
|
|
1222
|
+
]
|
|
1223
|
+
});
|
|
1216
1224
|
}).optional(),
|
|
1217
1225
|
review: z.object({ conditional: z.array(conditionalReviewSchema).min(1) }).strict().optional(),
|
|
1218
|
-
schemaVersion: z.literal(
|
|
1226
|
+
schemaVersion: z.literal(6),
|
|
1219
1227
|
verification: z.object({ commands: z.array(commandSchema).min(1) })
|
|
1220
1228
|
}).strict().superRefine((profile, context) => {
|
|
1221
1229
|
const commandNames = /* @__PURE__ */ new Set();
|
|
@@ -1307,16 +1315,19 @@ const PREFIX_NARROWING_NOTE = "note: prefix matching becomes glob matching, so a
|
|
|
1307
1315
|
const migratedPolicyLines = (policy, root) => {
|
|
1308
1316
|
const field = "proof.classificationPolicy";
|
|
1309
1317
|
const lines = [];
|
|
1310
|
-
const rename = (from,
|
|
1318
|
+
const rename = (from, translate) => {
|
|
1311
1319
|
const entries = stringList(policy[from]);
|
|
1312
|
-
if (from in policy) lines.push(`${field}.${from} ${quoteList(entries)} ->
|
|
1313
|
-
};
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
rename("
|
|
1318
|
-
rename("
|
|
1319
|
-
|
|
1320
|
+
if (from in policy) lines.push(`${field}.${from} ${quoteList(entries)} -> impact.inert ${quoteList(entries.map(translate))}`);
|
|
1321
|
+
};
|
|
1322
|
+
const dropTrivial = (from) => {
|
|
1323
|
+
if (from in policy) lines.push(`${field}.${from} ${quoteList(stringList(policy[from]))} -> ${TRIVIAL_GLOBS_DROPPED}`);
|
|
1324
|
+
};
|
|
1325
|
+
rename("docsOnlyFiles", unchanged);
|
|
1326
|
+
rename("docsOnlyPrefixes", migratedPrefixGlob(root));
|
|
1327
|
+
rename("docsOnlyMarkdownPrefixes", migratedMarkdownPrefixGlob);
|
|
1328
|
+
dropTrivial("trivialFiles");
|
|
1329
|
+
dropTrivial("trivialPrefixes");
|
|
1330
|
+
if ("docsOnlyPrefixes" in policy) lines.push(PREFIX_NARROWING_NOTE);
|
|
1320
1331
|
return lines;
|
|
1321
1332
|
};
|
|
1322
1333
|
/**
|
|
@@ -1351,6 +1362,17 @@ function hasCorrectnessOnlyReview(review) {
|
|
|
1351
1362
|
const { modes } = review;
|
|
1352
1363
|
return Object.keys(review).length > 0 && Object.keys(review).every((field) => field === "conditional" || field === "defaultMaxCycles" || field === "modes") && Array.isArray(modes) && modes.length > 0 && modes.every((mode) => mode === "correctness") && (review.conditional === void 0 || Array.isArray(review.conditional) && review.conditional.length === 0);
|
|
1353
1364
|
}
|
|
1365
|
+
const TRIVIAL_GLOBS_DROPPED = "removed; unowned paths verify in full. Declare a glob in impact.inert only when the path is deliberately repository-inert (docs battery).";
|
|
1366
|
+
const classificationPolicyFoldLines = (input) => {
|
|
1367
|
+
const proof = isRecord$2(input.proof) ? input.proof : void 0;
|
|
1368
|
+
if (!proof) return [];
|
|
1369
|
+
const lines = [];
|
|
1370
|
+
const policy = isRecord$2(proof.classificationPolicy) ? proof.classificationPolicy : void 0;
|
|
1371
|
+
if (policy && "docsOnly" in policy) lines.push(`proof.classificationPolicy.docsOnly ${quoteList(stringList(policy.docsOnly))} -> impact.inert (keep only globs for paths no impact target subscribes to)`);
|
|
1372
|
+
if (policy && "trivial" in policy) lines.push(`proof.classificationPolicy.trivial ${quoteList(stringList(policy.trivial))} -> ${TRIVIAL_GLOBS_DROPPED}`);
|
|
1373
|
+
lines.push("proof -> removed; the impact declarations own path classification. The docs/process-only rung now requires zero affected impact targets and every changed path matching impact.inert (#742).");
|
|
1374
|
+
return lines;
|
|
1375
|
+
};
|
|
1354
1376
|
const v4MigrationLines = (input) => {
|
|
1355
1377
|
const lines = [];
|
|
1356
1378
|
if (isRecord$2(input.project) && "key" in input.project) lines.push("project.key -> removed; current proof writers derive projectKey from repository.name");
|
|
@@ -1373,9 +1395,12 @@ const v4MigrationLines = (input) => {
|
|
|
1373
1395
|
function legacyProfileMigration(input, root) {
|
|
1374
1396
|
if (!(isRecord$2(input) && typeof input.schemaVersion === "number" && LEGACY_PROFILE_SCHEMA_VERSIONS.includes(input.schemaVersion))) return;
|
|
1375
1397
|
const foundVersion = input.schemaVersion;
|
|
1376
|
-
const lines = [`schemaVersion ${foundVersion} ->
|
|
1398
|
+
const lines = [`schemaVersion ${foundVersion} -> 6`];
|
|
1377
1399
|
if (foundVersion === 2) lines.push(...v2MigrationLines(input, root));
|
|
1378
|
-
lines.push(...v4MigrationLines(input));
|
|
1400
|
+
lines.push(...classificationPolicyFoldLines(input), ...v4MigrationLines(input));
|
|
1401
|
+
if (Array.isArray(input.requiredChecks)) {
|
|
1402
|
+
for (const [index, check] of input.requiredChecks.entries()) if (isRecord$2(check) && isRecord$2(check.scope) && stringList(check.scope.classifications).includes("trivial")) lines.push(`requiredChecks[${index}].scope.classifications "trivial" -> removed; scope on "docs/process-only" or "non-trivial"`);
|
|
1403
|
+
}
|
|
1379
1404
|
return {
|
|
1380
1405
|
foundVersion,
|
|
1381
1406
|
lines
|
|
@@ -1435,7 +1460,7 @@ function hqLaneRefBaseUrlFromProfile(profile) {
|
|
|
1435
1460
|
return hqLaneRefBaseUrl(profile.hq.endpoint);
|
|
1436
1461
|
}
|
|
1437
1462
|
function formatLegacyProfileError(profilePath, foundVersion, migration) {
|
|
1438
|
-
return [`Profile is invalid: ${profilePath}: schemaVersion ${foundVersion} is not supported; migrate to schemaVersion
|
|
1463
|
+
return [`Profile is invalid: ${profilePath}: schemaVersion ${foundVersion} is not supported; migrate to schemaVersion 6 (ADR 0023).`, ...migration.map((line) => ` ${line}`)].join("\n");
|
|
1439
1464
|
}
|
|
1440
1465
|
function formatProfileError(profilePath, error) {
|
|
1441
1466
|
return `Profile is invalid: ${profilePath}: ${error.issues.map((issue) => {
|
|
@@ -2396,6 +2421,15 @@ const NON_CONTENT_CLIENT_STATUSES = new Set([
|
|
|
2396
2421
|
408,
|
|
2397
2422
|
429
|
|
2398
2423
|
]);
|
|
2424
|
+
/**
|
|
2425
|
+
* HTTP 422 is HQ's schema validator refusing the content
|
|
2426
|
+
* (`invalid_ingest_event`): the same bytes produce the same refusal on every
|
|
2427
|
+
* retry under the current ingest schema (#739). Other content rejections stay
|
|
2428
|
+
* unclassified — a 400 or 404 may describe the route or the moment, not the
|
|
2429
|
+
* bytes — so only the one status HQ documents as a validation verdict counts
|
|
2430
|
+
* as permanent.
|
|
2431
|
+
*/
|
|
2432
|
+
const isPermanentRejection = (reason) => reason === "http-status-422";
|
|
2399
2433
|
const drainFailureStatus = (reason) => {
|
|
2400
2434
|
const status = Number(reason.replace("http-status-", ""));
|
|
2401
2435
|
return reason.startsWith("http-status-") && Number.isFinite(status) && status >= 400 && status < 500 && !NON_CONTENT_CLIENT_STATUSES.has(status) ? "rejected" : "unreachable";
|
|
@@ -2481,6 +2515,18 @@ const replayCloseoutSpool = async (layout, endpoint, clientId, clientSecret, req
|
|
|
2481
2515
|
}
|
|
2482
2516
|
attempted += 1;
|
|
2483
2517
|
const outcome = await attemptTransport(request, endpoint, clientId, clientSecret, body, Math.min(transportBudgetMs, remainingMs(replayDeadline)), report !== void 0);
|
|
2518
|
+
const permanent = !outcome.ok && isPermanentRejection(outcome.reason);
|
|
2519
|
+
if (permanent && options.evictPermanentRejections === true) {
|
|
2520
|
+
if (!await mutateBoundDirectory(spool, spoolIdentity, performance.now() + flushBudgetMs, async () => rename(claimPath, `${eventPath}${UNDELIVERABLE_SUFFIX}`))) return;
|
|
2521
|
+
report?.({
|
|
2522
|
+
detail: `${outcome.detail === void 0 ? outcome.reason : `${outcome.reason}: ${outcome.detail}`}; HQ refuses these bytes on every retry under its current ingest schema; dispositioned as ${path.basename(eventPath)}${UNDELIVERABLE_SUFFIX}`,
|
|
2523
|
+
eventId: entry.event.eventId,
|
|
2524
|
+
kind: entry.event.kind,
|
|
2525
|
+
spool,
|
|
2526
|
+
status: "undeliverable"
|
|
2527
|
+
});
|
|
2528
|
+
continue;
|
|
2529
|
+
}
|
|
2484
2530
|
if (report) {
|
|
2485
2531
|
const common = {
|
|
2486
2532
|
eventId: entry.event.eventId,
|
|
@@ -2493,6 +2539,7 @@ const replayCloseoutSpool = async (layout, endpoint, clientId, clientSecret, req
|
|
|
2493
2539
|
} : {
|
|
2494
2540
|
...common,
|
|
2495
2541
|
detail: outcome.detail === void 0 ? outcome.reason : `${outcome.reason}: ${outcome.detail}`,
|
|
2542
|
+
...permanent ? { permanent: true } : {},
|
|
2496
2543
|
status: drainFailureStatus(outcome.reason)
|
|
2497
2544
|
});
|
|
2498
2545
|
}
|
|
@@ -2982,6 +3029,7 @@ async function flushHqSpool(input, dependencies = {}) {
|
|
|
2982
3029
|
const spools = [];
|
|
2983
3030
|
for (const layout of layouts) {
|
|
2984
3031
|
await replayCloseoutSpool(layout, endpoint, input.clientId, input.clientSecret, request, transportBudgetMs, deadline, deadline, flushBudgetMs, void 0, {
|
|
3032
|
+
...input.evictPermanentRejections === true ? { evictPermanentRejections: true } : {},
|
|
2985
3033
|
maxEntries: Number.POSITIVE_INFINITY,
|
|
2986
3034
|
report: spoolReport
|
|
2987
3035
|
});
|
|
@@ -4834,7 +4882,7 @@ const boundaryCheckProofSchema = z.object({
|
|
|
4834
4882
|
boundary: z.string().min(1).optional(),
|
|
4835
4883
|
command: z.literal("patronage-factory boundary:check"),
|
|
4836
4884
|
coveredSet: z.array(z.object({
|
|
4837
|
-
issue: z.number().int().positive(),
|
|
4885
|
+
issue: z.number().int().positive().optional(),
|
|
4838
4886
|
pr: z.number().int().positive(),
|
|
4839
4887
|
sha: evidenceShaSchema,
|
|
4840
4888
|
state: z.enum(["merged", "open"]),
|
|
@@ -5956,6 +6004,167 @@ function createBoundaryCheckCommand(output, action) {
|
|
|
5956
6004
|
if (record.status !== "ready") throw new Error(`boundary:check refused: ${record.blockingReasons.join("; ")}`);
|
|
5957
6005
|
})));
|
|
5958
6006
|
}
|
|
6007
|
+
const COMMIT_SHA_PATTERN$1 = /^[0-9a-f]{40}$/iu;
|
|
6008
|
+
const ZERO_SHA_PATTERN$1 = /^0{40}$/u;
|
|
6009
|
+
const demandedTargets$1 = (profile, reason) => (profile?.impact?.targets ?? []).map(({ name }) => ({
|
|
6010
|
+
basis: reason,
|
|
6011
|
+
name,
|
|
6012
|
+
state: "demanded"
|
|
6013
|
+
}));
|
|
6014
|
+
const refusal$1 = ({ base, head, profile, reason }) => ({
|
|
6015
|
+
...base ? { baseSha: base } : {},
|
|
6016
|
+
basis: {
|
|
6017
|
+
changedFiles: [],
|
|
6018
|
+
classification: "refused",
|
|
6019
|
+
reasons: [reason]
|
|
6020
|
+
},
|
|
6021
|
+
decision: "refused",
|
|
6022
|
+
...head ? { headSha: head } : {},
|
|
6023
|
+
schemaVersion: 1,
|
|
6024
|
+
targets: demandedTargets$1(profile, reason),
|
|
6025
|
+
unsubscribedPaths: []
|
|
6026
|
+
});
|
|
6027
|
+
const sortKeys = (value) => {
|
|
6028
|
+
if (Array.isArray(value)) return value.map(sortKeys);
|
|
6029
|
+
if (typeof value === "object" && value !== null) return Object.fromEntries(Object.keys(value).toSorted().map((key) => [key, sortKeys(value[key])]));
|
|
6030
|
+
return value;
|
|
6031
|
+
};
|
|
6032
|
+
/**
|
|
6033
|
+
* Canonical JSON for content comparison; `undefined` when unparseable. Keys
|
|
6034
|
+
* are sorted recursively so a pure representation change (EOL, whitespace,
|
|
6035
|
+
* key order) never refuses; only different values do.
|
|
6036
|
+
*/
|
|
6037
|
+
const canonicalJson = (content) => {
|
|
6038
|
+
try {
|
|
6039
|
+
return JSON.stringify(sortKeys(JSON.parse(content)));
|
|
6040
|
+
} catch {
|
|
6041
|
+
return;
|
|
6042
|
+
}
|
|
6043
|
+
};
|
|
6044
|
+
const assertCommitIdentity$1 = (value, name) => {
|
|
6045
|
+
if (value === void 0 || !COMMIT_SHA_PATTERN$1.test(value) || ZERO_SHA_PATTERN$1.test(value)) throw new Error(`${name} must be a non-zero 40-character commit SHA`);
|
|
6046
|
+
return value.toLowerCase();
|
|
6047
|
+
};
|
|
6048
|
+
const assertReachableCommit$1 = (cwd, sha, name) => {
|
|
6049
|
+
try {
|
|
6050
|
+
runCapture("git", [
|
|
6051
|
+
"cat-file",
|
|
6052
|
+
"-e",
|
|
6053
|
+
`${sha}^{commit}`
|
|
6054
|
+
], cwd);
|
|
6055
|
+
} catch {
|
|
6056
|
+
throw new Error(`${name} commit ${sha} is not reachable in this checkout`);
|
|
6057
|
+
}
|
|
6058
|
+
};
|
|
6059
|
+
const writeGithubOutputs$1 = (outputPath, decision) => {
|
|
6060
|
+
appendFileSync(outputPath, `${[
|
|
6061
|
+
`decision=${decision.decision}`,
|
|
6062
|
+
`basis=${JSON.stringify(decision.basis)}`,
|
|
6063
|
+
`unsubscribed_paths=${JSON.stringify(decision.unsubscribedPaths)}`,
|
|
6064
|
+
...decision.targets.map((target) => `${productionImpactTargetOutput(target.name)}=${target.state}`)
|
|
6065
|
+
].join("\n")}\n`, "utf-8");
|
|
6066
|
+
};
|
|
6067
|
+
/**
|
|
6068
|
+
* Classify candidate impact from one exact base/head identity. The delta is
|
|
6069
|
+
* the merge-base-to-head diff, and the lockfile sides are read at exactly the
|
|
6070
|
+
* merge base and head — the same inputs `pr:verify` gives the impact stamp
|
|
6071
|
+
* for the same candidate commits. The result may withdraw hosted routing
|
|
6072
|
+
* work only; every doubt path returns a refused decision whose known targets
|
|
6073
|
+
* remain demanded.
|
|
6074
|
+
*/
|
|
6075
|
+
const runCandidateImpact = (args) => {
|
|
6076
|
+
const cwd = path.resolve(args.cwd ?? process.cwd());
|
|
6077
|
+
let profile;
|
|
6078
|
+
let profilePath;
|
|
6079
|
+
let decision;
|
|
6080
|
+
try {
|
|
6081
|
+
const { path: loadedPath, profile: loadedProfile } = loadProjectProfile({
|
|
6082
|
+
cwd,
|
|
6083
|
+
profilePath: args.profilePath
|
|
6084
|
+
});
|
|
6085
|
+
profile = loadedProfile;
|
|
6086
|
+
profilePath = loadedPath;
|
|
6087
|
+
const base = assertCommitIdentity$1(args.base, "base");
|
|
6088
|
+
const head = assertCommitIdentity$1(args.head, "head");
|
|
6089
|
+
assertReachableCommit$1(cwd, base, "base");
|
|
6090
|
+
assertReachableCommit$1(cwd, head, "head");
|
|
6091
|
+
const checkedOutHead = runCapture("git", ["rev-parse", "HEAD"], cwd).stdout.trim().toLowerCase();
|
|
6092
|
+
if (checkedOutHead !== head) throw new Error(`head commit ${head} is not the checked-out HEAD ${checkedOutHead}; refusing to mix commit identities with declarations from another tree`);
|
|
6093
|
+
const relativeProfilePath = path.relative(cwd, profilePath);
|
|
6094
|
+
const committedProfile = showFileAtRef(cwd, head, profilePath);
|
|
6095
|
+
if (committedProfile === void 0) throw new Error(`profile ${relativeProfilePath} is not readable at head commit ${head}; refusing to classify from declarations the head does not carry`);
|
|
6096
|
+
if (canonicalJson(committedProfile) === void 0 || canonicalJson(committedProfile) !== canonicalJson(readFileSync(profilePath, "utf-8"))) throw new Error(`profile ${relativeProfilePath} on disk differs from head commit ${head}; refusing to classify from declarations the head does not carry`);
|
|
6097
|
+
const mergeBase = runCapture("git", [
|
|
6098
|
+
"merge-base",
|
|
6099
|
+
base,
|
|
6100
|
+
head
|
|
6101
|
+
], cwd).stdout.trim().toLowerCase();
|
|
6102
|
+
if (!COMMIT_SHA_PATTERN$1.test(mergeBase)) throw new Error(`no merge base exists between base ${base} and head ${head}`);
|
|
6103
|
+
const changedFiles = [...new Set(filesFromNameStatus(runCapture("git", [
|
|
6104
|
+
"diff",
|
|
6105
|
+
"--find-renames",
|
|
6106
|
+
"--name-status",
|
|
6107
|
+
mergeBase,
|
|
6108
|
+
head
|
|
6109
|
+
], cwd).stdout))].toSorted();
|
|
6110
|
+
const stamp = computeImpactStamp({
|
|
6111
|
+
changedFiles,
|
|
6112
|
+
profile,
|
|
6113
|
+
profilePath: path.relative(cwd, profilePath),
|
|
6114
|
+
readLockfile: (side) => showFileAtRef(cwd, side === "base" ? mergeBase : head, path.join(cwd, LOCKFILE_PATH))
|
|
6115
|
+
});
|
|
6116
|
+
const usable = stamp.basis === "target-scoped";
|
|
6117
|
+
decision = {
|
|
6118
|
+
baseSha: base,
|
|
6119
|
+
basis: {
|
|
6120
|
+
changedFiles,
|
|
6121
|
+
classification: stamp.basis,
|
|
6122
|
+
reasons: stamp.reasons
|
|
6123
|
+
},
|
|
6124
|
+
decision: usable ? "usable" : "refused",
|
|
6125
|
+
headSha: head,
|
|
6126
|
+
mergeBaseSha: mergeBase,
|
|
6127
|
+
schemaVersion: 1,
|
|
6128
|
+
targets: stamp.targets.map((target) => ({
|
|
6129
|
+
basis: target.basis,
|
|
6130
|
+
name: target.name,
|
|
6131
|
+
state: usable && target.impact === "not-affected" ? "withdrawn" : "demanded"
|
|
6132
|
+
})),
|
|
6133
|
+
unsubscribedPaths: stamp.unsubscribedPaths
|
|
6134
|
+
};
|
|
6135
|
+
} catch (error) {
|
|
6136
|
+
decision = refusal$1({
|
|
6137
|
+
base: args.base,
|
|
6138
|
+
head: args.head,
|
|
6139
|
+
profile,
|
|
6140
|
+
reason: `candidate impact classification refused: ${error instanceof Error ? error.message : String(error)}`
|
|
6141
|
+
});
|
|
6142
|
+
}
|
|
6143
|
+
if (args.githubOutput) writeGithubOutputs$1(path.resolve(cwd, args.githubOutput), decision);
|
|
6144
|
+
if (args.githubSummary) appendFileSync(path.resolve(cwd, args.githubSummary), `### Candidate impact\n\n${candidateImpactSummaryLines(decision).join("\n")}\n`, "utf-8");
|
|
6145
|
+
return decision;
|
|
6146
|
+
};
|
|
6147
|
+
const candidateImpactSummaryLines = (decision) => decision.basis.classification === "refused" ? [
|
|
6148
|
+
"Candidate impact: refused; all routed work remains demanded.",
|
|
6149
|
+
...decision.basis.reasons,
|
|
6150
|
+
unsubscribedPathsLine(void 0, "classification was refused")
|
|
6151
|
+
] : [`Candidate impact: ${decision.basis.classification}; ${decision.targets.filter(({ state }) => state === "demanded").length}/${decision.targets.length} target(s) demanded.`, unsubscribedPathsLine(decision.unsubscribedPaths)];
|
|
6152
|
+
//#endregion
|
|
6153
|
+
//#region src/commands/candidate-impact.ts
|
|
6154
|
+
function createCandidateImpactCommand(output) {
|
|
6155
|
+
return markCwdOptionDefault(new Command("candidate:impact").description("Classify candidate targets from an exact pull-request base/head identity").option("--base <sha>", "candidate base commit SHA").option("--head <sha>", "candidate head commit SHA").option("--cwd <path>", "repository working directory", collectCwdOption).option("--profile <path>", "path to the project profile JSON file").option("--github-output <path>", "append stable decision outputs to a GitHub Actions output file").option("--github-summary <path>", "append a human-readable decision to a GitHub Actions summary file").option("--json", "print the complete decision as JSON").action((options) => {
|
|
6156
|
+
const decision = runCandidateImpact({
|
|
6157
|
+
base: options.base,
|
|
6158
|
+
cwd: resolveCwdOption(options.cwd),
|
|
6159
|
+
githubOutput: options.githubOutput,
|
|
6160
|
+
githubSummary: options.githubSummary,
|
|
6161
|
+
head: options.head,
|
|
6162
|
+
profilePath: options.profile
|
|
6163
|
+
});
|
|
6164
|
+
if (options.json) output.stdout.write(`${JSON.stringify(decision, null, 2)}\n`);
|
|
6165
|
+
else output.stdout.write(`${candidateImpactSummaryLines(decision).join("\n")}\n`);
|
|
6166
|
+
}));
|
|
6167
|
+
}
|
|
5959
6168
|
/** `tool` discriminator every emitted report carries. */
|
|
5960
6169
|
const CI_ANALYZE_TOOL = "psf-ci-analyze";
|
|
5961
6170
|
/** Cohort label for runs the caller did not assign to a cohort. */
|
|
@@ -9364,26 +9573,37 @@ function createFactoryCheckPublisher(output, dependencies = {}) {
|
|
|
9364
9573
|
}
|
|
9365
9574
|
//#endregion
|
|
9366
9575
|
//#region src/pr-classification.ts
|
|
9367
|
-
function
|
|
9368
|
-
|
|
9576
|
+
function ownershipForFile(profile, file) {
|
|
9577
|
+
if (file === "pnpm-lock.yaml") return { kind: "graph-input" };
|
|
9578
|
+
const subscribed = (profile.impact?.targets ?? []).find((target) => matchesAnyGlob(target.paths ?? [], file));
|
|
9579
|
+
if (subscribed) return {
|
|
9580
|
+
kind: "target",
|
|
9581
|
+
target: subscribed.name
|
|
9582
|
+
};
|
|
9583
|
+
if (matchesAnyGlob(profile.impact?.inert ?? [], file)) return { kind: "inert" };
|
|
9584
|
+
return { kind: "unowned" };
|
|
9369
9585
|
}
|
|
9370
|
-
function
|
|
9371
|
-
|
|
9372
|
-
}
|
|
9373
|
-
|
|
9374
|
-
|
|
9375
|
-
|
|
9376
|
-
|
|
9586
|
+
function reasonForOwnership(file, ownership) {
|
|
9587
|
+
switch (ownership.kind) {
|
|
9588
|
+
case "graph-input": return `${file}: dependency graph input; never inert`;
|
|
9589
|
+
case "inert": return `${file}: repository-inert ownership (impact.inert)`;
|
|
9590
|
+
case "target": return `${file}: affects product target "${ownership.target}"`;
|
|
9591
|
+
default: return `${file}: no declared owner; fail closed to non-trivial`;
|
|
9592
|
+
}
|
|
9377
9593
|
}
|
|
9378
9594
|
function classifyDiff(profile, files) {
|
|
9379
9595
|
const sortedFiles = [...files].toSorted();
|
|
9380
|
-
|
|
9381
|
-
|
|
9382
|
-
|
|
9383
|
-
reasons
|
|
9596
|
+
if (sortedFiles.length === 0) return {
|
|
9597
|
+
classification: "non-trivial",
|
|
9598
|
+
reasons: ["No changed files detected; defaulting to non-trivial verification."]
|
|
9384
9599
|
};
|
|
9385
|
-
|
|
9386
|
-
|
|
9600
|
+
const ownerships = sortedFiles.map((file) => ({
|
|
9601
|
+
file,
|
|
9602
|
+
ownership: ownershipForFile(profile, file)
|
|
9603
|
+
}));
|
|
9604
|
+
const reasons = ownerships.map(({ file, ownership }) => reasonForOwnership(file, ownership));
|
|
9605
|
+
if (ownerships.every(({ ownership }) => ownership.kind === "inert")) return {
|
|
9606
|
+
classification: "docs/process-only",
|
|
9387
9607
|
reasons
|
|
9388
9608
|
};
|
|
9389
9609
|
return {
|
|
@@ -9391,13 +9611,41 @@ function classifyDiff(profile, files) {
|
|
|
9391
9611
|
reasons
|
|
9392
9612
|
};
|
|
9393
9613
|
}
|
|
9614
|
+
/**
|
|
9615
|
+
* The candidate classification `pr:verify` records: path ownership gated by
|
|
9616
|
+
* the impact stamp's positive evidence. The docs/process-only rung requires a
|
|
9617
|
+
* `target-scoped` stamp with zero affected targets ON TOP of full inert
|
|
9618
|
+
* ownership — a conservative stamp (unreadable or unsupported lockfile sides,
|
|
9619
|
+
* no declared targets, computation doubt) refuses the reduced rung, so a
|
|
9620
|
+
* fail-closed stamp can never coexist with a reduced verification battery
|
|
9621
|
+
* (#742 review cycle 3).
|
|
9622
|
+
*
|
|
9623
|
+
* `classifyDiffForRun` below stays the pure path predicate. Its remaining
|
|
9624
|
+
* stamp-less consumer is pr-ready's docs-only DELTA check, where the delta
|
|
9625
|
+
* rides on an already fully verified baseline and a lockfile-bearing delta
|
|
9626
|
+
* already refuses through the graph-input rule.
|
|
9627
|
+
*/
|
|
9628
|
+
function classifyDiffForRunWithStamp(profile, files, stamp, options) {
|
|
9629
|
+
const base = classifyDiffForRun(profile, files, options);
|
|
9630
|
+
if (base.classification === "non-trivial") return base;
|
|
9631
|
+
if (stamp.basis !== "target-scoped") return {
|
|
9632
|
+
classification: "non-trivial",
|
|
9633
|
+
reasons: [...base.reasons, "impact stamp is conservative; the docs/process-only rung needs a target-scoped stamp with zero affected targets"]
|
|
9634
|
+
};
|
|
9635
|
+
const affected = stamp.targets.filter((target) => target.impact === "affected");
|
|
9636
|
+
if (affected.length > 0) return {
|
|
9637
|
+
classification: "non-trivial",
|
|
9638
|
+
reasons: [...base.reasons, `impact stamp records ${affected.length} affected target(s); forced non-trivial`]
|
|
9639
|
+
};
|
|
9640
|
+
return base;
|
|
9641
|
+
}
|
|
9394
9642
|
function classifyDiffForRun(profile, files, options) {
|
|
9395
9643
|
const relativeProfile = path.relative(options.cwd, options.profilePath).split(path.sep).join("/");
|
|
9396
9644
|
const base = classifyDiff(profile, files);
|
|
9397
9645
|
if (base.classification === "non-trivial") return base;
|
|
9398
9646
|
if (files.some((file) => file === relativeProfile || file === options.profilePath)) return {
|
|
9399
9647
|
classification: "non-trivial",
|
|
9400
|
-
reasons: [...base.reasons, `${relativeProfile}: edits the
|
|
9648
|
+
reasons: [...base.reasons, `${relativeProfile}: edits the ownership declarations; forced non-trivial`]
|
|
9401
9649
|
};
|
|
9402
9650
|
return base;
|
|
9403
9651
|
}
|
|
@@ -9957,11 +10205,29 @@ function runPrVerify(args, dependencies = {}) {
|
|
|
9957
10205
|
profilePath: args.profilePath
|
|
9958
10206
|
});
|
|
9959
10207
|
const files = git.changedFiles(cwd, args.base);
|
|
9960
|
-
const
|
|
10208
|
+
const relativeProfile = path.relative(cwd, profilePath).split(path.sep).join("/");
|
|
10209
|
+
const { headSha, mergeBaseSha: proofMergeBaseSha, patchId } = resolveCandidateIdentity({
|
|
10210
|
+
base: args.base,
|
|
10211
|
+
cwd,
|
|
10212
|
+
git
|
|
10213
|
+
});
|
|
10214
|
+
const readLockfileAtRef = git.showFileAtRef ?? showFileAtRef;
|
|
10215
|
+
let impactStamp;
|
|
10216
|
+
try {
|
|
10217
|
+
impactStamp = computeImpactStamp({
|
|
10218
|
+
changedFiles: files,
|
|
10219
|
+
profile,
|
|
10220
|
+
profilePath: relativeProfile,
|
|
10221
|
+
readLockfile: (side) => readLockfileAtRef(cwd, side === "base" ? proofMergeBaseSha : headSha, path.resolve(cwd, LOCKFILE_PATH))
|
|
10222
|
+
});
|
|
10223
|
+
} catch (error) {
|
|
10224
|
+
if (error instanceof ImpactStampConfigError) throw error;
|
|
10225
|
+
impactStamp = conservativeImpactStamp(profile.impact?.targets ?? [], [`impact stamp computation failed (${error instanceof Error ? error.message : String(error)}); fail closed to full impact`]);
|
|
10226
|
+
}
|
|
10227
|
+
const classification = classifyDiffForRunWithStamp(profile, files, impactStamp, {
|
|
9961
10228
|
cwd,
|
|
9962
10229
|
profilePath
|
|
9963
10230
|
});
|
|
9964
|
-
const relativeProfile = path.relative(cwd, profilePath).split(path.sep).join("/");
|
|
9965
10231
|
const rootSharedGlobs = [...DEFAULT_ROOT_SHARED_GLOBS, relativeProfile];
|
|
9966
10232
|
const context = createVerificationRunContext({
|
|
9967
10233
|
baseRef: args.base,
|
|
@@ -9989,24 +10255,6 @@ function runPrVerify(args, dependencies = {}) {
|
|
|
9989
10255
|
console.log(`pr:verify mode: ${resolvedMode}${args.mode === "auto" ? " (auto)" : ""}`);
|
|
9990
10256
|
console.log(`classification: ${classification.classification}`);
|
|
9991
10257
|
for (const line of context.runSummaryLines()) console.log(line);
|
|
9992
|
-
const { headSha, mergeBaseSha: proofMergeBaseSha, patchId } = resolveCandidateIdentity({
|
|
9993
|
-
base: args.base,
|
|
9994
|
-
cwd,
|
|
9995
|
-
git
|
|
9996
|
-
});
|
|
9997
|
-
const readLockfileAtRef = git.showFileAtRef ?? showFileAtRef;
|
|
9998
|
-
let impactStamp;
|
|
9999
|
-
try {
|
|
10000
|
-
impactStamp = computeImpactStamp({
|
|
10001
|
-
changedFiles: files,
|
|
10002
|
-
profile,
|
|
10003
|
-
profilePath: relativeProfile,
|
|
10004
|
-
readLockfile: (side) => readLockfileAtRef(cwd, side === "base" ? proofMergeBaseSha : headSha, path.resolve(cwd, LOCKFILE_PATH))
|
|
10005
|
-
});
|
|
10006
|
-
} catch (error) {
|
|
10007
|
-
if (error instanceof ImpactStampConfigError) throw error;
|
|
10008
|
-
impactStamp = conservativeImpactStamp(profile.impact?.targets ?? [], [`impact stamp computation failed (${error instanceof Error ? error.message : String(error)}); fail closed to full impact`]);
|
|
10009
|
-
}
|
|
10010
10258
|
for (const line of impactStampSummaryLines(impactStamp)) console.log(line);
|
|
10011
10259
|
const vetoedTargets = boundFailingCheckNames({
|
|
10012
10260
|
candidate: {
|
|
@@ -12451,6 +12699,7 @@ async function runHqFlush(args, dependencies = {}) {
|
|
|
12451
12699
|
clientId: resolution.credentials.clientId,
|
|
12452
12700
|
clientSecret: resolution.credentials.clientSecret,
|
|
12453
12701
|
endpoint: profile.hq.endpoint,
|
|
12702
|
+
...args.evictPermanent === true ? { evictPermanentRejections: true } : {},
|
|
12454
12703
|
...explicitDirectories,
|
|
12455
12704
|
repository
|
|
12456
12705
|
}, {
|
|
@@ -12462,7 +12711,7 @@ async function runHqFlush(args, dependencies = {}) {
|
|
|
12462
12711
|
status: "flushed"
|
|
12463
12712
|
};
|
|
12464
12713
|
}
|
|
12465
|
-
const outcomeLine = (outcome) => `[${outcome.status}] ${outcome.kind} ${outcome.eventId}${outcome.detail === void 0 ? "" : ` — ${outcome.detail}`}`;
|
|
12714
|
+
const outcomeLine = (outcome) => `[${outcome.status}${outcome.permanent === true ? ", permanent" : ""}] ${outcome.kind} ${outcome.eventId}${outcome.detail === void 0 ? "" : ` — ${outcome.detail}`}`;
|
|
12466
12715
|
const orphanLine = (orphan) => `ORPHAN SPOOL: ${orphan.directory} — ${orphan.unlistable > 0 && orphan.pending === 0 ? "could not be inspected" : `${orphan.pending} event(s)`}${orphan.oldestQueuedAt === void 0 ? "" : `, oldest ${orphan.oldestQueuedAt}`}`;
|
|
12467
12716
|
/**
|
|
12468
12717
|
* Orphan lines, plus the recovery instruction. This run did not drain these
|
|
@@ -12478,6 +12727,7 @@ function renderHqFlush(result) {
|
|
|
12478
12727
|
...result.outcomes.map(outcomeLine),
|
|
12479
12728
|
`delivered ${result.delivered}, duplicate ${result.duplicate}, rejected ${result.rejected}, undeliverable ${result.undeliverable}, unreachable ${result.unreachable}`,
|
|
12480
12729
|
...result.undeliverable > 0 ? [`${result.undeliverable} event(s) can never be delivered and were dispositioned in place, renamed with \`.undeliverable\` and left readable; they no longer count as work waiting.`] : [],
|
|
12730
|
+
...result.outcomes.some((outcome) => outcome.permanent === true && outcome.status === "rejected") ? ["Permanent rejection(s) above will fail identically on every retry; evict them with `psf hq:flush --evict-permanent`, or wait for an HQ ingest schema fix."] : [],
|
|
12481
12731
|
result.incomplete ? `INCOMPLETE: ${result.remaining} event(s) still spooled; the drain did not finish. Run hq:flush again.` : `spool drained: ${result.remaining} event(s) remain (rejections stay until HQ accepts them)`,
|
|
12482
12732
|
...orphanLines(result.orphans)
|
|
12483
12733
|
].join("\n")}\n`;
|
|
@@ -12503,10 +12753,11 @@ const hqFlushExitCode = (result) => {
|
|
|
12503
12753
|
//#endregion
|
|
12504
12754
|
//#region src/commands/hq-flush.ts
|
|
12505
12755
|
function createHqFlushCommand(output, action = runHqFlush) {
|
|
12506
|
-
return markCwdOptionDefault(new Command("hq:flush").description("Drain this repository's spooled HQ evidence and report every event; exits 1 when transport failed and 2 when the drain did not finish").option("--cwd <path>", "working directory to evaluate", collectCwdOption).option("--dir <path>", "drain an explicit spool directory instead of the default locations; repeatable", (value, previous = []) => [...previous, value]).option("--json", "print the flush result as JSON").option("--profile <path>", "path to the project profile JSON file").action(async (options) => {
|
|
12756
|
+
return markCwdOptionDefault(new Command("hq:flush").description("Drain this repository's spooled HQ evidence and report every event; exits 1 when transport failed and 2 when the drain did not finish").option("--cwd <path>", "working directory to evaluate", collectCwdOption).option("--dir <path>", "drain an explicit spool directory instead of the default locations; repeatable", (value, previous = []) => [...previous, value]).option("--evict-permanent", "disposition schema-permanent rejections (HTTP 422) in place with `.undeliverable` instead of retaining them").option("--json", "print the flush result as JSON").option("--profile <path>", "path to the project profile JSON file").action(async (options) => {
|
|
12507
12757
|
const result = await action({
|
|
12508
12758
|
cwd: resolveCwdOption(options.cwd),
|
|
12509
12759
|
...options.dir ? { dir: options.dir } : {},
|
|
12760
|
+
...options.evictPermanent ? { evictPermanent: true } : {},
|
|
12510
12761
|
json: Boolean(options.json),
|
|
12511
12762
|
...options.profile ? { profilePath: options.profile } : {}
|
|
12512
12763
|
});
|
|
@@ -16132,6 +16383,7 @@ function createProgram(options = {}) {
|
|
|
16132
16383
|
program.addCommand(createBoundaryCheckCommand(output, options.actions?.boundaryCheck));
|
|
16133
16384
|
program.addCommand(createPrVerifyCommand(output));
|
|
16134
16385
|
program.addCommand(createProductionImpactCommand(output));
|
|
16386
|
+
program.addCommand(createCandidateImpactCommand(output));
|
|
16135
16387
|
program.addCommand(createCloseoutCommand(output));
|
|
16136
16388
|
program.addCommand(createEpicPublishStructureCommand(output));
|
|
16137
16389
|
program.addCommand(createPrReviewCommand(output, options.actions?.prReview));
|
package/dist/schemas.d.ts
CHANGED
|
@@ -113,6 +113,32 @@ interface FollowUpAction {
|
|
|
113
113
|
declare const DIFF_CLASSIFICATIONS: readonly ["docs/process-only", "trivial", "non-trivial"];
|
|
114
114
|
type DiffClassification = (typeof DIFF_CLASSIFICATIONS)[number];
|
|
115
115
|
//#endregion
|
|
116
|
+
//#region src/impact-stamp.d.ts
|
|
117
|
+
/**
|
|
118
|
+
* The recorded stamp. `targets` satisfies the completeness invariant: every
|
|
119
|
+
* target the profile declares is present, each with its exact basis — never
|
|
120
|
+
* silently absent. `basis: "conservative"` means an unmodelled or unprovable
|
|
121
|
+
* input widened every target to full impact.
|
|
122
|
+
*/
|
|
123
|
+
declare const impactStampSchema: z.ZodObject<{
|
|
124
|
+
basis: z.ZodEnum<{
|
|
125
|
+
"target-scoped": "target-scoped";
|
|
126
|
+
conservative: "conservative";
|
|
127
|
+
}>;
|
|
128
|
+
reasons: z.ZodArray<z.ZodString>;
|
|
129
|
+
stampVersion: z.ZodLiteral<3>;
|
|
130
|
+
targets: z.ZodArray<z.ZodObject<{
|
|
131
|
+
basis: z.ZodString;
|
|
132
|
+
impact: z.ZodEnum<{
|
|
133
|
+
affected: "affected";
|
|
134
|
+
"not-affected": "not-affected";
|
|
135
|
+
}>;
|
|
136
|
+
name: z.ZodString;
|
|
137
|
+
}, z.core.$strip>>;
|
|
138
|
+
unsubscribedPaths: z.ZodArray<z.ZodString>;
|
|
139
|
+
}, z.core.$strip>;
|
|
140
|
+
type ImpactStamp = z.infer<typeof impactStampSchema>;
|
|
141
|
+
//#endregion
|
|
116
142
|
//#region src/pr-verify-mode.d.ts
|
|
117
143
|
/**
|
|
118
144
|
* The verification mode `pr:verify` resolved for a run.
|
|
@@ -484,32 +510,6 @@ interface PrReadyProof {
|
|
|
484
510
|
waivedDemands?: WaivedDemand[];
|
|
485
511
|
}
|
|
486
512
|
//#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
|
-
"target-scoped": "target-scoped";
|
|
497
|
-
conservative: "conservative";
|
|
498
|
-
}>;
|
|
499
|
-
reasons: z.ZodArray<z.ZodString>;
|
|
500
|
-
stampVersion: z.ZodLiteral<3>;
|
|
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
|
-
unsubscribedPaths: z.ZodArray<z.ZodString>;
|
|
510
|
-
}, z.core.$strip>;
|
|
511
|
-
type ImpactStamp = z.infer<typeof impactStampSchema>;
|
|
512
|
-
//#endregion
|
|
513
513
|
//#region src/pr-readiness/external-evidence.d.ts
|
|
514
514
|
declare const evidenceEnvelopeBaseSchema: z.ZodObject<{
|
|
515
515
|
check: z.ZodString;
|
|
@@ -950,7 +950,7 @@ declare const boundaryCheckProofSchema: z.ZodObject<{
|
|
|
950
950
|
boundary: z.ZodOptional<z.ZodString>;
|
|
951
951
|
command: z.ZodLiteral<"patronage-factory boundary:check">;
|
|
952
952
|
coveredSet: z.ZodArray<z.ZodObject<{
|
|
953
|
-
issue: z.ZodNumber
|
|
953
|
+
issue: z.ZodOptional<z.ZodNumber>;
|
|
954
954
|
pr: z.ZodNumber;
|
|
955
955
|
sha: z.ZodString;
|
|
956
956
|
state: z.ZodEnum<{
|
package/dist/schemas.js
CHANGED
|
@@ -943,7 +943,7 @@ const boundaryCheckProofSchema = z.object({
|
|
|
943
943
|
boundary: z.string().min(1).optional(),
|
|
944
944
|
command: z.literal("patronage-factory boundary:check"),
|
|
945
945
|
coveredSet: z.array(z.object({
|
|
946
|
-
issue: z.number().int().positive(),
|
|
946
|
+
issue: z.number().int().positive().optional(),
|
|
947
947
|
pr: z.number().int().positive(),
|
|
948
948
|
sha: evidenceShaSchema,
|
|
949
949
|
state: z.enum(["merged", "open"]),
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@patronage/software-factory",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.16",
|
|
4
4
|
"description": "Shared Patronage software factory CLI and project-profile validation tools",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -37,7 +37,7 @@
|
|
|
37
37
|
"picomatch": "^4.0.5",
|
|
38
38
|
"yaml": "^2.9.0",
|
|
39
39
|
"zod": "4.4.3",
|
|
40
|
-
"@patronage/factory-ci": "1.0.0-alpha.
|
|
40
|
+
"@patronage/factory-ci": "1.0.0-alpha.16"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@types/node": "24.13.3",
|
|
@@ -62,7 +62,8 @@
|
|
|
62
62
|
"prebuild": "bash ../scripts/ensure-worktree-bootstrap.sh",
|
|
63
63
|
"build": "tsdown",
|
|
64
64
|
"boundaries": "node scripts/package-boundaries.mjs",
|
|
65
|
-
"api:check": "pnpm build &&
|
|
65
|
+
"api:check": "pnpm build && pnpm api:check:prepared",
|
|
66
|
+
"api:check:prepared": "node scripts/public-api-surface.mjs",
|
|
66
67
|
"precli": "bash ../scripts/ensure-worktree-bootstrap.sh",
|
|
67
68
|
"cli": "tsx src/index.ts",
|
|
68
69
|
"cleanup:check": "knip --files --dependencies",
|