ai-foreman 1.7.19 → 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 (205) hide show
  1. package/README.md +29 -5
  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 +92 -5
  20. package/dist/buildAdmission.d.ts +61 -0
  21. package/dist/buildAdmission.js +286 -0
  22. package/dist/buildApproval.d.ts +2 -0
  23. package/dist/buildApproval.js +17 -0
  24. package/dist/buildAssignment.d.ts +33 -0
  25. package/dist/buildAssignment.js +215 -0
  26. package/dist/buildInterventions.d.ts +90 -0
  27. package/dist/buildInterventions.js +334 -0
  28. package/dist/buildLaunchGate.d.ts +1 -0
  29. package/dist/buildLaunchGate.js +36 -0
  30. package/dist/buildOwnershipReconciliation.d.ts +19 -0
  31. package/dist/buildOwnershipReconciliation.js +41 -0
  32. package/dist/buildOwnershipRepair.d.ts +33 -0
  33. package/dist/buildOwnershipRepair.js +111 -0
  34. package/dist/buildRuns.d.ts +24 -1
  35. package/dist/buildRuns.js +216 -70
  36. package/dist/buildSyntheticWork.d.ts +2 -0
  37. package/dist/buildSyntheticWork.js +30 -0
  38. package/dist/buildWorkAdmission.d.ts +17 -0
  39. package/dist/buildWorkAdmission.js +57 -0
  40. package/dist/buildWorkAuthorization.d.ts +2 -0
  41. package/dist/buildWorkAuthorization.js +23 -0
  42. package/dist/buildWorkContext.d.ts +3 -0
  43. package/dist/buildWorkContext.js +6 -0
  44. package/dist/buildWorkMigration.d.ts +9 -0
  45. package/dist/buildWorkMigration.js +206 -0
  46. package/dist/buildWorkReconciliation.d.ts +12 -0
  47. package/dist/buildWorkReconciliation.js +70 -0
  48. package/dist/builderGuidanceFollowup.d.ts +15 -0
  49. package/dist/builderGuidanceFollowup.js +34 -0
  50. package/dist/cli/doctor.js +3 -2
  51. package/dist/cli/graph.d.ts +2 -0
  52. package/dist/cli/graph.js +75 -0
  53. package/dist/cli/manager.d.ts +8 -0
  54. package/dist/cli/manager.js +234 -12
  55. package/dist/cli/recovery.js +3 -2
  56. package/dist/cli/runtimeAuthPrompt.d.ts +5 -0
  57. package/dist/cli/runtimeAuthPrompt.js +41 -7
  58. package/dist/cli/start.d.ts +8 -0
  59. package/dist/cli/start.js +2091 -1849
  60. package/dist/cli/status.js +20 -5
  61. package/dist/cli/tickets.js +9 -7
  62. package/dist/continuity.d.ts +16 -1
  63. package/dist/continuity.js +68 -12
  64. package/dist/foreman.d.ts +12 -4
  65. package/dist/foreman.js +397 -60
  66. package/dist/graph/boundary.d.ts +15 -0
  67. package/dist/graph/boundary.js +50 -0
  68. package/dist/graph/bridge.d.ts +9 -0
  69. package/dist/graph/bridge.js +146 -0
  70. package/dist/graph/bridge.py +205 -0
  71. package/dist/graph/config.d.ts +11 -0
  72. package/dist/graph/config.js +40 -0
  73. package/dist/graph/corpus.d.ts +16 -0
  74. package/dist/graph/corpus.js +188 -0
  75. package/dist/graph/derived.d.ts +17 -0
  76. package/dist/graph/derived.js +73 -0
  77. package/dist/graph/host.d.ts +4 -0
  78. package/dist/graph/host.js +160 -0
  79. package/dist/graph/install.d.ts +3 -0
  80. package/dist/graph/install.js +50 -0
  81. package/dist/graph/lifecycle.d.ts +11 -0
  82. package/dist/graph/lifecycle.js +124 -0
  83. package/dist/graph/maintenance.d.ts +43 -0
  84. package/dist/graph/maintenance.js +301 -0
  85. package/dist/graph/manager.d.ts +9 -0
  86. package/dist/graph/manager.js +92 -0
  87. package/dist/graph/preparation.d.ts +41 -0
  88. package/dist/graph/preparation.js +43 -0
  89. package/dist/graph/read.d.ts +27 -0
  90. package/dist/graph/read.js +152 -0
  91. package/dist/graph/semantic.d.ts +17 -0
  92. package/dist/graph/semantic.js +27 -0
  93. package/dist/graph/session.d.ts +8 -0
  94. package/dist/graph/session.js +92 -0
  95. package/dist/graph/sources.d.ts +4 -0
  96. package/dist/graph/sources.js +11 -0
  97. package/dist/graph/storage.d.ts +38 -0
  98. package/dist/graph/storage.js +159 -0
  99. package/dist/graph/turn.d.ts +17 -0
  100. package/dist/graph/turn.js +360 -0
  101. package/dist/graph/util.d.ts +5 -0
  102. package/dist/graph/util.js +51 -0
  103. package/dist/handoffs.d.ts +1 -0
  104. package/dist/handoffs.js +5 -1
  105. package/dist/humanDecision.d.ts +21 -2
  106. package/dist/humanDecision.js +169 -10
  107. package/dist/index.js +4 -1
  108. package/dist/installOwnership.d.ts +46 -0
  109. package/dist/installOwnership.js +440 -0
  110. package/dist/linuxProbeInventory.d.ts +12 -0
  111. package/dist/linuxProbeInventory.js +48 -0
  112. package/dist/log.d.ts +1 -1
  113. package/dist/log.js +7 -1
  114. package/dist/managerEvidence.d.ts +25 -0
  115. package/dist/managerEvidence.js +326 -0
  116. package/dist/managerEvidenceArtifacts.d.ts +38 -0
  117. package/dist/managerEvidenceArtifacts.js +118 -0
  118. package/dist/managerPacket.d.ts +2 -0
  119. package/dist/managerPacket.js +23 -0
  120. package/dist/markers.d.ts +1 -1
  121. package/dist/markers.js +2 -2
  122. package/dist/observability.js +1 -1
  123. package/dist/processIdentity.d.ts +21 -1
  124. package/dist/processIdentity.js +87 -5
  125. package/dist/providerPhase.d.ts +13 -0
  126. package/dist/providerPhase.js +26 -0
  127. package/dist/qaAuthorizedDisposition.d.ts +24 -0
  128. package/dist/qaAuthorizedDisposition.js +26 -0
  129. package/dist/qaBuildGate.d.ts +4 -0
  130. package/dist/qaBuildGate.js +216 -0
  131. package/dist/qaBuilderCoverage.d.ts +5 -0
  132. package/dist/qaBuilderCoverage.js +78 -0
  133. package/dist/qaContractCoverage.d.ts +10 -0
  134. package/dist/qaContractCoverage.js +55 -0
  135. package/dist/qaContractDelivery.d.ts +23 -0
  136. package/dist/qaContractDelivery.js +145 -0
  137. package/dist/qaContractFreshness.d.ts +15 -0
  138. package/dist/qaContractFreshness.js +36 -0
  139. package/dist/qaDeliveryJournal.d.ts +10 -0
  140. package/dist/qaDeliveryJournal.js +16 -1
  141. package/dist/qaEffectiveConfig.d.ts +20 -0
  142. package/dist/qaEffectiveConfig.js +40 -0
  143. package/dist/qaEvidenceReader.d.ts +14 -0
  144. package/dist/qaEvidenceReader.js +176 -0
  145. package/dist/qaFailureDelivery.d.ts +2 -0
  146. package/dist/qaFailureDelivery.js +107 -10
  147. package/dist/qaHandbackMigration.d.ts +1 -1
  148. package/dist/qaHandbackMigration.js +3 -3
  149. package/dist/qaPreparation.d.ts +26 -0
  150. package/dist/qaPreparation.js +150 -0
  151. package/dist/qaPreparationInputs.d.ts +8 -0
  152. package/dist/qaPreparationInputs.js +47 -0
  153. package/dist/qaPreparationMetrics.d.ts +145 -0
  154. package/dist/qaPreparationMetrics.js +120 -0
  155. package/dist/qaPreparationPolicy.d.ts +5 -0
  156. package/dist/qaPreparationPolicy.js +51 -0
  157. package/dist/qaPreparationStore.d.ts +157 -0
  158. package/dist/qaPreparationStore.js +412 -0
  159. package/dist/qaProtocolV2.d.ts +28 -0
  160. package/dist/qaProtocolV2.js +12 -0
  161. package/dist/qaReview.d.ts +6 -0
  162. package/dist/qaReview.js +293 -41
  163. package/dist/qaTimeline.d.ts +18 -0
  164. package/dist/qaTimeline.js +123 -0
  165. package/dist/qaVerificationContract.d.ts +17 -0
  166. package/dist/qaVerificationContract.js +236 -0
  167. package/dist/readinessCleanup.d.ts +19 -0
  168. package/dist/readinessCleanup.js +122 -0
  169. package/dist/readinessGate.d.ts +1 -0
  170. package/dist/readinessGate.js +82 -0
  171. package/dist/recoveryGuidance.d.ts +17 -0
  172. package/dist/recoveryGuidance.js +38 -0
  173. package/dist/runtimeAuth.d.ts +1 -0
  174. package/dist/runtimeAuth.js +5 -1
  175. package/dist/runtimeReadiness.d.ts +13 -0
  176. package/dist/runtimeReadiness.js +206 -10
  177. package/dist/sessionIdentity.d.ts +2 -0
  178. package/dist/sessionIdentity.js +14 -0
  179. package/dist/sessionLifecycle.js +3 -3
  180. package/dist/sources/sourceRegistry.js +7 -0
  181. package/dist/stateTransfer.js +85 -8
  182. package/dist/statusReporter.d.ts +1 -0
  183. package/dist/statusReporter.js +1 -0
  184. package/dist/supervisedStart.d.ts +6 -0
  185. package/dist/supervisedStart.js +178 -62
  186. package/dist/supervisor.d.ts +2 -0
  187. package/dist/supervisor.js +26 -2
  188. package/dist/terminalStyle.d.ts +21 -0
  189. package/dist/terminalStyle.js +50 -0
  190. package/dist/ticketPopulation.d.ts +1 -0
  191. package/dist/ticketPopulation.js +5 -1
  192. package/dist/tickets/commands.js +2 -2
  193. package/dist/tickets/queue.js +1 -1
  194. package/dist/tickets/ticketSchema.d.ts +198 -0
  195. package/dist/tickets/ticketSchema.js +2 -0
  196. package/dist/windowsProbeJob.d.ts +12 -0
  197. package/dist/windowsProbeJob.js +144 -0
  198. package/dist/workflowDb.d.ts +121 -6
  199. package/dist/workflowDb.js +874 -30
  200. package/dist/workflowReader.d.ts +116 -2
  201. package/dist/workflowReader.js +131 -4
  202. package/package.json +89 -8
  203. package/scripts/copy-graph-assets.mjs +3 -0
  204. package/scripts/graph-benchmark.mjs +20 -0
  205. package/scripts/graph-evaluation.mjs +85 -0
@@ -0,0 +1,412 @@
1
+ import { currentGraphDerivedAccess, registerGraphDerived, graphDerivedAllowed, inheritGraphDerivedAccess } from "./graph/derived.js";
2
+ import { bytesDigest } from "./graph/util.js";
3
+ import { persistedEquivalentResolver } from "./qaAuthorizedDisposition.js";
4
+ import { measureQaPreparation, validateFindingAssessment, validateFindingClassification } from "./qaPreparationMetrics.js";
5
+ import { canonicalContractJson as canonicalJson } from "./qaVerificationContract.js";
6
+ import { contractDigest, verifyContractDigest, validateCandidate, validateAssessment } from "./qaVerificationContract.js";
7
+ import { validateDepthDecision } from "./qaPreparationPolicy.js";
8
+ export function migrateQaPreparation(db) {
9
+ db.exec(`
10
+ CREATE TABLE IF NOT EXISTS qa_preparation_policy(run_id TEXT PRIMARY KEY REFERENCES workflow_runs(run_id),record_json TEXT NOT NULL);
11
+ CREATE TABLE IF NOT EXISTS qa_preparation_budgets(id TEXT PRIMARY KEY,run_id TEXT NOT NULL REFERENCES workflow_runs(run_id),work_id TEXT NOT NULL,admission_digest TEXT NOT NULL,record_json TEXT NOT NULL,UNIQUE(run_id,work_id,admission_digest));
12
+ CREATE TABLE IF NOT EXISTS qa_depth_decisions(decision_id TEXT PRIMARY KEY,run_id TEXT NOT NULL REFERENCES workflow_runs(run_id),work_id TEXT NOT NULL,record_json TEXT NOT NULL);
13
+ CREATE TABLE IF NOT EXISTS qa_preparation_attempts(operation_id TEXT PRIMARY KEY,budget_id TEXT NOT NULL REFERENCES qa_preparation_budgets(id),state TEXT NOT NULL,intent_json TEXT NOT NULL,result_json TEXT);
14
+ CREATE TABLE IF NOT EXISTS qa_preparation_progress(budget_id TEXT NOT NULL REFERENCES qa_preparation_budgets(id),stage TEXT NOT NULL,digest TEXT NOT NULL,PRIMARY KEY(budget_id,stage));
15
+ CREATE TABLE IF NOT EXISTS qa_verification_contracts(digest TEXT PRIMARY KEY,run_id TEXT NOT NULL REFERENCES workflow_runs(run_id),work_id TEXT NOT NULL,admission_digest TEXT NOT NULL,revision INTEGER NOT NULL,record_json TEXT NOT NULL,UNIQUE(run_id,work_id,admission_digest,revision));
16
+ CREATE TABLE IF NOT EXISTS qa_contract_heads(run_id TEXT NOT NULL REFERENCES workflow_runs(run_id),work_id TEXT NOT NULL,admission_digest TEXT NOT NULL,digest TEXT REFERENCES qa_verification_contracts(digest),generation INTEGER NOT NULL,state TEXT NOT NULL,detail TEXT,PRIMARY KEY(run_id,work_id,admission_digest));
17
+ CREATE TABLE IF NOT EXISTS qa_contract_check_history(contract_id TEXT NOT NULL,check_id TEXT NOT NULL,revision INTEGER NOT NULL,meaning_digest TEXT NOT NULL,record_json TEXT NOT NULL,PRIMARY KEY(contract_id,check_id,revision));
18
+ CREATE TABLE IF NOT EXISTS qa_contract_receipts(operation_id TEXT PRIMARY KEY,contract_digest TEXT NOT NULL REFERENCES qa_verification_contracts(digest),record_json TEXT NOT NULL);
19
+ CREATE TABLE IF NOT EXISTS qa_contract_artifacts(digest TEXT PRIMARY KEY,kind TEXT NOT NULL,record_json TEXT NOT NULL);
20
+ CREATE TABLE IF NOT EXISTS qa_preparation_events(event_id TEXT PRIMARY KEY,run_id TEXT NOT NULL REFERENCES workflow_runs(run_id),work_id TEXT NOT NULL,kind TEXT NOT NULL,record_json TEXT NOT NULL);
21
+ `);
22
+ for (const table of ["qa_preparation_policy", "qa_depth_decisions", "qa_verification_contracts", "qa_contract_check_history", "qa_contract_receipts", "qa_contract_artifacts", "qa_preparation_events"]) {
23
+ for (const action of ["UPDATE", "DELETE"])
24
+ db.exec(`CREATE TRIGGER IF NOT EXISTS preparation_immutable_${table}_${action} BEFORE ${action} ON ${table} BEGIN SELECT RAISE(ABORT,'Preparation authority and evidence are immutable'); END`);
25
+ }
26
+ const scopes = {
27
+ qa_preparation_policy: "ROW.run_id", qa_preparation_budgets: "ROW.run_id", qa_depth_decisions: "ROW.run_id", qa_verification_contracts: "ROW.run_id", qa_contract_heads: "ROW.run_id", qa_preparation_events: "ROW.run_id",
28
+ qa_preparation_attempts: "(SELECT run_id FROM qa_preparation_budgets WHERE id=ROW.budget_id)", qa_preparation_progress: "(SELECT run_id FROM qa_preparation_budgets WHERE id=ROW.budget_id)", qa_contract_receipts: "(SELECT run_id FROM qa_verification_contracts WHERE digest=ROW.contract_digest)",
29
+ };
30
+ for (const [table, scope] of Object.entries(scopes))
31
+ for (const action of ["INSERT", "UPDATE", "DELETE"]) {
32
+ const expression = scope.replaceAll("ROW", action === "DELETE" ? "OLD" : "NEW");
33
+ db.exec(`CREATE TRIGGER IF NOT EXISTS preparation_owner_${table}_${action} BEFORE ${action} ON ${table} BEGIN SELECT CASE WHEN rafi_qa_preparation_owner(${expression})<>1 THEN RAISE(ABORT,'Original preparation owner required') END; END`);
34
+ }
35
+ }
36
+ /** Tagged enforcing authority also fences older callers that omit new fields. */
37
+ export function migrateQaPreparationGuards(db) {
38
+ const scopes = {
39
+ qa_preparation_policy: "ROW.run_id", qa_preparation_budgets: "ROW.run_id", qa_depth_decisions: "ROW.run_id", qa_verification_contracts: "ROW.run_id", qa_contract_heads: "ROW.run_id", qa_preparation_events: "ROW.run_id",
40
+ qa_preparation_attempts: "(SELECT run_id FROM qa_preparation_budgets WHERE id=ROW.budget_id)", qa_preparation_progress: "(SELECT run_id FROM qa_preparation_budgets WHERE id=ROW.budget_id)", qa_contract_receipts: "(SELECT run_id FROM qa_verification_contracts WHERE digest=ROW.contract_digest)",
41
+ };
42
+ for (const [table, scope] of Object.entries(scopes))
43
+ for (const action of ["INSERT", "UPDATE", "DELETE"]) {
44
+ const run = scope.replaceAll("ROW", action === "DELETE" ? "OLD" : "NEW");
45
+ db.exec(`CREATE TRIGGER IF NOT EXISTS preparation_lease_${table}_${action} BEFORE ${action} ON ${table}
46
+ WHEN (EXISTS(SELECT 1 FROM build_admission) OR rafi_build_writer_run()<>'' OR EXISTS(SELECT 1 FROM build_runtime_runs WHERE run_id=${run}))
47
+ AND NOT EXISTS(SELECT 1 FROM build_admission WHERE json_extract(record_json,'$.token')=rafi_build_writer_token() AND (json_extract(record_json,'$.runId')=${run} OR EXISTS(SELECT 1 FROM build_child_runs WHERE child=${run} AND parent=json_extract(record_json,'$.runId'))))
48
+ AND NOT (NOT EXISTS(SELECT 1 FROM build_admission) AND EXISTS(SELECT 1 FROM project_lease WHERE owner=rafi_build_lease_owner() AND generation=rafi_build_lease_generation() AND run_id=${run}))
49
+ BEGIN SELECT RAISE(ABORT,'Preparation requires the original durable lease generation'); END`);
50
+ }
51
+ db.exec(`
52
+ CREATE TRIGGER IF NOT EXISTS preparation_assignment_guard BEFORE INSERT ON operation_journal
53
+ WHEN NEW.kind='build-assignment' AND EXISTS(SELECT 1 FROM qa_preparation_policy p WHERE p.run_id=NEW.run_id AND json_extract(p.record_json,'$.mode')='enforce')
54
+ AND NOT EXISTS(SELECT 1 FROM qa_contract_heads h JOIN qa_contract_receipts r ON r.contract_digest=h.digest
55
+ WHERE h.run_id=NEW.run_id AND h.work_id=json_extract(NEW.intent_json,'$.ticketId') AND h.admission_digest=json_extract(NEW.intent_json,'$.requirementsDigest') AND h.state='ready'
56
+ AND r.operation_id=json_extract(NEW.intent_json,'$.contractBinding.receiptOperationId') AND h.digest=json_extract(NEW.intent_json,'$.contractBinding.digest')
57
+ AND json_extract(r.record_json,'$.sessionId')=json_extract(NEW.intent_json,'$.contractBinding.session.sessionId')
58
+ AND json_extract(r.record_json,'$.generation')=json_extract(NEW.intent_json,'$.contractBinding.session.generation')
59
+ AND json_extract(r.record_json,'$.workspace')=json_extract(NEW.intent_json,'$.contractBinding.session.workspace')
60
+ AND json_extract(r.record_json,'$.compactionSequence')=json_extract(NEW.intent_json,'$.contractBinding.session.compactionSequence'))
61
+ BEGIN SELECT RAISE(ABORT,'Enforcing assignment requires tagged actual-session contract delivery'); END;
62
+ `);
63
+ const authority = `EXISTS(SELECT 1 FROM qa_contract_heads h JOIN qa_verification_contracts c ON c.digest=h.digest JOIN qa_contract_artifacts a ON a.digest=json_extract(NEW.certificate_json,'$.contractCoverage.coverageDigest')
64
+ WHERE h.run_id=NEW.run_id AND h.work_id=NEW.ticket_id AND h.state='ready' AND h.digest=json_extract(NEW.certificate_json,'$.contractCoverage.contractDigest')
65
+ AND json_extract(NEW.certificate_json,'$.contractCoverage.version')=1 AND c.revision=json_extract(NEW.certificate_json,'$.contractCoverage.revision')
66
+ AND a.kind='final-coverage' AND json_extract(a.record_json,'$.contractDigest')=h.digest
67
+ AND json_extract(a.record_json,'$.sourceDigest')=NEW.source_state_digest AND json_extract(a.record_json,'$.inputBasisDigest')=NEW.review_basis_digest
68
+ AND json_extract(a.record_json,'$.attemptId')=json_extract(NEW.certificate_json,'$.contractCoverage.attemptId') AND json_extract(a.record_json,'$.sessionId')=json_extract(NEW.certificate_json,'$.contractCoverage.sessionId'))`;
69
+ for (const action of ["INSERT", "UPDATE"])
70
+ db.exec(`CREATE TRIGGER IF NOT EXISTS preparation_certificate_${action} BEFORE ${action} ON qa_pass_certificates
71
+ WHEN EXISTS(SELECT 1 FROM qa_preparation_policy p WHERE p.run_id=NEW.run_id AND json_extract(p.record_json,'$.mode')='enforce')
72
+ AND (NEW.consumed_by IS NULL OR NEW.consumed_by NOT LIKE 'invalidated:%') AND NOT (${authority})
73
+ BEGIN SELECT RAISE(ABORT,'Enforcing certificate requires current tagged contract coverage'); END`);
74
+ }
75
+ /** Uses the existing workflow connection and caller's original lease fences. */
76
+ export class QaPreparationStore {
77
+ db;
78
+ assertOwner;
79
+ projectDir;
80
+ constructor(db, assertOwner, projectDir) {
81
+ this.db = db;
82
+ this.assertOwner = assertOwner;
83
+ this.projectDir = projectDir;
84
+ db.function("rafi_qa_preparation_owner", (_runId) => 1);
85
+ }
86
+ protectedJson(value) {
87
+ const json = canonicalJson(value), access = currentGraphDerivedAccess();
88
+ if (access && (!Array.isArray(access) || access.length))
89
+ registerGraphDerived(this.db, json, access);
90
+ return json;
91
+ }
92
+ decode(value) {
93
+ if (this.projectDir && !graphDerivedAllowed(this.db, this.projectDir, bytesDigest(value)))
94
+ throw new Error("Preparation contains revoked graph-derived evidence; explicit source-based recovery is required");
95
+ if (this.db.prepare("SELECT 1 FROM sqlite_master WHERE name='graph_derived_refs'").get()) {
96
+ const row = this.db.prepare("SELECT access_json FROM graph_derived_refs WHERE digest=?").get(bytesDigest(value));
97
+ if (row) {
98
+ const access = JSON.parse(row.access_json);
99
+ inheritGraphDerivedAccess(Array.isArray(access) ? access : [access]);
100
+ }
101
+ }
102
+ return JSON.parse(value);
103
+ }
104
+ atomic(work) { return this.db.transaction(work).immediate(); }
105
+ equivalentResolver() { return persistedEquivalentResolver(this.db); }
106
+ metric(runId, workId, admissionDigest, eventId, kind, details = {}) {
107
+ const policy = this.policy(runId);
108
+ if (!policy)
109
+ return;
110
+ const budget = this.budget(runId, workId, admissionDigest);
111
+ const existing = this.eventRecord(eventId);
112
+ const value = { ...details, version: 1, eventId, kind, workKey: `${runId}/${workId}/${admissionDigest}`, admissionDigest, at: existing?.at ?? details.at ?? new Date().toISOString(), mode: existing?.mode ?? policy.mode, level: existing ? existing.level : budget?.level, cohort: existing?.cohort ?? details.cohort ?? `policy:${policy.policyVersion}/level:${budget?.level ?? "unknown"}` };
113
+ if (kind === "finding-classification")
114
+ validateFindingClassification(value, reference => Boolean(this.db.prepare("SELECT 1 FROM qa_contract_artifacts WHERE digest=?").get(reference)));
115
+ if (kind === "finding-classification" && value.cause !== "unknown") {
116
+ const assessment = this.artifact(value.assessmentRef, "finding-assessment");
117
+ validateFindingAssessment(assessment);
118
+ const contract = this.contract(assessment.contractDigest);
119
+ if (assessment.findingId !== value.findingId || assessment.cause !== value.cause || assessment.assessor !== value.classifier || contract.runId !== runId || contract.workId !== workId || contract.admissionDigest !== admissionDigest)
120
+ throw new Error("Supported classification lacks its exact scoped assessment");
121
+ }
122
+ if (kind === "sample") {
123
+ const sample = value.sample;
124
+ if (!sample?.assessor?.trim() || typeof sample.coverageAdequate !== "boolean" || !sample.checkUsefulness?.length)
125
+ throw new Error("Usefulness sample requires an assessor and specific check observations");
126
+ const contract = this.contract(sample.contractDigest), seen = new Set();
127
+ if (contract.runId !== runId || contract.workId !== workId || contract.admissionDigest !== admissionDigest)
128
+ throw new Error("Usefulness sample belongs to foreign scope");
129
+ if (contract.preparationEvidence.some(evidence => evidence.sessionId === sample.assessor) || this.receipts(contract.contentDigest).some(receipt => receipt.sessionId === sample.assessor))
130
+ throw new Error("Usefulness sampling requires an independent assessor");
131
+ for (const check of sample.checkUsefulness) {
132
+ if (seen.has(check.checkId) || !contract.checks.some(expected => expected.id === check.checkId) || !check.result?.trim() || !check.observation?.trim())
133
+ throw new Error("Usefulness sample has unknown, duplicate or unexplained checks");
134
+ seen.add(check.checkId);
135
+ }
136
+ const review = this.db.prepare("SELECT t.receipt_json,b.basis_json FROM qa_review_bases b JOIN qa_turns t ON t.run_id=b.run_id AND t.ticket_id=b.ticket_id AND t.review_basis_digest=b.digest WHERE b.run_id=? AND b.ticket_id=? AND b.digest=? AND t.status='completed'").all(runId, workId, sample.reviewDigest);
137
+ if (!review.some(row => JSON.parse(row.basis_json).contractBinding?.digest === contract.contentDigest && JSON.parse(row.receipt_json).terminalEventObserved === true))
138
+ throw new Error("Usefulness sample requires a retained terminal review bound to its contract");
139
+ if (!value.evidenceRefs?.length || value.evidenceRefs.some(reference => !this.db.prepare("SELECT 1 FROM qa_contract_artifacts WHERE digest=?").get(reference)))
140
+ throw new Error("Usefulness sample requires retained input/investigation evidence");
141
+ }
142
+ this.event(runId, workId, eventId, "metric", value);
143
+ }
144
+ findingAssessment(runId, workId, admissionDigest, assessment, evidenceRefs) {
145
+ this.assertOwner(runId);
146
+ validateFindingAssessment(assessment);
147
+ const contract = this.contract(assessment.contractDigest);
148
+ if (contract.runId !== runId || contract.workId !== workId || contract.admissionDigest !== admissionDigest || assessment.expectedCheckIds.some(id => !contract.checks.some(check => check.id === id)))
149
+ throw new Error("Finding classification belongs to foreign scope or checks");
150
+ const finding = this.db.prepare("SELECT b.basis_json FROM qa_findings f JOIN qa_reports r ON r.report_occurrence_id=f.report_occurrence_id JOIN qa_review_bases b ON b.run_id=r.run_id AND b.ticket_id=r.ticket_id AND b.digest=r.review_basis_digest WHERE f.finding_id=? AND r.report_digest=? AND r.run_id=? AND r.ticket_id=?").get(assessment.findingId, assessment.reviewDigest, runId, workId);
151
+ if (!finding || JSON.parse(finding.basis_json).contractBinding?.digest !== contract.contentDigest)
152
+ throw new Error("Finding assessment requires its retained report and contract-bound review basis");
153
+ if ([...assessment.originalInputEvidence, ...assessment.builderEvidence, ...assessment.scopeAuthorityEvidence, ...evidenceRefs].some(reference => !this.db.prepare("SELECT 1 FROM qa_contract_artifacts WHERE digest=?").get(reference)))
154
+ throw new Error("Finding assessment references unavailable retained evidence");
155
+ if (assessment.independent && (contract.preparationEvidence.some(evidence => evidence.sessionId === assessment.assessor) || this.receipts(contract.contentDigest).some(receipt => receipt.sessionId === assessment.assessor)))
156
+ throw new Error("Finding assessment cannot attribute independence to the author or Builder");
157
+ const assessmentRef = this.putArtifact("finding-assessment", assessment);
158
+ this.metric(runId, workId, admissionDigest, `metric:finding-assessment:${assessmentRef}`, "finding-classification", { findingId: assessment.findingId, classifier: assessment.assessor, cause: assessment.cause, confidence: assessment.cause === "unknown" ? "unknown" : "supported", evidenceRefs: [...evidenceRefs, assessmentRef], assessmentRef, outsideContract: assessment.cause === "outside-contract" });
159
+ return assessmentRef;
160
+ }
161
+ eventRecord(eventId) { return this.read("qa_preparation_events", "event_id", eventId); }
162
+ metrics(runId) {
163
+ const rows = this.db.prepare("SELECT record_json FROM qa_preparation_events WHERE run_id=? AND kind='metric' ORDER BY rowid").all(runId);
164
+ return measureQaPreparation(rows.map(row => this.decode(row.record_json)));
165
+ }
166
+ policy(runId) { return this.read("qa_preparation_policy", "run_id", runId); }
167
+ freezePolicy(runId, policy) {
168
+ this.assertOwner(runId);
169
+ return this.atomic(() => {
170
+ const prior = this.policy(runId);
171
+ if (prior)
172
+ return prior;
173
+ this.db.prepare("INSERT INTO qa_preparation_policy VALUES(?,?)").run(runId, this.protectedJson(policy));
174
+ return policy;
175
+ });
176
+ }
177
+ budget(runId, workId, admissionDigest) {
178
+ const row = this.db.prepare("SELECT record_json FROM qa_preparation_budgets WHERE run_id=? AND work_id=? AND admission_digest=?").get(runId, workId, admissionDigest);
179
+ return row && this.decode(row.record_json);
180
+ }
181
+ ensureBudget(runId, workId, admissionDigest, level, nowMs) {
182
+ this.assertOwner(runId);
183
+ return this.atomic(() => {
184
+ const prior = this.budget(runId, workId, admissionDigest);
185
+ if (prior)
186
+ return prior;
187
+ const policy = this.policy(runId);
188
+ if (!policy)
189
+ throw new Error("Preparation policy must be frozen before dispatch");
190
+ const id = contractDigest("logical-preparation", { runId, workId, admissionDigest });
191
+ const budget = { id, runId, workId, admissionDigest, policy, level, startMs: nowMs, deadlineMs: nowMs + policy.wallTimeMs[level - 1], reservations: [], usage: { inputTokens: null, outputTokens: null, costUsd: null }, extensions: [] };
192
+ this.db.prepare("INSERT INTO qa_preparation_budgets VALUES(?,?,?,?,?)").run(id, runId, workId, admissionDigest, this.protectedJson(budget));
193
+ return budget;
194
+ });
195
+ }
196
+ reserve(budget, operationId, kind, intent, nowMs, resultId) {
197
+ this.assertOwner(budget.runId);
198
+ this.atomic(() => {
199
+ const current = this.budget(budget.runId, budget.workId, budget.admissionDigest);
200
+ if (nowMs >= current.deadlineMs)
201
+ throw new Error("Preparation budget exhausted; authorized operational extension required");
202
+ const unresolved = current.reservations.find(item => !item.resultDigest);
203
+ if (unresolved)
204
+ throw new Error(`Uncertain preparation dispatch ${unresolved.operationId}; reconcile retained result before retry`);
205
+ const count = current.reservations.filter(item => item.kind === kind && (kind !== "repair" || item.resultId === resultId)).length;
206
+ const maximum = current.caps?.[kind] ?? (kind === "planning" ? 1 : kind === "investigation" ? current.level <= 2 ? 2 : 3 : kind === "assessment" ? 3 : 2);
207
+ if (count >= maximum)
208
+ throw new Error(`Preparation ${kind} allowance exhausted`);
209
+ current.reservations.push({ operationId, kind, ...(resultId ? { resultId } : {}) });
210
+ this.saveBudget(current);
211
+ this.db.prepare("INSERT INTO qa_preparation_attempts VALUES(?,?,?,?,NULL)").run(operationId, current.id, "uncertain", this.protectedJson(intent));
212
+ });
213
+ }
214
+ retainResult(budget, operationId, result, usage) {
215
+ this.assertOwner(budget.runId);
216
+ return this.atomic(() => {
217
+ const current = this.budget(budget.runId, budget.workId, budget.admissionDigest);
218
+ const reservation = current.reservations.find(item => item.operationId === operationId);
219
+ if (!reservation)
220
+ throw new Error("Unreserved preparation result");
221
+ const digest = this.putArtifact("provider-result", result);
222
+ if (reservation.resultDigest && reservation.resultDigest !== digest)
223
+ throw new Error("Conflicting retained preparation result");
224
+ if (!reservation.resultDigest) {
225
+ reservation.resultDigest = digest;
226
+ for (const key of ["inputTokens", "outputTokens", "costUsd"])
227
+ if (typeof usage?.[key] === "number")
228
+ current.usage[key] = (current.usage[key] ?? 0) + usage[key];
229
+ this.saveBudget(current);
230
+ this.db.prepare("UPDATE qa_preparation_attempts SET state='validating',result_json=? WHERE operation_id=?").run(this.protectedJson({ digest }), operationId);
231
+ }
232
+ return digest;
233
+ });
234
+ }
235
+ reviseDepth(budget, decision) {
236
+ this.assertOwner(budget.runId);
237
+ const issues = validateDepthDecision(decision);
238
+ if (issues.length)
239
+ throw new Error(issues.join("; "));
240
+ this.atomic(() => {
241
+ const current = this.budget(budget.runId, budget.workId, budget.admissionDigest);
242
+ current.level = decision.level;
243
+ current.deadlineMs = Math.max(current.deadlineMs, current.startMs + current.policy.wallTimeMs[decision.level - 1]);
244
+ this.saveBudget(current);
245
+ const prior = this.read("qa_depth_decisions", "decision_id", decision.decisionId);
246
+ const priorScope = this.db.prepare("SELECT run_id,work_id FROM qa_depth_decisions WHERE decision_id=?").get(decision.decisionId);
247
+ if (priorScope && (priorScope.run_id !== budget.runId || priorScope.work_id !== budget.workId))
248
+ throw new Error("Planner depth identity belongs to another admitted work");
249
+ if (prior && this.protectedJson(prior) !== this.protectedJson(decision))
250
+ throw new Error("Conflicting planner depth identity");
251
+ this.db.prepare("INSERT INTO qa_depth_decisions VALUES(?,?,?,?) ON CONFLICT(decision_id) DO NOTHING").run(decision.decisionId, budget.runId, budget.workId, this.protectedJson(decision));
252
+ });
253
+ }
254
+ retainProgress(budget, stage, value) {
255
+ this.assertOwner(budget.runId);
256
+ const digest = this.putArtifact(`progress-${stage}`, value);
257
+ if (stage === "inventory") {
258
+ const prior = this.progress(budget, stage);
259
+ if (prior && this.protectedJson(prior) !== this.protectedJson(value))
260
+ throw new Error("Logical preparation inventory changed; authorized amendment required");
261
+ }
262
+ this.db.prepare("INSERT INTO qa_preparation_progress VALUES(?,?,?) ON CONFLICT(budget_id,stage) DO UPDATE SET digest=excluded.digest").run(budget.id, stage, digest);
263
+ }
264
+ beginAmendment(budget, inventory, authority) {
265
+ this.assertOwner(budget.runId);
266
+ this.atomic(() => {
267
+ const head = this.head(budget.runId, budget.workId, budget.admissionDigest);
268
+ if (head.digest !== authority.predecessorDigest || !["ready", "amendment-required", "incomplete", "reconciling", "validating", "preparing"].includes(head.state) || !authority.reason.trim())
269
+ throw new Error("Amendment requires current predecessor and explicit input authority");
270
+ const current = this.budget(budget.runId, budget.workId, budget.admissionDigest);
271
+ if (current.reservations.some(item => !item.resultDigest))
272
+ throw new Error("Uncertain preparation dispatch requires reconciliation before another input amendment");
273
+ const priorInventory = this.progress(budget, "inventory");
274
+ this.putArtifact("amendment-intent", { budgetId: budget.id, priorInventory, inventory, authority });
275
+ this.db.prepare("DELETE FROM qa_preparation_progress WHERE budget_id=? AND stage IN ('draft','assessment','challenge','inventory')").run(budget.id);
276
+ this.retainProgress(budget, "inventory", inventory);
277
+ this.retainProgress(budget, "amendment-inputs", { inputDigest: authority.inputDigest, predecessorDigest: authority.predecessorDigest });
278
+ this.setState(budget.runId, budget.workId, budget.admissionDigest, "reconciling", authority.reason, head.generation);
279
+ });
280
+ }
281
+ progress(budget, stage) {
282
+ const row = this.db.prepare("SELECT digest FROM qa_preparation_progress WHERE budget_id=? AND stage=?").get(budget.id, stage);
283
+ return row ? this.artifact(row.digest, `progress-${stage}`) : undefined;
284
+ }
285
+ /** Explicit operator action against an answered, exactly scoped durable decision. */
286
+ applyAnsweredExtensions(budget) {
287
+ const rows = this.db.prepare("SELECT decision_id FROM human_decisions WHERE run_id=? AND status='answered' AND json_extract(decision_json,'$.interruptionId')=? ORDER BY updated_at,decision_id").all(budget.runId, `qa-preparation:${budget.id}`);
288
+ for (const row of rows)
289
+ this.extendBudget(budget, row.decision_id);
290
+ }
291
+ extendBudget(budget, decisionId) {
292
+ this.assertOwner(budget.runId);
293
+ this.atomic(() => {
294
+ const row = this.db.prepare("SELECT decision_json FROM human_decisions WHERE decision_id=? AND run_id=?").get(decisionId, budget.runId);
295
+ const decision = row ? JSON.parse(row.decision_json) : undefined;
296
+ if (!decision || decision.status !== "answered" || decision.interruptionId !== `qa-preparation:${budget.id}` || !decision.answer || !decision.answeredAt)
297
+ throw new Error("Budget extension requires an answered scoped operator decision");
298
+ const extension = JSON.parse(decision.answer);
299
+ const current = this.budget(budget.runId, budget.workId, budget.admissionDigest);
300
+ if (current.extensions.some(item => item.authorityId === decisionId))
301
+ return;
302
+ if (extension.budgetId !== budget.id || !extension.reason?.trim() || !Number.isSafeInteger(extension.deadlineMs) || extension.deadlineMs <= current.deadlineMs)
303
+ throw new Error("Invalid authorized budget extension");
304
+ for (const [kind, maximum] of Object.entries(extension.caps ?? {}))
305
+ if (!["planning", "investigation", "assessment", "challenge", "escalation", "repair"].includes(kind) || !Number.isSafeInteger(maximum) || maximum < current.reservations.filter(item => item.kind === kind).length)
306
+ throw new Error("Budget extension cannot reset consumption");
307
+ current.extensions.push({ authorityId: decisionId, actor: "human", reason: extension.reason, previousDeadlineMs: current.deadlineMs, deadlineMs: extension.deadlineMs });
308
+ current.deadlineMs = extension.deadlineMs;
309
+ current.caps = { ...current.caps, ...extension.caps };
310
+ this.saveBudget(current);
311
+ });
312
+ }
313
+ putArtifact(kind, value) {
314
+ const digest = contractDigest(`artifact-${kind}`, value);
315
+ this.db.prepare("INSERT INTO qa_contract_artifacts VALUES(?,?,?) ON CONFLICT(digest) DO NOTHING").run(digest, kind, this.protectedJson(value));
316
+ return digest;
317
+ }
318
+ artifact(digest, kind) {
319
+ const row = this.db.prepare("SELECT record_json FROM qa_contract_artifacts WHERE digest=? AND kind=?").get(digest, kind);
320
+ if (!row)
321
+ throw new Error("Missing immutable preparation artifact");
322
+ const value = this.decode(row.record_json);
323
+ if (contractDigest(`artifact-${kind}`, value) !== digest)
324
+ throw new Error("Corrupt preparation artifact");
325
+ return value;
326
+ }
327
+ head(runId, workId, admissionDigest) {
328
+ const row = this.db.prepare("SELECT digest,generation,state,detail FROM qa_contract_heads WHERE run_id=? AND work_id=? AND admission_digest=?").get(runId, workId, admissionDigest);
329
+ return row ? { ...row, digest: row.digest ?? undefined, detail: row.detail && this.projectDir && !graphDerivedAllowed(this.db, this.projectDir, bytesDigest(row.detail)) ? "Graph-derived preparation detail withheld; source-based recovery required" : row.detail ?? undefined } : { generation: 0, state: "preparation-required" };
330
+ }
331
+ contract(digest) {
332
+ const contract = this.read("qa_verification_contracts", "digest", digest);
333
+ if (!contract)
334
+ throw new Error("Missing verification contract");
335
+ verifyContractDigest(contract);
336
+ return contract;
337
+ }
338
+ setState(runId, workId, admissionDigest, state, detail, expectedGeneration) {
339
+ this.assertOwner(runId);
340
+ const access = currentGraphDerivedAccess();
341
+ if (access)
342
+ registerGraphDerived(this.db, detail, access);
343
+ this.atomic(() => {
344
+ const head = this.head(runId, workId, admissionDigest);
345
+ if (head.generation !== expectedGeneration)
346
+ throw new Error("Stale preparation owner/head");
347
+ this.db.prepare("INSERT INTO qa_contract_heads VALUES(?,?,?,?,?,?,?) ON CONFLICT(run_id,work_id,admission_digest) DO UPDATE SET generation=excluded.generation,state=excluded.state,detail=excluded.detail").run(runId, workId, admissionDigest, head.digest ?? null, head.generation + 1, state, detail);
348
+ });
349
+ }
350
+ publish(contract, expectedGeneration, nowMs) {
351
+ this.assertOwner(contract.runId);
352
+ verifyContractDigest(contract);
353
+ this.atomic(() => {
354
+ const assessment = this.artifact(contract.semanticAssessmentDigest, "semantic-assessment");
355
+ const inventory = this.progress(this.budget(contract.runId, contract.workId, contract.admissionDigest), "inventory");
356
+ if (!inventory)
357
+ throw new Error("Ready publication requires retained authoritative inventory");
358
+ for (const observation of contract.baseline)
359
+ this.artifact(observation.evidenceDigest, "baseline-observation");
360
+ const issues = [...validateCandidate(contract, inventory, this.equivalentResolver()), ...validateAssessment(contract, assessment, assessment.authorSessionId)];
361
+ if (contract.depthDecision.level === 5) {
362
+ if (!contract.challengeReceiptDigest)
363
+ issues.push("Missing independent challenge");
364
+ else {
365
+ const challenge = this.artifact(contract.challengeReceiptDigest, "approach-challenge");
366
+ issues.push(...validateAssessment(contract, challenge, assessment.authorSessionId));
367
+ if (challenge.approachConcerns.some(concern => concern.material))
368
+ issues.push("Unresolved approach concerns");
369
+ }
370
+ }
371
+ if (issues.length)
372
+ throw new Error(issues.join("; "));
373
+ const head = this.head(contract.runId, contract.workId, contract.admissionDigest);
374
+ const budget = this.budget(contract.runId, contract.workId, contract.admissionDigest);
375
+ if (!budget || nowMs >= budget.deadlineMs || budget.reservations.some(item => !item.resultDigest))
376
+ throw new Error("Expired or uncertain preparation cannot publish ready");
377
+ if (head.generation !== expectedGeneration || contract.predecessorDigest !== (head.digest ?? undefined) || contract.revision !== (head.digest ? this.contract(head.digest).revision + 1 : 1))
378
+ throw new Error("Stale contract revision/publication");
379
+ if (head.digest) {
380
+ const prior = this.contract(head.digest);
381
+ for (const check of contract.checks) {
382
+ const old = prior.checks.find(row => row.id === check.id);
383
+ if (old && this.protectedJson(old) !== this.protectedJson(check))
384
+ throw new Error(`Changed check meaning must allocate a successor: ${check.id}`);
385
+ }
386
+ this.putArtifact("amendment-reconciliation", { predecessor: prior.contentDigest, current: contract.contentDigest, removed: prior.checks.filter(row => !contract.checks.some(check => check.id === row.id)).map(row => row.id), added: contract.checks.filter(row => !prior.checks.some(check => check.id === row.id)).map(row => row.id), carryForward: [], rerun: contract.checks.map(check => check.id), authority: this.head(contract.runId, contract.workId, contract.admissionDigest).detail });
387
+ }
388
+ for (const check of contract.checks)
389
+ this.db.prepare("INSERT INTO qa_contract_check_history VALUES(?,?,?,?,?)").run(contract.contractId, check.id, contract.revision, contractDigest("check-history", check), this.protectedJson(check));
390
+ this.db.prepare("INSERT INTO qa_verification_contracts VALUES(?,?,?,?,?,?)").run(contract.contentDigest, contract.runId, contract.workId, contract.admissionDigest, contract.revision, this.protectedJson(contract));
391
+ this.db.prepare("INSERT INTO qa_contract_heads VALUES(?,?,?,?,?,'ready',NULL) ON CONFLICT(run_id,work_id,admission_digest) DO UPDATE SET digest=excluded.digest,generation=excluded.generation,state='ready',detail=NULL").run(contract.runId, contract.workId, contract.admissionDigest, contract.contentDigest, head.generation + 1);
392
+ });
393
+ }
394
+ receipt(receipt) {
395
+ this.assertOwner(receipt.runId);
396
+ const head = this.head(receipt.runId, receipt.workId, receipt.admissionDigest);
397
+ if (head.state !== "ready" || head.digest !== receipt.contractDigest)
398
+ throw new Error("Delivery cannot acknowledge stale contract");
399
+ this.db.prepare("INSERT INTO qa_contract_receipts VALUES(?,?,?)").run(receipt.operationId, receipt.contractDigest, this.protectedJson(receipt));
400
+ }
401
+ receipts(digest) { return this.db.prepare("SELECT record_json FROM qa_contract_receipts WHERE contract_digest=?").all(digest).map(row => this.decode(row.record_json)); }
402
+ event(runId, workId, eventId, kind, value) {
403
+ this.assertOwner(runId);
404
+ const json = this.protectedJson(value);
405
+ const prior = this.db.prepare("SELECT record_json FROM qa_preparation_events WHERE event_id=?").get(eventId);
406
+ if (prior && prior.record_json !== json)
407
+ throw new Error("Conflicting telemetry replay");
408
+ this.db.prepare("INSERT INTO qa_preparation_events VALUES(?,?,?,?,?) ON CONFLICT(event_id) DO NOTHING").run(eventId, runId, workId, kind, json);
409
+ }
410
+ saveBudget(budget) { this.db.prepare("UPDATE qa_preparation_budgets SET record_json=? WHERE id=?").run(this.protectedJson(budget), budget.id); }
411
+ read(table, key, value) { const row = this.db.prepare(`SELECT record_json FROM ${table} WHERE ${key}=?`).get(value); return row && this.decode(row.record_json); }
412
+ }
@@ -34,6 +34,14 @@ export interface QaConfinementV2 {
34
34
  policyDigest: string;
35
35
  }
36
36
  export interface QaReviewBasisV2 {
37
+ contractBinding?: {
38
+ version: 1;
39
+ transportVersion?: 1;
40
+ revision: number;
41
+ digest: string;
42
+ admissionDigest: string;
43
+ commonConfigDigest: string;
44
+ };
37
45
  version: 2;
38
46
  ticketDigest: string;
39
47
  instructionDigest: string;
@@ -141,6 +149,14 @@ export interface BuilderRemediationReceiptV3 extends Omit<BuilderRemediationRece
141
149
  turnRecordId: string;
142
150
  }
143
151
  export interface QaPassCertificateV2 {
152
+ contractCoverage?: {
153
+ version: 1;
154
+ contractDigest: string;
155
+ revision: number;
156
+ coverageDigest: string;
157
+ attemptId: string;
158
+ sessionId: string;
159
+ };
144
160
  version: 2;
145
161
  certificateId: string;
146
162
  runId: string;
@@ -206,6 +222,11 @@ export type QaReducerEventV2 = {
206
222
  reason: string;
207
223
  } | {
208
224
  type: "operator-menu";
225
+ } | {
226
+ type: "failed-review-restored";
227
+ reviewNumber: number;
228
+ sourceStateDigest: string;
229
+ reportDigest: string;
209
230
  } | {
210
231
  type: "review-passed";
211
232
  passCertificateId: string;
@@ -214,11 +235,18 @@ export type QaReducerEventV2 = {
214
235
  } | {
215
236
  type: "pass-invalidated";
216
237
  reason: string;
238
+ } | {
239
+ type: "builder-guidance-followup-intended";
217
240
  } | {
218
241
  type: "finalization-started";
219
242
  } | {
220
243
  type: "finalization-invalidated";
221
244
  reason: string;
245
+ } | {
246
+ type: "contract-amended";
247
+ predecessorDigest: string;
248
+ contractDigest: string;
249
+ reason: string;
222
250
  } | {
223
251
  type: "completed";
224
252
  };
@@ -7,6 +7,10 @@ export function initialQaReducerState(runId, ticketId) {
7
7
  export function reduceQaState(current, event) {
8
8
  const next = { ...current, revision: current.revision + 1 };
9
9
  switch (event.type) {
10
+ case "contract-amended":
11
+ if (["completed", "waived", "finalizing", "turn-intended", "turn-uncertain", "remediation-intended", "remediation-uncertain"].includes(current.state) || !event.reason.trim())
12
+ invalid(current, event);
13
+ return { ...next, state: "recheck-required", reviewBasisDigest: undefined, retrySlot: undefined, passCertificateId: undefined };
10
14
  case "source-frozen":
11
15
  if (!["idle", "recheck-required", "operator-menu", "turn-uncertain", "remediation-uncertain", "passed"].includes(current.state))
12
16
  invalid(current, event);
@@ -59,6 +63,10 @@ export function reduceQaState(current, event) {
59
63
  if (!["idle", "source-frozen", "review-ready", "turn-intended", "turn-uncertain", "review-failed", "remediation-uncertain", "recheck-required"].includes(current.state))
60
64
  invalid(current, event);
61
65
  return { ...next, state: "operator-menu", retrySlot: undefined };
66
+ case "failed-review-restored":
67
+ if (current.state !== "operator-menu" || current.reviewNumber !== event.reviewNumber || current.sourceStateDigest !== event.sourceStateDigest || !current.openReportDigests.includes(event.reportDigest))
68
+ invalid(current, event);
69
+ return { ...next, state: "review-failed", retrySlot: undefined };
62
70
  case "review-passed":
63
71
  if (current.state !== "turn-intended")
64
72
  invalid(current, event);
@@ -71,6 +79,10 @@ export function reduceQaState(current, event) {
71
79
  if (current.state !== "passed" || !event.reason.trim())
72
80
  invalid(current, event);
73
81
  return { ...next, state: "operator-menu", retrySlot: undefined, passCertificateId: undefined };
82
+ case "builder-guidance-followup-intended":
83
+ if (current.state !== "passed")
84
+ invalid(current, event);
85
+ return { ...next, state: "operator-menu", retrySlot: undefined, passCertificateId: undefined, remediationGeneration: current.remediationGeneration + 1 };
74
86
  case "finalization-started":
75
87
  if (current.state !== "passed" && current.state !== "waived")
76
88
  invalid(current, event);
@@ -1,3 +1,4 @@
1
+ import { type BuilderGuidanceFollowup } from "./buildAssignment.js";
1
2
  import { type ProviderSessionRefV1, type QaFailureReportV1, type SessionStrategy } from "rafi-spec";
2
3
  import type { BuilderAdapter, CompactResult } from "./adapters/types.js";
3
4
  import type { RunObserver } from "./observability.js";
@@ -45,6 +46,7 @@ export interface QaNonconvergenceContext {
45
46
  export type QaNonconvergenceDecision = {
46
47
  action: "retry" | "pause" | "waive" | "remediate";
47
48
  remediation?: string;
49
+ authorizationRef?: string;
48
50
  };
49
51
  export type QaFixRequest = {
50
52
  kind: "validated-report";
@@ -146,6 +148,9 @@ export interface IsolatedQaOptions {
146
148
  fix?: (request: QaFixRequest) => Promise<QaFixResult>;
147
149
  /** Canonical production delivery path for validated QA failure reports. */
148
150
  deliverFailure?: (request: QaFailureDeliveryInput) => Promise<QaFailureDeliveryResult>;
151
+ /** Explicit resume may deliver waiting Builder guidance using existing scoped authority. */
152
+ resumeBuilderGuidance?: boolean;
153
+ deliverBuilderFollowup?: (instruction: string, followup: BuilderGuidanceFollowup) => Promise<QaFixResult>;
149
154
  maxCycles: number;
150
155
  /** Durable QA execution scope; review/fix budgets survive worker restarts. */
151
156
  recovery: {
@@ -185,6 +190,7 @@ export interface IsolatedQaOptions {
185
190
  /** Accumulated authoritative QA/fix history, populated by runIsolatedQa. */
186
191
  qaHistory?: QaReportHistoryEntry[];
187
192
  qaRuntimeContext?: unknown;
193
+ frozenValidationChecklist?: string[];
188
194
  /** A durable packet selected by build:resume and transferred by validated handoff. */
189
195
  resumedRecovery?: QaRecoveryPacket;
190
196
  /** True when a continuity wrapper validates and strips deltas before returning turn text. */