ai-foreman 1.7.20 → 1.7.21

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.
Files changed (189) hide show
  1. package/README.md +12 -2
  2. package/dist/activity.d.ts +3 -0
  3. package/dist/activity.js +18 -6
  4. package/dist/adapters/claude.d.ts +16 -0
  5. package/dist/adapters/claude.js +28 -3
  6. package/dist/adapters/codex.d.ts +20 -0
  7. package/dist/adapters/codex.js +99 -5
  8. package/dist/adapters/recovering.d.ts +9 -0
  9. package/dist/adapters/recovering.js +10 -0
  10. package/dist/adapters/types.d.ts +15 -0
  11. package/dist/agentRun.d.ts +7 -0
  12. package/dist/agentRun.js +24 -13
  13. package/dist/branch/currentGuard.d.ts +1 -0
  14. package/dist/branch/currentGuard.js +1 -0
  15. package/dist/branch/finalization.js +6 -1
  16. package/dist/branch/git.d.ts +1 -0
  17. package/dist/branch/git.js +15 -1
  18. package/dist/branch/runner.d.ts +2 -0
  19. package/dist/branch/runner.js +90 -4
  20. package/dist/buildAdmission.js +2 -0
  21. package/dist/buildAssignment.d.ts +33 -0
  22. package/dist/buildAssignment.js +215 -0
  23. package/dist/buildInterventions.d.ts +90 -0
  24. package/dist/buildInterventions.js +334 -0
  25. package/dist/buildOwnershipReconciliation.d.ts +19 -0
  26. package/dist/buildOwnershipReconciliation.js +41 -0
  27. package/dist/buildOwnershipRepair.d.ts +33 -0
  28. package/dist/buildOwnershipRepair.js +111 -0
  29. package/dist/buildRuns.d.ts +6 -0
  30. package/dist/buildRuns.js +23 -17
  31. package/dist/buildSyntheticWork.d.ts +2 -0
  32. package/dist/buildSyntheticWork.js +30 -0
  33. package/dist/buildWorkAdmission.d.ts +17 -0
  34. package/dist/buildWorkAdmission.js +57 -0
  35. package/dist/buildWorkAuthorization.d.ts +2 -0
  36. package/dist/buildWorkAuthorization.js +23 -0
  37. package/dist/buildWorkContext.d.ts +3 -0
  38. package/dist/buildWorkContext.js +6 -0
  39. package/dist/buildWorkMigration.d.ts +9 -0
  40. package/dist/buildWorkMigration.js +206 -0
  41. package/dist/buildWorkReconciliation.d.ts +12 -0
  42. package/dist/buildWorkReconciliation.js +70 -0
  43. package/dist/builderGuidanceFollowup.d.ts +15 -0
  44. package/dist/builderGuidanceFollowup.js +34 -0
  45. package/dist/cli/doctor.js +3 -2
  46. package/dist/cli/graph.d.ts +2 -0
  47. package/dist/cli/graph.js +75 -0
  48. package/dist/cli/manager.d.ts +8 -0
  49. package/dist/cli/manager.js +234 -12
  50. package/dist/cli/recovery.js +3 -2
  51. package/dist/cli/runtimeAuthPrompt.js +28 -8
  52. package/dist/cli/start.js +152 -48
  53. package/dist/cli/status.js +20 -5
  54. package/dist/cli/tickets.js +9 -7
  55. package/dist/continuity.d.ts +11 -0
  56. package/dist/continuity.js +22 -6
  57. package/dist/foreman.d.ts +10 -2
  58. package/dist/foreman.js +330 -39
  59. package/dist/graph/boundary.d.ts +15 -0
  60. package/dist/graph/boundary.js +50 -0
  61. package/dist/graph/bridge.d.ts +9 -0
  62. package/dist/graph/bridge.js +146 -0
  63. package/dist/graph/bridge.py +205 -0
  64. package/dist/graph/config.d.ts +11 -0
  65. package/dist/graph/config.js +40 -0
  66. package/dist/graph/corpus.d.ts +16 -0
  67. package/dist/graph/corpus.js +188 -0
  68. package/dist/graph/derived.d.ts +17 -0
  69. package/dist/graph/derived.js +73 -0
  70. package/dist/graph/host.d.ts +4 -0
  71. package/dist/graph/host.js +160 -0
  72. package/dist/graph/install.d.ts +3 -0
  73. package/dist/graph/install.js +50 -0
  74. package/dist/graph/lifecycle.d.ts +11 -0
  75. package/dist/graph/lifecycle.js +124 -0
  76. package/dist/graph/maintenance.d.ts +43 -0
  77. package/dist/graph/maintenance.js +301 -0
  78. package/dist/graph/manager.d.ts +9 -0
  79. package/dist/graph/manager.js +92 -0
  80. package/dist/graph/preparation.d.ts +41 -0
  81. package/dist/graph/preparation.js +43 -0
  82. package/dist/graph/read.d.ts +27 -0
  83. package/dist/graph/read.js +152 -0
  84. package/dist/graph/semantic.d.ts +17 -0
  85. package/dist/graph/semantic.js +27 -0
  86. package/dist/graph/session.d.ts +8 -0
  87. package/dist/graph/session.js +92 -0
  88. package/dist/graph/sources.d.ts +4 -0
  89. package/dist/graph/sources.js +11 -0
  90. package/dist/graph/storage.d.ts +38 -0
  91. package/dist/graph/storage.js +159 -0
  92. package/dist/graph/turn.d.ts +17 -0
  93. package/dist/graph/turn.js +360 -0
  94. package/dist/graph/util.d.ts +5 -0
  95. package/dist/graph/util.js +51 -0
  96. package/dist/handoffs.d.ts +1 -0
  97. package/dist/handoffs.js +5 -1
  98. package/dist/humanDecision.d.ts +21 -2
  99. package/dist/humanDecision.js +169 -11
  100. package/dist/index.js +2 -0
  101. package/dist/installOwnership.d.ts +46 -0
  102. package/dist/installOwnership.js +440 -0
  103. package/dist/linuxProbeInventory.d.ts +12 -0
  104. package/dist/linuxProbeInventory.js +48 -0
  105. package/dist/log.d.ts +1 -1
  106. package/dist/log.js +7 -1
  107. package/dist/managerEvidence.d.ts +25 -0
  108. package/dist/managerEvidence.js +326 -0
  109. package/dist/managerEvidenceArtifacts.d.ts +38 -0
  110. package/dist/managerEvidenceArtifacts.js +118 -0
  111. package/dist/managerPacket.d.ts +2 -0
  112. package/dist/managerPacket.js +23 -0
  113. package/dist/markers.d.ts +1 -1
  114. package/dist/markers.js +2 -2
  115. package/dist/observability.js +1 -1
  116. package/dist/processIdentity.js +7 -0
  117. package/dist/providerPhase.d.ts +13 -0
  118. package/dist/providerPhase.js +26 -0
  119. package/dist/qaAuthorizedDisposition.d.ts +24 -0
  120. package/dist/qaAuthorizedDisposition.js +26 -0
  121. package/dist/qaBuildGate.d.ts +4 -0
  122. package/dist/qaBuildGate.js +216 -0
  123. package/dist/qaBuilderCoverage.d.ts +5 -0
  124. package/dist/qaBuilderCoverage.js +78 -0
  125. package/dist/qaContractCoverage.d.ts +10 -0
  126. package/dist/qaContractCoverage.js +55 -0
  127. package/dist/qaContractDelivery.d.ts +23 -0
  128. package/dist/qaContractDelivery.js +145 -0
  129. package/dist/qaContractFreshness.d.ts +15 -0
  130. package/dist/qaContractFreshness.js +36 -0
  131. package/dist/qaDeliveryJournal.d.ts +10 -0
  132. package/dist/qaDeliveryJournal.js +16 -1
  133. package/dist/qaEffectiveConfig.d.ts +20 -0
  134. package/dist/qaEffectiveConfig.js +40 -0
  135. package/dist/qaEvidenceReader.d.ts +14 -0
  136. package/dist/qaEvidenceReader.js +176 -0
  137. package/dist/qaFailureDelivery.d.ts +2 -0
  138. package/dist/qaFailureDelivery.js +107 -10
  139. package/dist/qaHandbackMigration.d.ts +1 -1
  140. package/dist/qaHandbackMigration.js +3 -3
  141. package/dist/qaPreparation.d.ts +26 -0
  142. package/dist/qaPreparation.js +150 -0
  143. package/dist/qaPreparationInputs.d.ts +8 -0
  144. package/dist/qaPreparationInputs.js +47 -0
  145. package/dist/qaPreparationMetrics.d.ts +145 -0
  146. package/dist/qaPreparationMetrics.js +120 -0
  147. package/dist/qaPreparationPolicy.d.ts +5 -0
  148. package/dist/qaPreparationPolicy.js +51 -0
  149. package/dist/qaPreparationStore.d.ts +157 -0
  150. package/dist/qaPreparationStore.js +412 -0
  151. package/dist/qaProtocolV2.d.ts +28 -0
  152. package/dist/qaProtocolV2.js +12 -0
  153. package/dist/qaReview.d.ts +6 -0
  154. package/dist/qaReview.js +234 -24
  155. package/dist/qaTimeline.d.ts +18 -0
  156. package/dist/qaTimeline.js +123 -0
  157. package/dist/qaVerificationContract.d.ts +17 -0
  158. package/dist/qaVerificationContract.js +236 -0
  159. package/dist/readinessCleanup.js +12 -5
  160. package/dist/recoveryGuidance.d.ts +9 -0
  161. package/dist/recoveryGuidance.js +13 -0
  162. package/dist/runtimeAuth.d.ts +1 -0
  163. package/dist/runtimeAuth.js +5 -1
  164. package/dist/runtimeReadiness.d.ts +1 -0
  165. package/dist/runtimeReadiness.js +13 -1
  166. package/dist/sessionIdentity.d.ts +2 -0
  167. package/dist/sessionIdentity.js +14 -0
  168. package/dist/sessionLifecycle.js +3 -3
  169. package/dist/sources/sourceRegistry.js +7 -0
  170. package/dist/stateTransfer.js +50 -1
  171. package/dist/statusReporter.d.ts +1 -0
  172. package/dist/statusReporter.js +1 -0
  173. package/dist/supervisedStart.js +18 -8
  174. package/dist/terminalStyle.d.ts +21 -0
  175. package/dist/terminalStyle.js +50 -0
  176. package/dist/ticketPopulation.d.ts +1 -0
  177. package/dist/ticketPopulation.js +5 -1
  178. package/dist/tickets/commands.js +2 -2
  179. package/dist/tickets/queue.js +1 -1
  180. package/dist/tickets/ticketSchema.d.ts +198 -0
  181. package/dist/tickets/ticketSchema.js +2 -0
  182. package/dist/workflowDb.d.ts +51 -5
  183. package/dist/workflowDb.js +338 -7
  184. package/dist/workflowReader.d.ts +109 -2
  185. package/dist/workflowReader.js +48 -4
  186. package/package.json +84 -7
  187. package/scripts/copy-graph-assets.mjs +3 -0
  188. package/scripts/graph-benchmark.mjs +20 -0
  189. package/scripts/graph-evaluation.mjs +85 -0
@@ -0,0 +1,145 @@
1
+ export type FindingCause = "disclosed-builder-miss" | "preparation-omission" | "implementation-defect" | "new-authorized-scope" | "outside-contract" | "unknown";
2
+ export interface FindingAssessmentV1 {
3
+ version: 1;
4
+ findingId: string;
5
+ contractDigest: string;
6
+ reviewDigest: string;
7
+ assessor: string;
8
+ independent: boolean;
9
+ cause: FindingCause;
10
+ explanation: string;
11
+ knowableBeforeImplementation: boolean | null;
12
+ actionableBeforeImplementation: boolean | null;
13
+ expectedCheckIds: string[];
14
+ originalInputEvidence: string[];
15
+ builderEvidence: string[];
16
+ scopeAuthorityEvidence: string[];
17
+ }
18
+ export declare function validateFindingAssessment(assessment: FindingAssessmentV1): void;
19
+ export interface QaMetricEvent {
20
+ version: 1;
21
+ eventId: string;
22
+ workKey: string;
23
+ admissionDigest: string;
24
+ at: string;
25
+ mode: "legacy" | "shadow" | "enforce";
26
+ level?: number;
27
+ cohort: string;
28
+ kind: "approved" | "implementation-started" | "preparation-failure" | "substantive-review" | "format-repair" | "provider-retry" | "remediation" | "amendment" | "completed" | "waived" | "cancelled" | "abandoned" | "finding-classification" | "quality-observation" | "sample" | "phase-observation";
29
+ status?: "passed" | "failed" | "blocked" | "not-run" | "disabled" | "advisory-only";
30
+ equivalentReviewData?: boolean;
31
+ phase?: "preparation" | "planning" | "challenge" | "builder" | "review" | "repair" | "remediation";
32
+ durationMs?: number;
33
+ costUsd?: number | null;
34
+ inputTokens?: number | null;
35
+ outputTokens?: number | null;
36
+ findingId?: string;
37
+ findingIds?: string[];
38
+ cause?: FindingCause;
39
+ outsideContract?: boolean;
40
+ classifier?: string;
41
+ evidenceRefs?: string[];
42
+ assessmentRef?: string;
43
+ confidence?: "supported" | "unknown";
44
+ corrects?: string;
45
+ observationDay?: 7 | 30;
46
+ reopened?: boolean;
47
+ escapedDefect?: boolean;
48
+ sample?: {
49
+ contractDigest: string;
50
+ reviewDigest: string;
51
+ assessor: string;
52
+ checkUsefulness: Array<{
53
+ checkId: string;
54
+ result: string;
55
+ duplicate?: boolean;
56
+ irrelevant?: boolean;
57
+ observation: string;
58
+ }>;
59
+ excessiveInvestigation?: string;
60
+ coverageAdequate: boolean;
61
+ };
62
+ }
63
+ export declare function validateFindingClassification(event: QaMetricEvent, resolveEvidence: (reference: string) => boolean): void;
64
+ /** Stable identities deduplicate replays; amendments do not reset work's first review. */
65
+ export declare function measureQaPreparation(events: QaMetricEvent[]): {
66
+ approved: number;
67
+ started: number;
68
+ comparableStarted: number;
69
+ excludedNonEquivalentReviews: number;
70
+ approvedNeverStarted: number;
71
+ preparationFailures: number;
72
+ firstReviews: number;
73
+ firstReviewPasses: number;
74
+ firstReviewPassRate: number | null;
75
+ endToEndFirstAttempt: number;
76
+ endToEndRate: number | null;
77
+ omissionWorks: number;
78
+ omissionRounds: number;
79
+ omissionIncidence: number | null;
80
+ omissionRoundsPerStarted: number | null;
81
+ causes: Record<FindingCause, number>;
82
+ statuses: Record<string, number>;
83
+ quality: {
84
+ day7: {
85
+ observed: number;
86
+ reopened: number;
87
+ escaped: number;
88
+ unknown: number;
89
+ };
90
+ day30: {
91
+ observed: number;
92
+ reopened: number;
93
+ escaped: number;
94
+ unknown: number;
95
+ };
96
+ };
97
+ phaseCosts: Record<string, {
98
+ knownUsd: number;
99
+ knownSamples: number;
100
+ unknownSamples: number;
101
+ durationMs: number;
102
+ }>;
103
+ outsideContractEvents: number;
104
+ formatRepairs: number;
105
+ providerRetries: number;
106
+ byMode: {
107
+ [k: string]: {
108
+ works: number;
109
+ started: number;
110
+ };
111
+ };
112
+ samples: {
113
+ eventId: string;
114
+ workKey: string;
115
+ mode: "legacy" | "shadow" | "enforce";
116
+ level: number | undefined;
117
+ cohort: string;
118
+ at: string;
119
+ costUsd: number | null;
120
+ durationMs: number | null;
121
+ evidenceRefs: string[];
122
+ contractDigest?: string | undefined;
123
+ reviewDigest?: string | undefined;
124
+ assessor?: string | undefined;
125
+ checkUsefulness?: {
126
+ checkId: string;
127
+ result: string;
128
+ duplicate?: boolean;
129
+ irrelevant?: boolean;
130
+ observation: string;
131
+ }[] | undefined;
132
+ excessiveInvestigation?: string;
133
+ coverageAdequate?: boolean | undefined;
134
+ }[];
135
+ cohortSizes: {
136
+ [k: string]: number;
137
+ };
138
+ };
139
+ export declare function relativeOmissionReduction(baseline: {
140
+ incidence: number | null;
141
+ cohort: string;
142
+ }, current: {
143
+ incidence: number | null;
144
+ cohort: string;
145
+ }): number | null;
@@ -0,0 +1,120 @@
1
+ export function validateFindingAssessment(assessment) {
2
+ if (assessment.version !== 1 || !assessment.findingId || !assessment.assessor || !assessment.explanation?.trim() || !assessment.contractDigest || !assessment.reviewDigest)
3
+ throw new Error("Finding assessment lacks retained scope, identity or explanation");
4
+ if (assessment.cause === "preparation-omission" && (!assessment.independent || assessment.knowableBeforeImplementation !== true || assessment.actionableBeforeImplementation !== true || !assessment.originalInputEvidence.length))
5
+ throw new Error("An omission requires independent evidence that the check was knowable and actionable before implementation");
6
+ if (assessment.cause === "disclosed-builder-miss" && (!assessment.expectedCheckIds.length || !assessment.builderEvidence.length))
7
+ throw new Error("Disclosed Builder miss requires the delivered check and Builder evidence");
8
+ if (assessment.cause === "new-authorized-scope" && !assessment.scopeAuthorityEvidence.length)
9
+ throw new Error("New scope requires retained authorization evidence");
10
+ }
11
+ export function validateFindingClassification(event, resolveEvidence) {
12
+ if (event.kind !== "finding-classification" || !event.findingId || !event.classifier || !event.cause)
13
+ throw new Error("Classification requires stable finding and classifier identities");
14
+ if (!event.evidenceRefs?.length || event.evidenceRefs.some(reference => !resolveEvidence(reference)))
15
+ throw new Error("Classification requires retained evidence");
16
+ if (event.cause !== "unknown" && event.confidence !== "supported")
17
+ throw new Error("Unsupported classification must remain unknown");
18
+ if (event.cause === "preparation-omission" && (!event.assessmentRef || !resolveEvidence(event.assessmentRef)))
19
+ throw new Error("Knowability/actionability omission requires independent assessment");
20
+ }
21
+ /** Stable identities deduplicate replays; amendments do not reset work's first review. */
22
+ export function measureQaPreparation(events) {
23
+ const unique = new Map();
24
+ for (const event of events) {
25
+ if (event.version !== 1 || !event.eventId || !event.workKey || !Number.isFinite(Date.parse(event.at)))
26
+ throw new Error("Invalid metric event");
27
+ const existing = unique.get(event.eventId);
28
+ if (existing && JSON.stringify(existing) !== JSON.stringify(event))
29
+ throw new Error("Conflicting metric replay");
30
+ unique.set(event.eventId, event);
31
+ }
32
+ const ordered = [...unique.values()].sort((left, right) => Date.parse(left.at) - Date.parse(right.at));
33
+ const works = new Map();
34
+ for (const event of ordered) {
35
+ const group = works.get(event.workKey) ?? [];
36
+ group.push(event);
37
+ works.set(event.workKey, group);
38
+ }
39
+ let approved = 0, started = 0, comparableStarted = 0, excludedNonEquivalentReviews = 0, approvedNeverStarted = 0, preparationFailures = 0, firstReviews = 0, firstReviewPasses = 0, endToEndFirstAttempt = 0, omissionWorks = 0, omissionRounds = 0;
40
+ const statuses = {}, causes = { "disclosed-builder-miss": 0, "preparation-omission": 0, "implementation-defect": 0, "new-authorized-scope": 0, "outside-contract": 0, unknown: 0 };
41
+ const quality = { day7: { observed: 0, reopened: 0, escaped: 0, unknown: 0 }, day30: { observed: 0, reopened: 0, escaped: 0, unknown: 0 } };
42
+ const phaseCosts = {};
43
+ for (const group of works.values()) {
44
+ const hasApproved = group.some(event => event.kind === "approved"), hasStarted = group.some(event => event.kind === "implementation-started");
45
+ if (hasApproved)
46
+ approved++;
47
+ if (hasStarted)
48
+ started++;
49
+ if (hasApproved && !hasStarted)
50
+ approvedNeverStarted++;
51
+ const comparable = group.some(event => event.mode === "enforce" || event.equivalentReviewData === true);
52
+ if (hasStarted && comparable)
53
+ comparableStarted++;
54
+ if (group.some(event => event.kind === "preparation-failure"))
55
+ preparationFailures++;
56
+ const firstObserved = group.find(event => event.kind === "substantive-review" && event.status !== "disabled" && event.status !== "advisory-only");
57
+ if (firstObserved && firstObserved.mode !== "enforce" && firstObserved.equivalentReviewData !== true)
58
+ excludedNonEquivalentReviews++;
59
+ const first = firstObserved && (firstObserved.mode === "enforce" || firstObserved.equivalentReviewData === true) ? firstObserved : undefined;
60
+ if (first) {
61
+ firstReviews++;
62
+ statuses[first.status ?? "unknown"] = (statuses[first.status ?? "unknown"] ?? 0) + 1;
63
+ if (first.status === "passed") {
64
+ firstReviewPasses++;
65
+ if (hasStarted && group.some(event => event.kind === "completed"))
66
+ endToEndFirstAttempt++;
67
+ }
68
+ }
69
+ const classifications = new Map();
70
+ for (const event of group)
71
+ if (event.kind === "finding-classification" && event.findingId)
72
+ classifications.set(event.findingId, event);
73
+ for (const event of classifications.values())
74
+ causes[event.cause ?? "unknown"]++;
75
+ const omissionFindings = new Set([...classifications].filter(([, event]) => event.cause === "preparation-omission" && event.confidence === "supported" && event.assessmentRef).map(([id]) => id));
76
+ const omissionCorrections = group.filter(event => event.kind === "remediation" && group.some(review => review.kind === "substantive-review" && Date.parse(review.at) > Date.parse(event.at) && review.status !== "disabled" && review.status !== "advisory-only") && [event.findingId, ...(event.findingIds ?? [])].some(id => id && omissionFindings.has(id)));
77
+ if (hasStarted && comparable && omissionCorrections.length) {
78
+ omissionWorks++;
79
+ omissionRounds += omissionCorrections.length;
80
+ }
81
+ const completion = group.find(event => event.kind === "completed");
82
+ if (completion)
83
+ for (const [day, field] of [[7, "day7"], [30, "day30"]]) {
84
+ const observation = group.find(event => event.kind === "quality-observation" && event.observationDay === day && Date.parse(event.at) >= Date.parse(completion.at) + day * 86400000 && typeof event.reopened === "boolean" && typeof event.escapedDefect === "boolean");
85
+ if (!observation)
86
+ quality[field].unknown++;
87
+ else {
88
+ quality[field].observed++;
89
+ if (observation.reopened)
90
+ quality[field].reopened++;
91
+ if (observation.escapedDefect)
92
+ quality[field].escaped++;
93
+ }
94
+ }
95
+ for (const event of group) {
96
+ if (["waived", "cancelled", "abandoned"].includes(event.kind))
97
+ statuses[event.kind] = (statuses[event.kind] ?? 0) + 1;
98
+ if (event.phase) {
99
+ const cost = phaseCosts[event.phase] ??= { knownUsd: 0, knownSamples: 0, unknownSamples: 0, durationMs: 0 };
100
+ cost.durationMs += event.durationMs ?? 0;
101
+ if (typeof event.costUsd === "number") {
102
+ cost.knownUsd += event.costUsd;
103
+ cost.knownSamples++;
104
+ }
105
+ else
106
+ cost.unknownSamples++;
107
+ }
108
+ }
109
+ }
110
+ const fraction = (n, d) => d ? n / d : null;
111
+ return { approved, started, comparableStarted, excludedNonEquivalentReviews, approvedNeverStarted, preparationFailures, firstReviews, firstReviewPasses, firstReviewPassRate: fraction(firstReviewPasses, firstReviews), endToEndFirstAttempt, endToEndRate: fraction(endToEndFirstAttempt, comparableStarted), omissionWorks, omissionRounds, omissionIncidence: fraction(omissionWorks, comparableStarted), omissionRoundsPerStarted: fraction(omissionRounds, comparableStarted), causes, statuses, quality, phaseCosts,
112
+ outsideContractEvents: [...unique.values()].filter(event => event.kind === "finding-classification" && event.outsideContract).length,
113
+ formatRepairs: ordered.filter(event => event.kind === "format-repair").length, providerRetries: ordered.filter(event => event.kind === "provider-retry").length,
114
+ byMode: Object.fromEntries(["legacy", "shadow", "enforce"].map(mode => [mode, { works: new Set(ordered.filter(event => event.mode === mode).map(event => event.workKey)).size, started: new Set(ordered.filter(event => event.mode === mode && event.kind === "implementation-started").map(event => event.workKey)).size }])), samples: ordered.filter(event => event.kind === "sample").map(event => ({ ...event.sample, eventId: event.eventId, workKey: event.workKey, mode: event.mode, level: event.level, cohort: event.cohort, at: event.at, costUsd: event.costUsd ?? null, durationMs: event.durationMs ?? null, evidenceRefs: event.evidenceRefs ?? [] })), cohortSizes: Object.fromEntries([...new Set(ordered.map(event => event.cohort))].map(cohort => [cohort, new Set(ordered.filter(event => event.cohort === cohort).map(event => event.workKey)).size])) };
115
+ }
116
+ export function relativeOmissionReduction(baseline, current) {
117
+ if (baseline.cohort !== current.cohort || baseline.incidence === null || baseline.incidence === 0 || current.incidence === null)
118
+ return null;
119
+ return (baseline.incidence - current.incidence) / baseline.incidence;
120
+ }
@@ -0,0 +1,5 @@
1
+ import { type QaPreparationConfigV1, type QaPreparationDepthDecisionV1, type QaPreparationLevel } from "rafi-spec";
2
+ export declare const DEPTH_OBLIGATIONS: readonly [readonly ["behavior-mapping", "prerequisites", "scope-boundaries"], readonly ["implementation-inspection", "test-inspection", "edge-cases", "verification-sequence"], readonly ["dependency-map", "failure-cases", "state-transitions", "regression-compatibility"], readonly ["consequential-invariants", "adversarial-cases", "concurrency-retry", "rollback-recovery"], readonly ["approach-proposal", "independent-challenge", "coordinated-verification"]];
3
+ export declare function depthObligations(level: QaPreparationLevel): string[];
4
+ export declare function resolveQaPreparationConfig(value: unknown): QaPreparationConfigV1;
5
+ export declare function validateDepthDecision(value: unknown, predecessor?: QaPreparationDepthDecisionV1): string[];
@@ -0,0 +1,51 @@
1
+ import { Ajv } from "ajv";
2
+ import { QA_PREPARATION_POLICY_VERSION, QA_RISK_MINIMUMS, qaPreparationDepthDecisionV1Schema } from "rafi-spec";
3
+ const validateDecision = new Ajv({ allErrors: true }).compile(qaPreparationDepthDecisionV1Schema);
4
+ export const DEPTH_OBLIGATIONS = [
5
+ ["behavior-mapping", "prerequisites", "scope-boundaries"],
6
+ ["implementation-inspection", "test-inspection", "edge-cases", "verification-sequence"],
7
+ ["dependency-map", "failure-cases", "state-transitions", "regression-compatibility"],
8
+ ["consequential-invariants", "adversarial-cases", "concurrency-retry", "rollback-recovery"],
9
+ ["approach-proposal", "independent-challenge", "coordinated-verification"],
10
+ ];
11
+ export function depthObligations(level) { return DEPTH_OBLIGATIONS.slice(0, level).flat(); }
12
+ export function resolveQaPreparationConfig(value) {
13
+ const defaults = { mode: "legacy", policyVersion: QA_PREPARATION_POLICY_VERSION, wallTimeMs: [300_000, 600_000, 1_200_000, 1_800_000, 2_700_000] };
14
+ if (value === undefined)
15
+ return defaults;
16
+ if (!value || typeof value !== "object" || Array.isArray(value))
17
+ throw new Error("qa_preparation must be an object");
18
+ const raw = value;
19
+ if (Object.keys(raw).some(key => !["mode", "policyVersion", "wallTimeMs"].includes(key)))
20
+ throw new Error("Unknown QA preparation policy option");
21
+ if (!["legacy", "shadow", "enforce"].includes(String(raw.mode)))
22
+ throw new Error("qa_preparation.mode must be legacy, shadow, or enforce");
23
+ if (raw.policyVersion !== undefined && raw.policyVersion !== defaults.policyVersion)
24
+ throw new Error("Unsupported QA preparation policy version");
25
+ if (raw.wallTimeMs !== undefined && (!Array.isArray(raw.wallTimeMs) || raw.wallTimeMs.length !== 5 || raw.wallTimeMs.some((v, i) => !Number.isSafeInteger(v) || v < defaults.wallTimeMs[i])))
26
+ throw new Error("QA preparation ceilings must specify five integer limits at least as large as policy defaults");
27
+ return { ...defaults, mode: raw.mode, ...(raw.wallTimeMs ? { wallTimeMs: raw.wallTimeMs } : {}) };
28
+ }
29
+ export function validateDepthDecision(value, predecessor) {
30
+ if (!validateDecision(value))
31
+ return (validateDecision.errors ?? []).map(error => `${error.instancePath} ${error.message}`);
32
+ const decision = value;
33
+ const issues = [];
34
+ const minimum = Math.max(2, ...decision.riskFactors.map(risk => QA_RISK_MINIMUMS[risk.category]));
35
+ if (!Number.isFinite(Date.parse(decision.timestamp)))
36
+ issues.push("Invalid planner timestamp");
37
+ if (decision.level === 1 && (!decision.lowRisk || decision.riskFactors.length))
38
+ issues.push("Focused requires explicit low-risk assessment and no consequential risks");
39
+ const effectiveMinimum = decision.level === 1 && decision.lowRisk && !decision.riskFactors.length ? 1 : minimum;
40
+ if (decision.minimumLevel !== effectiveMinimum || decision.level < effectiveMinimum)
41
+ issues.push(`Planner decision is below or misstates host minimum ${effectiveMinimum}`);
42
+ if (decision.riskFactors.some(risk => !decision.minimumReasons.includes(risk.category)))
43
+ issues.push("Minimum reasons must enumerate evidenced risk categories");
44
+ if (predecessor) {
45
+ if (decision.predecessorDecisionId !== predecessor.decisionId || decision.decisionId === predecessor.decisionId)
46
+ issues.push("Depth revision must retain its predecessor identity");
47
+ if (decision.level < predecessor.level && /(?:budget|cost|expens|provider.*fail|timeout|exhaust)/i.test(decision.rationale))
48
+ issues.push("Cost or provider failure cannot justify a downgrade");
49
+ }
50
+ return issues;
51
+ }
@@ -0,0 +1,157 @@
1
+ import { type FindingAssessmentV1, type QaMetricEvent } from "./qaPreparationMetrics.js";
2
+ import type Database from "better-sqlite3";
3
+ import type { QaPreparationConfigV1, QaPreparationDepthDecisionV1, QaPreparationLevel, QaVerificationContractV1, QaContractDeliveryReceiptV1 } from "rafi-spec";
4
+ export type PreparationState = "preparation-required" | "preparing" | "validating" | "ready" | "incomplete" | "blocked" | "uncertain" | "amendment-required" | "reconciling";
5
+ export interface LogicalPreparationBudget {
6
+ id: string;
7
+ runId: string;
8
+ workId: string;
9
+ admissionDigest: string;
10
+ policy: QaPreparationConfigV1;
11
+ startMs: number;
12
+ deadlineMs: number;
13
+ level: QaPreparationLevel;
14
+ reservations: Array<{
15
+ operationId: string;
16
+ kind: "planning" | "investigation" | "assessment" | "challenge" | "escalation" | "repair";
17
+ resultId?: string;
18
+ resultDigest?: string;
19
+ }>;
20
+ usage: {
21
+ inputTokens: number | null;
22
+ outputTokens: number | null;
23
+ costUsd: number | null;
24
+ };
25
+ extensions: Array<{
26
+ authorityId: string;
27
+ actor: string;
28
+ reason: string;
29
+ previousDeadlineMs: number;
30
+ deadlineMs: number;
31
+ }>;
32
+ caps?: Partial<Record<LogicalPreparationBudget["reservations"][number]["kind"], number>>;
33
+ }
34
+ export declare function migrateQaPreparation(db: Database.Database): void;
35
+ /** Tagged enforcing authority also fences older callers that omit new fields. */
36
+ export declare function migrateQaPreparationGuards(db: Database.Database): void;
37
+ /** Uses the existing workflow connection and caller's original lease fences. */
38
+ export declare class QaPreparationStore {
39
+ private readonly db;
40
+ private readonly assertOwner;
41
+ private readonly projectDir?;
42
+ constructor(db: Database.Database, assertOwner: (runId: string) => void, projectDir?: string | undefined);
43
+ private protectedJson;
44
+ private decode;
45
+ private atomic;
46
+ equivalentResolver(): import("./qaAuthorizedDisposition.js").EquivalentAuthorityResolver;
47
+ metric(runId: string, workId: string, admissionDigest: string, eventId: string, kind: QaMetricEvent["kind"], details?: Partial<QaMetricEvent>): void;
48
+ findingAssessment(runId: string, workId: string, admissionDigest: string, assessment: FindingAssessmentV1, evidenceRefs: string[]): string;
49
+ eventRecord<T>(eventId: string): T | undefined;
50
+ metrics(runId: string): {
51
+ approved: number;
52
+ started: number;
53
+ comparableStarted: number;
54
+ excludedNonEquivalentReviews: number;
55
+ approvedNeverStarted: number;
56
+ preparationFailures: number;
57
+ firstReviews: number;
58
+ firstReviewPasses: number;
59
+ firstReviewPassRate: number | null;
60
+ endToEndFirstAttempt: number;
61
+ endToEndRate: number | null;
62
+ omissionWorks: number;
63
+ omissionRounds: number;
64
+ omissionIncidence: number | null;
65
+ omissionRoundsPerStarted: number | null;
66
+ causes: Record<import("./qaPreparationMetrics.js").FindingCause, number>;
67
+ statuses: Record<string, number>;
68
+ quality: {
69
+ day7: {
70
+ observed: number;
71
+ reopened: number;
72
+ escaped: number;
73
+ unknown: number;
74
+ };
75
+ day30: {
76
+ observed: number;
77
+ reopened: number;
78
+ escaped: number;
79
+ unknown: number;
80
+ };
81
+ };
82
+ phaseCosts: Record<string, {
83
+ knownUsd: number;
84
+ knownSamples: number;
85
+ unknownSamples: number;
86
+ durationMs: number;
87
+ }>;
88
+ outsideContractEvents: number;
89
+ formatRepairs: number;
90
+ providerRetries: number;
91
+ byMode: {
92
+ [k: string]: {
93
+ works: number;
94
+ started: number;
95
+ };
96
+ };
97
+ samples: {
98
+ eventId: string;
99
+ workKey: string;
100
+ mode: "legacy" | "shadow" | "enforce";
101
+ level: number | undefined;
102
+ cohort: string;
103
+ at: string;
104
+ costUsd: number | null;
105
+ durationMs: number | null;
106
+ evidenceRefs: string[];
107
+ contractDigest?: string | undefined;
108
+ reviewDigest?: string | undefined;
109
+ assessor?: string | undefined;
110
+ checkUsefulness?: {
111
+ checkId: string;
112
+ result: string;
113
+ duplicate?: boolean;
114
+ irrelevant?: boolean;
115
+ observation: string;
116
+ }[] | undefined;
117
+ excessiveInvestigation?: string;
118
+ coverageAdequate?: boolean | undefined;
119
+ }[];
120
+ cohortSizes: {
121
+ [k: string]: number;
122
+ };
123
+ };
124
+ policy(runId: string): QaPreparationConfigV1 | undefined;
125
+ freezePolicy(runId: string, policy: QaPreparationConfigV1): QaPreparationConfigV1;
126
+ budget(runId: string, workId: string, admissionDigest: string): LogicalPreparationBudget | undefined;
127
+ ensureBudget(runId: string, workId: string, admissionDigest: string, level: QaPreparationLevel, nowMs: number): LogicalPreparationBudget;
128
+ reserve(budget: LogicalPreparationBudget, operationId: string, kind: LogicalPreparationBudget["reservations"][number]["kind"], intent: unknown, nowMs: number, resultId?: string): void;
129
+ retainResult(budget: LogicalPreparationBudget, operationId: string, result: unknown, usage?: Partial<LogicalPreparationBudget["usage"]>): string;
130
+ reviseDepth(budget: LogicalPreparationBudget, decision: QaPreparationDepthDecisionV1): void;
131
+ retainProgress(budget: LogicalPreparationBudget, stage: string, value: unknown): void;
132
+ beginAmendment(budget: LogicalPreparationBudget, inventory: import("rafi-spec").RequirementRef[], authority: {
133
+ reason: string;
134
+ inputDigest: string;
135
+ predecessorDigest: string;
136
+ }): void;
137
+ progress<T>(budget: LogicalPreparationBudget, stage: string): T | undefined;
138
+ /** Explicit operator action against an answered, exactly scoped durable decision. */
139
+ applyAnsweredExtensions(budget: LogicalPreparationBudget): void;
140
+ extendBudget(budget: LogicalPreparationBudget, decisionId: string): void;
141
+ putArtifact(kind: string, value: unknown): string;
142
+ artifact<T>(digest: string, kind: string): T;
143
+ head(runId: string, workId: string, admissionDigest: string): {
144
+ digest?: string;
145
+ generation: number;
146
+ state: PreparationState;
147
+ detail?: string;
148
+ };
149
+ contract(digest: string): QaVerificationContractV1;
150
+ setState(runId: string, workId: string, admissionDigest: string, state: PreparationState, detail: string, expectedGeneration: number): void;
151
+ publish(contract: QaVerificationContractV1, expectedGeneration: number, nowMs: number): void;
152
+ receipt(receipt: QaContractDeliveryReceiptV1): void;
153
+ receipts(digest: string): QaContractDeliveryReceiptV1[];
154
+ event(runId: string, workId: string, eventId: string, kind: string, value: unknown): void;
155
+ private saveBudget;
156
+ private read;
157
+ }