ai-foreman 1.7.29 → 2.0.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/README.md +1 -1
- package/dist/adapters/claude.d.ts +23 -12
- package/dist/adapters/claude.js +227 -23
- package/dist/adapters/codex.d.ts +17 -7
- package/dist/adapters/codex.js +195 -17
- package/dist/adapters/recovering.d.ts +2 -0
- package/dist/adapters/recovering.js +5 -1
- package/dist/adapters/structured.d.ts +71 -0
- package/dist/adapters/structured.js +213 -0
- package/dist/adapters/types.d.ts +85 -9
- package/dist/agentRun.d.ts +2 -0
- package/dist/agentRun.js +2 -1
- package/dist/boundedTurnEvidence.d.ts +18 -0
- package/dist/boundedTurnEvidence.js +76 -0
- package/dist/branch/currentGuard.d.ts +2 -0
- package/dist/branch/currentGuard.js +2 -0
- package/dist/branch/planner.js +1 -4
- package/dist/branch/runner.d.ts +2 -1
- package/dist/branch/runner.js +10 -7
- package/dist/buildAdmission.js +4 -0
- package/dist/buildApproval.d.ts +15 -0
- package/dist/buildApproval.js +25 -0
- package/dist/buildAssignment.d.ts +1 -0
- package/dist/buildAssignment.js +82 -23
- package/dist/buildInterventions.d.ts +2 -2
- package/dist/buildInterventions.js +11 -4
- package/dist/buildLaunchGate.js +3 -2
- package/dist/builderGuidanceFollowup.js +17 -6
- package/dist/cli/manager.js +22 -20
- package/dist/cli/start.js +22 -26
- package/dist/cli/tickets.d.ts +0 -7
- package/dist/cli/tickets.js +41 -40
- package/dist/continuity.d.ts +10 -14
- package/dist/continuity.js +198 -179
- package/dist/continuityOrigin.d.ts +13 -0
- package/dist/continuityOrigin.js +91 -0
- package/dist/foreman.d.ts +10 -4
- package/dist/foreman.js +51 -193
- package/dist/graph/boundary.d.ts +1 -1
- package/dist/graph/boundary.js +1 -1
- package/dist/graph/host.js +8 -19
- package/dist/graph/manager.d.ts +1 -1
- package/dist/graph/manager.js +1 -1
- package/dist/graph/preparation.d.ts +38 -0
- package/dist/graph/preparation.js +7 -6
- package/dist/graph/semantic.d.ts +1 -0
- package/dist/graph/semantic.js +4 -1
- package/dist/graph/session.js +7 -2
- package/dist/graph/turn.d.ts +2 -3
- package/dist/graph/turn.js +28 -40
- package/dist/handoffProtocol.js +2 -2
- package/dist/handoffs.d.ts +3 -1
- package/dist/handoffs.js +90 -59
- package/dist/hostGraphApplicability.d.ts +2 -1
- package/dist/hostGraphApplicability.js +17 -19
- package/dist/hostQaAugmentation.js +21 -4
- package/dist/hostQaComparison.d.ts +21 -2
- package/dist/hostQaComparison.js +43 -29
- package/dist/hostSourcePreservation.d.ts +38 -0
- package/dist/hostSourcePreservation.js +72 -0
- package/dist/interviews.d.ts +7 -0
- package/dist/interviews.js +18 -1
- package/dist/legacyCommunicationDisplay.d.ts +2 -0
- package/dist/legacyCommunicationDisplay.js +2 -0
- package/dist/managerPacket.js +3 -3
- package/dist/markers.d.ts +4 -3
- package/dist/markers.js +4 -20
- package/dist/probeIpc.d.ts +14 -0
- package/dist/probeIpc.js +29 -0
- package/dist/qaBuildGate.d.ts +21 -0
- package/dist/qaBuildGate.js +47 -12
- package/dist/qaBuilderCoverage.js +22 -13
- package/dist/qaBuilderEvidence.d.ts +16 -1
- package/dist/qaBuilderEvidence.js +48 -8
- package/dist/qaContractCoverage.d.ts +2 -2
- package/dist/qaContractCoverage.js +4 -9
- package/dist/qaContractDelivery.js +34 -20
- package/dist/qaCoverageAssembly.d.ts +22 -0
- package/dist/qaCoverageAssembly.js +16 -0
- package/dist/qaDeliveryJournal.d.ts +2 -0
- package/dist/qaDeliveryJournal.js +19 -0
- package/dist/qaFailureDelivery.js +97 -85
- package/dist/qaHostTransformation.d.ts +26 -0
- package/dist/qaHostTransformation.js +100 -0
- package/dist/qaPreparation.d.ts +8 -20
- package/dist/qaPreparation.js +133 -248
- package/dist/qaPreparationInputs.js +19 -0
- package/dist/qaPrerequisites.d.ts +2 -1
- package/dist/qaPrerequisites.js +21 -1
- package/dist/qaProtocolV2.d.ts +15 -0
- package/dist/qaRecovery.js +21 -4
- package/dist/qaReview.js +220 -395
- package/dist/qaReviewEvidence.d.ts +7 -3
- package/dist/qaReviewEvidence.js +15 -4
- package/dist/qaRuntime.js +1 -1
- package/dist/qaSnapshot.js +71 -21
- package/dist/qaStructuredResponse.d.ts +10 -0
- package/dist/qaStructuredResponse.js +57 -0
- package/dist/qaTimeline.js +5 -3
- package/dist/qaVerificationContract.d.ts +1 -2
- package/dist/qaVerificationContract.js +24 -35
- package/dist/readinessGate.js +5 -4
- package/dist/runtimeReadiness.js +23 -15
- package/dist/statusReporter.d.ts +2 -0
- package/dist/statusReporter.js +2 -0
- package/dist/structuredCutover.d.ts +31 -0
- package/dist/structuredCutover.js +510 -0
- package/dist/structuredCutoverFiles.d.ts +10 -0
- package/dist/structuredCutoverFiles.js +181 -0
- package/dist/structuredDispatchAuthority.d.ts +22 -0
- package/dist/structuredDispatchAuthority.js +110 -0
- package/dist/structuredDispatchRecord.d.ts +29 -0
- package/dist/structuredDispatchRecord.js +36 -0
- package/dist/structuredLiveBudget.d.ts +2 -0
- package/dist/structuredLiveBudget.js +23 -0
- package/dist/structuredOperation.d.ts +18 -0
- package/dist/structuredOperation.js +257 -0
- package/dist/supervisedStart.js +35 -21
- package/dist/ticketPlanning.d.ts +1 -373
- package/dist/ticketPlanning.js +2 -26
- package/dist/ticketPopulation.d.ts +0 -3
- package/dist/ticketPopulation.js +4 -21
- package/dist/ticketPopulationPageAuthority.d.ts +13 -0
- package/dist/ticketPopulationPageAuthority.js +30 -0
- package/dist/ticketPopulationPaging.d.ts +27 -0
- package/dist/ticketPopulationPaging.js +105 -0
- package/dist/tickets/renderMarkdown.d.ts +14 -0
- package/dist/tickets/renderMarkdown.js +39 -5
- package/dist/tickets/ticketLoader.js +10 -0
- package/dist/tickets/ticketSchema.d.ts +107 -0
- package/dist/tickets/ticketSchema.js +4 -1
- package/dist/workflowDb.d.ts +100 -2
- package/dist/workflowDb.js +763 -39
- package/dist/workflowReader.d.ts +1 -0
- package/dist/workflowReader.js +9 -0
- package/package.json +15 -7
|
@@ -0,0 +1,510 @@
|
|
|
1
|
+
import { persistedProviderWireContract } from "rafi-spec";
|
|
2
|
+
import { qaDigest } from "./qaProtocolV2.js";
|
|
3
|
+
import { verifyCheckpointOrigin } from "./continuityOrigin.js";
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
|
+
import { canonicalJson, qaVerdictOutput } from "rafi-spec";
|
|
6
|
+
import { persistedDispatchTuple, verifyPersistedDispatch } from "./structuredDispatchRecord.js";
|
|
7
|
+
export const STRUCTURED_CUTOVER = "003_structured_communication_v2_cutover";
|
|
8
|
+
export const STRUCTURED_AUDIT_CLOSURE = "004_structured_communication_audit_closure";
|
|
9
|
+
export const STRUCTURED_CUTOVER_MATRIX_VERSION = 3;
|
|
10
|
+
export const STRUCTURED_CUTOVER_REASON = "structured-communication-v2-cutover";
|
|
11
|
+
/** User definitions, answers, settings and history are retained, never executable after fencing. */
|
|
12
|
+
const retained = new Set(["workflow_runs", "human_decisions", "build_work_scope", "build_instructions", "build_instruction_events",
|
|
13
|
+
"role_settings", "run_autonomy_policy", "workflow_events", "workflow_telemetry", "workflow_issues", "legacy_imports",
|
|
14
|
+
"build_owned_processes", "build_runtime_runs", "build_child_runs", "build_retry_lineage", "build_work_events", "structured_cutover_quarantine"]);
|
|
15
|
+
/** Reviewed authority tables; descendants without run_id are selected through their declared FK. */
|
|
16
|
+
const authority = new Set(["structured_handoff_lineage", "continuity_checkpoint_origins", "structured_dispatch_processing", "structured_submission_claims", "operation_journal", "continuity_events", "continuity_checkpoints", "continuity_heads", "handoffs", "role_mutation_leases",
|
|
17
|
+
"provider_sessions", "provider_session_bindings", "context_samples", "session_usage_samples", "compaction_attempts", "live_settings_acknowledgments",
|
|
18
|
+
"recovery_interruptions", "recovery_attempts", "recovery_decisions", "supervisor_leases", "branch_resume_sessions", "publication_transactions",
|
|
19
|
+
"qa_turns", "qa_turn_events", "qa_sessions", "qa_retry_slots", "qa_source_states", "qa_review_bases", "qa_ticket_heads", "qa_transitions",
|
|
20
|
+
"qa_reports", "qa_findings", "qa_report_chains", "qa_report_dispositions", "qa_review_attempts", "qa_remediation_attempts", "qa_remediation_receipts",
|
|
21
|
+
"qa_failure_handoffs", "qa_delivery_turns", "qa_handoffs", "qa_operation_journal", "qa_pass_certificates", "qa_finalization_steps",
|
|
22
|
+
"qa_packet_projections", "qa_recovery_heads", "qa_remediation_stops", "qa_remediation_authorizations", "qa_delivery_invocations",
|
|
23
|
+
"qa_preparation_policy", "qa_preparation_budgets", "qa_preparation_attempts", "qa_preparation_progress", "qa_preparation_events",
|
|
24
|
+
"qa_depth_decisions", "qa_verification_contracts", "qa_contract_heads", "qa_contract_receipts", "qa_contract_check_history",
|
|
25
|
+
"build_assignments", "build_work_admissions", "build_ownership_conflicts", "build_reconciliations", "build_instruction_deliveries", "build_instruction_streams",
|
|
26
|
+
"build_admission", "build_launches", "page_ledgers", "page_ledger_pages"]);
|
|
27
|
+
export const STRUCTURED_AUTHORITY_TABLES = Object.freeze([...authority, "qa_contract_artifacts", "communication_evidence_refs", "structured_communication_cutover", "structured_cutover_quarantine", "content_refs"]);
|
|
28
|
+
const quote = (name) => { if (!/^[a-z_][a-z_0-9]*$/.test(name))
|
|
29
|
+
throw new Error("Unclassified cutover SQL identity"); return `"${name}"`; };
|
|
30
|
+
export function cutoverOwnershipMatrix(db) {
|
|
31
|
+
const entries = [];
|
|
32
|
+
for (const { name } of db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%' ORDER BY name").all()) {
|
|
33
|
+
const columns = db.prepare(`PRAGMA table_info(${quote(name)})`).all().map(column => column.name);
|
|
34
|
+
const foreignKeys = db.prepare(`PRAGMA foreign_key_list(${quote(name)})`).all();
|
|
35
|
+
if (!authority.has(name) && !retained.has(name) && name !== "project_lease"
|
|
36
|
+
&& (columns.includes("run_id") || foreignKeys.some(key => authority.has(key.table) || key.table === "workflow_runs")))
|
|
37
|
+
throw new Error(`Cutover ownership matrix does not classify ${name}`);
|
|
38
|
+
if (authority.has(name) || retained.has(name)) {
|
|
39
|
+
const owner = retained.has(name) ? "retained-artifact" : columns.includes("run_id") ? "run-id"
|
|
40
|
+
: name === "provider_session_bindings" ? "session-key" : name === "qa_contract_check_history" ? "contract-id" : !foreignKeys.length && columns.includes("record_json") ? "json-run-id" : "foreign-key";
|
|
41
|
+
if (owner === "foreign-key" && !foreignKeys.length)
|
|
42
|
+
throw new Error(`Cutover ownership matrix has no owner for ${name}`);
|
|
43
|
+
entries.push({ table: name, disposition: retained.has(name) ? "retain" : "purge-owned", columns, foreignKeys,
|
|
44
|
+
jsonColumns: columns.filter(column => column.endsWith("_json")), owner,
|
|
45
|
+
evidenceReferenceExtractor: "normalized-refs-with-conservative-unknown-retention", deletionOrder: null });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const pending = entries.filter(entry => entry.disposition === "purge-owned");
|
|
49
|
+
for (let ordinal = 0; pending.length; ordinal++) {
|
|
50
|
+
const index = pending.findIndex(parent => !pending.some(child => child.table !== parent.table && child.foreignKeys.some(key => key.table === parent.table)));
|
|
51
|
+
if (index < 0)
|
|
52
|
+
throw new Error("Cutover ownership matrix has an unresolved foreign-key cycle");
|
|
53
|
+
pending.splice(index, 1)[0].deletionOrder = ordinal;
|
|
54
|
+
}
|
|
55
|
+
return entries;
|
|
56
|
+
}
|
|
57
|
+
export function assertStructuredCutoverComplete(db) {
|
|
58
|
+
try {
|
|
59
|
+
if (!db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='structured_communication_cutover'").get())
|
|
60
|
+
throw new Error("Missing completion table");
|
|
61
|
+
const row = db.prepare("SELECT details_json FROM structured_communication_cutover WHERE migration=?").get(STRUCTURED_AUDIT_CLOSURE);
|
|
62
|
+
if (!db.prepare("SELECT 1 FROM structured_communication_cutover WHERE migration=?").get(STRUCTURED_CUTOVER) || !row ||
|
|
63
|
+
JSON.parse(row.details_json)?.ownershipMatrixVersion !== STRUCTURED_CUTOVER_MATRIX_VERSION)
|
|
64
|
+
throw new Error("Incomplete completion record");
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
throw new Error("v2-cutover-pending: open with the current writer to complete structured communication cutover");
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/** Classification is structural; JSON spelling or merely containing a tuple key is never proof. */
|
|
71
|
+
export function invalidStructuredRun(db, run, projectDir = "") {
|
|
72
|
+
try {
|
|
73
|
+
return classifyStructuredRun(db, run, projectDir);
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return true;
|
|
77
|
+
} // Malformed owned records are quarantined independently.
|
|
78
|
+
}
|
|
79
|
+
function classifyStructuredRun(db, run, projectDir) {
|
|
80
|
+
if (run.legacy || run.checkpoint === STRUCTURED_CUTOVER_REASON)
|
|
81
|
+
return true;
|
|
82
|
+
let state;
|
|
83
|
+
try {
|
|
84
|
+
state = JSON.parse(run.state_json);
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
if (!state || typeof state !== "object" || Array.isArray(state))
|
|
90
|
+
return true;
|
|
91
|
+
const operations = db.prepare("SELECT * FROM operation_journal WHERE run_id=? AND kind='provider-dispatch'").all(run.run_id);
|
|
92
|
+
for (const operation of operations) {
|
|
93
|
+
try {
|
|
94
|
+
if (operation.status !== "confirmed")
|
|
95
|
+
return true;
|
|
96
|
+
const intent = JSON.parse(operation.intent_json);
|
|
97
|
+
const tuple = persistedDispatchTuple(intent.sealedDispatch);
|
|
98
|
+
if (canonicalJson(intent.tuple) !== intent.sealedDispatch.tupleJson || tuple.dispatchId !== operation.idempotency_key)
|
|
99
|
+
return true;
|
|
100
|
+
const descriptor = persistedProviderWireContract(tuple);
|
|
101
|
+
if (!descriptor)
|
|
102
|
+
return true;
|
|
103
|
+
if (operation.status === "confirmed") {
|
|
104
|
+
const result = JSON.parse(operation.result_json);
|
|
105
|
+
const evidence = db.prepare("SELECT content FROM content_refs WHERE digest=?").get(result.responseDigest);
|
|
106
|
+
if (!evidence || createHash("sha256").update(evidence.content).digest("hex") !== result.responseDigest)
|
|
107
|
+
return true;
|
|
108
|
+
const retained = JSON.parse(evidence.content.toString());
|
|
109
|
+
if (retained.version !== 2 || retained.tupleDigest !== tuple.digest)
|
|
110
|
+
return true;
|
|
111
|
+
let receipt = retained.receipt;
|
|
112
|
+
if (receipt?.state === "candidate") {
|
|
113
|
+
const value = db.prepare("SELECT content FROM content_refs WHERE digest=?").get(retained.valueDigest);
|
|
114
|
+
if (!value || createHash("sha256").update(value.content).digest("hex") !== retained.valueDigest)
|
|
115
|
+
return true;
|
|
116
|
+
receipt = { ...receipt, value: receipt.representation === "json-text" ? value.content.toString() : JSON.parse(value.content.toString()) };
|
|
117
|
+
}
|
|
118
|
+
verifyPersistedDispatch(descriptor.canonical, intent.sealedDispatch, receipt);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return true;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
for (const row of db.prepare("SELECT intent_json,receipt_json,status FROM qa_turns WHERE run_id=?").all(run.run_id)) {
|
|
126
|
+
try {
|
|
127
|
+
const intent = JSON.parse(row.intent_json), receipt = JSON.parse(row.receipt_json);
|
|
128
|
+
persistedDispatchTuple(intent.sealedDispatch);
|
|
129
|
+
const generic = operations.find(operation => operation.idempotency_key === intent.operationId);
|
|
130
|
+
if (!generic || row.status !== "completed" || receipt.dispatch !== "completed" || !receipt.terminalEventObserved || receipt.operationId !== intent.operationId || canonicalJson(intent.sealedDispatch) !== canonicalJson(receipt.sealedDispatch) ||
|
|
131
|
+
canonicalJson(intent.sealedDispatch) !== canonicalJson(JSON.parse(generic.intent_json).sealedDispatch))
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
catch {
|
|
135
|
+
return true;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
if (db.prepare("SELECT 1 FROM sqlite_master WHERE name='qa_delivery_turns'").get())
|
|
139
|
+
for (const row of db.prepare("SELECT d.record_json,d.status FROM qa_delivery_turns d JOIN qa_reports r ON r.report_occurrence_id=d.report_occurrence_id WHERE r.run_id=?").all(run.run_id)) {
|
|
140
|
+
try {
|
|
141
|
+
const record = JSON.parse(row.record_json), generic = operations.find(operation => operation.idempotency_key === record.turnRecordId);
|
|
142
|
+
persistedDispatchTuple(record.sealedDispatch);
|
|
143
|
+
if (!generic || row.status !== "completed" || canonicalJson(record.sealedDispatch) !== canonicalJson(JSON.parse(generic.intent_json).sealedDispatch))
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
return true;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
const rows = (table) => db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name=?").get(table)
|
|
151
|
+
? db.prepare(`SELECT * FROM ${quote(table)} WHERE run_id=?`).all(run.run_id) : [];
|
|
152
|
+
const nativeTurns = db.prepare("SELECT * FROM qa_turns WHERE run_id=?").all(run.run_id);
|
|
153
|
+
for (const certificate of rows("qa_pass_certificates")) {
|
|
154
|
+
try {
|
|
155
|
+
const value = JSON.parse(certificate.certificate_json);
|
|
156
|
+
if (value.certificateId !== certificate.certificate_id || value.runId !== run.run_id || value.ticketId !== certificate.ticket_id || value.qaRevision !== certificate.qa_revision ||
|
|
157
|
+
value.sourceStateDigest !== certificate.source_state_digest || value.reviewBasisDigest !== certificate.review_basis_digest || value.turnReceiptDigest !== certificate.turn_receipt_digest)
|
|
158
|
+
return true;
|
|
159
|
+
const turn = nativeTurns.find(turn => turn.ticket_id === value.ticketId && turn.source_state_digest === value.sourceStateDigest && turn.review_basis_digest === value.reviewBasisDigest &&
|
|
160
|
+
turn.status === "completed" && qaDigest("turn-receipt", JSON.parse(turn.receipt_json)) === value.turnReceiptDigest);
|
|
161
|
+
if (!turn)
|
|
162
|
+
return true;
|
|
163
|
+
const receipt = JSON.parse(turn.receipt_json), intent = JSON.parse(turn.intent_json);
|
|
164
|
+
const operation = operations.find(operation => operation.idempotency_key === receipt.operationId);
|
|
165
|
+
if (!operation || !receipt.terminalEventObserved || receipt.dispatch !== "completed" || canonicalJson(receipt.sealedDispatch) !== canonicalJson(intent.sealedDispatch))
|
|
166
|
+
return true;
|
|
167
|
+
const result = JSON.parse(operation.result_json);
|
|
168
|
+
const read = (digest) => {
|
|
169
|
+
const bytes = db.prepare("SELECT content FROM content_refs WHERE digest=?").get(digest)?.content;
|
|
170
|
+
if (!bytes || createHash("sha256").update(bytes).digest("hex") !== digest)
|
|
171
|
+
throw new Error("Invalid certificate evidence");
|
|
172
|
+
return bytes;
|
|
173
|
+
};
|
|
174
|
+
const retained = JSON.parse(read(result.responseDigest).toString());
|
|
175
|
+
if (retained.hostTurnId !== receipt.providerTurnId)
|
|
176
|
+
return true;
|
|
177
|
+
const native = { ...retained.receipt, value: retained.receipt.representation === "json-text" ? read(retained.valueDigest).toString() : JSON.parse(read(retained.valueDigest).toString()) };
|
|
178
|
+
const verdict = verifyPersistedDispatch(qaVerdictOutput, intent.sealedDispatch, native);
|
|
179
|
+
if (verdict.result.kind !== "completed" || verdict.result.payload.kind !== "passed")
|
|
180
|
+
return true;
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
for (const review of rows("qa_review_attempts")) {
|
|
187
|
+
if (!nativeTurns.some(turn => turn.ticket_id === review.ticket_id && turn.review_number === review.review_number && turn.source_state_digest === review.source_digest))
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
for (const report of rows("qa_reports")) {
|
|
191
|
+
try {
|
|
192
|
+
const turn = nativeTurns.find(turn => turn.ticket_id === report.ticket_id && turn.review_number === report.review_number && turn.source_state_digest === report.source_state_digest && turn.review_basis_digest === report.review_basis_digest);
|
|
193
|
+
if (!turn)
|
|
194
|
+
return true;
|
|
195
|
+
const receipt = JSON.parse(turn.receipt_json), intent = JSON.parse(turn.intent_json);
|
|
196
|
+
const operation = operations.find(operation => operation.idempotency_key === receipt.operationId);
|
|
197
|
+
if (!operation)
|
|
198
|
+
return true;
|
|
199
|
+
const retained = JSON.parse(db.prepare("SELECT content FROM content_refs WHERE digest=?").get(JSON.parse(operation.result_json).responseDigest).content.toString());
|
|
200
|
+
const value = db.prepare("SELECT content FROM content_refs WHERE digest=?").get(retained.valueDigest).content.toString();
|
|
201
|
+
const response = verifyPersistedDispatch(qaVerdictOutput, intent.sealedDispatch, { ...retained.receipt, value: retained.receipt.representation === "json-text" ? value : JSON.parse(value) });
|
|
202
|
+
if (response.result.kind !== "completed" || response.result.payload.kind !== "failed" || canonicalJson(response.result.payload.report) !== canonicalJson(JSON.parse(report.report_json)))
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
return true;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
for (const attempt of rows("qa_remediation_attempts")) {
|
|
210
|
+
try {
|
|
211
|
+
const record = JSON.parse(attempt.record_json);
|
|
212
|
+
const dispatches = operations.filter(operation => JSON.parse(operation.intent_json).tuple?.actionId === attempt.attempt_id);
|
|
213
|
+
if (!dispatches.length || record.status !== "succeeded" || !rows("qa_remediation_receipts").some(receipt => receipt.operation_id === attempt.attempt_id))
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
for (const receipt of rows("qa_remediation_receipts")) {
|
|
221
|
+
try {
|
|
222
|
+
const value = JSON.parse(receipt.receipt_json);
|
|
223
|
+
if (value.version !== 3 || value.runId !== run.run_id || value.ticketId !== receipt.ticket_id || !operations.some(operation => operation.idempotency_key === value.turnRecordId))
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
for (const step of rows("qa_finalization_steps")) {
|
|
231
|
+
const certificate = rows("qa_pass_certificates").find(row => row.certificate_id === step.certificate_id);
|
|
232
|
+
if (!certificate || certificate.ticket_id !== step.ticket_id || !certificate.consumed_at || certificate.consumed_by !== step.kind)
|
|
233
|
+
return true;
|
|
234
|
+
}
|
|
235
|
+
for (const row of rows("qa_ticket_heads")) {
|
|
236
|
+
const head = JSON.parse(row.state_json);
|
|
237
|
+
if (head.runId !== run.run_id || head.ticketId !== row.ticket_id || head.revision !== row.revision || head.state !== row.state)
|
|
238
|
+
return true;
|
|
239
|
+
if (["passed", "finalizing", "completed"].includes(head.state) && !rows("qa_pass_certificates").some(certificate => certificate.certificate_id === head.passCertificateId && certificate.ticket_id === row.ticket_id
|
|
240
|
+
&& certificate.source_state_digest === head.sourceStateDigest && certificate.review_basis_digest === head.reviewBasisDigest))
|
|
241
|
+
return true;
|
|
242
|
+
if ((head.openReportDigests ?? []).some((digest) => !rows("qa_reports").some(report => report.report_digest === digest && report.ticket_id === row.ticket_id)))
|
|
243
|
+
return true;
|
|
244
|
+
}
|
|
245
|
+
for (const checkpoint of rows("continuity_checkpoints")) {
|
|
246
|
+
try {
|
|
247
|
+
verifyCheckpointOrigin(db, projectDir, checkpoint.checkpoint_id);
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
return true;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
const hasSession = (session) => operations.some(operation => {
|
|
254
|
+
const intent = JSON.parse(operation.intent_json), ref = intent.sessionRef;
|
|
255
|
+
return ref && session && ["sessionId", "provider", "role", "stream", "generation", "cwd", "configRoot", "workspaceIdentity", "ticketId", "deliveryUnitId"].every(key => ref[key] === session[key]);
|
|
256
|
+
});
|
|
257
|
+
// V2 QA session records intentionally omit V1's optional workspace labels.
|
|
258
|
+
// Bind them to their own native QA owner and ticket instead of inferring
|
|
259
|
+
// missing fields or comparing them to an unrelated generic session.
|
|
260
|
+
const hasQaSession = (session, ticketId) => nativeTurns.some(turn => {
|
|
261
|
+
const intent = JSON.parse(turn.intent_json);
|
|
262
|
+
return intent.ticketId === ticketId && session && ["provider", "sessionId", "role", "stream", "generation", "cwd", "configRoot"].every(key => intent.providerSession[key] === session[key]);
|
|
263
|
+
});
|
|
264
|
+
for (const row of rows("qa_failure_handoffs")) {
|
|
265
|
+
if (!rows("qa_reports").some(report => report.report_digest === row.report_digest && report.ticket_id === row.ticket_id)
|
|
266
|
+
|| row.builder_session_json && !hasSession(JSON.parse(row.builder_session_json)))
|
|
267
|
+
return true;
|
|
268
|
+
if (["remediation-reported", "completed"].includes(row.state) && !operations.some(operation => {
|
|
269
|
+
const intent = JSON.parse(operation.intent_json);
|
|
270
|
+
return intent.tuple?.actionId === row.operation_id && intent.tuple?.operation === "builder-remediation";
|
|
271
|
+
}))
|
|
272
|
+
return true;
|
|
273
|
+
}
|
|
274
|
+
for (const row of rows("qa_handoffs")) {
|
|
275
|
+
const receipt = JSON.parse(row.receipt_json);
|
|
276
|
+
const checkpoint = rows("continuity_checkpoints").find(checkpoint => checkpoint.digest === receipt.acceptanceCheckpointDigest && checkpoint.role === "qa");
|
|
277
|
+
if (row.status !== "accepted" || receipt.runId !== run.run_id || !checkpoint || !hasQaSession(receipt.successor, row.ticket_id))
|
|
278
|
+
return true;
|
|
279
|
+
verifyCheckpointOrigin(db, projectDir, checkpoint.checkpoint_id);
|
|
280
|
+
}
|
|
281
|
+
for (const lease of rows("role_mutation_leases")) {
|
|
282
|
+
try {
|
|
283
|
+
if (!lease.provider_session_ref_json || !hasSession(JSON.parse(lease.provider_session_ref_json)))
|
|
284
|
+
return true;
|
|
285
|
+
}
|
|
286
|
+
catch {
|
|
287
|
+
return true;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
for (const contract of rows("qa_verification_contracts")) {
|
|
291
|
+
try {
|
|
292
|
+
const value = JSON.parse(contract.record_json);
|
|
293
|
+
const evidence = value.preparationEvidence;
|
|
294
|
+
if (!Array.isArray(evidence) || !evidence.length || evidence.some(entry => !entry || !operations.some(operation => operation.idempotency_key === entry.operationId && JSON.parse(operation.intent_json).tuple.sessionId === entry.sessionId)
|
|
295
|
+
|| !db.prepare("SELECT 1 FROM content_refs WHERE digest=?").get(entry.evidenceDigest)))
|
|
296
|
+
return true;
|
|
297
|
+
}
|
|
298
|
+
catch {
|
|
299
|
+
return true;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
for (const head of rows("qa_recovery_heads")) {
|
|
303
|
+
const projection = rows("qa_packet_projections").find(projection => projection.packet_digest === head.packet_digest && projection.ticket_id === head.ticket_id);
|
|
304
|
+
if (!projection || !projection.manifest_json)
|
|
305
|
+
return true;
|
|
306
|
+
const manifest = JSON.parse(projection.manifest_json);
|
|
307
|
+
if (manifest.runId !== run.run_id || manifest.ticketId !== head.ticket_id || manifest.revision !== head.revision
|
|
308
|
+
|| !nativeTurns.some(turn => turn.ticket_id === head.ticket_id && turn.review_number === manifest.reviewAttempt))
|
|
309
|
+
return true;
|
|
310
|
+
}
|
|
311
|
+
for (const session of rows("provider_sessions")) {
|
|
312
|
+
try {
|
|
313
|
+
if (!session.session_ref_json || !hasSession(JSON.parse(session.session_ref_json)))
|
|
314
|
+
return true;
|
|
315
|
+
if (session.session_key) {
|
|
316
|
+
const binding = db.prepare("SELECT session_ref_json FROM provider_session_bindings WHERE session_key=?").get(session.session_key);
|
|
317
|
+
if (!binding || canonicalJson(JSON.parse(binding.session_ref_json)) !== canonicalJson(JSON.parse(session.session_ref_json)))
|
|
318
|
+
return true;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
catch {
|
|
322
|
+
return true;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
for (const session of rows("qa_sessions")) {
|
|
326
|
+
try {
|
|
327
|
+
if (!hasQaSession(JSON.parse(session.session_json).session, session.ticket_id))
|
|
328
|
+
return true;
|
|
329
|
+
}
|
|
330
|
+
catch {
|
|
331
|
+
return true;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
for (const session of rows("branch_resume_sessions")) {
|
|
335
|
+
try {
|
|
336
|
+
const value = JSON.parse(session.session_json);
|
|
337
|
+
if (!value.sessionRef || !hasSession(value.sessionRef))
|
|
338
|
+
return true;
|
|
339
|
+
}
|
|
340
|
+
catch {
|
|
341
|
+
return true;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
for (const handoff of rows("handoffs")) {
|
|
345
|
+
if (handoff.state !== "accepted" || !handoff.acceptance_receipt_digest || !rows("continuity_checkpoint_origins").some(row => JSON.parse(row.origin_json).publicationKind === "handoff_acceptance"))
|
|
346
|
+
return true;
|
|
347
|
+
}
|
|
348
|
+
for (const head of rows("continuity_heads")) {
|
|
349
|
+
if (head.role !== "run" && !rows("continuity_checkpoints").some(row => row.digest === head.digest && row.role === head.role))
|
|
350
|
+
return true;
|
|
351
|
+
}
|
|
352
|
+
// Embedded executable projections require their own concrete owner references.
|
|
353
|
+
for (const [key, value] of Object.entries(state)) {
|
|
354
|
+
if (["builder", "qa", "sessionBindings"].includes(key) && value) {
|
|
355
|
+
const invalidSession = (candidate) => {
|
|
356
|
+
if (!candidate || typeof candidate !== "object")
|
|
357
|
+
return false;
|
|
358
|
+
if (Array.isArray(candidate))
|
|
359
|
+
return candidate.some(invalidSession);
|
|
360
|
+
const object = candidate;
|
|
361
|
+
if ("sessionId" in object && !hasSession(object.sessionRef ?? object))
|
|
362
|
+
return true;
|
|
363
|
+
if (object.sessionRef && !hasSession(object.sessionRef))
|
|
364
|
+
return true;
|
|
365
|
+
return Object.values(object).some(invalidSession);
|
|
366
|
+
};
|
|
367
|
+
if (invalidSession(value))
|
|
368
|
+
return true;
|
|
369
|
+
}
|
|
370
|
+
if (key === "receipts" && value && Object.values(value).some(receipt => !receipt || typeof receipt !== "object" || !operations.some(operation => operation.idempotency_key === receipt.dispatchId)))
|
|
371
|
+
return true;
|
|
372
|
+
}
|
|
373
|
+
for (const ticket of state.progress?.completedTickets ?? []) {
|
|
374
|
+
const completion = db.prepare("SELECT intent_json FROM operation_journal WHERE run_id=? AND kind IN ('ticket-complete','synthetic-completion') AND status='confirmed'").all(run.run_id);
|
|
375
|
+
if (!completion.some(row => {
|
|
376
|
+
const intent = JSON.parse(row.intent_json);
|
|
377
|
+
return [intent.ticket, intent.ticketId, intent.workId].includes(ticket) && typeof intent.dispatchId === "string" && operations.some(operation => operation.idempotency_key === intent.dispatchId);
|
|
378
|
+
}))
|
|
379
|
+
return true;
|
|
380
|
+
}
|
|
381
|
+
if (!operations.length && (state.version !== undefined || state.progress?.completedTickets?.length || rows("qa_recovery_heads").length || rows("qa_verification_contracts").length))
|
|
382
|
+
return true;
|
|
383
|
+
return false;
|
|
384
|
+
}
|
|
385
|
+
/** Archive then delete only selected rows. Foreign-key closure computes child ownership and deletion order. */
|
|
386
|
+
export function purgeStructuredRun(db, runId, matrix, fileReferences = new Set()) {
|
|
387
|
+
const selected = new Map();
|
|
388
|
+
for (const entry of matrix) {
|
|
389
|
+
const rows = entry.columns.includes("run_id") ? db.prepare(`SELECT rowid AS _cutover_rowid,* FROM ${quote(entry.table)} WHERE run_id=?`).all(runId)
|
|
390
|
+
: entry.disposition === "purge-owned" && entry.columns.includes("record_json")
|
|
391
|
+
? db.prepare(`SELECT rowid AS _cutover_rowid,* FROM ${quote(entry.table)} WHERE json_valid(record_json) AND json_extract(record_json,'$.runId')=?`).all(runId) : [];
|
|
392
|
+
selected.set(entry.table, new Map(rows.map(row => [row._cutover_rowid, row])));
|
|
393
|
+
}
|
|
394
|
+
const sessionRows = selected.get("provider_sessions");
|
|
395
|
+
const bindingRows = selected.get("provider_session_bindings");
|
|
396
|
+
if (sessionRows && bindingRows)
|
|
397
|
+
for (const session of sessionRows.values()) {
|
|
398
|
+
if (!session.session_key)
|
|
399
|
+
continue;
|
|
400
|
+
const shared = db.prepare("SELECT 1 FROM provider_sessions WHERE session_key=? AND run_id<>? LIMIT 1").get(session.session_key, runId);
|
|
401
|
+
if (shared)
|
|
402
|
+
continue;
|
|
403
|
+
const binding = db.prepare("SELECT rowid AS _cutover_rowid,* FROM provider_session_bindings WHERE session_key=?").get(session.session_key);
|
|
404
|
+
if (binding)
|
|
405
|
+
bindingRows.set(binding._cutover_rowid, binding);
|
|
406
|
+
}
|
|
407
|
+
for (let changed = true; changed;) {
|
|
408
|
+
changed = false;
|
|
409
|
+
for (const entry of matrix.filter(entry => entry.disposition === "purge-owned")) {
|
|
410
|
+
const groups = new Map();
|
|
411
|
+
for (const fk of entry.foreignKeys)
|
|
412
|
+
groups.set(fk.id, [...groups.get(fk.id) ?? [], fk]);
|
|
413
|
+
for (const keys of groups.values()) {
|
|
414
|
+
const parents = selected.get(keys[0].table);
|
|
415
|
+
if (!parents?.size)
|
|
416
|
+
continue;
|
|
417
|
+
for (const row of db.prepare(`SELECT rowid AS _cutover_rowid,* FROM ${quote(entry.table)}`).all()) {
|
|
418
|
+
if (selected.get(entry.table).has(row._cutover_rowid))
|
|
419
|
+
continue;
|
|
420
|
+
if ([...parents.values()].some(parent => keys.every(key => row[key.from] !== null && row[key.from] === parent[key.to]))) {
|
|
421
|
+
selected.get(entry.table).set(row._cutover_rowid, row);
|
|
422
|
+
changed = true;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
// Check history predates the FK-owned contract tables. Its ownership key
|
|
428
|
+
// is the stable contractId in the persisted contract, not its digest.
|
|
429
|
+
const contracts = selected.get("qa_verification_contracts");
|
|
430
|
+
const history = selected.get("qa_contract_check_history");
|
|
431
|
+
if (contracts?.size && history) {
|
|
432
|
+
const ids = new Set([...contracts.values()].flatMap(row => {
|
|
433
|
+
try {
|
|
434
|
+
const value = JSON.parse(row.record_json);
|
|
435
|
+
return typeof value.contractId === "string" ? [value.contractId] : [];
|
|
436
|
+
}
|
|
437
|
+
catch {
|
|
438
|
+
return [];
|
|
439
|
+
}
|
|
440
|
+
}));
|
|
441
|
+
const survivors = db.prepare("SELECT rowid AS id,record_json FROM qa_verification_contracts").all();
|
|
442
|
+
for (const row of survivors)
|
|
443
|
+
if (!contracts.has(row.id)) {
|
|
444
|
+
try {
|
|
445
|
+
ids.delete(JSON.parse(row.record_json).contractId);
|
|
446
|
+
}
|
|
447
|
+
catch {
|
|
448
|
+
ids.clear();
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
for (const row of db.prepare("SELECT rowid AS _cutover_rowid,* FROM qa_contract_check_history").all()) {
|
|
452
|
+
if (ids.has(row.contract_id) && !history.has(row._cutover_rowid)) {
|
|
453
|
+
history.set(row._cutover_rowid, row);
|
|
454
|
+
changed = true;
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
const pending = matrix.filter(entry => entry.disposition === "purge-owned");
|
|
460
|
+
const order = [];
|
|
461
|
+
while (pending.length) {
|
|
462
|
+
const index = pending.findIndex(parent => !pending.some(child => child.table !== parent.table && child.foreignKeys.some(fk => fk.table === parent.table)));
|
|
463
|
+
if (index < 0)
|
|
464
|
+
throw new Error("Cutover ownership matrix has an unresolved foreign-key cycle");
|
|
465
|
+
order.push(pending.splice(index, 1)[0]);
|
|
466
|
+
}
|
|
467
|
+
let count = 0;
|
|
468
|
+
const communicationOwners = [...selected.get("operation_journal")?.values() ?? []].map(row => row.idempotency_key);
|
|
469
|
+
const evidenceCandidates = new Set();
|
|
470
|
+
for (const owner of communicationOwners) {
|
|
471
|
+
for (const row of db.prepare("SELECT evidence_digest FROM communication_evidence_refs WHERE owner_kind='provider-dispatch' AND owner_id=?").all(owner))
|
|
472
|
+
evidenceCandidates.add(row.evidence_digest);
|
|
473
|
+
db.prepare("DELETE FROM communication_evidence_refs WHERE owner_kind='provider-dispatch' AND owner_id=?").run(owner);
|
|
474
|
+
}
|
|
475
|
+
for (const entry of order)
|
|
476
|
+
for (const [id, row] of selected.get(entry.table)) {
|
|
477
|
+
db.prepare("INSERT INTO structured_cutover_quarantine(run_id,table_name,row_json) VALUES(?,?,?)").run(runId, entry.table, JSON.stringify(row));
|
|
478
|
+
db.prepare(`DELETE FROM ${quote(entry.table)} WHERE rowid=?`).run(id);
|
|
479
|
+
count++;
|
|
480
|
+
}
|
|
481
|
+
// Only normalized references prove ownership. Legacy blobs without such a
|
|
482
|
+
// reference are ambiguous and are deliberately retained. Any surviving DB
|
|
483
|
+
// reference, including unknown JSON fields and historical user answers,
|
|
484
|
+
// keeps shared bytes alive.
|
|
485
|
+
const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'").all();
|
|
486
|
+
for (const digest of evidenceCandidates) {
|
|
487
|
+
if (fileReferences.has("*") || fileReferences.has(digest))
|
|
488
|
+
continue;
|
|
489
|
+
if (db.prepare("SELECT 1 FROM communication_evidence_refs WHERE evidence_digest=?").get(digest))
|
|
490
|
+
continue;
|
|
491
|
+
let shared = false;
|
|
492
|
+
for (const { name } of tables) {
|
|
493
|
+
if (name === "structured_cutover_quarantine" || name === "structured_communication_cutover")
|
|
494
|
+
continue;
|
|
495
|
+
for (const row of db.prepare(`SELECT * FROM ${quote(name)}`).all()) {
|
|
496
|
+
if (name === "content_refs" && evidenceCandidates.has(row.digest))
|
|
497
|
+
continue;
|
|
498
|
+
if (Object.values(row).some(value => typeof value === "string" && value.includes(digest) || Buffer.isBuffer(value) && value.includes(Buffer.from(digest)))) {
|
|
499
|
+
shared = true;
|
|
500
|
+
break;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
if (shared)
|
|
504
|
+
break;
|
|
505
|
+
}
|
|
506
|
+
if (!shared)
|
|
507
|
+
db.prepare("DELETE FROM content_refs WHERE digest=?").run(digest);
|
|
508
|
+
}
|
|
509
|
+
return count;
|
|
510
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type Database from "better-sqlite3";
|
|
2
|
+
/** Only these declared local projections can carry resumable communication state. */
|
|
3
|
+
export declare const COMMUNICATION_FILE_OWNERSHIP: readonly {
|
|
4
|
+
directory: string;
|
|
5
|
+
kind: string;
|
|
6
|
+
recurse: boolean;
|
|
7
|
+
}[];
|
|
8
|
+
/** Unknown/oversized file state conservatively retains evidence until classified. */
|
|
9
|
+
export declare function communicationFileEvidenceReferences(root: string): Set<string>;
|
|
10
|
+
export declare function quarantineCommunicationFiles(root: string, db: Database.Database): void;
|