@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 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 = 5;
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<5>;
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: "review" | "code";
3448
+ purpose: "code" | "review";
3438
3449
  slotResolution: {
3439
- effort: "high" | "medium" | "low" | "xhigh";
3450
+ effort: "high" | "low" | "medium" | "xhigh";
3440
3451
  engine: string;
3441
3452
  layerSource: "committedDefault" | "operatorSession" | "userConfig";
3442
3453
  model: string;
3443
- tier: "review" | "delegate" | "oracle" | "workhorse" | "scout";
3454
+ tier: "oracle" | "review" | "delegate" | "workhorse" | "scout";
3444
3455
  fallbackTier?: "delegate" | undefined;
3445
3456
  };
3446
3457
  startedAt: string;
3447
- tier: "review" | "delegate" | "oracle" | "workhorse" | "scout";
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: "trivial" | "docs/process-only" | "non-trivial";
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: ("trivial" | "docs/process-only" | "non-trivial")[];
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" | "medium" | "low" | undefined;
3830
- highestOpenSeverity?: "unknown" | "critical" | "high" | "medium" | "low" | undefined;
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-complete" | "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.15";
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 v5 replaced. Not loadable — {@link legacyProfileMigration} turns
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({ targets: z.array(impactTargetSchema).min(1).superRefine((targets, context) => {
1191
- const names = targets.map((target) => target.name);
1192
- if (new Set(names).size !== names.length) context.addIssue({
1193
- code: "custom",
1194
- message: "impact.targets entries must have distinct names."
1195
- });
1196
- }) }).strict();
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(5),
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, to, translate) => {
1318
+ const rename = (from, translate) => {
1311
1319
  const entries = stringList(policy[from]);
1312
- if (from in policy) lines.push(`${field}.${from} ${quoteList(entries)} -> ${field}.${to} ${quoteList(entries.map(translate))}`);
1313
- };
1314
- rename("docsOnlyFiles", "docsOnly", unchanged);
1315
- rename("docsOnlyPrefixes", "docsOnly", migratedPrefixGlob(root));
1316
- rename("docsOnlyMarkdownPrefixes", "docsOnly", migratedMarkdownPrefixGlob);
1317
- rename("trivialFiles", "trivial", unchanged);
1318
- rename("trivialPrefixes", "trivial", migratedPrefixGlob(root));
1319
- if (["docsOnlyPrefixes", "trivialPrefixes"].some((from) => from in policy)) lines.push(PREFIX_NARROWING_NOTE);
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} -> 5`];
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 5 (ADR 0023).`, ...migration.map((line) => ` ${line}`)].join("\n");
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 isDocsOnlyFile(profile, file) {
9368
- return matchesAnyGlob(profile.proof.classificationPolicy.docsOnly, file);
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 isTrivialFile(profile, file) {
9371
- return isDocsOnlyFile(profile, file) || matchesAnyGlob(profile.proof.classificationPolicy.trivial, file);
9372
- }
9373
- function reasonForFile(profile, file) {
9374
- if (isDocsOnlyFile(profile, file)) return `${file}: docs/process file`;
9375
- if (isTrivialFile(profile, file)) return `${file}: trivial factory-support file`;
9376
- return `${file}: implementation-affecting file`;
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
- const reasons = sortedFiles.length > 0 ? sortedFiles.map((file) => reasonForFile(profile, file)) : ["No changed files detected; defaulting to non-trivial verification."];
9381
- if (sortedFiles.length > 0 && sortedFiles.every((file) => isDocsOnlyFile(profile, file))) return {
9382
- classification: "docs/process-only",
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
- if (sortedFiles.length > 0 && sortedFiles.every((file) => isTrivialFile(profile, file))) return {
9386
- classification: "trivial",
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 classification policy; forced non-trivial`]
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 classification = classifyDiffForRun(profile, files, {
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.15",
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.15"
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 && node scripts/public-api-surface.mjs",
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",