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.
- package/README.md +29 -5
- package/dist/activity.d.ts +3 -0
- package/dist/activity.js +18 -6
- package/dist/adapters/claude.d.ts +16 -0
- package/dist/adapters/claude.js +28 -3
- package/dist/adapters/codex.d.ts +20 -0
- package/dist/adapters/codex.js +99 -5
- package/dist/adapters/recovering.d.ts +9 -0
- package/dist/adapters/recovering.js +10 -0
- package/dist/adapters/types.d.ts +15 -0
- package/dist/agentRun.d.ts +7 -0
- package/dist/agentRun.js +24 -13
- package/dist/branch/currentGuard.d.ts +1 -0
- package/dist/branch/currentGuard.js +1 -0
- package/dist/branch/finalization.js +6 -1
- package/dist/branch/git.d.ts +1 -0
- package/dist/branch/git.js +15 -1
- package/dist/branch/runner.d.ts +2 -0
- package/dist/branch/runner.js +92 -5
- package/dist/buildAdmission.d.ts +61 -0
- package/dist/buildAdmission.js +286 -0
- package/dist/buildApproval.d.ts +2 -0
- package/dist/buildApproval.js +17 -0
- package/dist/buildAssignment.d.ts +33 -0
- package/dist/buildAssignment.js +215 -0
- package/dist/buildInterventions.d.ts +90 -0
- package/dist/buildInterventions.js +334 -0
- package/dist/buildLaunchGate.d.ts +1 -0
- package/dist/buildLaunchGate.js +36 -0
- package/dist/buildOwnershipReconciliation.d.ts +19 -0
- package/dist/buildOwnershipReconciliation.js +41 -0
- package/dist/buildOwnershipRepair.d.ts +33 -0
- package/dist/buildOwnershipRepair.js +111 -0
- package/dist/buildRuns.d.ts +24 -1
- package/dist/buildRuns.js +216 -70
- package/dist/buildSyntheticWork.d.ts +2 -0
- package/dist/buildSyntheticWork.js +30 -0
- package/dist/buildWorkAdmission.d.ts +17 -0
- package/dist/buildWorkAdmission.js +57 -0
- package/dist/buildWorkAuthorization.d.ts +2 -0
- package/dist/buildWorkAuthorization.js +23 -0
- package/dist/buildWorkContext.d.ts +3 -0
- package/dist/buildWorkContext.js +6 -0
- package/dist/buildWorkMigration.d.ts +9 -0
- package/dist/buildWorkMigration.js +206 -0
- package/dist/buildWorkReconciliation.d.ts +12 -0
- package/dist/buildWorkReconciliation.js +70 -0
- package/dist/builderGuidanceFollowup.d.ts +15 -0
- package/dist/builderGuidanceFollowup.js +34 -0
- package/dist/cli/doctor.js +3 -2
- package/dist/cli/graph.d.ts +2 -0
- package/dist/cli/graph.js +75 -0
- package/dist/cli/manager.d.ts +8 -0
- package/dist/cli/manager.js +234 -12
- package/dist/cli/recovery.js +3 -2
- package/dist/cli/runtimeAuthPrompt.d.ts +5 -0
- package/dist/cli/runtimeAuthPrompt.js +41 -7
- package/dist/cli/start.d.ts +8 -0
- package/dist/cli/start.js +2091 -1849
- package/dist/cli/status.js +20 -5
- package/dist/cli/tickets.js +9 -7
- package/dist/continuity.d.ts +16 -1
- package/dist/continuity.js +68 -12
- package/dist/foreman.d.ts +12 -4
- package/dist/foreman.js +397 -60
- package/dist/graph/boundary.d.ts +15 -0
- package/dist/graph/boundary.js +50 -0
- package/dist/graph/bridge.d.ts +9 -0
- package/dist/graph/bridge.js +146 -0
- package/dist/graph/bridge.py +205 -0
- package/dist/graph/config.d.ts +11 -0
- package/dist/graph/config.js +40 -0
- package/dist/graph/corpus.d.ts +16 -0
- package/dist/graph/corpus.js +188 -0
- package/dist/graph/derived.d.ts +17 -0
- package/dist/graph/derived.js +73 -0
- package/dist/graph/host.d.ts +4 -0
- package/dist/graph/host.js +160 -0
- package/dist/graph/install.d.ts +3 -0
- package/dist/graph/install.js +50 -0
- package/dist/graph/lifecycle.d.ts +11 -0
- package/dist/graph/lifecycle.js +124 -0
- package/dist/graph/maintenance.d.ts +43 -0
- package/dist/graph/maintenance.js +301 -0
- package/dist/graph/manager.d.ts +9 -0
- package/dist/graph/manager.js +92 -0
- package/dist/graph/preparation.d.ts +41 -0
- package/dist/graph/preparation.js +43 -0
- package/dist/graph/read.d.ts +27 -0
- package/dist/graph/read.js +152 -0
- package/dist/graph/semantic.d.ts +17 -0
- package/dist/graph/semantic.js +27 -0
- package/dist/graph/session.d.ts +8 -0
- package/dist/graph/session.js +92 -0
- package/dist/graph/sources.d.ts +4 -0
- package/dist/graph/sources.js +11 -0
- package/dist/graph/storage.d.ts +38 -0
- package/dist/graph/storage.js +159 -0
- package/dist/graph/turn.d.ts +17 -0
- package/dist/graph/turn.js +360 -0
- package/dist/graph/util.d.ts +5 -0
- package/dist/graph/util.js +51 -0
- package/dist/handoffs.d.ts +1 -0
- package/dist/handoffs.js +5 -1
- package/dist/humanDecision.d.ts +21 -2
- package/dist/humanDecision.js +169 -10
- package/dist/index.js +4 -1
- package/dist/installOwnership.d.ts +46 -0
- package/dist/installOwnership.js +440 -0
- package/dist/linuxProbeInventory.d.ts +12 -0
- package/dist/linuxProbeInventory.js +48 -0
- package/dist/log.d.ts +1 -1
- package/dist/log.js +7 -1
- package/dist/managerEvidence.d.ts +25 -0
- package/dist/managerEvidence.js +326 -0
- package/dist/managerEvidenceArtifacts.d.ts +38 -0
- package/dist/managerEvidenceArtifacts.js +118 -0
- package/dist/managerPacket.d.ts +2 -0
- package/dist/managerPacket.js +23 -0
- package/dist/markers.d.ts +1 -1
- package/dist/markers.js +2 -2
- package/dist/observability.js +1 -1
- package/dist/processIdentity.d.ts +21 -1
- package/dist/processIdentity.js +87 -5
- package/dist/providerPhase.d.ts +13 -0
- package/dist/providerPhase.js +26 -0
- package/dist/qaAuthorizedDisposition.d.ts +24 -0
- package/dist/qaAuthorizedDisposition.js +26 -0
- package/dist/qaBuildGate.d.ts +4 -0
- package/dist/qaBuildGate.js +216 -0
- package/dist/qaBuilderCoverage.d.ts +5 -0
- package/dist/qaBuilderCoverage.js +78 -0
- package/dist/qaContractCoverage.d.ts +10 -0
- package/dist/qaContractCoverage.js +55 -0
- package/dist/qaContractDelivery.d.ts +23 -0
- package/dist/qaContractDelivery.js +145 -0
- package/dist/qaContractFreshness.d.ts +15 -0
- package/dist/qaContractFreshness.js +36 -0
- package/dist/qaDeliveryJournal.d.ts +10 -0
- package/dist/qaDeliveryJournal.js +16 -1
- package/dist/qaEffectiveConfig.d.ts +20 -0
- package/dist/qaEffectiveConfig.js +40 -0
- package/dist/qaEvidenceReader.d.ts +14 -0
- package/dist/qaEvidenceReader.js +176 -0
- package/dist/qaFailureDelivery.d.ts +2 -0
- package/dist/qaFailureDelivery.js +107 -10
- package/dist/qaHandbackMigration.d.ts +1 -1
- package/dist/qaHandbackMigration.js +3 -3
- package/dist/qaPreparation.d.ts +26 -0
- package/dist/qaPreparation.js +150 -0
- package/dist/qaPreparationInputs.d.ts +8 -0
- package/dist/qaPreparationInputs.js +47 -0
- package/dist/qaPreparationMetrics.d.ts +145 -0
- package/dist/qaPreparationMetrics.js +120 -0
- package/dist/qaPreparationPolicy.d.ts +5 -0
- package/dist/qaPreparationPolicy.js +51 -0
- package/dist/qaPreparationStore.d.ts +157 -0
- package/dist/qaPreparationStore.js +412 -0
- package/dist/qaProtocolV2.d.ts +28 -0
- package/dist/qaProtocolV2.js +12 -0
- package/dist/qaReview.d.ts +6 -0
- package/dist/qaReview.js +293 -41
- package/dist/qaTimeline.d.ts +18 -0
- package/dist/qaTimeline.js +123 -0
- package/dist/qaVerificationContract.d.ts +17 -0
- package/dist/qaVerificationContract.js +236 -0
- package/dist/readinessCleanup.d.ts +19 -0
- package/dist/readinessCleanup.js +122 -0
- package/dist/readinessGate.d.ts +1 -0
- package/dist/readinessGate.js +82 -0
- package/dist/recoveryGuidance.d.ts +17 -0
- package/dist/recoveryGuidance.js +38 -0
- package/dist/runtimeAuth.d.ts +1 -0
- package/dist/runtimeAuth.js +5 -1
- package/dist/runtimeReadiness.d.ts +13 -0
- package/dist/runtimeReadiness.js +206 -10
- package/dist/sessionIdentity.d.ts +2 -0
- package/dist/sessionIdentity.js +14 -0
- package/dist/sessionLifecycle.js +3 -3
- package/dist/sources/sourceRegistry.js +7 -0
- package/dist/stateTransfer.js +85 -8
- package/dist/statusReporter.d.ts +1 -0
- package/dist/statusReporter.js +1 -0
- package/dist/supervisedStart.d.ts +6 -0
- package/dist/supervisedStart.js +178 -62
- package/dist/supervisor.d.ts +2 -0
- package/dist/supervisor.js +26 -2
- package/dist/terminalStyle.d.ts +21 -0
- package/dist/terminalStyle.js +50 -0
- package/dist/ticketPopulation.d.ts +1 -0
- package/dist/ticketPopulation.js +5 -1
- package/dist/tickets/commands.js +2 -2
- package/dist/tickets/queue.js +1 -1
- package/dist/tickets/ticketSchema.d.ts +198 -0
- package/dist/tickets/ticketSchema.js +2 -0
- package/dist/windowsProbeJob.d.ts +12 -0
- package/dist/windowsProbeJob.js +144 -0
- package/dist/workflowDb.d.ts +121 -6
- package/dist/workflowDb.js +874 -30
- package/dist/workflowReader.d.ts +116 -2
- package/dist/workflowReader.js +131 -4
- package/package.json +89 -8
- package/scripts/copy-graph-assets.mjs +3 -0
- package/scripts/graph-benchmark.mjs +20 -0
- package/scripts/graph-evaluation.mjs +85 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { dispatchWithGraphAccess } from "./graph/session.js";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { lstatSync, mkdirSync, readFileSync, realpathSync, renameSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
5
|
+
import { contractDigest, parsePreparationArtifact, renderVerificationContract, verifyContractDigest } from "./qaVerificationContract.js";
|
|
6
|
+
import { OperationDeadline } from "./util/deadline.js";
|
|
7
|
+
// Durable receipts prove historical transport. They cannot prove a freshly
|
|
8
|
+
// constructed adapter has an armed native-compaction barrier.
|
|
9
|
+
const activeDeliveries = new WeakMap();
|
|
10
|
+
function activeDeliveryKey(digest, session) { return JSON.stringify({ digest, session }); }
|
|
11
|
+
export function actualContractSession(adapter, workspace, configRoot) {
|
|
12
|
+
const ref = adapter.sessionRef?.();
|
|
13
|
+
if (!ref || ref.role !== "builder" || ref.sessionId !== adapter.sessionId() || ref.cwd !== realpathSync(workspace) || ref.configRoot !== realpathSync(configRoot) || ref.provider !== adapter.agent)
|
|
14
|
+
throw new Error("Contract delivery requires the actual scoped Builder session/workspace");
|
|
15
|
+
return { provider: ref.provider, sessionId: ref.sessionId, generation: ref.generation, workspace: ref.cwd, configRoot: ref.configRoot, compactionSequence: adapter.contractCompactionSequence?.() ?? 0 };
|
|
16
|
+
}
|
|
17
|
+
export function materializeContract(contract, workspace) {
|
|
18
|
+
verifyContractDigest(contract);
|
|
19
|
+
const root = realpathSync(workspace);
|
|
20
|
+
// .foreman is already excluded by product-source capture and snapshots.
|
|
21
|
+
const directory = join(root, ".foreman", "qa-contracts");
|
|
22
|
+
for (const part of [join(root, ".foreman"), directory]) {
|
|
23
|
+
try {
|
|
24
|
+
if (lstatSync(part).isSymbolicLink())
|
|
25
|
+
throw new Error("Contract projection cannot follow a control-directory symlink");
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
if (error.code !== "ENOENT")
|
|
29
|
+
throw error;
|
|
30
|
+
}
|
|
31
|
+
mkdirSync(part, { recursive: true });
|
|
32
|
+
}
|
|
33
|
+
const json = JSON.stringify(contract, null, 2), markdown = renderVerificationContract(contract);
|
|
34
|
+
for (const [extension, content] of [["json", json], ["md", markdown]]) {
|
|
35
|
+
const target = join(directory, `${contract.contentDigest}.${extension}`), temporary = `${target}.${randomUUID()}.tmp`;
|
|
36
|
+
writeFileSync(temporary, content, { mode: 0o444 });
|
|
37
|
+
renameSync(temporary, target);
|
|
38
|
+
if (readFileSync(target, "utf8") !== content)
|
|
39
|
+
throw new Error("Contract projection digest verification failed");
|
|
40
|
+
}
|
|
41
|
+
return { json: join(directory, `${contract.contentDigest}.json`), markdown: join(directory, `${contract.contentDigest}.md`), resourceDigest: contractDigest("delivery-resources", { json, markdown }) };
|
|
42
|
+
}
|
|
43
|
+
/** Complete retained review inputs, accessible inside the confined QA snapshot. */
|
|
44
|
+
export function finalReviewContractContext(contract, store, workspace, builderClaims = []) {
|
|
45
|
+
const resources = materializeContract(contract, workspace);
|
|
46
|
+
const baselineEvidence = contract.baseline.map(observation => {
|
|
47
|
+
const artifact = store.artifact(observation.evidenceDigest, "baseline-observation");
|
|
48
|
+
return { digest: observation.evidenceDigest, artifact, ...(artifact.providerReceipt ? { providerEvidence: store.artifact(artifact.providerReceipt, "investigation-evidence") } : {}) };
|
|
49
|
+
});
|
|
50
|
+
const context = { version: 1, contract, baselineEvidence, builderClaims };
|
|
51
|
+
const serialized = JSON.stringify(context);
|
|
52
|
+
const digest = contractDigest("final-review-context", context);
|
|
53
|
+
const path = join(resolve(resources.json, ".."), `${digest}.review.json`);
|
|
54
|
+
const temporary = `${path}.${randomUUID()}.tmp`;
|
|
55
|
+
writeFileSync(temporary, serialized, { mode: 0o444 });
|
|
56
|
+
renameSync(temporary, path);
|
|
57
|
+
if (readFileSync(path, "utf8") !== serialized)
|
|
58
|
+
throw new Error("Final review context projection verification failed");
|
|
59
|
+
return `\nComplete retained QA context: ${path}\nFinal review context digest: ${digest}. Contract JSON: ${resources.json}. Read the complete context before reporting coverage. Baseline evidence is historical, bound to its original source; assess relevance and expected transitions independently. Builder claims are untrusted and require independent verification. If any required resource is inaccessible, report blocked coverage rather than pass.\n${Buffer.byteLength(serialized) <= 128 * 1024 ? `Full structured review context:\n${serialized}\n` : "The full context exceeds the inline limit; use the read-only JSON resource without omitting sections.\n"}`;
|
|
60
|
+
}
|
|
61
|
+
export async function deliverVerificationContract(contract, store, adapter, workspace, configRoot, context) {
|
|
62
|
+
const capabilities = adapter.contractCapabilities?.();
|
|
63
|
+
if (!capabilities?.sameSessionAcceptance || !capabilities.nativeCompactionBarrier || !adapter.enableContractEnforcement || !adapter.acceptContractDelivery)
|
|
64
|
+
throw new Error(`unsupported-capability: ${adapter.agent} has no tested same-session acceptance/native compaction barrier`);
|
|
65
|
+
activeDeliveries.delete(adapter);
|
|
66
|
+
adapter.enableContractEnforcement();
|
|
67
|
+
await adapter.prepareSession?.();
|
|
68
|
+
const session = actualContractSession(adapter, workspace, configRoot);
|
|
69
|
+
const resources = materializeContract(contract, workspace);
|
|
70
|
+
const operationId = `contract-acceptance:${randomUUID()}`;
|
|
71
|
+
const expiresAt = Date.now() + 120_000;
|
|
72
|
+
const acceptTurn = (text) => new OperationDeadline("Contract acceptance", Math.max(1, expiresAt - Date.now())).run(() => dispatchWithGraphAccess(configRoot, adapter, text, { purpose: "contract-acceptance", responseOnly: true }, (prompt, policy) => adapter.sendTurn(prompt, policy)), () => { void adapter.close(); });
|
|
73
|
+
const instruction = `${context}\n\n${renderVerificationContract(contract)}\n\nFull structured contract:\n${JSON.stringify(contract)}\n\nDurable artifacts: ${resources.json}, ${resources.markdown}. This is a tools-disabled acceptance turn in the actual Builder conversation. Return RAFI_QA_ACCEPTANCE_START/END containing {"workId":"${contract.workId}","revision":${contract.revision},"digest":"${contract.contentDigest}","missingSections":[]}. This confirms transport/version only. Implementation starts only after host acceptance.`;
|
|
74
|
+
let finalInstruction = instruction;
|
|
75
|
+
if (Buffer.byteLength(instruction) > 256 * 1024) {
|
|
76
|
+
const sections = segmentContractTransport(instruction);
|
|
77
|
+
if (sections.length > 256)
|
|
78
|
+
throw new Error("Contract exceeds bounded segmented delivery capacity");
|
|
79
|
+
const manifest = sections.map((content, index) => ({ index, digest: contractDigest("delivery-section", content), bytes: Buffer.byteLength(content) }));
|
|
80
|
+
store.event(contract.runId, contract.workId, `${operationId}:manifest`, "delivery-manifest", { session, contractDigest: contract.contentDigest, manifest });
|
|
81
|
+
for (const section of manifest) {
|
|
82
|
+
const segmentInstruction = `Contract transport segment ${section.index + 1}/${sections.length}. Tools disabled; retain this entire section in this conversation. Contract digest: ${contract.contentDigest}.\n${sections[section.index]}\nReturn RAFI_QA_SEGMENT_START/END containing ${JSON.stringify({ index: section.index, digest: section.digest, missingSections: [] })}. No implementation is authorized.`;
|
|
83
|
+
store.event(contract.runId, contract.workId, `${operationId}:segment:${section.index}:intent`, "delivery-segment-intended", { session, section });
|
|
84
|
+
if (Date.now() >= expiresAt)
|
|
85
|
+
throw new Error("Bounded contract delivery deadline exhausted");
|
|
86
|
+
const result = await acceptTurn(segmentInstruction);
|
|
87
|
+
store.event(contract.runId, contract.workId, `${operationId}:segment:${section.index}:result`, "delivery-segment-result", result);
|
|
88
|
+
if (result.isError || result.failure || JSON.stringify(actualContractSession(adapter, workspace, configRoot)) !== JSON.stringify(session))
|
|
89
|
+
throw new Error("Segmented contract transport failed or crossed a session boundary");
|
|
90
|
+
const acknowledgment = parsePreparationArtifact(result.text, "RAFI_QA_SEGMENT", 64 * 1024);
|
|
91
|
+
if (acknowledgment.index !== section.index || acknowledgment.digest !== section.digest || !Array.isArray(acknowledgment.missingSections) || acknowledgment.missingSections.length)
|
|
92
|
+
throw new Error("Segmented contract acknowledgment is incomplete");
|
|
93
|
+
}
|
|
94
|
+
finalInstruction = `All ${sections.length} sections of contract ${contract.contentDigest} have been delivered in this actual Builder conversation. Verify the complete section inventory ${JSON.stringify(manifest)}. Tools disabled. Return RAFI_QA_ACCEPTANCE_START/END containing ${JSON.stringify({ workId: contract.workId, revision: contract.revision, digest: contract.contentDigest, missingSections: [] })}. If any content or condition is inaccessible, list the missing sections; do not accept partial context.`;
|
|
95
|
+
}
|
|
96
|
+
store.event(contract.runId, contract.workId, operationId, "delivery-intended", { session, contractDigest: contract.contentDigest, instructionDigest: contractDigest("delivery-instruction", instruction) });
|
|
97
|
+
if (Date.now() >= expiresAt)
|
|
98
|
+
throw new Error("Bounded contract delivery deadline exhausted");
|
|
99
|
+
const response = await acceptTurn(finalInstruction);
|
|
100
|
+
store.event(contract.runId, contract.workId, `${operationId}:result`, "delivery-result", { response, session });
|
|
101
|
+
if (response.isError || response.failure)
|
|
102
|
+
throw new Error("Actual Builder contract acceptance failed; inspect retained delivery result");
|
|
103
|
+
const acceptance = parsePreparationArtifact(response.text, "RAFI_QA_ACCEPTANCE", 64 * 1024);
|
|
104
|
+
const after = actualContractSession(adapter, workspace, configRoot);
|
|
105
|
+
if (JSON.stringify(session) !== JSON.stringify(after) || acceptance.workId !== contract.workId || acceptance.revision !== contract.revision || acceptance.digest !== contract.contentDigest || !Array.isArray(acceptance.missingSections) || acceptance.missingSections.length)
|
|
106
|
+
throw new Error("Missing/foreign/stale contract acceptance");
|
|
107
|
+
const receipt = { version: 1, operationId, runId: contract.runId, workId: contract.workId, admissionDigest: contract.admissionDigest, revision: contract.revision, contractDigest: contract.contentDigest, resourceDigest: resources.resourceDigest, ...session, responseDigest: contractDigest("acceptance-response", response.rawResponse ?? response.text), deliveredAt: new Date().toISOString() };
|
|
108
|
+
store.receipt(receipt);
|
|
109
|
+
adapter.acceptContractDelivery(session.compactionSequence);
|
|
110
|
+
activeDeliveries.set(adapter, activeDeliveryKey(contract.contentDigest, session));
|
|
111
|
+
return receipt;
|
|
112
|
+
}
|
|
113
|
+
/** Byte bounded, lossless Unicode transport. Concatenation reconstructs the exact original input. */
|
|
114
|
+
export function segmentContractTransport(content, maximumBytes = 96 * 1024) {
|
|
115
|
+
if (!Number.isSafeInteger(maximumBytes) || maximumBytes < 4)
|
|
116
|
+
throw new Error("Invalid contract segment capacity");
|
|
117
|
+
const result = [];
|
|
118
|
+
let section = "", bytes = 0;
|
|
119
|
+
for (const character of content) {
|
|
120
|
+
const size = Buffer.byteLength(character);
|
|
121
|
+
if (bytes + size > maximumBytes) {
|
|
122
|
+
result.push(section);
|
|
123
|
+
section = "";
|
|
124
|
+
bytes = 0;
|
|
125
|
+
}
|
|
126
|
+
section += character;
|
|
127
|
+
bytes += size;
|
|
128
|
+
}
|
|
129
|
+
if (section)
|
|
130
|
+
result.push(section);
|
|
131
|
+
return result;
|
|
132
|
+
}
|
|
133
|
+
export function assertContractReceipt(store, runId, workId, admissionDigest, session, adapter) {
|
|
134
|
+
const head = store.head(runId, workId, admissionDigest);
|
|
135
|
+
if (head.state !== "ready" || !head.digest)
|
|
136
|
+
throw new Error(`Implementation requires ready verification contract: ${head.state}`);
|
|
137
|
+
const contract = store.contract(head.digest);
|
|
138
|
+
if (!session || !store.receipts(head.digest).some(receipt => receipt.runId === runId && receipt.workId === workId && receipt.admissionDigest === admissionDigest && receipt.revision === contract.revision && ["provider", "sessionId", "generation", "workspace", "configRoot", "compactionSequence"].every(key => receipt[key] === session[key])))
|
|
139
|
+
throw new Error("Implementation requires current actual-session contract receipt");
|
|
140
|
+
if (resolve(session.workspace) !== session.workspace)
|
|
141
|
+
throw new Error("Contract workspace identity is not canonical");
|
|
142
|
+
if (adapter && activeDeliveries.get(adapter) !== activeDeliveryKey(contract.contentDigest, session))
|
|
143
|
+
throw new Error("Implementation requires current adapter contract receipt and armed enforcement barrier");
|
|
144
|
+
return contract;
|
|
145
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { ContractInputRef, QaVerificationContractV1 } from "rafi-spec";
|
|
2
|
+
/** Source implementing the contract is not a semantic freshness input. */
|
|
3
|
+
export declare function compareContractInputs(contract: QaVerificationContractV1, current: ContractInputRef[]): {
|
|
4
|
+
changed: string[];
|
|
5
|
+
unavailable: string[];
|
|
6
|
+
targetedBaseline: string[];
|
|
7
|
+
};
|
|
8
|
+
export declare function reconcileContractChecks(before: QaVerificationContractV1, after: QaVerificationContractV1): {
|
|
9
|
+
unchanged: string[];
|
|
10
|
+
changed: string[];
|
|
11
|
+
added: string[];
|
|
12
|
+
removed: string[];
|
|
13
|
+
rerun: string[];
|
|
14
|
+
carryForwardCandidates: string[];
|
|
15
|
+
};
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { contractDigest } from "./qaVerificationContract.js";
|
|
2
|
+
/** Source implementing the contract is not a semantic freshness input. */
|
|
3
|
+
export function compareContractInputs(contract, current) {
|
|
4
|
+
const changed = [], unavailable = [], targetedBaseline = [];
|
|
5
|
+
for (const input of contract.inputs) {
|
|
6
|
+
const next = current.find(ref => ref.id === input.id);
|
|
7
|
+
if (!next || next.availability !== "available") {
|
|
8
|
+
unavailable.push(input.id);
|
|
9
|
+
continue;
|
|
10
|
+
}
|
|
11
|
+
if (next.digest !== input.digest || next.authority !== input.authority || next.revision !== input.revision)
|
|
12
|
+
(input.kind === "baseline" ? targetedBaseline : changed).push(input.id);
|
|
13
|
+
}
|
|
14
|
+
for (const input of current)
|
|
15
|
+
if (!contract.inputs.some(ref => ref.id === input.id) && input.kind !== "baseline")
|
|
16
|
+
changed.push(input.id);
|
|
17
|
+
return { changed, unavailable, targetedBaseline };
|
|
18
|
+
}
|
|
19
|
+
export function reconcileContractChecks(before, after) {
|
|
20
|
+
if (before.runId !== after.runId || before.workId !== after.workId || after.predecessorDigest !== before.contentDigest || after.revision !== before.revision + 1)
|
|
21
|
+
throw new Error("Amendment must identify its exact predecessor/work");
|
|
22
|
+
const unchanged = [], changed = [], added = [], removed = [];
|
|
23
|
+
for (const check of after.checks) {
|
|
24
|
+
const prior = before.checks.find(row => row.id === check.id);
|
|
25
|
+
if (!prior)
|
|
26
|
+
added.push(check.id);
|
|
27
|
+
else if (contractDigest("check-meaning", prior) === contractDigest("check-meaning", check))
|
|
28
|
+
unchanged.push(check.id);
|
|
29
|
+
else
|
|
30
|
+
throw new Error(`Changed meaning cannot reuse stable check ID ${check.id}; allocate an explicit successor`);
|
|
31
|
+
}
|
|
32
|
+
for (const prior of before.checks)
|
|
33
|
+
if (!after.checks.some(check => check.id === prior.id))
|
|
34
|
+
removed.push(prior.id);
|
|
35
|
+
return { unchanged, changed, added, removed, rerun: [...changed, ...added], carryForwardCandidates: unchanged };
|
|
36
|
+
}
|
|
@@ -68,3 +68,13 @@ export interface QaDeliveryTurnV3 {
|
|
|
68
68
|
captureElapsedMs?: number;
|
|
69
69
|
validationElapsedMs?: number;
|
|
70
70
|
}
|
|
71
|
+
/** V4 distinguishes evidence continuations from the one response-only repair. */
|
|
72
|
+
export interface QaDeliveryTurnV4 extends Omit<QaDeliveryTurnV3, "version" | "kind"> {
|
|
73
|
+
version: 4;
|
|
74
|
+
kind: "remediation" | "graph-continuation" | "response-repair";
|
|
75
|
+
}
|
|
76
|
+
export type QaDeliveryTurn = QaDeliveryTurnV3 | QaDeliveryTurnV4;
|
|
77
|
+
/** Independent of preparation's schema revision: old writers cannot mutate an
|
|
78
|
+
* evidence-continuation journal they would interpret as a response-only repair. */
|
|
79
|
+
export declare function registerQaGraphJournalWriter(db: import("better-sqlite3").Database): void;
|
|
80
|
+
export declare function ensureQaGraphJournalGuards(db: import("better-sqlite3").Database): void;
|
|
@@ -1 +1,16 @@
|
|
|
1
|
-
|
|
1
|
+
/** Independent of preparation's schema revision: old writers cannot mutate an
|
|
2
|
+
* evidence-continuation journal they would interpret as a response-only repair. */
|
|
3
|
+
export function registerQaGraphJournalWriter(db) {
|
|
4
|
+
db.function("rafi_graph_delivery_protocol", () => 4);
|
|
5
|
+
}
|
|
6
|
+
export function ensureQaGraphJournalGuards(db) {
|
|
7
|
+
const tables = ["qa_delivery_turns", "qa_review_attempts", "qa_remediation_attempts", "qa_turns", "qa_ticket_heads", "qa_operation_journal", "qa_pass_certificates", "build_assignments"];
|
|
8
|
+
db.transaction(() => {
|
|
9
|
+
for (const table of tables) {
|
|
10
|
+
if (!db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name=?").get(table))
|
|
11
|
+
continue;
|
|
12
|
+
for (const action of ["INSERT", "UPDATE", "DELETE"])
|
|
13
|
+
db.exec(`CREATE TRIGGER IF NOT EXISTS graph_delivery_v4_${table}_${action} BEFORE ${action} ON ${table} BEGIN SELECT CASE WHEN rafi_graph_delivery_protocol() <> 4 THEN RAISE(ABORT,'Graph delivery journal requires a compatible Rafi writer') END; END;`);
|
|
14
|
+
}
|
|
15
|
+
})();
|
|
16
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { ResolvedAgentSettings } from "rafi-spec";
|
|
2
|
+
export declare const FINAL_QA_ROLE_SUFFIX = "You are an independent QA reviewer. Do not edit source, tickets, configuration, or project documentation. You may run tests and create only harmless ignored caches or coverage output.";
|
|
3
|
+
export declare function finalQaRoleInstructions(rules: string): string;
|
|
4
|
+
export declare function loadQaPreparationPolicy(projectDir: string): import("special-agents").QaPreparationConfigV1;
|
|
5
|
+
/** Resolve only explicit rule/skill content; never project secrets or mutable .rafi state. */
|
|
6
|
+
export declare function resolveEffectiveQaConfiguration(projectDir: string, settings: Pick<ResolvedAgentSettings, "make">): {
|
|
7
|
+
digest: string;
|
|
8
|
+
configRoot: string;
|
|
9
|
+
qaRules: string;
|
|
10
|
+
builderRules: string;
|
|
11
|
+
checklist: string[];
|
|
12
|
+
skills: {
|
|
13
|
+
name: string;
|
|
14
|
+
path: string;
|
|
15
|
+
content: string;
|
|
16
|
+
digest: string;
|
|
17
|
+
}[];
|
|
18
|
+
roleSource: "compiled" | "library" | "fallback";
|
|
19
|
+
builderRoleSource: "compiled" | "library" | "fallback";
|
|
20
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
2
|
+
import { join, resolve, sep } from "node:path";
|
|
3
|
+
import { loadSkill } from "special-agents";
|
|
4
|
+
import { loadRoleBundle } from "./roles.js";
|
|
5
|
+
import { loadTicketSetupConfigWithDefaults, loadRafiConfigObject } from "./tickets/setupConfig.js";
|
|
6
|
+
import { resolveQaPreparationConfig } from "./qaPreparationPolicy.js";
|
|
7
|
+
import { contractDigest } from "./qaVerificationContract.js";
|
|
8
|
+
export const FINAL_QA_ROLE_SUFFIX = "You are an independent QA reviewer. Do not edit source, tickets, configuration, or project documentation. You may run tests and create only harmless ignored caches or coverage output.";
|
|
9
|
+
export function finalQaRoleInstructions(rules) { return `${rules}\n\n${FINAL_QA_ROLE_SUFFIX}`; }
|
|
10
|
+
export function loadQaPreparationPolicy(projectDir) {
|
|
11
|
+
return resolveQaPreparationConfig(loadRafiConfigObject(projectDir)?.qa_preparation);
|
|
12
|
+
}
|
|
13
|
+
/** Resolve only explicit rule/skill content; never project secrets or mutable .rafi state. */
|
|
14
|
+
export function resolveEffectiveQaConfiguration(projectDir, settings) {
|
|
15
|
+
const root = realpathSync(projectDir);
|
|
16
|
+
for (const role of ["qa", "builder"])
|
|
17
|
+
for (const file of ["system.md", "meta.json"]) {
|
|
18
|
+
const path = join(root, ".rafi", "compiled", role, file);
|
|
19
|
+
if (existsSync(path) && !realpathSync(path).startsWith(root + sep))
|
|
20
|
+
throw new Error("Compiled role configuration escapes the canonical root");
|
|
21
|
+
}
|
|
22
|
+
const bundle = loadRoleBundle("qa", { projectDir: root });
|
|
23
|
+
const builder = loadRoleBundle("builder", { projectDir: root });
|
|
24
|
+
const checklist = [...loadTicketSetupConfigWithDefaults(root).build.validation_checklist];
|
|
25
|
+
const skills = bundle.skills.map(name => {
|
|
26
|
+
if (!/^[A-Za-z0-9_-]+$/.test(name))
|
|
27
|
+
throw new Error(`Unsafe QA skill reference ${name}`);
|
|
28
|
+
const runtimeRoot = settings.make === "codex" ? ".codex" : ".claude";
|
|
29
|
+
const path = [join(root, runtimeRoot, "skills", name, "SKILL.md"), join(root, ".agents", "skills", name, "SKILL.md")].find(existsSync);
|
|
30
|
+
if (path && !realpathSync(path).startsWith(root + sep))
|
|
31
|
+
throw new Error(`QA skill ${name} escapes the canonical configuration root`);
|
|
32
|
+
const skill = path ? { name, body: readFileSync(path, "utf8") } : loadSkill(name);
|
|
33
|
+
if (!skill.body?.trim())
|
|
34
|
+
throw new Error(`QA skill ${name} has no dispatchable content`);
|
|
35
|
+
const content = `## ${skill.name}\n${skill.body.trim()}`;
|
|
36
|
+
return { name, path: path ?? `special-agents:${name}`, content, digest: contractDigest("skill-content", content) };
|
|
37
|
+
});
|
|
38
|
+
const common = { configRoot: resolve(root), qaRules: bundle.system, builderRules: builder.system, checklist, skills, roleSource: bundle.source, builderRoleSource: builder.source };
|
|
39
|
+
return { ...common, digest: contractDigest("effective-qa-config", common) };
|
|
40
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ManagerEvidenceAvailability } from "rafi-spec";
|
|
2
|
+
export type EvidenceRow = Record<string, unknown>;
|
|
3
|
+
export interface QaEvidenceSnapshot {
|
|
4
|
+
asOf: string;
|
|
5
|
+
runId: string;
|
|
6
|
+
availability: ManagerEvidenceAvailability;
|
|
7
|
+
capabilities: string[];
|
|
8
|
+
gaps: string[];
|
|
9
|
+
rows: Record<string, EvidenceRow[]>;
|
|
10
|
+
blobs: Map<string, Buffer>;
|
|
11
|
+
}
|
|
12
|
+
/** Inspection never registers a writer, runs a migration, or probes packet paths. */
|
|
13
|
+
export declare function readQaEvidenceSnapshot(projectDir: string, runId: string): QaEvidenceSnapshot;
|
|
14
|
+
export declare function evidenceRecord(row: EvidenceRow, field?: string): EvidenceRow | undefined;
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { graphDerivedAllowed } from "./graph/derived.js";
|
|
2
|
+
import Database from "better-sqlite3";
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import { join, resolve } from "node:path";
|
|
5
|
+
import { WORKFLOW_DB_FILE } from "./workflowDb.js";
|
|
6
|
+
import { evidenceDigest } from "./managerEvidenceArtifacts.js";
|
|
7
|
+
const scopedTables = ["qa_review_attempts", "qa_reports", "qa_remediation_attempts", "qa_turns", "qa_ticket_heads", "qa_recovery_heads", "qa_transitions", "qa_pass_certificates", "qa_finalization_steps", "qa_failure_handoffs", "qa_operation_journal", "qa_packet_projections", "qa_source_states", "qa_review_bases", "qa_remediation_receipts", "qa_remediation_stops", "build_work_scope", "build_work_admissions", "build_assignments", "build_ownership_conflicts", "build_reconciliations", "build_work_events", "build_instructions", "build_instruction_streams"];
|
|
8
|
+
/** Inspection never registers a writer, runs a migration, or probes packet paths. */
|
|
9
|
+
export function readQaEvidenceSnapshot(projectDir, runId) {
|
|
10
|
+
const result = { runId, asOf: new Date().toISOString(), availability: "missing", capabilities: [], gaps: [], rows: {}, blobs: new Map() };
|
|
11
|
+
const path = join(resolve(projectDir), WORKFLOW_DB_FILE);
|
|
12
|
+
if (!existsSync(path)) {
|
|
13
|
+
result.gaps.push("workflow database is missing");
|
|
14
|
+
return result;
|
|
15
|
+
}
|
|
16
|
+
let db;
|
|
17
|
+
try {
|
|
18
|
+
db = new Database(path, { readonly: true, fileMustExist: true });
|
|
19
|
+
db.pragma("query_only = ON");
|
|
20
|
+
const connection = db;
|
|
21
|
+
connection.transaction(() => {
|
|
22
|
+
const tables = new Set(connection.prepare("SELECT name FROM sqlite_master WHERE type='table'").all().map(row => row.name));
|
|
23
|
+
const read = (name, query, ...args) => {
|
|
24
|
+
if (!tables.has(name)) {
|
|
25
|
+
result.gaps.push(`${name}: unsupported legacy schema`);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
try {
|
|
29
|
+
result.rows[name] = connection.prepare(query).all(...args);
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
if (!/no such column|no such table/i.test(String(error)))
|
|
33
|
+
throw error;
|
|
34
|
+
result.gaps.push(`${name}: unsupported legacy columns`);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
result.capabilities.push(name);
|
|
38
|
+
// Invalid JSON must remain visible instead of becoming empty history.
|
|
39
|
+
for (const row of result.rows[name])
|
|
40
|
+
for (const [field, value] of Object.entries(row)) {
|
|
41
|
+
if (field.endsWith("_json") && typeof value === "string") {
|
|
42
|
+
if (!graphDerivedAllowed(connection, projectDir, evidenceDigest(Buffer.from(value)))) {
|
|
43
|
+
row[field] = null;
|
|
44
|
+
row[`${field}_availability`] = "revoked-graph-evidence";
|
|
45
|
+
result.gaps.push(`${name}/${field}: graph-derived evidence withheld by current policy`);
|
|
46
|
+
continue;
|
|
47
|
+
}
|
|
48
|
+
try {
|
|
49
|
+
JSON.parse(value);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
result.gaps.push(`${name}/${field}: corrupt retained JSON`);
|
|
53
|
+
result.availability = "corrupt";
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
read("workflow_runs", "SELECT * FROM workflow_runs WHERE run_id=?", runId);
|
|
59
|
+
if (!result.rows.workflow_runs?.length) {
|
|
60
|
+
result.gaps.push("requested workflow run is missing");
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
if (result.availability !== "corrupt")
|
|
64
|
+
result.availability = "present";
|
|
65
|
+
for (const table of scopedTables)
|
|
66
|
+
read(table, `SELECT * FROM ${table} WHERE run_id=?`, runId);
|
|
67
|
+
read("qa_remediation_authorizations", "SELECT * FROM qa_remediation_authorizations WHERE run_id=?", runId);
|
|
68
|
+
read("workflow_events", "SELECT * FROM workflow_events WHERE run_id=? ORDER BY sequence", runId);
|
|
69
|
+
read("continuity_events", "SELECT * FROM continuity_events WHERE run_id=? ORDER BY sequence", runId);
|
|
70
|
+
read("operation_journal", "SELECT * FROM operation_journal WHERE run_id=?", runId);
|
|
71
|
+
read("human_decisions", "SELECT * FROM human_decisions WHERE run_id=?", runId);
|
|
72
|
+
if (tables.has("qa_delivery_turns") && tables.has("qa_reports")) {
|
|
73
|
+
const columns = connection.prepare("PRAGMA table_info(qa_reports)").all();
|
|
74
|
+
if (columns.some(column => column.name === "report_occurrence_id"))
|
|
75
|
+
read("qa_delivery_turns", "SELECT d.* FROM qa_delivery_turns d JOIN qa_reports r ON r.report_occurrence_id=d.report_occurrence_id WHERE r.run_id=?", runId);
|
|
76
|
+
else
|
|
77
|
+
result.gaps.push("qa_delivery_turns: occurrence identity unsupported");
|
|
78
|
+
}
|
|
79
|
+
else
|
|
80
|
+
result.gaps.push("qa_delivery_turns: unsupported legacy schema");
|
|
81
|
+
for (const [name, query] of Object.entries({
|
|
82
|
+
build_instruction_deliveries: "SELECT d.* FROM build_instruction_deliveries d JOIN build_instructions i USING(instruction_id) WHERE i.run_id=?",
|
|
83
|
+
build_instruction_events: "SELECT e.* FROM build_instruction_events e JOIN build_instructions i USING(instruction_id) WHERE i.run_id=? ORDER BY e.sequence",
|
|
84
|
+
qa_report_dispositions: "SELECT d.* FROM qa_report_dispositions d JOIN qa_reports r USING(report_occurrence_id) WHERE r.run_id=? ORDER BY d.sequence",
|
|
85
|
+
qa_report_chains: "SELECT c.* FROM qa_report_chains c JOIN qa_reports r ON r.report_occurrence_id=c.predecessor_occurrence_id WHERE r.run_id=?",
|
|
86
|
+
}))
|
|
87
|
+
read(name, query, runId);
|
|
88
|
+
for (const report of result.rows.qa_reports ?? []) {
|
|
89
|
+
if (typeof report.report_occurrence_id !== "string") {
|
|
90
|
+
result.gaps.push("qa_reports: legacy digest identity may be ambiguous; use the retained run/work/review scope");
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const parent = (result.rows.qa_review_attempts ?? []).find(row => row.ticket_id === report.ticket_id && row.review_number === report.review_number);
|
|
94
|
+
if (!parent || parent.source_digest !== report.source_state_digest || parent.report_digest !== report.report_digest) {
|
|
95
|
+
result.gaps.push(`qa_reports/${report.report_occurrence_id}: contradictory or missing review parent`);
|
|
96
|
+
result.availability = "corrupt";
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
for (const [name, rows] of Object.entries(result.rows))
|
|
100
|
+
if (name.startsWith("qa_"))
|
|
101
|
+
for (const row of rows) {
|
|
102
|
+
if ("run_id" in row && row.run_id !== runId || "ticket_id" in row && typeof row.ticket_id !== "string") {
|
|
103
|
+
result.gaps.push(`${name}: corrupt scoped row identity`);
|
|
104
|
+
result.availability = "corrupt";
|
|
105
|
+
}
|
|
106
|
+
if ("review_number" in row && (!Number.isSafeInteger(row.review_number) || Number(row.review_number) < 1)) {
|
|
107
|
+
result.gaps.push(`${name}: corrupt review ordinal`);
|
|
108
|
+
result.availability = "corrupt";
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// Copy only blobs referenced by scoped rows. Never allow a guessed digest read.
|
|
112
|
+
const references = new Set();
|
|
113
|
+
const collect = (value) => {
|
|
114
|
+
if (typeof value === "string" && /^[a-f0-9]{64}$/.test(value))
|
|
115
|
+
references.add(value);
|
|
116
|
+
else if (Array.isArray(value))
|
|
117
|
+
value.forEach(collect);
|
|
118
|
+
else if (value && typeof value === "object")
|
|
119
|
+
Object.values(value).forEach(collect);
|
|
120
|
+
};
|
|
121
|
+
for (const rows of Object.values(result.rows))
|
|
122
|
+
for (const row of rows)
|
|
123
|
+
for (const [field, value] of Object.entries(row)) {
|
|
124
|
+
if (field.endsWith("_json") && typeof value === "string") {
|
|
125
|
+
try {
|
|
126
|
+
collect(JSON.parse(value));
|
|
127
|
+
}
|
|
128
|
+
catch { /* already disclosed */ }
|
|
129
|
+
}
|
|
130
|
+
else
|
|
131
|
+
collect(value);
|
|
132
|
+
}
|
|
133
|
+
if (!tables.has("content_refs"))
|
|
134
|
+
result.gaps.push("content_refs: unsupported legacy schema");
|
|
135
|
+
else
|
|
136
|
+
for (const digest of references) {
|
|
137
|
+
if (!graphDerivedAllowed(connection, projectDir, digest)) {
|
|
138
|
+
result.gaps.push(`content ${digest}: graph access revoked`);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
const row = connection.prepare("SELECT content FROM content_refs WHERE digest=?").get(digest);
|
|
142
|
+
if (row) {
|
|
143
|
+
const bytes = Buffer.from(row.content);
|
|
144
|
+
if (evidenceDigest(bytes) !== digest) {
|
|
145
|
+
result.gaps.push(`content ${digest}: corrupt digest`);
|
|
146
|
+
result.availability = "corrupt";
|
|
147
|
+
}
|
|
148
|
+
else
|
|
149
|
+
result.blobs.set(digest, bytes);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
if (!result.capabilities.includes("qa_review_attempts") && result.availability !== "corrupt")
|
|
153
|
+
result.availability = "unsupported_legacy";
|
|
154
|
+
})();
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
result.availability = /malformed|not a database|corrupt/i.test(String(error)) ? "corrupt" : "unreadable";
|
|
158
|
+
result.gaps.push(`workflow evidence ${result.availability}: ${String(error)}`);
|
|
159
|
+
// Do not expose a partial transaction as a complete snapshot.
|
|
160
|
+
result.rows = {};
|
|
161
|
+
result.blobs.clear();
|
|
162
|
+
}
|
|
163
|
+
finally {
|
|
164
|
+
db?.close();
|
|
165
|
+
}
|
|
166
|
+
return result;
|
|
167
|
+
}
|
|
168
|
+
export function evidenceRecord(row, field = "record_json") {
|
|
169
|
+
try {
|
|
170
|
+
const parsed = JSON.parse(String(row[field]));
|
|
171
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : undefined;
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
return undefined;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
@@ -105,9 +105,11 @@ export declare class QaFailureDeliveryService {
|
|
|
105
105
|
private readonly faults;
|
|
106
106
|
constructor(faults?: QaDeliveryFaultHooks);
|
|
107
107
|
deliver(input: QaFailureDeliveryInput, controller: QaFailureDeliveryController): Promise<QaFailureDeliveryResult>;
|
|
108
|
+
private deliverOwned;
|
|
108
109
|
private deliverAttempt;
|
|
109
110
|
private turnIntent;
|
|
110
111
|
private dispatch;
|
|
112
|
+
private dispatchOwned;
|
|
111
113
|
private classify;
|
|
112
114
|
private drift;
|
|
113
115
|
private buildHandoff;
|