@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/CONTEXT.md +3 -3
- package/README.md +15 -7
- package/dist/index.d.ts +934 -766
- package/dist/index.js +8046 -7861
- package/dist/schemas.d.ts +228 -18
- package/dist/schemas.js +271 -2
- package/package.json +1 -1
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: "
|
|
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.
|
|
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.
|
|
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 };
|