@patronage/software-factory 0.20.0 → 0.23.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/schemas.d.ts CHANGED
@@ -1,5 +1,24 @@
1
1
  import { z } from "zod";
2
2
 
3
+ //#region src/demand-waiver.d.ts
4
+ /**
5
+ * A demand that was in force, was NOT met, and was waived by the operator.
6
+ *
7
+ * `unmetReasons` is required and non-empty: every reason the demand refused is
8
+ * carried through verbatim. There is no field on this record that could say
9
+ * "satisfied", and no code path constructs one without a refusal to carry.
10
+ */
11
+ interface WaivedDemand {
12
+ demand: string;
13
+ operator: string;
14
+ rationale: string;
15
+ recordedAt: string;
16
+ session: string;
17
+ /** The demand's refusals at evaluation time, preserved verbatim. */
18
+ unmetReasons: string[];
19
+ }
20
+ declare const waivedDemandSchema: z.ZodType<WaivedDemand>;
21
+ //#endregion
3
22
  //#region src/boundary-review-proof.d.ts
4
23
  declare const boundaryReviewProofBaseSchema: z.ZodObject<{
5
24
  boundary: z.ZodString;
@@ -64,20 +83,6 @@ interface FollowUpAction {
64
83
  command: string;
65
84
  }
66
85
  //#endregion
67
- //#region src/diff-classification.d.ts
68
- declare const DIFF_CLASSIFICATIONS: readonly ["docs/process-only", "trivial", "non-trivial"];
69
- type DiffClassification = (typeof DIFF_CLASSIFICATIONS)[number];
70
- //#endregion
71
- //#region src/pr-verify-mode.d.ts
72
- /**
73
- * The verification mode `pr:verify` resolved for a run.
74
- *
75
- * Canonically declared here rather than inside `pr-readiness/` so that modules
76
- * on either side of that boundary — the readiness proof shape and the durable
77
- * check-run payload — can name the same union without importing each other.
78
- */
79
- type ResolvedPrVerifyMode = "docs-only" | "trivial" | "full";
80
- //#endregion
81
86
  //#region src/merge-preflight/worktree-held-branch.d.ts
82
87
  /**
83
88
  * Detects when a PR head branch is checked out in a local git worktree.
@@ -164,12 +169,33 @@ interface PrMergeCheckProof {
164
169
  notices?: string[];
165
170
  pr: number;
166
171
  status: "pass" | "fail";
172
+ /**
173
+ * Demands that were in force, were NOT met, and were waived by the operator
174
+ * (#354). Each entry carries the demand's refusals verbatim, so a waived
175
+ * demand can never read as a met one; the merge proceeds on the recorded
176
+ * operator act, not on evidence.
177
+ */
178
+ waivedDemands?: WaivedDemand[];
167
179
  /** Present when the PR head branch is checked out in a local worktree. */
168
180
  worktreeHeldBranch?: WorktreeHeldBranch;
169
181
  /** All merge-relevant branches held by local worktrees (head and default). */
170
182
  worktreeHeldBranches?: WorktreeHeldBranch[];
171
183
  }
172
184
  //#endregion
185
+ //#region src/diff-classification.d.ts
186
+ declare const DIFF_CLASSIFICATIONS: readonly ["docs/process-only", "trivial", "non-trivial"];
187
+ type DiffClassification = (typeof DIFF_CLASSIFICATIONS)[number];
188
+ //#endregion
189
+ //#region src/pr-verify-mode.d.ts
190
+ /**
191
+ * The verification mode `pr:verify` resolved for a run.
192
+ *
193
+ * Canonically declared here rather than inside `pr-readiness/` so that modules
194
+ * on either side of that boundary — the readiness proof shape and the durable
195
+ * check-run payload — can name the same union without importing each other.
196
+ */
197
+ type ResolvedPrVerifyMode = "docs-only" | "trivial" | "full";
198
+ //#endregion
173
199
  //#region src/packages/review-prompt-sections/schema.d.ts
174
200
  declare const reviewPromptSectionSchema: z.ZodObject<{
175
201
  provenance: z.ZodEnum<{
@@ -286,7 +312,7 @@ interface PrReviewProof {
286
312
  cleanedPaths: string[];
287
313
  ladder?: PrReviewLadderState;
288
314
  reviewRequirement?: {
289
- reason: "docs-only-profile-bypass";
315
+ reason: "no-applicable-mode";
290
316
  status: "not-required";
291
317
  };
292
318
  reviews: PrReviewResult[];
@@ -720,9 +746,9 @@ declare const closeoutArtifactSchema: z.ZodObject<{
720
746
  unit: z.ZodEnum<{
721
747
  boolean: "boolean";
722
748
  text: "text";
749
+ count: "count";
723
750
  tokens: "tokens";
724
751
  usd: "usd";
725
- count: "count";
726
752
  ratio: "ratio";
727
753
  }>;
728
754
  value: z.ZodUnion<readonly [z.ZodNumber, z.ZodString, z.ZodBoolean, z.ZodNull]>;
@@ -732,6 +758,187 @@ declare const closeoutArtifactSchema: z.ZodObject<{
732
758
  }, z.core.$strict>;
733
759
  type CloseoutArtifact = z.infer<typeof closeoutArtifactSchema>;
734
760
  //#endregion
761
+ //#region src/retro-envelope.d.ts
762
+ /**
763
+ * Versioned retro envelope schema (epic #27 wave 2, issue #34).
764
+ *
765
+ * One envelope per lane, built at `factory:closeout` and delivered through the
766
+ * typed gate-sink as the `retro-envelope` ingest kind. Re-derived in TypeScript
767
+ * from the `spike/telemetry-layer2` S5 scratch schema (reference-only, never
768
+ * merged). This module is the single source of truth for the v1 wire shape
769
+ * and its bounds ({@link RETRO_ENVELOPE_WIRE_BOUNDS}) — producer and consumer
770
+ * alike. HQ imports these exports directly from the Worker-safe
771
+ * `@patronage/software-factory/schemas` subpath
772
+ * (`software-factory-hq/src/contracts/retro-schemas.ts`) instead of
773
+ * maintaining a parallel hand-written copy, so there is exactly one wire
774
+ * contract and no drift-detection machinery is needed (issue #350; formerly
775
+ * a hand-written twin plus a 767-line parity test, #46).
776
+ *
777
+ * DESIGN INVARIANT: cross-family token sums must be UNREPRESENTABLE.
778
+ *
779
+ * The two model families use different tokenizers, prices, and accounting
780
+ * conventions, so any token total that spans Claude and GPT is a lie:
781
+ *
782
+ * 1. There is no combined/total token field anywhere in the envelope.
783
+ * 2. `tokenFamilies` is strict — its only keys are `claude` and `gpt`; data
784
+ * cannot smuggle in a third "all"/"combined" slot.
785
+ * 3. The family BLOCKS are structurally different shapes with DIFFERENT keys
786
+ * (claude is a single flat block keyed on freshInput/cacheReadInput/
787
+ * cacheCreationInput; gpt is `{ roles: [...] }`). The exclusive input-tier
788
+ * COUNTS share no key name across families. The residual names shared
789
+ * between the claude block and a gpt ROLE entry are pinned to exactly
790
+ * {costUsd, model, output} (PR #32 advisory): `costUsd` is deliberate —
791
+ * USD is the one cross-family summable unit (rule 4); `model` is an
792
+ * unsummable label; `output` is the same name at DIFFERENT depths (lane
793
+ * block vs per-role entry), frozen by a tripwire test in
794
+ * `retro-envelope.test.ts` so the overlap cannot grow. Renaming `output`
795
+ * is a schemaVersion-2 wire change, deliberately not spent in v1.
796
+ * 4. Cost is per-family USD and nullable. Combined totals are allowed in USD
797
+ * only, and only as a projection-time sum of per-family USD.
798
+ *
799
+ * Field names also encode the S2/S3 reader lessons: Claude `freshInput` alone
800
+ * is not prompt size (true input context = freshInput + cacheReadInput +
801
+ * cacheCreationInput, requestId-deduped), and codex `inputInclusiveOfCache`
802
+ * already includes `cachedInput`, so `freshInputDerived` (inclusive − cached)
803
+ * is the only value safe to feed a per-token pricer.
804
+ *
805
+ * COMPLETENESS POSTURE: harvest may have no usable native log for a lane, so
806
+ * `tokenFamilies` may legitimately be absent. The closeout build gate demands
807
+ * a valid envelope, not available telemetry. A families-absent envelope keeps
808
+ * its operator-visible data gaps and is a replayable advisory HQ event, so it
809
+ * never substitutes unavailable usage with zero. The wire shape (field names,
810
+ * types, structure) stays byte-parity with HQ v1.
811
+ */
812
+ declare const RETRO_ENVELOPE_SCHEMA_VERSION = 1;
813
+ /**
814
+ * v1 wire bounds — the single source the schemas below are built from and the
815
+ * builder's sanitization seam clamps to (`retro-envelope-builder.ts` imports
816
+ * these; it keeps no bound constants of its own). HQ imports this module
817
+ * directly (issue #350), so there is one set of bounds, not a second copy to
818
+ * keep in sync.
819
+ */
820
+ declare const RETRO_ENVELOPE_WIRE_BOUNDS: {
821
+ /** archiveRef pointer (key/URL) max characters. */readonly archiveRefMaxChars: 2048; /** refs.branch max characters (git ref length ceiling). */
822
+ readonly branchMaxChars: 255; /** gates[] wire cap — the builder keeps the most recent records. */
823
+ readonly gatesMax: 500; /** gpt roles[] cap (delegated roles per lane). */
824
+ readonly gptRolesMax: 20; /** Cap for list fields: joinKeys id arrays, phases, dataGaps. */
825
+ readonly listMax: 100; /** Bounded name fields: agentRunId, gate, phase name, refs, join-key ids. */
826
+ readonly nameMaxChars: 200; /** Short identifier fields: repo name/owner, model, role. */
827
+ readonly shortMaxChars: 100; /** Free-text fields: dataGaps entries, outcome.verdict. */
828
+ readonly textMaxChars: 500;
829
+ };
830
+ declare const retroEnvelopeV1Schema: z.ZodObject<{
831
+ agentRunId: z.ZodString;
832
+ archiveRef: z.ZodOptional<z.ZodString>;
833
+ cycles: z.ZodObject<{
834
+ gateRunsToFirstGreen: z.ZodNumber;
835
+ reviewerFixRounds: z.ZodNumber;
836
+ thermoFixRounds: z.ZodNumber;
837
+ }, z.core.$strict>;
838
+ dataGaps: z.ZodDefault<z.ZodArray<z.ZodString>>;
839
+ gates: z.ZodArray<z.ZodObject<{
840
+ cycle: z.ZodNumber;
841
+ duration: z.ZodNumber;
842
+ gate: z.ZodString;
843
+ outcome: z.ZodEnum<{
844
+ pass: "pass";
845
+ fail: "fail";
846
+ skip: "skip";
847
+ }>;
848
+ startedAt: z.ZodISODateTime;
849
+ }, z.core.$strict>>;
850
+ generatedAt: z.ZodISODateTime;
851
+ interventions: z.ZodObject<{
852
+ count: z.ZodNumber;
853
+ }, z.core.$strict>;
854
+ joinKeys: z.ZodObject<{
855
+ claudeSessionIds: z.ZodDefault<z.ZodArray<z.ZodString>>;
856
+ codexThreadIds: z.ZodDefault<z.ZodArray<z.ZodString>>;
857
+ }, z.core.$strict>;
858
+ kind: z.ZodLiteral<"retro-envelope">;
859
+ outcome: z.ZodOptional<z.ZodObject<{
860
+ mergeCheck: z.ZodOptional<z.ZodEnum<{
861
+ pass: "pass";
862
+ fail: "fail";
863
+ "not-run": "not-run";
864
+ }>>;
865
+ status: z.ZodEnum<{
866
+ fail: "fail";
867
+ success: "success";
868
+ blocked: "blocked";
869
+ "ship-with-followups": "ship-with-followups";
870
+ }>;
871
+ verdict: z.ZodOptional<z.ZodString>;
872
+ }, z.core.$strict>>;
873
+ phases: z.ZodArray<z.ZodObject<{
874
+ at: z.ZodISODateTime;
875
+ deltaSec: z.ZodOptional<z.ZodNumber>;
876
+ name: z.ZodString;
877
+ }, z.core.$strict>>;
878
+ refs: z.ZodObject<{
879
+ branch: z.ZodOptional<z.ZodString>;
880
+ epic: z.ZodOptional<z.ZodString>;
881
+ headSha: z.ZodOptional<z.ZodString>;
882
+ issue: z.ZodOptional<z.ZodString>;
883
+ prNumber: z.ZodOptional<z.ZodNumber>;
884
+ }, z.core.$strict>;
885
+ repo: z.ZodObject<{
886
+ name: z.ZodString;
887
+ owner: z.ZodString;
888
+ }, z.core.$strict>;
889
+ schemaVersion: z.ZodLiteral<1>;
890
+ tokenFamilies: z.ZodObject<{
891
+ claude: z.ZodOptional<z.ZodObject<{
892
+ cacheCreationInput: z.ZodNumber;
893
+ cacheReadInput: z.ZodNumber;
894
+ costUsd: z.ZodNullable<z.ZodNumber>;
895
+ family: z.ZodLiteral<"claude">;
896
+ freshInput: z.ZodNumber;
897
+ model: z.ZodString;
898
+ output: z.ZodNumber;
899
+ requests: z.ZodNumber;
900
+ }, z.core.$strict>>;
901
+ gpt: z.ZodOptional<z.ZodObject<{
902
+ family: z.ZodLiteral<"gpt">;
903
+ roles: z.ZodArray<z.ZodObject<{
904
+ cachedInput: z.ZodNumber;
905
+ costUsd: z.ZodNullable<z.ZodNumber>;
906
+ freshInputDerived: z.ZodNumber;
907
+ inputInclusiveOfCache: z.ZodNumber;
908
+ model: z.ZodString;
909
+ output: z.ZodNumber;
910
+ reasoningOutput: z.ZodNumber;
911
+ role: z.ZodString;
912
+ threadId: z.ZodOptional<z.ZodString>;
913
+ }, z.core.$strict>>;
914
+ }, z.core.$strict>>;
915
+ }, z.core.$strict>;
916
+ wallClock: z.ZodObject<{
917
+ endTs: z.ZodISODateTime;
918
+ startTs: z.ZodISODateTime;
919
+ totalSec: z.ZodNumber;
920
+ }, z.core.$strict>;
921
+ }, z.core.$strict>;
922
+ type RetroEnvelope = z.infer<typeof retroEnvelopeV1Schema>;
923
+ /**
924
+ * Versioned payload validators, keyed by schema major. Unknown majors never
925
+ * reach these — {@link parseRetroEnvelope} returns them raw and marked
926
+ * degraded, mirroring HQ's ingest skew posture (stored raw, never dropped).
927
+ */
928
+ declare const RETRO_ENVELOPE_VALIDATORS: Record<number, z.ZodType<RetroEnvelope, unknown>>;
929
+ declare const SUPPORTED_RETRO_ENVELOPE_SCHEMA_VERSIONS: readonly number[];
930
+ type ParsedRetroEnvelope = {
931
+ disposition: "trusted";
932
+ envelope: RetroEnvelope;
933
+ schemaVersion: number;
934
+ } | {
935
+ disposition: "degraded";
936
+ raw: unknown;
937
+ schemaVersion: number | undefined;
938
+ };
939
+ /** Epic anchor for an envelope: refs.epic, else refs.issue, else the PR. */
940
+ declare const retroEpicReference: (refs: RetroEnvelope["refs"]) => string | undefined;
941
+ //#endregion
735
942
  //#region src/schemas.d.ts
736
943
  declare const EVIDENCE_ENVELOPE_SCHEMA_VERSION = 1;
737
944
  declare const evidenceEnvelopeSchema: z.ZodObject<{
@@ -865,7 +1072,9 @@ declare const prReviewProofSchema: z.ZodObject<{
865
1072
  patchId: z.ZodString;
866
1073
  reviewCycle: z.ZodOptional<z.ZodNumber>;
867
1074
  reviewRequirement: z.ZodOptional<z.ZodObject<{
868
- reason: z.ZodLiteral<"docs-only-profile-bypass">;
1075
+ reason: z.ZodEnum<{
1076
+ "no-applicable-mode": "no-applicable-mode";
1077
+ }>;
869
1078
  status: z.ZodLiteral<"not-required">;
870
1079
  }, z.core.$strict>>;
871
1080
  reviews: z.ZodArray<z.ZodObject<{
@@ -1097,6 +1306,7 @@ declare const prMergeCheckProofSchema: z.ZodObject<{
1097
1306
  pass: "pass";
1098
1307
  fail: "fail";
1099
1308
  }>;
1309
+ waivedDemands: z.ZodOptional<z.ZodArray<z.ZodType<WaivedDemand, unknown, z.core.$ZodTypeInternals<WaivedDemand, unknown>>>>;
1100
1310
  worktreeHeldBranch: z.ZodOptional<z.ZodObject<{
1101
1311
  branch: z.ZodString;
1102
1312
  worktreePath: z.ZodString;
@@ -1427,4 +1637,4 @@ declare const prReadyProofSchema: z.ZodObject<{
1427
1637
  }>;
1428
1638
  }, z.core.$strip>;
1429
1639
  //#endregion
1430
- export { BOUNDARY_CHECK_SCHEMA_VERSION, BOUNDARY_REVIEW_PROOF_KIND, BoundaryCheckProofRecord, type BoundaryReviewProof, type CloseoutArtifact, EVIDENCE_ENVELOPE_SCHEMA_VERSION, type EvidenceEnvelope, PR_MERGE_CHECK_SCHEMA_VERSION, PR_READY_SCHEMA_VERSION, PR_REVIEW_SCHEMA_VERSION, type PrMergeCheckProof, type PrReadyProof, type PrReviewProof, type PrVerifyProof, SUPPORTED_PR_VERIFY_SCHEMA_VERSIONS, boundaryCheckProofSchema, boundaryReviewProofSchema, closeoutArtifactSchema, evidenceEnvelopeSchema, prMergeCheckProofSchema, prReadyProofSchema, prReviewProofSchema, prVerifyProofSchema };
1640
+ export { BOUNDARY_CHECK_SCHEMA_VERSION, BOUNDARY_REVIEW_PROOF_KIND, BoundaryCheckProofRecord, type BoundaryReviewProof, type CloseoutArtifact, EVIDENCE_ENVELOPE_SCHEMA_VERSION, type EvidenceEnvelope, PR_MERGE_CHECK_SCHEMA_VERSION, PR_READY_SCHEMA_VERSION, PR_REVIEW_SCHEMA_VERSION, type ParsedRetroEnvelope, type PrMergeCheckProof, type PrReadyProof, type PrReviewProof, type PrVerifyProof, RETRO_ENVELOPE_SCHEMA_VERSION, RETRO_ENVELOPE_VALIDATORS, RETRO_ENVELOPE_WIRE_BOUNDS, type RetroEnvelope, SUPPORTED_PR_VERIFY_SCHEMA_VERSIONS, SUPPORTED_RETRO_ENVELOPE_SCHEMA_VERSIONS, type WaivedDemand, boundaryCheckProofSchema, boundaryReviewProofSchema, closeoutArtifactSchema, evidenceEnvelopeSchema, prMergeCheckProofSchema, prReadyProofSchema, prReviewProofSchema, prVerifyProofSchema, retroEnvelopeV1Schema, retroEpicReference, waivedDemandSchema };
package/dist/schemas.js CHANGED
@@ -1,4 +1,39 @@
1
1
  import { z } from "zod";
2
+ //#region src/demand-waiver.ts
3
+ const DEMAND_WAIVER_SCHEMA_VERSION = 1;
4
+ const shaSchema = z.string().regex(/^[0-9a-f]{40}$/u);
5
+ /**
6
+ * A resolved demand's key: a family, optionally qualified by the resolved
7
+ * value that makes it this demand *instance* (`review-rung:human`,
8
+ * `required-check:core`, `merge-freeze`). The consumer that resolved the
9
+ * demand owns the key; this module only matches on it, which is why no
10
+ * control name is enumerated here.
11
+ */
12
+ const demandKeySchema = z.string().regex(/^[a-z][a-z0-9-]*(?::[A-Za-z0-9._/-]+)?$/u, "A demand key is a lowercase family, optionally qualified by its resolved value (e.g. review-rung:human).");
13
+ const demandWaiverSchema = z.object({
14
+ candidate: z.object({
15
+ headSha: shaSchema,
16
+ pr: z.number().int().positive()
17
+ }),
18
+ demand: demandKeySchema,
19
+ operator: z.string().trim().min(1),
20
+ rationale: z.string().trim().min(1),
21
+ recordedAt: z.iso.datetime(),
22
+ session: z.string().trim().min(1)
23
+ });
24
+ z.object({
25
+ command: z.literal("patronage-factory demand:waive"),
26
+ schemaVersion: z.literal(DEMAND_WAIVER_SCHEMA_VERSION),
27
+ waivers: z.array(demandWaiverSchema)
28
+ });
29
+ const waivedDemandSchema = z.object({
30
+ demand: demandKeySchema,
31
+ operator: z.string().trim().min(1),
32
+ rationale: z.string().trim().min(1),
33
+ recordedAt: z.iso.datetime(),
34
+ session: z.string().trim().min(1),
35
+ unmetReasons: z.array(z.string().min(1)).min(1)
36
+ });
2
37
  const MAX_CLOSEOUT_ROWS = 1e3;
3
38
  const IdentifierSchema = z.string().min(1).max(500);
4
39
  const NarrativeSchema = z.string().min(1).max(1e4);
@@ -103,6 +138,239 @@ const closeoutArtifactSchema = z.object({
103
138
  schemaVersion: z.literal(3)
104
139
  }).strict();
105
140
  //#endregion
141
+ //#region src/retro-envelope.ts
142
+ /**
143
+ * Versioned retro envelope schema (epic #27 wave 2, issue #34).
144
+ *
145
+ * One envelope per lane, built at `factory:closeout` and delivered through the
146
+ * typed gate-sink as the `retro-envelope` ingest kind. Re-derived in TypeScript
147
+ * from the `spike/telemetry-layer2` S5 scratch schema (reference-only, never
148
+ * merged). This module is the single source of truth for the v1 wire shape
149
+ * and its bounds ({@link RETRO_ENVELOPE_WIRE_BOUNDS}) — producer and consumer
150
+ * alike. HQ imports these exports directly from the Worker-safe
151
+ * `@patronage/software-factory/schemas` subpath
152
+ * (`software-factory-hq/src/contracts/retro-schemas.ts`) instead of
153
+ * maintaining a parallel hand-written copy, so there is exactly one wire
154
+ * contract and no drift-detection machinery is needed (issue #350; formerly
155
+ * a hand-written twin plus a 767-line parity test, #46).
156
+ *
157
+ * DESIGN INVARIANT: cross-family token sums must be UNREPRESENTABLE.
158
+ *
159
+ * The two model families use different tokenizers, prices, and accounting
160
+ * conventions, so any token total that spans Claude and GPT is a lie:
161
+ *
162
+ * 1. There is no combined/total token field anywhere in the envelope.
163
+ * 2. `tokenFamilies` is strict — its only keys are `claude` and `gpt`; data
164
+ * cannot smuggle in a third "all"/"combined" slot.
165
+ * 3. The family BLOCKS are structurally different shapes with DIFFERENT keys
166
+ * (claude is a single flat block keyed on freshInput/cacheReadInput/
167
+ * cacheCreationInput; gpt is `{ roles: [...] }`). The exclusive input-tier
168
+ * COUNTS share no key name across families. The residual names shared
169
+ * between the claude block and a gpt ROLE entry are pinned to exactly
170
+ * {costUsd, model, output} (PR #32 advisory): `costUsd` is deliberate —
171
+ * USD is the one cross-family summable unit (rule 4); `model` is an
172
+ * unsummable label; `output` is the same name at DIFFERENT depths (lane
173
+ * block vs per-role entry), frozen by a tripwire test in
174
+ * `retro-envelope.test.ts` so the overlap cannot grow. Renaming `output`
175
+ * is a schemaVersion-2 wire change, deliberately not spent in v1.
176
+ * 4. Cost is per-family USD and nullable. Combined totals are allowed in USD
177
+ * only, and only as a projection-time sum of per-family USD.
178
+ *
179
+ * Field names also encode the S2/S3 reader lessons: Claude `freshInput` alone
180
+ * is not prompt size (true input context = freshInput + cacheReadInput +
181
+ * cacheCreationInput, requestId-deduped), and codex `inputInclusiveOfCache`
182
+ * already includes `cachedInput`, so `freshInputDerived` (inclusive − cached)
183
+ * is the only value safe to feed a per-token pricer.
184
+ *
185
+ * COMPLETENESS POSTURE: harvest may have no usable native log for a lane, so
186
+ * `tokenFamilies` may legitimately be absent. The closeout build gate demands
187
+ * a valid envelope, not available telemetry. A families-absent envelope keeps
188
+ * its operator-visible data gaps and is a replayable advisory HQ event, so it
189
+ * never substitutes unavailable usage with zero. The wire shape (field names,
190
+ * types, structure) stays byte-parity with HQ v1.
191
+ */
192
+ const RETRO_ENVELOPE_SCHEMA_VERSION = 1;
193
+ /**
194
+ * v1 wire bounds — the single source the schemas below are built from and the
195
+ * builder's sanitization seam clamps to (`retro-envelope-builder.ts` imports
196
+ * these; it keeps no bound constants of its own). HQ imports this module
197
+ * directly (issue #350), so there is one set of bounds, not a second copy to
198
+ * keep in sync.
199
+ */
200
+ const RETRO_ENVELOPE_WIRE_BOUNDS = {
201
+ /** archiveRef pointer (key/URL) max characters. */
202
+ archiveRefMaxChars: 2048,
203
+ /** refs.branch max characters (git ref length ceiling). */
204
+ branchMaxChars: 255,
205
+ /** gates[] wire cap — the builder keeps the most recent records. */
206
+ gatesMax: 500,
207
+ /** gpt roles[] cap (delegated roles per lane). */
208
+ gptRolesMax: 20,
209
+ /** Cap for list fields: joinKeys id arrays, phases, dataGaps. */
210
+ listMax: 100,
211
+ /** Bounded name fields: agentRunId, gate, phase name, refs, join-key ids. */
212
+ nameMaxChars: 200,
213
+ /** Short identifier fields: repo name/owner, model, role. */
214
+ shortMaxChars: 100,
215
+ /** Free-text fields: dataGaps entries, outcome.verdict. */
216
+ textMaxChars: 500
217
+ };
218
+ const nonNegInt = z.number().int().nonnegative();
219
+ const isoTimestamp = z.iso.datetime();
220
+ const boundedName = z.string().min(1).max(RETRO_ENVELOPE_WIRE_BOUNDS.nameMaxChars);
221
+ const boundedShortName = z.string().min(1).max(RETRO_ENVELOPE_WIRE_BOUNDS.shortMaxChars);
222
+ const boundedText = z.string().min(1).max(RETRO_ENVELOPE_WIRE_BOUNDS.textMaxChars);
223
+ const RetroRepoSchema = z.object({
224
+ name: boundedShortName,
225
+ owner: boundedShortName
226
+ }).strict();
227
+ const RetroRefsSchema = z.object({
228
+ branch: z.string().min(1).max(RETRO_ENVELOPE_WIRE_BOUNDS.branchMaxChars).optional(),
229
+ epic: boundedName.optional(),
230
+ headSha: z.string().regex(/^[0-9a-f]{7,40}$/u).optional(),
231
+ issue: boundedName.optional(),
232
+ prNumber: z.number().int().positive().optional()
233
+ }).strict();
234
+ /**
235
+ * Per-lane join keys: how the envelope re-joins raw per-session sources.
236
+ * `agentRunId` lives at the envelope root; these carry the per-family session
237
+ * identities (Claude `session.id`, codex `threadId`).
238
+ */
239
+ const RetroJoinKeysSchema = z.object({
240
+ claudeSessionIds: z.array(boundedName).max(RETRO_ENVELOPE_WIRE_BOUNDS.listMax).default([]),
241
+ codexThreadIds: z.array(boundedName).max(RETRO_ENVELOPE_WIRE_BOUNDS.listMax).default([])
242
+ }).strict();
243
+ const RetroWallClockSchema = z.object({
244
+ endTs: isoTimestamp,
245
+ startTs: isoTimestamp,
246
+ totalSec: z.number().nonnegative()
247
+ }).strict();
248
+ const RetroPhaseMarkSchema = z.object({
249
+ at: isoTimestamp,
250
+ deltaSec: z.number().nonnegative().optional(),
251
+ name: boundedName
252
+ }).strict();
253
+ /** Gate-timing ledger entry: `{gate, startedAt, duration, outcome, cycle}`. */
254
+ const RetroGateLedgerEntrySchema = z.object({
255
+ cycle: nonNegInt,
256
+ duration: z.number().nonnegative(),
257
+ gate: boundedName,
258
+ outcome: z.enum([
259
+ "pass",
260
+ "fail",
261
+ "skip"
262
+ ]),
263
+ startedAt: isoTimestamp
264
+ }).strict();
265
+ /**
266
+ * Claude family: Anthropic-style EXCLUSIVE input tiers. True input context is
267
+ * freshInput + cacheReadInput + cacheCreationInput; values are
268
+ * requestId-deduped (S2: naive row sums overcount input ~1.94x).
269
+ */
270
+ const claudeTokenBlockSchema = z.object({
271
+ cacheCreationInput: nonNegInt,
272
+ cacheReadInput: nonNegInt,
273
+ costUsd: z.number().nonnegative().nullable(),
274
+ family: z.literal("claude"),
275
+ freshInput: nonNegInt,
276
+ model: boundedShortName,
277
+ output: nonNegInt,
278
+ requests: nonNegInt
279
+ }).strict();
280
+ /**
281
+ * GPT/codex family, per delegated role. codex reports `input_tokens`
282
+ * INCLUSIVE of cached tokens; `freshInputDerived` = inclusive − cached is the
283
+ * only value safe for a per-token pricer (S3: skipping this overprices ~3.9x).
284
+ */
285
+ const gptRoleUsageSchema = z.object({
286
+ cachedInput: nonNegInt,
287
+ costUsd: z.number().nonnegative().nullable(),
288
+ freshInputDerived: nonNegInt,
289
+ inputInclusiveOfCache: nonNegInt,
290
+ model: boundedShortName,
291
+ output: nonNegInt,
292
+ reasoningOutput: nonNegInt,
293
+ role: boundedShortName,
294
+ threadId: boundedName.optional()
295
+ }).strict().refine((usage) => usage.freshInputDerived === usage.inputInclusiveOfCache - usage.cachedInput, { message: "freshInputDerived must equal inputInclusiveOfCache - cachedInput (S3 accounting rule)" });
296
+ const gptTokenBlockSchema = z.object({
297
+ family: z.literal("gpt"),
298
+ roles: z.array(gptRoleUsageSchema).min(1).max(RETRO_ENVELOPE_WIRE_BOUNDS.gptRolesMax)
299
+ }).strict();
300
+ /**
301
+ * Strict: only `claude` and `gpt`, both optional, no third slot.
302
+ *
303
+ * A lane may have no usable native session data. The build gate accepts that
304
+ * state and the advisory sink journals the valid envelope with its recorded
305
+ * gaps, without inventing usage.
306
+ */
307
+ const retroTokenFamiliesSchema = z.object({
308
+ claude: claudeTokenBlockSchema.optional(),
309
+ gpt: gptTokenBlockSchema.optional()
310
+ }).strict();
311
+ const RetroCycleCountersSchema = z.object({
312
+ gateRunsToFirstGreen: nonNegInt,
313
+ reviewerFixRounds: nonNegInt,
314
+ thermoFixRounds: nonNegInt
315
+ }).strict();
316
+ const RetroOutcomeSchema = z.object({
317
+ mergeCheck: z.enum([
318
+ "pass",
319
+ "fail",
320
+ "not-run"
321
+ ]).optional(),
322
+ status: z.enum([
323
+ "success",
324
+ "blocked",
325
+ "fail",
326
+ "ship-with-followups"
327
+ ]),
328
+ verdict: boundedText.optional()
329
+ }).strict();
330
+ const retroEnvelopeV1Schema = z.object({
331
+ agentRunId: boundedName,
332
+ /** Pointer (key/URL) to #8's durable proof archive — never the payload. */
333
+ archiveRef: z.string().min(1).max(RETRO_ENVELOPE_WIRE_BOUNDS.archiveRefMaxChars).optional(),
334
+ cycles: RetroCycleCountersSchema,
335
+ dataGaps: z.array(boundedText).max(RETRO_ENVELOPE_WIRE_BOUNDS.listMax).default([]),
336
+ gates: z.array(RetroGateLedgerEntrySchema).max(RETRO_ENVELOPE_WIRE_BOUNDS.gatesMax),
337
+ generatedAt: isoTimestamp,
338
+ interventions: z.object({ count: nonNegInt }).strict(),
339
+ joinKeys: RetroJoinKeysSchema,
340
+ kind: z.literal("retro-envelope"),
341
+ outcome: RetroOutcomeSchema.optional(),
342
+ phases: z.array(RetroPhaseMarkSchema).max(RETRO_ENVELOPE_WIRE_BOUNDS.listMax),
343
+ refs: RetroRefsSchema,
344
+ repo: RetroRepoSchema,
345
+ schemaVersion: z.literal(1),
346
+ tokenFamilies: retroTokenFamiliesSchema,
347
+ wallClock: RetroWallClockSchema
348
+ }).strict().refine((envelope) => envelope.refs.epic !== void 0 || envelope.refs.issue !== void 0 || envelope.refs.prNumber !== void 0, { message: "refs must anchor to an epic, issue, or PR" });
349
+ /**
350
+ * Version 1 envelopes written before #170 carried an inert `harness` label.
351
+ * Accept that exact historical field and normalize it away. The current strict
352
+ * schema above neither emits nor exposes it, and all other unknown fields still
353
+ * fail validation.
354
+ */
355
+ const retroEnvelopeV1ReaderSchema = z.preprocess((candidate) => {
356
+ if (candidate === null || typeof candidate !== "object" || Array.isArray(candidate) || !("harness" in candidate)) return candidate;
357
+ const record = candidate;
358
+ if (!boundedShortName.safeParse(record.harness).success) return candidate;
359
+ const { harness: _historicalHarness, ...current } = record;
360
+ return current;
361
+ }, retroEnvelopeV1Schema);
362
+ /**
363
+ * Versioned payload validators, keyed by schema major. Unknown majors never
364
+ * reach these — {@link parseRetroEnvelope} returns them raw and marked
365
+ * degraded, mirroring HQ's ingest skew posture (stored raw, never dropped).
366
+ */
367
+ const RETRO_ENVELOPE_VALIDATORS = { [1]: retroEnvelopeV1ReaderSchema };
368
+ const SUPPORTED_RETRO_ENVELOPE_SCHEMA_VERSIONS = Object.keys(RETRO_ENVELOPE_VALIDATORS).map(Number);
369
+ /** Epic anchor for an envelope: refs.epic, else refs.issue, else the PR. */
370
+ const retroEpicReference = (refs) => {
371
+ return (refs.epic ?? refs.issue ?? (refs.prNumber === void 0 ? void 0 : `pr#${refs.prNumber}`))?.replace(/^#/u, "");
372
+ };
373
+ //#endregion
106
374
  //#region src/schemas.ts
107
375
  const EVIDENCE_CHECK_TYPES = ["review", "verify"];
108
376
  const EVIDENCE_REVIEW_RUNGS = [
@@ -411,7 +679,7 @@ const prReviewProofSchema = z.object({
411
679
  patchId: z.string().regex(/^[0-9a-f]{40,64}$/u),
412
680
  reviewCycle: z.number().int().positive().optional(),
413
681
  reviewRequirement: z.object({
414
- reason: z.literal("docs-only-profile-bypass"),
682
+ reason: z.enum(["no-applicable-mode"]),
415
683
  status: z.literal("not-required")
416
684
  }).strict().optional(),
417
685
  reviews: z.array(prReviewResultSchema),
@@ -561,6 +829,7 @@ const prMergeCheckProofSchema = z.object({
561
829
  pr: z.number().int().positive(),
562
830
  schemaVersion: z.literal(1),
563
831
  status: z.enum(["pass", "fail"]),
832
+ waivedDemands: z.array(waivedDemandSchema).optional(),
564
833
  worktreeHeldBranch: z.object({
565
834
  branch: z.string().min(1),
566
835
  worktreePath: z.string().min(1)
@@ -768,4 +1037,4 @@ const prReadyProofSchema = z.object({
768
1037
  ])
769
1038
  });
770
1039
  //#endregion
771
- export { BOUNDARY_CHECK_SCHEMA_VERSION, BOUNDARY_REVIEW_PROOF_KIND, EVIDENCE_ENVELOPE_SCHEMA_VERSION, PR_MERGE_CHECK_SCHEMA_VERSION, PR_READY_SCHEMA_VERSION, PR_REVIEW_SCHEMA_VERSION, SUPPORTED_PR_VERIFY_SCHEMA_VERSIONS, boundaryCheckProofSchema, boundaryReviewProofSchema, closeoutArtifactSchema, evidenceEnvelopeSchema, prMergeCheckProofSchema, prReadyProofSchema, prReviewProofSchema, prVerifyProofSchema };
1040
+ export { BOUNDARY_CHECK_SCHEMA_VERSION, BOUNDARY_REVIEW_PROOF_KIND, EVIDENCE_ENVELOPE_SCHEMA_VERSION, PR_MERGE_CHECK_SCHEMA_VERSION, PR_READY_SCHEMA_VERSION, PR_REVIEW_SCHEMA_VERSION, RETRO_ENVELOPE_SCHEMA_VERSION, RETRO_ENVELOPE_VALIDATORS, RETRO_ENVELOPE_WIRE_BOUNDS, SUPPORTED_PR_VERIFY_SCHEMA_VERSIONS, SUPPORTED_RETRO_ENVELOPE_SCHEMA_VERSIONS, boundaryCheckProofSchema, boundaryReviewProofSchema, closeoutArtifactSchema, evidenceEnvelopeSchema, prMergeCheckProofSchema, prReadyProofSchema, prReviewProofSchema, prVerifyProofSchema, retroEnvelopeV1Schema, retroEpicReference, waivedDemandSchema };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@patronage/software-factory",
3
- "version": "0.20.0",
3
+ "version": "0.23.0",
4
4
  "description": "Shared Patronage software factory CLI and project-profile validation tools",
5
5
  "license": "MIT",
6
6
  "repository": {