ai-foreman 1.7.20 → 1.7.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +12 -2
- 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 +90 -4
- package/dist/buildAdmission.js +2 -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/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 +6 -0
- package/dist/buildRuns.js +23 -17
- 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.js +28 -8
- package/dist/cli/start.js +152 -48
- package/dist/cli/status.js +20 -5
- package/dist/cli/tickets.js +9 -7
- package/dist/continuity.d.ts +11 -0
- package/dist/continuity.js +22 -6
- package/dist/foreman.d.ts +10 -2
- package/dist/foreman.js +330 -39
- 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 -11
- package/dist/index.js +2 -0
- 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.js +7 -0
- 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 +234 -24
- 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.js +12 -5
- package/dist/recoveryGuidance.d.ts +9 -0
- package/dist/recoveryGuidance.js +13 -0
- package/dist/runtimeAuth.d.ts +1 -0
- package/dist/runtimeAuth.js +5 -1
- package/dist/runtimeReadiness.d.ts +1 -0
- package/dist/runtimeReadiness.js +13 -1
- 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 +50 -1
- package/dist/statusReporter.d.ts +1 -0
- package/dist/statusReporter.js +1 -0
- package/dist/supervisedStart.js +18 -8
- 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/workflowDb.d.ts +51 -5
- package/dist/workflowDb.js +338 -7
- package/dist/workflowReader.d.ts +109 -2
- package/dist/workflowReader.js +48 -4
- package/package.json +84 -7
- package/scripts/copy-graph-assets.mjs +3 -0
- package/scripts/graph-benchmark.mjs +20 -0
- package/scripts/graph-evaluation.mjs +85 -0
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
import { dispatchWithGraphAccess } from "./graph/session.js";
|
|
2
|
+
import { currentGraphDerivedAccess, inheritGraphDerivedAccess, withGraphDerivedAccess } from "./graph/derived.js";
|
|
3
|
+
import { collectBuilderContractCoverage, assertBuilderContractCoverage } from "./qaBuilderCoverage.js";
|
|
4
|
+
import { queueGraphMaintenance, maintainGraphTask } from "./graph/boundary.js";
|
|
5
|
+
import { ensureBuilderContract } from "./qaBuildGate.js";
|
|
6
|
+
import { actualContractSession, assertContractReceipt } from "./qaContractDelivery.js";
|
|
7
|
+
import { graphContext, parseGraphRequest, prepareGraphInstruction, sendGraphTurn } from "./graph/turn.js";
|
|
1
8
|
import { randomUUID } from "node:crypto";
|
|
2
9
|
import { realpathSync } from "node:fs";
|
|
3
10
|
import { reportOccurrenceId } from "./qaHandbackMigration.js";
|
|
@@ -13,6 +20,16 @@ export class QaFailureDeliveryService {
|
|
|
13
20
|
this.faults = faults;
|
|
14
21
|
}
|
|
15
22
|
async deliver(input, controller) {
|
|
23
|
+
const inherited = currentGraphDerivedAccess();
|
|
24
|
+
const grants = inherited ? [...(Array.isArray(inherited) ? inherited : [inherited])] : [];
|
|
25
|
+
try {
|
|
26
|
+
return await withGraphDerivedAccess(grants, () => this.deliverOwned(input, controller));
|
|
27
|
+
}
|
|
28
|
+
finally {
|
|
29
|
+
inheritGraphDerivedAccess(grants);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
async deliverOwned(input, controller) {
|
|
16
33
|
const began = performance.now();
|
|
17
34
|
const handoffId = qaDigest("qa-failure-handoff", { runId: input.runId, ticketId: input.ticket.id, reviewAttemptId: input.reviewAttemptId, reportDigest: input.reportDigest, remediationGeneration: input.remediationGeneration + 1 });
|
|
18
35
|
const operationId = qaDigest("builder-remediation-operation", { runId: input.runId, ticketId: input.ticket.id, reviewAttemptId: input.reviewAttemptId, reportDigest: input.reportDigest, handoffId, remediationGeneration: input.remediationGeneration + 1 });
|
|
@@ -105,7 +122,7 @@ export class QaFailureDeliveryService {
|
|
|
105
122
|
return this.drift(input, "source-drift-before-delivery", preBoundary.digest);
|
|
106
123
|
const findingRefs = createQaFindingRefs({ runId: input.runId, ticketId: input.ticket.id, reviewAttemptId: input.reviewAttemptId, reportDigest: input.reportDigest, rawFindingIds: input.report.findings.map(f => f.id) });
|
|
107
124
|
const handoff = this.buildHandoff(input, preBoundary, findingRefs);
|
|
108
|
-
|
|
125
|
+
let instruction = renderQaFailureHandoff(handoff) + (input.operatorAnswer ? `\nAuthorized operator answer (${input.operatorAnswer.decisionId}): ${JSON.stringify(input.operatorAnswer.answer)}` : "");
|
|
109
126
|
enforceMandatoryPromptLimit(Buffer.from(instruction));
|
|
110
127
|
const operationId = qaDigest("builder-remediation-operation", { runId: input.runId, ticketId: input.ticket.id, reviewAttemptId: input.reviewAttemptId, reportDigest: input.reportDigest, handoffId, remediationGeneration: input.remediationGeneration + 1 });
|
|
111
128
|
const prepareDb = new WorkflowDb(input.projectDir);
|
|
@@ -136,6 +153,7 @@ export class QaFailureDeliveryService {
|
|
|
136
153
|
builder = await phase("before-dispatch-readiness", () => controller.beforeTurn(builder, instruction, input.builderWorktree));
|
|
137
154
|
controller.setAdapter?.(builder);
|
|
138
155
|
}
|
|
156
|
+
instruction += "\n" + await ensureBuilderContract(input.projectDir, input.runId, input.ticket.id, input.builderWorktree, builder);
|
|
139
157
|
const preDispatch = await phase("source-before-dispatch", () => captureFrozenQaSourceAsync(input.builderWorktree));
|
|
140
158
|
if (preDispatch.digest !== input.reviewedSourceStateDigest)
|
|
141
159
|
return this.drift(input, "source-drift-before-dispatch", preDispatch.digest, handoffId);
|
|
@@ -148,15 +166,27 @@ export class QaFailureDeliveryService {
|
|
|
148
166
|
}
|
|
149
167
|
// Byte counts are not token counts. This deliberately conservative bound also
|
|
150
168
|
// reserves wrapper instructions, response and continuity against known capacity.
|
|
169
|
+
const graphDeliveryContext = graphContext(input.projectDir, input.builderWorktree, "remediation", instruction, { operationId: `graph-delivery:${operationId}`, logicalTaskId: operationId, sourceRef: preDispatch.digest, runId: input.runId, workId: input.ticket.id, qualifies: input.report.findings.some(f => f.locations.length > 1) });
|
|
170
|
+
let graphPrepared = await prepareGraphInstruction(instruction, graphDeliveryContext);
|
|
151
171
|
const usage = await builder.contextUsage?.();
|
|
172
|
+
if (usage?.maximum && Buffer.byteLength(graphPrepared.instruction) + 16_384 > usage.maximum - usage.used)
|
|
173
|
+
graphPrepared = { instruction, bytes: 0 };
|
|
174
|
+
instruction = graphPrepared.instruction;
|
|
152
175
|
if (usage?.maximum && Buffer.byteLength(instruction) + 16_384 > usage.maximum - usage.used)
|
|
153
176
|
return { ok: false, outcome: "response-invalid", detail: "QA handback capacity error: mandatory requirements/findings and response reserve exceed available model context", handoffId, operationId };
|
|
154
177
|
const db = new WorkflowDb(input.projectDir);
|
|
155
178
|
let intent;
|
|
156
179
|
let record;
|
|
180
|
+
let guidanceIds = [];
|
|
157
181
|
try {
|
|
158
182
|
intent = db.atomic(() => {
|
|
159
183
|
const head = validateReviewBinding(db, input);
|
|
184
|
+
const admission = db.assertAdmittedWork(input.runId, input.ticket.id);
|
|
185
|
+
if (db.qaPreparationStore().policy(input.runId)?.mode === "enforce")
|
|
186
|
+
assertContractReceipt(db.qaPreparationStore(), input.runId, input.ticket.id, admission.requirementsDigest, actualContractSession(builder, input.builderWorktree, input.projectDir), builder);
|
|
187
|
+
const guidance = db.reserveGuidance(input.runId, input.ticket.id, "builder", operationId, preDispatch.digest, instruction);
|
|
188
|
+
instruction = guidance.text;
|
|
189
|
+
guidanceIds = guidance.ids;
|
|
160
190
|
const policy = db.autonomyPolicy(input.runId);
|
|
161
191
|
const maximum = Math.min(input.maxRemediationOperations ?? 3, policy?.rules["qa.nonconvergence"].max_attempts ?? policy?.limits.builderQaFixesPerTicket ?? 3);
|
|
162
192
|
db.reserveQaRemediation(input.runId, input.ticket.id, input.reviewAttemptId, operationId, maximum, input.authorizationId);
|
|
@@ -172,8 +202,40 @@ export class QaFailureDeliveryService {
|
|
|
172
202
|
db.close();
|
|
173
203
|
}
|
|
174
204
|
this.faults.afterIntent?.();
|
|
175
|
-
let response
|
|
176
|
-
let
|
|
205
|
+
let response;
|
|
206
|
+
let graphSequence = 0;
|
|
207
|
+
const exchangeResult = await phase("initial-work-and-validation", () => sendGraphTurn(builder, instruction, graphDeliveryContext, { handback: true, logicalActionId: operationId }, async (prompt) => {
|
|
208
|
+
if (graphSequence > 0) {
|
|
209
|
+
const nextDb = new WorkflowDb(input.projectDir);
|
|
210
|
+
try {
|
|
211
|
+
const head = nextDb.qaTicketHead(input.runId, input.ticket.id);
|
|
212
|
+
const active = nextDb.qaRemediationAttempt(operationId);
|
|
213
|
+
if (head.revision !== intent.expectedRevision || head.state !== "remediation-intended" || active?.status !== "started" || active.reviewAttemptId !== input.reviewAttemptId)
|
|
214
|
+
throw new Error("Graph continuation lost its remediation authority");
|
|
215
|
+
record = this.turnIntent(nextDb, operationId, occurrence, session, prompt, graphSequence, "graph-continuation");
|
|
216
|
+
}
|
|
217
|
+
finally {
|
|
218
|
+
nextDb.close();
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
graphSequence++;
|
|
222
|
+
response = await this.dispatch(input, builder, record, prompt);
|
|
223
|
+
if (!response.turn)
|
|
224
|
+
return { text: response.detail ?? "Remediation dispatch uncertain", isError: true, costUsd: 0, numTurns: 0 };
|
|
225
|
+
if (response.record.validationErrors?.length)
|
|
226
|
+
return { ...response.turn, isError: true };
|
|
227
|
+
return response.turn;
|
|
228
|
+
}, { ...graphPrepared, instruction }));
|
|
229
|
+
const guidanceDb = new WorkflowDb(input.projectDir);
|
|
230
|
+
try {
|
|
231
|
+
guidanceDb.finishGuidance(guidanceIds, "builder", { submitted: response.record.status === "completed" && response.record.providerTurnId ? true : undefined, receipt: response.record.providerTurnId ? response.record : undefined });
|
|
232
|
+
}
|
|
233
|
+
finally {
|
|
234
|
+
guidanceDb.close();
|
|
235
|
+
}
|
|
236
|
+
let outcome = exchangeResult.isError ? "delivery-uncertain" : this.classify(response);
|
|
237
|
+
if (exchangeResult.isError)
|
|
238
|
+
response.detail = exchangeResult.text;
|
|
177
239
|
if (outcome === "response-invalid" && response.record.sourceCapture === "captured") {
|
|
178
240
|
const correction = [
|
|
179
241
|
"Builder QA remediation response correction only. Do not inspect files, run tools, edit source, or perform additional remediation.",
|
|
@@ -186,7 +248,7 @@ export class QaFailureDeliveryService {
|
|
|
186
248
|
enforceMandatoryPromptLimit(Buffer.from(correction));
|
|
187
249
|
const repairDb = new WorkflowDb(input.projectDir);
|
|
188
250
|
try {
|
|
189
|
-
record = this.turnIntent(repairDb, operationId, occurrence, session, correction,
|
|
251
|
+
record = this.turnIntent(repairDb, operationId, occurrence, session, correction, graphSequence, "response-repair");
|
|
190
252
|
}
|
|
191
253
|
finally {
|
|
192
254
|
repairDb.close();
|
|
@@ -195,6 +257,15 @@ export class QaFailureDeliveryService {
|
|
|
195
257
|
response = await phase("response-repair-and-validation", () => this.dispatch(input, builder, record, correction, before));
|
|
196
258
|
outcome = this.classify(response);
|
|
197
259
|
}
|
|
260
|
+
if (outcome === "remediation-reported" && response.turn) {
|
|
261
|
+
try {
|
|
262
|
+
await collectBuilderContractCoverage(input.projectDir, input.runId, input.ticket.id, input.builderWorktree, builder, operationId, instruction, response.turn);
|
|
263
|
+
}
|
|
264
|
+
catch (error) {
|
|
265
|
+
outcome = "response-invalid";
|
|
266
|
+
response.detail = String(error);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
198
269
|
if (outcome === "remediation-reported" || outcome === "blocked" || outcome === "needs-input") {
|
|
199
270
|
if (!response.turn?.continuityErrors?.length) {
|
|
200
271
|
try {
|
|
@@ -220,6 +291,8 @@ export class QaFailureDeliveryService {
|
|
|
220
291
|
const summaryDigest = finishDb.putEvidence("qa", Buffer.from(response.contract?.report?.summary ?? detail));
|
|
221
292
|
finishDb.atomic(() => {
|
|
222
293
|
this.faults.beforeOutcomeCommit?.();
|
|
294
|
+
if (outcome === "remediation-reported")
|
|
295
|
+
assertBuilderContractCoverage(finishDb, input.runId, input.ticket.id, operationId, response.record.postSourceDigest);
|
|
223
296
|
finishDb.commitQaRemediationOutcome({ recoveryAttemptId: intent.recoveryId, remediationAttemptId: operationId, outcome: outcome === "remediation-reported" ? "succeeded" : outcome === "delivery-uncertain" ? "uncertain" : "failed", detail, responseDigest: response.record.rawResponseDigest, summaryDigest, expectedRevision: intent.expectedRevision,
|
|
224
297
|
...(outcome === "remediation-reported" ? { receipt: { version: 3, operationId, runId: input.runId, ticketId: input.ticket.id, reportDigest: input.reportDigest, reportOccurrenceId: occurrence, turnRecordId: response.record.turnRecordId, sourceStateDigest: input.reviewedSourceStateDigest, requestDigest, responseDigest: response.record.rawResponseDigest, summaryDigest, providerTurnId: response.record.providerTurnId, completedAt: receipt.completedAt } } : {}) });
|
|
225
298
|
const state = outcome === "remediation-reported" ? "remediation-reported" : outcome === "blocked" || outcome === "needs-input" ? "builder-blocked" : outcome === "source-drift" ? "response-invalid" : outcome;
|
|
@@ -243,16 +316,33 @@ export class QaFailureDeliveryService {
|
|
|
243
316
|
finally {
|
|
244
317
|
finishDb.close();
|
|
245
318
|
}
|
|
319
|
+
if (outcome === "remediation-reported" && response.turn) {
|
|
320
|
+
try {
|
|
321
|
+
queueGraphMaintenance(input.projectDir, input.builderWorktree, operationId, response.turn.text, response.record.postSourceDigest !== preDispatch.digest);
|
|
322
|
+
await maintainGraphTask(input.projectDir, input.builderWorktree, operationId, builder);
|
|
323
|
+
}
|
|
324
|
+
catch (error) {
|
|
325
|
+
console.error(`rafi graph: optional maintenance failed after accepted handback: ${String(error)}`);
|
|
326
|
+
}
|
|
327
|
+
}
|
|
246
328
|
return { ok: outcome === "remediation-reported", outcome, detail, response: response.turn?.text, summary: response.contract?.report?.summary, providerTurnId: response.record.providerTurnId, handoffId, operationId, turnRecordId: response.record.turnRecordId };
|
|
247
329
|
}
|
|
248
|
-
turnIntent(db, operationId, occurrence, session, prompt, index) {
|
|
249
|
-
const turn = { version:
|
|
330
|
+
turnIntent(db, operationId, occurrence, session, prompt, index, kind = index ? "response-repair" : "remediation") {
|
|
331
|
+
const turn = { version: 4, turnRecordId: qaDigest("qa-delivery-turn-v4", { operationId, index }), operationId, reportOccurrenceId: occurrence, turnIndex: index, kind, ...(index ? { parentTurnRecordId: qaDigest("qa-delivery-turn-v4", { operationId, index: 0 }) } : {}), status: "intended", intendedSession: session, hostInstructionDigest: db.putEvidence("handoff", Buffer.from(prompt)), hostInstructionBytes: Buffer.byteLength(prompt), providerInstructionAvailability: "unavailable", sourceCapture: "pending", startedAt: new Date().toISOString() };
|
|
250
332
|
if (db.qaDeliveryTurns(operationId).some(t => t.turnIndex === index))
|
|
251
333
|
throw new Error("Delivery turn already intended; reconcile without redispatch");
|
|
252
334
|
db.recordQaDeliveryTurn(turn);
|
|
253
335
|
return turn;
|
|
254
336
|
}
|
|
255
337
|
async dispatch(input, builder, record, prompt, responseOnlySourceDigest) {
|
|
338
|
+
let result;
|
|
339
|
+
await dispatchWithGraphAccess(input.projectDir, builder, prompt, undefined, async () => {
|
|
340
|
+
result = await this.dispatchOwned(input, builder, record, prompt, responseOnlySourceDigest);
|
|
341
|
+
return result.turn ?? { text: result.detail ?? "Dispatch failed", isError: true, costUsd: 0, numTurns: 0 };
|
|
342
|
+
});
|
|
343
|
+
return result;
|
|
344
|
+
}
|
|
345
|
+
async dispatchOwned(input, builder, record, prompt, responseOnlySourceDigest) {
|
|
256
346
|
const events = [];
|
|
257
347
|
let unsubscribe;
|
|
258
348
|
let turn, error;
|
|
@@ -263,10 +353,10 @@ export class QaFailureDeliveryService {
|
|
|
263
353
|
if (stableBuilderSessionIdentity(validateBuilderSession(builder, input)) !== stableBuilderSessionIdentity(record.intendedSession))
|
|
264
354
|
throw new Error("Builder session identity changed before dispatch");
|
|
265
355
|
unsubscribe = builder.observeEvents(event => { events.push(event); });
|
|
266
|
-
turn = await withActivityPhase(record.
|
|
356
|
+
turn = await withActivityPhase(record.kind === "response-repair" ? "correcting Builder QA remediation response" : "delivering source-bound QA failure handoff to Builder", () => builder.sendTurn(prompt, { handback: true, responseOnly: record.kind === "response-repair" }));
|
|
267
357
|
}
|
|
268
358
|
catch (caught) {
|
|
269
|
-
error = `Builder ${record.
|
|
359
|
+
error = `Builder ${record.kind === "response-repair" ? "correction " : ""}dispatch uncertain: ${String(caught)}`;
|
|
270
360
|
}
|
|
271
361
|
finally {
|
|
272
362
|
unsubscribe?.();
|
|
@@ -332,13 +422,20 @@ export class QaFailureDeliveryService {
|
|
|
332
422
|
errors.push(`Post-dispatch source capture failed: ${String(caught)}`);
|
|
333
423
|
}
|
|
334
424
|
record.captureElapsedMs = performance.now() - captureStart;
|
|
335
|
-
if (record.
|
|
425
|
+
if (record.kind === "response-repair") {
|
|
336
426
|
record.responseOnlyViolations = record.toolCount ? ["Builder used tools during response correction"] : [];
|
|
337
427
|
record.responseOnlySourceChanged = Boolean(record.postSourceDigest && record.postSourceDigest !== responseOnlySourceDigest);
|
|
338
428
|
if (record.responseOnlySourceChanged)
|
|
339
429
|
record.responseOnlyViolations.push(`source changed during response correction (${responseOnlySourceDigest} -> ${record.postSourceDigest})`);
|
|
340
430
|
}
|
|
341
|
-
|
|
431
|
+
let graphRequest = false;
|
|
432
|
+
try {
|
|
433
|
+
graphRequest = Boolean(turn && parseGraphRequest(turn.cleanedResponse ?? turn.text));
|
|
434
|
+
}
|
|
435
|
+
catch (caught) {
|
|
436
|
+
errors.push(String(caught));
|
|
437
|
+
}
|
|
438
|
+
const contract = turn && !graphRequest ? parseBuilderQaRemediationContract(turn.cleanedResponse ?? turn.text, { handoffId: qaDigest("qa-failure-handoff", { runId: input.runId, ticketId: input.ticket.id, reviewAttemptId: input.reviewAttemptId, reportDigest: input.reportDigest, remediationGeneration: input.remediationGeneration + 1 }), findings: createQaFindingRefs({ runId: input.runId, ticketId: input.ticket.id, reviewAttemptId: input.reviewAttemptId, reportDigest: input.reportDigest, rawFindingIds: input.report.findings.map(f => f.id) }) }) : undefined;
|
|
342
439
|
record.parserErrors = [...(contract?.errors ?? []), ...(turn?.continuityErrors ?? [])];
|
|
343
440
|
record.validationErrors = errors;
|
|
344
441
|
if (contract?.report)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type Database from "better-sqlite3";
|
|
2
|
-
export declare const HANDBACK_SCHEMA_VERSION =
|
|
2
|
+
export declare const HANDBACK_SCHEMA_VERSION = 4;
|
|
3
3
|
/** Review numbers are durably unique within a run/ticket, including before V3. */
|
|
4
4
|
export declare function reportOccurrenceId(runId: string, ticketId: string, reviewNumber: number): string;
|
|
5
5
|
export declare function registerHandbackWriter(db: Database.Database): void;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { hostname } from "node:os";
|
|
3
3
|
import { qaDigest } from "./qaProtocolV2.js";
|
|
4
|
-
export const HANDBACK_SCHEMA_VERSION =
|
|
4
|
+
export const HANDBACK_SCHEMA_VERSION = 4;
|
|
5
5
|
const migration = "003_qa_report_occurrences_and_turns";
|
|
6
6
|
/** Review numbers are durably unique within a run/ticket, including before V3. */
|
|
7
7
|
export function reportOccurrenceId(runId, ticketId, reviewNumber) {
|
|
@@ -11,7 +11,7 @@ export function registerHandbackWriter(db) {
|
|
|
11
11
|
const version = db.pragma("user_version", { simple: true });
|
|
12
12
|
if (version > HANDBACK_SCHEMA_VERSION)
|
|
13
13
|
throw new Error(`Recovery database schema ${version} requires a newer Rafi writer`);
|
|
14
|
-
db.function("rafi_writer_protocol", () =>
|
|
14
|
+
db.function("rafi_writer_protocol", () => version >= 4 ? 4 : 3);
|
|
15
15
|
}
|
|
16
16
|
/** No provider work or file I/O occurs inside this migration transaction. */
|
|
17
17
|
export function migrateQaHandback(db, faults) {
|
|
@@ -138,7 +138,7 @@ export function migrateQaHandback(db, faults) {
|
|
|
138
138
|
for (const action of ["INSERT", "UPDATE", "DELETE"]) {
|
|
139
139
|
db.exec(`CREATE TRIGGER handback_v3_${name}_${action} BEFORE ${action} ON "${name}" BEGIN SELECT CASE WHEN rafi_writer_protocol() != 3 THEN RAISE(ABORT,'incompatible Rafi writer') END; END;`);
|
|
140
140
|
}
|
|
141
|
-
db.pragma(
|
|
141
|
+
db.pragma("user_version = 3");
|
|
142
142
|
}).immediate();
|
|
143
143
|
}
|
|
144
144
|
finally {
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { QaContractCandidateV1, RequirementRef, QaVerificationContractV1 } from "rafi-spec";
|
|
2
|
+
import type { QaPreparationStore, LogicalPreparationBudget } from "./qaPreparationStore.js";
|
|
3
|
+
export interface PreparationProviderResult {
|
|
4
|
+
operationId?: string;
|
|
5
|
+
text: string;
|
|
6
|
+
sessionId: string;
|
|
7
|
+
graphReceiptRefs?: string[];
|
|
8
|
+
usage?: Partial<LogicalPreparationBudget["usage"]>;
|
|
9
|
+
}
|
|
10
|
+
export interface PreparationDependencies {
|
|
11
|
+
store: QaPreparationStore;
|
|
12
|
+
now(): number;
|
|
13
|
+
/** Each dispatch is a fresh, confined conversation, with a persisted reservation. */
|
|
14
|
+
dispatch(phase: "planner" | "prepare" | "assess" | "challenge" | "repair", instruction: string, operationId: string, remainingMs: number): Promise<PreparationProviderResult>;
|
|
15
|
+
inputsCurrent(candidate: QaContractCandidateV1): boolean;
|
|
16
|
+
normalizeDraft?(draft: QaContractCandidateV1, response: PreparationProviderResult): QaContractCandidateV1;
|
|
17
|
+
}
|
|
18
|
+
export type PreparationOutcome = {
|
|
19
|
+
status: "ready";
|
|
20
|
+
contract: QaVerificationContractV1;
|
|
21
|
+
} | {
|
|
22
|
+
status: "incomplete" | "blocked" | "uncertain";
|
|
23
|
+
detail: string;
|
|
24
|
+
nextAction: string;
|
|
25
|
+
};
|
|
26
|
+
export declare function prepareVerificationContract(initial: QaContractCandidateV1, inventory: RequirementRef[], deps: PreparationDependencies): Promise<PreparationOutcome>;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { QA_RISK_MINIMUMS, qaContractCandidateV1Schema, qaSemanticAssessmentV1Schema, qaChallengeReceiptV1Schema, qaPreparationDepthDecisionV1Schema } from "rafi-spec";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { depthObligations, validateDepthDecision } from "./qaPreparationPolicy.js";
|
|
4
|
+
import { canonicalContractJson, assembleContract, draftDigest, inventoryDigest, parsePreparationArtifact, validateCandidate } from "./qaVerificationContract.js";
|
|
5
|
+
export async function prepareVerificationContract(initial, inventory, deps) {
|
|
6
|
+
const store = deps.store;
|
|
7
|
+
const head = store.head(initial.runId, initial.workId, initial.admissionDigest);
|
|
8
|
+
if (head.state === "ready" && head.digest) {
|
|
9
|
+
const contract = store.contract(head.digest);
|
|
10
|
+
if (deps.inputsCurrent(contract))
|
|
11
|
+
return { status: "ready", contract };
|
|
12
|
+
store.setState(initial.runId, initial.workId, initial.admissionDigest, "amendment-required", "Relevant authoritative inputs changed", head.generation);
|
|
13
|
+
return { status: "blocked", detail: "Contract amendment required", nextAction: "Reconcile changed inputs and retained evidence before publishing a successor" };
|
|
14
|
+
}
|
|
15
|
+
const decisionIssues = initial.depthDecision ? validateDepthDecision(initial.depthDecision) : ["Missing planner depth assessment"];
|
|
16
|
+
// Missing decisions begin with Standard's ceiling, never the synthetic Low placeholder.
|
|
17
|
+
const budget = store.ensureBudget(initial.runId, initial.workId, initial.admissionDigest, decisionIssues.length ? 2 : initial.depthDecision.level, deps.now());
|
|
18
|
+
const unresolvedDispatch = budget.reservations.find(item => !item.resultDigest);
|
|
19
|
+
if (unresolvedDispatch)
|
|
20
|
+
return { status: "uncertain", detail: head.detail ?? `Uncertain preparation dispatch ${unresolvedDispatch.operationId}`, nextAction: "Reconcile retained provider receipt before retrying" };
|
|
21
|
+
const retained = store.progress(budget, "draft");
|
|
22
|
+
const priorAssessment = store.progress(budget, "assessment");
|
|
23
|
+
const priorChallenge = store.progress(budget, "challenge");
|
|
24
|
+
const targetedConcerns = [...(priorAssessment?.concerns ?? []), ...(priorChallenge?.concerns ?? []), ...(priorChallenge?.approachConcerns ?? [])].filter(concern => concern.material);
|
|
25
|
+
const retainedDepth = store.progress(budget, "depth-decision");
|
|
26
|
+
store.retainProgress(budget, "inventory", inventory);
|
|
27
|
+
let candidate = retained ? structuredClone(retained.candidate) : structuredClone(initial);
|
|
28
|
+
if (retainedDepth)
|
|
29
|
+
candidate.depthDecision = retainedDepth;
|
|
30
|
+
if (retained && canonicalContractJson(candidate.inputs) !== canonicalContractJson(initial.inputs))
|
|
31
|
+
return { status: "blocked", detail: "Retained draft inputs changed", nextAction: "Reconcile authoritative input changes" };
|
|
32
|
+
const run = async (phase, kind, instruction, resultId) => {
|
|
33
|
+
const operationId = `qa-preparation:${randomUUID()}`;
|
|
34
|
+
store.setState(initial.runId, initial.workId, initial.admissionDigest, ["assess", "challenge"].includes(phase) ? "validating" : "preparing", `Read-only ${phase} operation intended`, store.head(initial.runId, initial.workId, initial.admissionDigest).generation);
|
|
35
|
+
store.reserve(budget, operationId, kind, { phase, instruction, inputDigest: inventoryDigest(inventory) }, deps.now(), resultId);
|
|
36
|
+
const current = store.budget(initial.runId, initial.workId, initial.admissionDigest);
|
|
37
|
+
const dispatchedAt = deps.now();
|
|
38
|
+
const result = await deps.dispatch(phase, instruction, operationId, Math.max(1, current.deadlineMs - deps.now()));
|
|
39
|
+
store.retainResult(budget, operationId, result, result.usage);
|
|
40
|
+
store.metric(initial.runId, initial.workId, initial.admissionDigest, `metric:preparation-operation:${operationId}`, kind === "repair" ? "format-repair" : "phase-observation", { phase: phase === "planner" ? "planning" : phase === "challenge" ? "challenge" : phase === "repair" ? "repair" : "preparation", durationMs: Math.max(0, deps.now() - dispatchedAt), costUsd: result.usage?.costUsd ?? null, inputTokens: result.usage?.inputTokens ?? null, outputTokens: result.usage?.outputTokens ?? null });
|
|
41
|
+
if (deps.now() >= current.deadlineMs)
|
|
42
|
+
throw new Error("Preparation deadline expired; partial evidence retained");
|
|
43
|
+
return { ...result, operationId };
|
|
44
|
+
};
|
|
45
|
+
const parsed = async (result, tag) => {
|
|
46
|
+
let response = result;
|
|
47
|
+
for (let repair = 0;; repair++) {
|
|
48
|
+
try {
|
|
49
|
+
return parsePreparationArtifact(response.text, tag);
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
if (repair >= 2)
|
|
53
|
+
throw error;
|
|
54
|
+
response = await run("repair", "repair", `Format-only repair; no investigation, tools, or scope changes. Return the exact retained result as one ${tag}_START JSON ${tag}_END envelope.\nRetained response:\n${response.text}`, result.operationId ?? result.sessionId);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
try {
|
|
59
|
+
if (!retained && !retainedDepth && decisionIssues.length) {
|
|
60
|
+
const response = await run("planner", "planning", `Read-only depth assessment of admitted scope. Select Standard or higher unless explicit Focused low-risk evidence is established. Return RAFI_QA_DEPTH_START/END with QaPreparationDepthDecisionV1. Schema: ${JSON.stringify(qaPreparationDepthDecisionV1Schema)}. Policy qa-preparation-v1. Host minimums: interactions/compatibility/state/external dependencies=3; authorization/security/data/migration/concurrency/recovery/deployment=4; unresolved architectural uncertainty=5.\n${JSON.stringify(initial)}`);
|
|
61
|
+
candidate.depthDecision = await parsed(response, "RAFI_QA_DEPTH");
|
|
62
|
+
candidate.depthDecision.plannerOperationId = response.operationId;
|
|
63
|
+
candidate.depthDecision.plannerIdentity = response.sessionId;
|
|
64
|
+
const issues = validateDepthDecision(candidate.depthDecision);
|
|
65
|
+
if (issues.length)
|
|
66
|
+
throw new Error(`depth-decision-incomplete: ${issues.join("; ")}`);
|
|
67
|
+
store.reviseDepth(budget, candidate.depthDecision);
|
|
68
|
+
}
|
|
69
|
+
else
|
|
70
|
+
store.reviseDepth(budget, candidate.depthDecision);
|
|
71
|
+
store.retainProgress(budget, "depth-decision", candidate.depthDecision);
|
|
72
|
+
let authorSessionId = retained?.authorSessionId ?? "";
|
|
73
|
+
for (let pass = 0; pass < (candidate.depthDecision.level <= 2 ? 2 : 3) && (!retained || targetedConcerns.length > 0 || validateCandidate(candidate, inventory, store.equivalentResolver()).length > 0); pass++) {
|
|
74
|
+
const response = await run("prepare", "investigation", `QA preparation only. Inspect relevant source/tests; do not implement, install dependencies, or expand admitted product scope. Return RAFI_QA_CANDIDATE_START/END with a complete QaContractCandidateV1. Schema: ${JSON.stringify(qaContractCandidateV1Schema)}. Preserve host identity, inputs, depth and every authoritative inventory requirement exactly. Supply concrete check methods/evidence, complete coverage and cited investigation for each obligation. No disposition or waiver can be agent-authored. If risks exceed selected depth, return a separate RAFI_QA_ESCALATION_START/END JSON block {"rationale":"...","riskFactors":[{"category":"...","rationale":"...","references":["..."]}]}; do not change the depth yourself.\nPreviously retained independent concerns requiring targeted investigation and concrete resolution: ${JSON.stringify(targetedConcerns)}\nRequired obligations: ${depthObligations(candidate.depthDecision.level).join(", ")}\nAuthoritative candidate/input inventory:\n${JSON.stringify(candidate)}`);
|
|
75
|
+
const draft = await parsed(response, "RAFI_QA_CANDIDATE");
|
|
76
|
+
if (draft.runId !== candidate.runId || draft.workId !== candidate.workId || draft.admissionDigest !== candidate.admissionDigest || draft.contractId !== candidate.contractId || draft.revision !== candidate.revision || draft.predecessorDigest !== candidate.predecessorDigest || draft.createdAt !== candidate.createdAt || canonicalContractJson(draft.depthDecision) !== canonicalContractJson(candidate.depthDecision) || canonicalContractJson(draft.inputs) !== canonicalContractJson(candidate.inputs))
|
|
77
|
+
throw new Error("Preparation changed host-owned identity/depth/input authority");
|
|
78
|
+
candidate = deps.normalizeDraft ? deps.normalizeDraft(draft, response) : draft;
|
|
79
|
+
authorSessionId = response.sessionId;
|
|
80
|
+
for (const evidence of candidate.preparationEvidence) {
|
|
81
|
+
evidence.operationId = response.operationId;
|
|
82
|
+
evidence.sessionId = response.sessionId;
|
|
83
|
+
}
|
|
84
|
+
if (response.text.includes("RAFI_QA_ESCALATION_START")) {
|
|
85
|
+
const request = parsePreparationArtifact(response.text, "RAFI_QA_ESCALATION");
|
|
86
|
+
if (!request.rationale?.trim() || !request.riskFactors?.length || request.riskFactors.some(risk => !(risk.category in QA_RISK_MINIMUMS) || !risk.references?.length))
|
|
87
|
+
throw new Error("Incomplete risk escalation evidence");
|
|
88
|
+
const minimum = Math.max(candidate.depthDecision.level, ...request.riskFactors.map(risk => QA_RISK_MINIMUMS[risk.category]));
|
|
89
|
+
const revision = await run("planner", "escalation", `Revise only the QA depth for this admitted scope. Do not expand product scope. Host minimum ${minimum}; keep predecessorDecisionId ${candidate.depthDecision.decisionId}. Return RAFI_QA_DEPTH_START/END following ${JSON.stringify(qaPreparationDepthDecisionV1Schema)}. Previous decision: ${JSON.stringify(candidate.depthDecision)}; cited risk evidence: ${JSON.stringify(request)}`);
|
|
90
|
+
const decision = await parsed(revision, "RAFI_QA_DEPTH");
|
|
91
|
+
decision.plannerIdentity = revision.sessionId;
|
|
92
|
+
decision.plannerOperationId = revision.operationId;
|
|
93
|
+
const errors = validateDepthDecision(decision, candidate.depthDecision);
|
|
94
|
+
if (decision.level < minimum || request.riskFactors.some(risk => !decision.riskFactors.some(actual => actual.category === risk.category)))
|
|
95
|
+
errors.push("Planner revision does not address evidenced risk minimum");
|
|
96
|
+
if (errors.length)
|
|
97
|
+
throw new Error(`depth-decision-incomplete: ${errors.join("; ")}`);
|
|
98
|
+
candidate.depthDecision = decision;
|
|
99
|
+
store.reviseDepth(budget, decision);
|
|
100
|
+
store.retainProgress(budget, "depth-decision", decision);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
const structural = validateCandidate(candidate, inventory, store.equivalentResolver());
|
|
104
|
+
if (!structural.length)
|
|
105
|
+
break;
|
|
106
|
+
candidate.unresolved.push({ reason: structural.join("; "), requirementIds: [], checkIds: [], material: true, owner: "qa", nextAction: "Targeted investigation to resolve contract validation gaps" });
|
|
107
|
+
}
|
|
108
|
+
store.retainProgress(budget, "draft", { candidate, authorSessionId });
|
|
109
|
+
const structural = validateCandidate(candidate, inventory, store.equivalentResolver());
|
|
110
|
+
if (structural.length)
|
|
111
|
+
throw new Error(`Contract incomplete: ${structural.join("; ")}`);
|
|
112
|
+
const binding = { draftPayloadDigest: draftDigest(candidate), inventoryDigest: inventoryDigest(candidate.requirements), policyVersion: candidate.depthDecision.policyVersion, authorSessionId };
|
|
113
|
+
const retainedAssessment = store.progress(budget, "assessment");
|
|
114
|
+
const assessmentResponse = retainedAssessment?.draftPayloadDigest === draftDigest(candidate) ? undefined : await run("assess", "assessment", `Independent semantic assessment in a fresh conversation. Assess each original requirement and selected-depth obligation; identify omissions, conflicts, practical verification gaps, accidental scope, and unsupported dispositions. Return RAFI_QA_ASSESSMENT_START/END with QaSemanticAssessmentV1 (schema: ${JSON.stringify(qaSemanticAssessmentV1Schema)}) and specific material concerns (empty only when resolved). Bind exactly ${JSON.stringify(binding)}.\nPrior independent concerns requiring concrete resolution evidence: ${JSON.stringify(targetedConcerns)}\nOriginal inventory:\n${JSON.stringify(inventory)}\nDraft:\n${JSON.stringify(candidate)}`);
|
|
115
|
+
const assessment = assessmentResponse ? await parsed(assessmentResponse, "RAFI_QA_ASSESSMENT") : retainedAssessment;
|
|
116
|
+
if (assessmentResponse) {
|
|
117
|
+
assessment.sessionId = assessmentResponse.sessionId;
|
|
118
|
+
assessment.operationId = assessmentResponse.operationId;
|
|
119
|
+
}
|
|
120
|
+
store.retainProgress(budget, "assessment", assessment);
|
|
121
|
+
let challenge = store.progress(budget, "challenge");
|
|
122
|
+
if (challenge?.draftPayloadDigest !== draftDigest(candidate))
|
|
123
|
+
challenge = undefined;
|
|
124
|
+
if (candidate.depthDecision.level === 5 && !challenge) {
|
|
125
|
+
const response = await run("challenge", "challenge", `Independent approach challenge, fresh assessor conversation. Planning only. Return RAFI_QA_CHALLENGE_START/END with QaChallengeReceiptV1 (schema: ${JSON.stringify(qaChallengeReceiptV1Schema)}): independently critique the author's retained approach-proposal evidence (do not author a replacement), independent contract conclusions and separate approachConcerns for scope, invariants, failure-boundaries, verification. Unresolved material concerns prevent readiness. Binding: ${JSON.stringify(binding)}. Previously retained concerns requiring concrete resolution: ${JSON.stringify(targetedConcerns)}.\n${JSON.stringify(candidate)}`);
|
|
126
|
+
challenge = await parsed(response, "RAFI_QA_CHALLENGE");
|
|
127
|
+
challenge.sessionId = response.sessionId;
|
|
128
|
+
challenge.operationId = response.operationId;
|
|
129
|
+
store.retainProgress(budget, "challenge", challenge);
|
|
130
|
+
}
|
|
131
|
+
const contract = assembleContract(candidate, inventory, assessment, authorSessionId, challenge, store.equivalentResolver());
|
|
132
|
+
store.putArtifact("semantic-assessment", assessment);
|
|
133
|
+
if (challenge)
|
|
134
|
+
store.putArtifact("approach-challenge", challenge);
|
|
135
|
+
if (!deps.inputsCurrent(candidate))
|
|
136
|
+
throw new Error("Authoritative preparation inputs changed before publication");
|
|
137
|
+
store.publish(contract, store.head(initial.runId, initial.workId, initial.admissionDigest).generation, deps.now());
|
|
138
|
+
store.event(initial.runId, initial.workId, `prepared:${contract.contentDigest}`, "preparation-ready", { contractDigest: contract.contentDigest, level: contract.depthDecision.level });
|
|
139
|
+
return { status: "ready", contract };
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
143
|
+
const current = store.budget(initial.runId, initial.workId, initial.admissionDigest);
|
|
144
|
+
const status = current.reservations.some(reservation => !reservation.resultDigest) ? "uncertain" : "incomplete";
|
|
145
|
+
store.setState(initial.runId, initial.workId, initial.admissionDigest, status, detail, store.head(initial.runId, initial.workId, initial.admissionDigest).generation);
|
|
146
|
+
store.metric(initial.runId, initial.workId, initial.admissionDigest, `metric:preparation-failure:${current.id}:${current.reservations.length}`, "preparation-failure", { status: "blocked" });
|
|
147
|
+
store.event(initial.runId, initial.workId, `preparation-failure:${current.id}:${current.reservations.length}`, "preparation-failure", { status, detail });
|
|
148
|
+
return { status, detail, nextAction: status === "uncertain" ? "Reconcile retained provider dispatch; do not replay uncertain work" : "Resolve remaining concerns or record authorized operational extension, then retry" };
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ContractInputRef, RequirementRef, StructuredPlanV1 } from "rafi-spec";
|
|
2
|
+
import type { TicketDef } from "./tickets/ticketSchema.js";
|
|
3
|
+
/** Resolve the admitted slice's exact approved plan revision, never a newer latest file. */
|
|
4
|
+
export declare function admittedPlanInput(projectDir: string, ticket: TicketDef): {
|
|
5
|
+
input: ContractInputRef;
|
|
6
|
+
plan: StructuredPlanV1;
|
|
7
|
+
requirements: RequirementRef[];
|
|
8
|
+
} | undefined;
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { loadRafiConfigObject } from "./tickets/setupConfig.js";
|
|
4
|
+
import { canonicalJson } from "./qaProtocolV2.js";
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
import { contractDigest } from "./qaVerificationContract.js";
|
|
7
|
+
/** Resolve the admitted slice's exact approved plan revision, never a newer latest file. */
|
|
8
|
+
export function admittedPlanInput(projectDir, ticket) {
|
|
9
|
+
if (!ticket.plan_ref)
|
|
10
|
+
return undefined;
|
|
11
|
+
const config = loadRafiConfigObject(projectDir);
|
|
12
|
+
const docsRoot = config?.docs?.root ?? "docs";
|
|
13
|
+
if (docsRoot.startsWith("/") || docsRoot.split(/[\\/]/).includes(".."))
|
|
14
|
+
throw new Error("Unsafe approved-plan document root");
|
|
15
|
+
const history = join(projectDir, docsRoot, "rafi-plans");
|
|
16
|
+
const files = [join(projectDir, docsRoot, "rafi-plan.json"), ...(existsSync(history) ? readdirSync(history).filter(name => name.endsWith(".json")).sort().reverse().slice(0, 1000).map(name => join(history, name)) : [])];
|
|
17
|
+
for (const path of files) {
|
|
18
|
+
if (!existsSync(path))
|
|
19
|
+
continue;
|
|
20
|
+
let plan;
|
|
21
|
+
try {
|
|
22
|
+
plan = JSON.parse(readFileSync(path, "utf8"));
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (plan.plan_id !== ticket.plan_ref.plan_id || plan.revision !== ticket.plan_ref.revision)
|
|
28
|
+
continue;
|
|
29
|
+
if (plan.content_digest !== createHash("sha256").update(canonicalJson({ ...plan, content_digest: "" })).digest("hex"))
|
|
30
|
+
throw new Error("Corrupt admitted approved-plan revision");
|
|
31
|
+
const slice = plan.slices.find(row => row.slice_ref === ticket.plan_ref.slice_ref);
|
|
32
|
+
if (!slice)
|
|
33
|
+
throw new Error("Admitted plan slice is absent from its exact approved revision");
|
|
34
|
+
const input = { id: "approved-plan", kind: "plan", reference: path, digest: plan.content_digest, authority: "approved-scope", revision: String(plan.revision), availability: "available" };
|
|
35
|
+
const requirements = [];
|
|
36
|
+
for (const [field, entries, location] of [["acceptance_criteria", plan.acceptance_criteria, ""], ["test_plan", plan.test_plan, ""], ["acceptance", slice.acceptance, `slices.${slice.slice_ref}.`], ["required_tests", slice.required_tests, `slices.${slice.slice_ref}.`]]) {
|
|
37
|
+
const occurrences = new Map();
|
|
38
|
+
entries.forEach((statement, index) => {
|
|
39
|
+
const fingerprint = contractDigest("plan-requirement", { field, statement }), occurrence = occurrences.get(fingerprint) ?? 0;
|
|
40
|
+
occurrences.set(fingerprint, occurrence + 1);
|
|
41
|
+
requirements.push({ id: `req-plan-${fingerprint.slice(0, 24)}-${occurrence}`, inputRef: input.id, locator: `${location}${field}[${index}]`, statement, digest: contractDigest("statement", statement), obligation: "mandatory", origin: "explicit", authority: "approved" });
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
return { input, plan, requirements };
|
|
45
|
+
}
|
|
46
|
+
throw new Error("Exact admitted approved-plan revision unavailable; restore its immutable artifact before preparation");
|
|
47
|
+
}
|