ai-foreman 1.7.29 → 2.0.1
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 +231 -26
- package/dist/adapters/codex.d.ts +19 -7
- package/dist/adapters/codex.js +202 -19
- 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/git.js +3 -8
- 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 +28 -9
- 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/buildRuns.d.ts +1 -0
- package/dist/buildRuns.js +6 -1
- package/dist/builderGuidanceFollowup.js +17 -6
- package/dist/cli/doctor.js +3 -1
- package/dist/cli/manager.js +22 -20
- package/dist/cli/start.js +85 -39
- package/dist/cli/tickets.d.ts +0 -8
- package/dist/cli/tickets.js +50 -60
- package/dist/continuity.d.ts +10 -16
- package/dist/continuity.js +223 -208
- package/dist/continuityOrigin.d.ts +13 -0
- package/dist/continuityOrigin.js +91 -0
- package/dist/dispatchNotSent.d.ts +4 -0
- package/dist/dispatchNotSent.js +10 -0
- package/dist/foreman.d.ts +15 -16
- package/dist/foreman.js +70 -278
- package/dist/gitExclude.d.ts +16 -0
- package/dist/gitExclude.js +46 -0
- 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 +3 -4
- package/dist/graph/turn.js +29 -41
- package/dist/handoffProtocol.js +2 -2
- package/dist/handoffs.d.ts +5 -3
- package/dist/handoffs.js +92 -88
- 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/humanDecision.d.ts +7 -0
- package/dist/humanDecision.js +11 -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/observability.js +3 -8
- package/dist/probeIpc.d.ts +14 -0
- package/dist/probeIpc.js +29 -0
- package/dist/qaBuildGate.d.ts +21 -0
- package/dist/qaBuildGate.js +60 -13
- 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 +153 -254
- package/dist/qaPreparationInputs.js +19 -0
- package/dist/qaPreparationStore.d.ts +11 -0
- package/dist/qaPreparationStore.js +44 -4
- package/dist/qaPrerequisites.d.ts +2 -1
- package/dist/qaPrerequisites.js +21 -1
- package/dist/qaProtocolV2.d.ts +15 -0
- package/dist/qaRecovery.d.ts +0 -3
- package/dist/qaRecovery.js +21 -27
- package/dist/qaReview.js +241 -406
- package/dist/qaReviewEvidence.d.ts +7 -3
- package/dist/qaReviewEvidence.js +15 -4
- package/dist/qaRuntime.js +1 -1
- package/dist/qaSnapshot.js +76 -21
- package/dist/qaStructuredResponse.d.ts +18 -0
- package/dist/qaStructuredResponse.js +81 -0
- package/dist/qaTimeline.js +2 -2
- package/dist/qaVerificationContract.d.ts +1 -2
- package/dist/qaVerificationContract.js +24 -35
- package/dist/readinessGate.js +5 -4
- package/dist/recoveryOwners.d.ts +32 -0
- package/dist/recoveryOwners.js +87 -0
- package/dist/runtimePreflight.d.ts +11 -0
- package/dist/runtimePreflight.js +77 -0
- package/dist/runtimeReadiness.js +23 -15
- package/dist/sources/sourceRegistry.d.ts +0 -3
- package/dist/sources/sourceRegistry.js +0 -42
- 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 +279 -0
- package/dist/supervisedStart.js +35 -21
- package/dist/ticketPlanning.d.ts +1 -376
- package/dist/ticketPlanning.js +2 -49
- 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 +127 -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 +116 -3
- package/dist/workflowDb.js +852 -61
- package/dist/workflowReader.d.ts +1 -0
- package/dist/workflowReader.js +9 -0
- package/package.json +23 -7
|
@@ -1,15 +1,22 @@
|
|
|
1
|
+
import { BoundedTurnEvents, boundedUtf8 as boundedEvidenceUtf8, receiptFingerprint, rawFingerprint } from "./boundedTurnEvidence.js";
|
|
2
|
+
import { providerWireContract, remediationOutput } from "rafi-spec";
|
|
3
|
+
import { journalStructuredDispatch, operationInstruction, operationResponse, nativePolicy } from "./structuredOperation.js";
|
|
4
|
+
import { durableOwnerDispatch } from "./structuredDispatchAuthority.js";
|
|
5
|
+
import { remediationStructuredResponse } from "./qaStructuredResponse.js";
|
|
1
6
|
import { dispatchWithGraphAccess } from "./graph/session.js";
|
|
2
7
|
import { currentGraphDerivedAccess, inheritGraphDerivedAccess, withGraphDerivedAccess } from "./graph/derived.js";
|
|
3
8
|
import { collectBuilderContractCoverage, assertBuilderContractCoverage } from "./qaBuilderCoverage.js";
|
|
9
|
+
import { observeHostBuilderTests } from "./qaBuilderEvidence.js";
|
|
10
|
+
import { captureBuildSource } from "./buildAssignment.js";
|
|
4
11
|
import { queueGraphMaintenance, maintainGraphTask } from "./graph/boundary.js";
|
|
5
12
|
import { ensureBuilderContract } from "./qaBuildGate.js";
|
|
6
13
|
import { actualContractSession, assertContractReceipt } from "./qaContractDelivery.js";
|
|
7
|
-
import { graphContext,
|
|
14
|
+
import { graphContext, prepareGraphInstruction, sendGraphTurn } from "./graph/turn.js";
|
|
8
15
|
import { randomUUID } from "node:crypto";
|
|
9
16
|
import { realpathSync } from "node:fs";
|
|
10
17
|
import { reportOccurrenceId } from "./qaHandbackMigration.js";
|
|
11
18
|
import { boundedQaHistory, evidenceDigest, utf8Prefix } from "./qaHandbackHistory.js";
|
|
12
|
-
import {
|
|
19
|
+
import { builderQaRemediationReportV3Schema, } from "rafi-spec";
|
|
13
20
|
import { withActivityPhase } from "./activity.js";
|
|
14
21
|
import { captureFrozenQaSourceAsync } from "./qaSnapshot.js";
|
|
15
22
|
import { canonicalJson, createQaFindingRefs, qaDigest } from "./qaProtocolV2.js";
|
|
@@ -92,7 +99,7 @@ export class QaFailureDeliveryService {
|
|
|
92
99
|
try {
|
|
93
100
|
let prior = read.qaFailureHandoff(handoffId);
|
|
94
101
|
if (prior?.state === "delivery-intended" && read.qaRemediationAttempt(prior.operationId)?.status === "succeeded") {
|
|
95
|
-
const historical = read.
|
|
102
|
+
const historical = read.verifiedBuilderRemediationReceipt(prior.operationId);
|
|
96
103
|
if (!historical || historical.runId !== input.runId || historical.ticketId !== input.ticket.id || historical.reportDigest !== input.reportDigest || !read.getEvidence(historical.responseDigest))
|
|
97
104
|
throw new Error("Partial historical handback needs receipt reconciliation; no redispatch permitted");
|
|
98
105
|
const receiptDigest = read.putEvidence("handoff", Buffer.from(canonicalEvidence({ version: 3, outcome: "remediation-reported", legacyEvidenceIncomplete: true, historicalReceipt: historical, reconciledAt: new Date().toISOString(), qaApproved: false })));
|
|
@@ -103,6 +110,13 @@ export class QaFailureDeliveryService {
|
|
|
103
110
|
const raw = prior.receiptDigest ? read.getEvidence(prior.receiptDigest) : undefined;
|
|
104
111
|
const receipt = raw ? JSON.parse(raw.toString()) : undefined;
|
|
105
112
|
const accepted = prior.state === "recheck-required" && read.qaRemediationAttempt(prior.operationId)?.status === "succeeded";
|
|
113
|
+
if (accepted) {
|
|
114
|
+
const verified = read.verifiedBuilderRemediationReceipt(prior.operationId);
|
|
115
|
+
if (!verified || verified.runId !== prior.runId || verified.ticketId !== prior.ticketId
|
|
116
|
+
|| verified.reportDigest !== prior.reportDigest || verified.responseDigest !== prior.responseDigest
|
|
117
|
+
|| verified.providerTurnId !== prior.providerTurnId)
|
|
118
|
+
throw new Error("Replayed QA remediation handoff lacks its verified native receipt");
|
|
119
|
+
}
|
|
106
120
|
return { ok: accepted, outcome: receipt?.outcome ?? (accepted ? "remediation-reported" : "delivery-uncertain"), detail: prior.detail ?? "Operation already dispatched; reconcile its durable evidence before further work", handoffId, operationId: prior.operationId,
|
|
107
121
|
turnRecordId: receipt?.turnRecordId, response: prior.responseDigest ? read.getEvidence(prior.responseDigest)?.toString() : undefined,
|
|
108
122
|
summary: accepted ? "Previously reported remediation; independent QA required" : undefined, providerTurnId: prior.providerTurnId };
|
|
@@ -175,7 +189,7 @@ export class QaFailureDeliveryService {
|
|
|
175
189
|
// Byte counts are not token counts. This deliberately conservative bound also
|
|
176
190
|
// reserves wrapper instructions, response and continuity against known capacity.
|
|
177
191
|
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) });
|
|
178
|
-
let graphPrepared = await prepareGraphInstruction(instruction, graphDeliveryContext);
|
|
192
|
+
let graphPrepared = await prepareGraphInstruction(instruction, graphDeliveryContext, undefined, remediationOutput);
|
|
179
193
|
const usage = await builder.contextUsage?.();
|
|
180
194
|
if (usage?.maximum && Buffer.byteLength(graphPrepared.instruction) + 16_384 > usage.maximum - usage.used)
|
|
181
195
|
graphPrepared = { instruction, bytes: 0 };
|
|
@@ -212,28 +226,37 @@ export class QaFailureDeliveryService {
|
|
|
212
226
|
this.faults.afterIntent?.();
|
|
213
227
|
let response;
|
|
214
228
|
let graphSequence = 0;
|
|
215
|
-
const
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
229
|
+
const hostToolObservations = [];
|
|
230
|
+
const initialSourceDigest = captureBuildSource(input.builderWorktree).digest;
|
|
231
|
+
const stopHostObservation = observeHostBuilderTests(builder, input.builderWorktree, hostToolObservations);
|
|
232
|
+
let exchangeResult;
|
|
233
|
+
try {
|
|
234
|
+
exchangeResult = await phase("initial-work-and-validation", () => sendGraphTurn(builder, instruction, graphDeliveryContext, nativePolicy(remediationOutput, { handback: true, logicalActionId: operationId, journalRunId: input.runId }), durableOwnerDispatch(input.projectDir, remediationOutput, { runId: input.runId, actionId: operationId }, async (prompt) => {
|
|
235
|
+
if (graphSequence > 0) {
|
|
236
|
+
const nextDb = new WorkflowDb(input.projectDir);
|
|
237
|
+
try {
|
|
238
|
+
const head = nextDb.qaTicketHead(input.runId, input.ticket.id);
|
|
239
|
+
const active = nextDb.qaRemediationAttempt(operationId);
|
|
240
|
+
if (head.revision !== intent.expectedRevision || head.state !== "remediation-intended" || active?.status !== "started" || active.reviewAttemptId !== input.reviewAttemptId)
|
|
241
|
+
throw new Error("Graph continuation lost its remediation authority");
|
|
242
|
+
record = this.turnIntent(nextDb, operationId, occurrence, session, prompt, graphSequence, "graph-continuation");
|
|
243
|
+
}
|
|
244
|
+
finally {
|
|
245
|
+
nextDb.close();
|
|
246
|
+
}
|
|
227
247
|
}
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
return
|
|
235
|
-
|
|
236
|
-
}
|
|
248
|
+
graphSequence++;
|
|
249
|
+
response = await this.dispatch(input, builder, record, prompt);
|
|
250
|
+
if (!response.turn)
|
|
251
|
+
return { text: response.detail ?? "Remediation dispatch uncertain", isError: true, costUsd: 0, numTurns: 0 };
|
|
252
|
+
if (response.record.validationErrors?.length)
|
|
253
|
+
return { ...response.turn, isError: true };
|
|
254
|
+
return response.turn;
|
|
255
|
+
}), { ...graphPrepared, instruction }));
|
|
256
|
+
}
|
|
257
|
+
finally {
|
|
258
|
+
stopHostObservation();
|
|
259
|
+
}
|
|
237
260
|
const guidanceDb = new WorkflowDb(input.projectDir);
|
|
238
261
|
try {
|
|
239
262
|
guidanceDb.finishGuidance(guidanceIds, "builder", { submitted: response.record.status === "completed" && response.record.providerTurnId ? true : undefined, receipt: response.record.providerTurnId ? response.record : undefined });
|
|
@@ -241,33 +264,16 @@ export class QaFailureDeliveryService {
|
|
|
241
264
|
finally {
|
|
242
265
|
guidanceDb.close();
|
|
243
266
|
}
|
|
244
|
-
let outcome =
|
|
245
|
-
if (exchangeResult.isError)
|
|
267
|
+
let outcome = this.classify(response);
|
|
268
|
+
if (exchangeResult.isError && outcome !== "response-invalid") {
|
|
269
|
+
outcome = "delivery-uncertain";
|
|
246
270
|
response.detail = exchangeResult.text;
|
|
247
|
-
if (outcome === "response-invalid" && response.record.sourceCapture === "captured") {
|
|
248
|
-
const correction = [
|
|
249
|
-
"Builder QA remediation response correction only. Do not inspect files, run tools, edit source, or perform additional remediation.",
|
|
250
|
-
"Preserve the original substantive result: do not invent changes, evidence, verification, answers, or turn a blocker into done.",
|
|
251
|
-
`Actual validation errors: ${JSON.stringify(response.record.parserErrors)}`,
|
|
252
|
-
`Original raw response evidence: ${response.record.rawResponseDigest}. The original turn remains in this same conversation.`,
|
|
253
|
-
`Post-remediation source digest: ${response.record.postSourceDigest}`,
|
|
254
|
-
responseInstructions(handoff),
|
|
255
|
-
].join("\n\n");
|
|
256
|
-
enforceMandatoryPromptLimit(Buffer.from(correction));
|
|
257
|
-
const repairDb = new WorkflowDb(input.projectDir);
|
|
258
|
-
try {
|
|
259
|
-
record = this.turnIntent(repairDb, operationId, occurrence, session, correction, graphSequence, "response-repair");
|
|
260
|
-
}
|
|
261
|
-
finally {
|
|
262
|
-
repairDb.close();
|
|
263
|
-
}
|
|
264
|
-
const before = response.record.postSourceDigest;
|
|
265
|
-
response = await phase("response-repair-and-validation", () => this.dispatch(input, builder, record, correction, before));
|
|
266
|
-
outcome = this.classify(response);
|
|
267
271
|
}
|
|
272
|
+
// Native malformed results retain completed side effects and stop at the
|
|
273
|
+
// operation boundary. A second model turn cannot prove content equivalence.
|
|
268
274
|
if (outcome === "remediation-reported" && response.turn) {
|
|
269
275
|
try {
|
|
270
|
-
await collectBuilderContractCoverage(input.projectDir, input.runId, input.ticket.id, input.builderWorktree, builder, operationId, instruction, response.turn);
|
|
276
|
+
await collectBuilderContractCoverage(input.projectDir, input.runId, input.ticket.id, input.builderWorktree, builder, operationId, instruction, response.turn, hostToolObservations, initialSourceDigest);
|
|
271
277
|
}
|
|
272
278
|
catch (error) {
|
|
273
279
|
outcome = "response-invalid";
|
|
@@ -326,7 +332,7 @@ export class QaFailureDeliveryService {
|
|
|
326
332
|
}
|
|
327
333
|
if (outcome === "remediation-reported" && response.turn) {
|
|
328
334
|
try {
|
|
329
|
-
queueGraphMaintenance(input.projectDir, input.builderWorktree, operationId, response.turn.
|
|
335
|
+
queueGraphMaintenance(input.projectDir, input.builderWorktree, operationId, operationResponse(response.turn, remediationOutput).result.payload?.graphMaintenance === "meaningful-indexed-change", response.record.postSourceDigest !== preDispatch.digest);
|
|
330
336
|
await maintainGraphTask(input.projectDir, input.builderWorktree, operationId, builder);
|
|
331
337
|
}
|
|
332
338
|
catch (error) {
|
|
@@ -335,23 +341,23 @@ export class QaFailureDeliveryService {
|
|
|
335
341
|
}
|
|
336
342
|
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 };
|
|
337
343
|
}
|
|
338
|
-
turnIntent(db, operationId, occurrence, session, prompt, index, kind =
|
|
344
|
+
turnIntent(db, operationId, occurrence, session, prompt, index, kind = "remediation") {
|
|
339
345
|
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() };
|
|
340
346
|
if (db.qaDeliveryTurns(operationId).some(t => t.turnIndex === index))
|
|
341
347
|
throw new Error("Delivery turn already intended; reconcile without redispatch");
|
|
342
348
|
db.recordQaDeliveryTurn(turn);
|
|
343
349
|
return turn;
|
|
344
350
|
}
|
|
345
|
-
async dispatch(input, builder, record, prompt
|
|
351
|
+
async dispatch(input, builder, record, prompt) {
|
|
346
352
|
let result;
|
|
347
353
|
await dispatchWithGraphAccess(input.projectDir, builder, prompt, undefined, async () => {
|
|
348
|
-
result = await this.dispatchOwned(input, builder, record, prompt
|
|
354
|
+
result = await this.dispatchOwned(input, builder, record, prompt);
|
|
349
355
|
return result.turn ?? { text: result.detail ?? "Dispatch failed", isError: true, costUsd: 0, numTurns: 0 };
|
|
350
356
|
});
|
|
351
357
|
return result;
|
|
352
358
|
}
|
|
353
|
-
async dispatchOwned(input, builder, record, prompt
|
|
354
|
-
const events =
|
|
359
|
+
async dispatchOwned(input, builder, record, prompt) {
|
|
360
|
+
const events = new BoundedTurnEvents(providerWireContract(remediationOutput, builder.agent).maximumResponseBytes);
|
|
355
361
|
let unsubscribe;
|
|
356
362
|
let turn, error;
|
|
357
363
|
const began = performance.now();
|
|
@@ -361,10 +367,14 @@ export class QaFailureDeliveryService {
|
|
|
361
367
|
if (stableBuilderSessionIdentity(validateBuilderSession(builder, input)) !== stableBuilderSessionIdentity(record.intendedSession))
|
|
362
368
|
throw new Error("Builder session identity changed before dispatch");
|
|
363
369
|
unsubscribe = builder.observeEvents(event => { events.push(event); });
|
|
364
|
-
|
|
370
|
+
// The QA record is the domain owner, while the common gateway owns the
|
|
371
|
+
// immutable transport tuple. Calling it here (rather than relying on
|
|
372
|
+
// the graph wrapper) gives QA the same prepare/seal-before-send boundary
|
|
373
|
+
// as every other structured dispatch without creating a second tuple.
|
|
374
|
+
turn = await withActivityPhase("delivering source-bound QA failure handoff to Builder", () => journalStructuredDispatch(input.projectDir, builder, `${prompt}\n\n${operationInstruction(remediationOutput)}`, nativePolicy(remediationOutput, { dispatchId: record.turnRecordId, logicalActionId: record.operationId, journalRunId: input.runId, handback: true }), (text, policy) => builder.sendTurn(text, policy)));
|
|
365
375
|
}
|
|
366
376
|
catch (caught) {
|
|
367
|
-
error = `Builder
|
|
377
|
+
error = `Builder dispatch uncertain: ${String(caught)}`;
|
|
368
378
|
}
|
|
369
379
|
finally {
|
|
370
380
|
unsubscribe?.();
|
|
@@ -374,18 +384,29 @@ export class QaFailureDeliveryService {
|
|
|
374
384
|
try {
|
|
375
385
|
// Preserve returned bytes before identity/source inspection can fail.
|
|
376
386
|
if (turn) {
|
|
387
|
+
record.sealedDispatch = db.structuredOwnerDispatch(record.turnRecordId);
|
|
377
388
|
const raw = turn.rawResponse ?? turn.text, cleaned = turn.cleanedResponse ?? turn.text;
|
|
378
|
-
|
|
389
|
+
record.structuredResponseDigest = db.operation(record.turnRecordId)?.result?.responseDigest;
|
|
390
|
+
Object.assign(record, { rawResponseDigest: (db.operation(record.turnRecordId)?.result).rawDigest, rawResponseBytes: Buffer.byteLength(raw), cleanedResponseDigest: db.putEvidence("qa", boundedEvidenceUtf8(cleaned, events.maximumBytes)), cleanedResponseBytes: Buffer.byteLength(cleaned), providerTurnId: turn.turnId, providerReturnedError: turn.isError, providerMetadata: turn.providerMetadata, failure: turn.failure && { ...turn.failure, diagnostics: boundedEvidenceUtf8(turn.failure.diagnostics, 4096).toString() } });
|
|
379
391
|
if (turn.providerInstruction !== undefined)
|
|
380
392
|
Object.assign(record, { providerInstructionDigest: db.putEvidence("handoff", Buffer.from(turn.providerInstruction)), providerInstructionBytes: Buffer.byteLength(turn.providerInstruction), providerInstructionAvailability: "captured" });
|
|
381
393
|
}
|
|
382
|
-
record.eventEvidenceDigest = db.putEvidence("qa", Buffer.from(canonicalEvidence(events)));
|
|
383
|
-
|
|
384
|
-
record.
|
|
394
|
+
record.eventEvidenceDigest = db.putEvidence("qa", Buffer.from(canonicalEvidence(events.metadata())));
|
|
395
|
+
db.recordCommunicationEvidenceRefs(record.turnRecordId, [record.rawResponseDigest, record.cleanedResponseDigest, record.eventEvidenceDigest].filter((digest) => Boolean(digest)));
|
|
396
|
+
record.toolCount = events.toolCount;
|
|
397
|
+
record.terminalCount = events.terminalCount;
|
|
385
398
|
db.recordQaDeliveryTurn(record);
|
|
386
399
|
this.faults.afterResponseStored?.();
|
|
387
400
|
const validationStarted = performance.now();
|
|
388
401
|
const errors = error ? [error] : [];
|
|
402
|
+
if (turn && !turn.isError && !turn.failure) {
|
|
403
|
+
try {
|
|
404
|
+
db.replayStructuredDispatch(record.turnRecordId, remediationOutput);
|
|
405
|
+
}
|
|
406
|
+
catch (caught) {
|
|
407
|
+
errors.push(`Persisted remediation receipt rejected: ${String(caught)}`);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
389
410
|
try {
|
|
390
411
|
record.observedSession = validateBuilderSession(builder, input);
|
|
391
412
|
if (stableBuilderSessionIdentity(record.observedSession) !== stableBuilderSessionIdentity(record.intendedSession))
|
|
@@ -395,10 +416,13 @@ export class QaFailureDeliveryService {
|
|
|
395
416
|
errors.push(String(caught));
|
|
396
417
|
}
|
|
397
418
|
if (turn) {
|
|
398
|
-
if (turn.
|
|
419
|
+
if (turn.failure || turn.isError && turn.structured?.state !== "rejected")
|
|
399
420
|
errors.push("Builder provider returned an error or failure metadata");
|
|
400
421
|
if (!turn.turnId)
|
|
401
422
|
errors.push("Builder completed without a provider turn identity");
|
|
423
|
+
const expectedWire = providerWireContract(remediationOutput, record.intendedSession.provider);
|
|
424
|
+
if (turn.structured?.state === "candidate" && (turn.structured.dispatchId !== record.turnRecordId || turn.structured.sessionId !== record.intendedSession.sessionId || turn.structured.profile !== expectedWire.profile || turn.structured.wireContractDigest !== expectedWire.descriptorDigest || turn.structured.wireSchemaDigest !== expectedWire.wireSchemaDigest || turn.structured.wireVariant !== expectedWire.variant))
|
|
425
|
+
errors.push("Native remediation receipt has a foreign dispatch or provider session");
|
|
402
426
|
const meta = turn.providerMetadata;
|
|
403
427
|
try {
|
|
404
428
|
if (!meta?.sessionRef || meta.provider !== record.intendedSession.provider || meta.sessionId !== record.intendedSession.sessionId || stableBuilderSessionIdentity(meta.sessionRef) !== stableBuilderSessionIdentity(record.intendedSession))
|
|
@@ -416,7 +440,7 @@ export class QaFailureDeliveryService {
|
|
|
416
440
|
catch (caught) {
|
|
417
441
|
errors.push(`Terminal event scope is inaccessible: ${String(caught)}`);
|
|
418
442
|
}
|
|
419
|
-
if (terminal.length !== 1 || terminal[0]?.turnId !== turn.turnId || terminal[0]?.result.turnId !== turn.turnId || terminal[0]?.result.isError || terminal[0]?.result.failure || (terminal[0]
|
|
443
|
+
if (events.terminalOverflow || events.terminalCount !== 1 || terminal.length !== 1 || terminal[0]?.turnId !== turn.turnId || terminal[0]?.result.turnId !== turn.turnId || terminal[0]?.result.isError || terminal[0]?.result.failure || (!terminal[0] || receiptFingerprint(terminal[0].result) !== receiptFingerprint(turn)) || (!terminal[0] || rawFingerprint(terminal[0].result) !== rawFingerprint(turn)))
|
|
420
444
|
errors.push("Builder completion is missing, duplicated, or uncorrelated");
|
|
421
445
|
}
|
|
422
446
|
const captureStart = performance.now();
|
|
@@ -426,33 +450,28 @@ export class QaFailureDeliveryService {
|
|
|
426
450
|
}
|
|
427
451
|
catch (caught) {
|
|
428
452
|
record.sourceCapture = "unavailable";
|
|
429
|
-
record.sourceCaptureError = String(caught);
|
|
453
|
+
record.sourceCaptureError = boundedEvidenceUtf8(String(caught), 4096).toString();
|
|
430
454
|
errors.push(`Post-dispatch source capture failed: ${String(caught)}`);
|
|
431
455
|
}
|
|
432
456
|
record.captureElapsedMs = performance.now() - captureStart;
|
|
433
|
-
if (record.kind === "response-repair") {
|
|
434
|
-
record.responseOnlyViolations = record.toolCount ? ["Builder used tools during response correction"] : [];
|
|
435
|
-
record.responseOnlySourceChanged = Boolean(record.postSourceDigest && record.postSourceDigest !== responseOnlySourceDigest);
|
|
436
|
-
if (record.responseOnlySourceChanged)
|
|
437
|
-
record.responseOnlyViolations.push(`source changed during response correction (${responseOnlySourceDigest} -> ${record.postSourceDigest})`);
|
|
438
|
-
}
|
|
439
457
|
let graphRequest = false;
|
|
458
|
+
const parserErrors = [];
|
|
440
459
|
try {
|
|
441
|
-
graphRequest = Boolean(turn &&
|
|
460
|
+
graphRequest = Boolean(turn && operationResponse(turn, remediationOutput).result.kind === "graph-request");
|
|
442
461
|
}
|
|
443
462
|
catch (caught) {
|
|
444
|
-
|
|
463
|
+
parserErrors.push(String(caught));
|
|
445
464
|
}
|
|
446
|
-
const contract = turn && !graphRequest ?
|
|
447
|
-
record.parserErrors = [...(contract?.errors ?? []), ...(turn?.continuityErrors ?? [])];
|
|
448
|
-
record.validationErrors = errors;
|
|
465
|
+
const contract = turn && !graphRequest ? remediationStructuredResponse(turn, { 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: selectRemediationChunk(input) }) }) : undefined;
|
|
466
|
+
record.parserErrors = [...parserErrors, ...(contract?.errors ?? []), ...(turn?.continuityErrors ?? [])].slice(0, 32).map(detail => boundedEvidenceUtf8(detail, 1024).toString());
|
|
467
|
+
record.validationErrors = errors.slice(0, 32).map(detail => boundedEvidenceUtf8(detail, 1024).toString());
|
|
449
468
|
if (contract?.report)
|
|
450
469
|
record.parsedResponseDigest = db.putEvidence("qa", Buffer.from(canonicalEvidence(contract.report)));
|
|
451
|
-
record.status = errors.length ? "delivery-uncertain" : "completed";
|
|
470
|
+
record.status = errors.length || !turn || turn.isError || turn.failure ? "delivery-uncertain" : "completed";
|
|
452
471
|
record.completedAt = new Date().toISOString();
|
|
453
472
|
record.validationElapsedMs = Math.max(0, performance.now() - validationStarted - record.captureElapsedMs);
|
|
454
473
|
db.recordQaDeliveryTurn(record);
|
|
455
|
-
return { turn, record, contract, detail: errors.length ? errors.join("; ")
|
|
474
|
+
return { turn, record, contract, detail: errors.length ? boundedEvidenceUtf8(errors.join("; "), 4096).toString() : undefined };
|
|
456
475
|
}
|
|
457
476
|
finally {
|
|
458
477
|
db.close();
|
|
@@ -461,10 +480,6 @@ export class QaFailureDeliveryService {
|
|
|
461
480
|
classify(result) {
|
|
462
481
|
if (result.record.validationErrors?.length)
|
|
463
482
|
return "delivery-uncertain";
|
|
464
|
-
if (result.record.responseOnlySourceChanged)
|
|
465
|
-
return "source-drift";
|
|
466
|
-
if (result.record.responseOnlyViolations?.length)
|
|
467
|
-
return "response-invalid";
|
|
468
483
|
if (result.contract?.valid && result.contract.status === "blocked")
|
|
469
484
|
return "blocked";
|
|
470
485
|
if (result.contract?.valid && result.contract.status === "needs_input")
|
|
@@ -654,11 +669,8 @@ function responseInstructions(handoff) {
|
|
|
654
669
|
`Required finding identities for this remediation chunk: ${JSON.stringify(handoff.report.findingRefs.map(f => ({ finding_key: f.findingKey, raw_id: f.rawId })))}`,
|
|
655
670
|
"Address, dispute with evidence, or mark blocked every finding in this remediation chunk. Do not address findings outside the chunk. A Builder claim never approves QA findings.",
|
|
656
671
|
"This is only a bounded remediation chunk. It does not approve the ticket. Any source change invalidates this chunk's source binding, and Rafi must perform a fresh full independent QA review afterward.",
|
|
657
|
-
"
|
|
658
|
-
"
|
|
659
|
-
`${BUILDER_QA_REMEDIATION_START}\n${JSON.stringify({ version: 3, handoff_id: handoff.handoffId, summary: "Truthful result", findings: handoff.report.findingRefs.map(f => ({ finding_key: f.findingKey, raw_id: f.rawId, disposition: "disputed", changes: ["Explain actual changes or why none were needed"], evidence: "Actual evidence", verification: [{ check: "Actual verification", outcome: "not_run", evidence: "Actual reason" }] })), observations: [] })}\n${BUILDER_QA_REMEDIATION_END}\nSTEP_STATUS: done | summary="short truthful summary"`,
|
|
660
|
-
'If blocked, use STEP_STATUS: blocked | reason="actual blocker and recovery needed". A V3 envelope with complete partial fixed/disputed/blocked coverage is allowed; blocked findings require category, reason, recovery, capability, evidence. Without partial results, the blocked status alone is valid.',
|
|
661
|
-
'If a decision is required, use STEP_STATUS: needs_input | question="actual unanswered question" without fabricating an envelope or answer.',
|
|
672
|
+
"Return one native builder-remediation response. The completed payload contains status done or blocked, a reason (null for done), the complete V3 report, and available coverage claims. Keep continuity in the same response object.",
|
|
673
|
+
"A report must cover every finding exactly once. Partial fixed/disputed/blocked coverage is valid only with status blocked, a truthful reason, and a structured blocker on each blocked finding. If no partial report is available, use the outer blocked result. Questions use the needs-input alternative.",
|
|
662
674
|
`Authoritative V3 schema: ${JSON.stringify(builderQaRemediationReportV3Schema)}`,
|
|
663
675
|
].join("\n\n");
|
|
664
676
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export type QaHostTransformationRule = "assessment-host-bindings-v1" | "depth-host-bindings-v1" | "draft-host-bindings-v1";
|
|
2
|
+
export interface QaHostTransformationV1 {
|
|
3
|
+
version: 1;
|
|
4
|
+
ruleVersion: QaHostTransformationRule;
|
|
5
|
+
operation: string;
|
|
6
|
+
tupleDigest: string;
|
|
7
|
+
inputDigest: string;
|
|
8
|
+
outputDigest: string;
|
|
9
|
+
authorityClass: "host-binding-and-observation";
|
|
10
|
+
justification: string;
|
|
11
|
+
effects: Array<{
|
|
12
|
+
path: string;
|
|
13
|
+
beforeDigest: string;
|
|
14
|
+
afterDigest: string;
|
|
15
|
+
}>;
|
|
16
|
+
}
|
|
17
|
+
/** Records only approved host effects; provider/user requirements remain immutable. */
|
|
18
|
+
export declare function qaHostTransformation(input: {
|
|
19
|
+
rule: QaHostTransformationRule;
|
|
20
|
+
operation: string;
|
|
21
|
+
tupleDigest: string;
|
|
22
|
+
before: unknown;
|
|
23
|
+
after: unknown;
|
|
24
|
+
workspacePaths?: Record<string, string>;
|
|
25
|
+
}): QaHostTransformationV1;
|
|
26
|
+
export declare function verifyQaHostTransformation(record: QaHostTransformationV1, before: unknown, after: unknown, workspacePaths?: Record<string, string>): void;
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { canonicalJson, structuredDigest } from "rafi-spec";
|
|
2
|
+
const hashValue = (value, present) => structuredDigest({ present, ...(present ? { value } : {}) });
|
|
3
|
+
/** Records only approved host effects; provider/user requirements remain immutable. */
|
|
4
|
+
export function qaHostTransformation(input) {
|
|
5
|
+
if (!["assessment-host-bindings-v1", "depth-host-bindings-v1", "draft-host-bindings-v1"].includes(input.rule))
|
|
6
|
+
throw new Error("Unknown QA host transformation rule");
|
|
7
|
+
if (!/^[a-f0-9]{64}$/.test(input.tupleDigest) || !input.operation)
|
|
8
|
+
throw new Error("QA transformation requires native tuple provenance");
|
|
9
|
+
const effects = [];
|
|
10
|
+
const original = input.before, transformed = input.after;
|
|
11
|
+
const ids = new Map();
|
|
12
|
+
if (input.rule === "draft-host-bindings-v1") {
|
|
13
|
+
if (transformed.checks.length < original.checks.length)
|
|
14
|
+
throw new Error("QA transformation cannot remove provider checks");
|
|
15
|
+
original.checks.forEach((check, index) => ids.set(check.id, transformed.checks[index].id));
|
|
16
|
+
}
|
|
17
|
+
const labelIds = [...ids.keys()].filter(id => id.startsWith("check-")).sort((a, b) => b.length - a.length);
|
|
18
|
+
const labelPattern = labelIds.length ? new RegExp(`(^|[^A-Za-z0-9_-])(${labelIds.map(id => id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|")})(?![A-Za-z0-9_-])`, "g") : undefined;
|
|
19
|
+
const relabel = (value) => typeof value === "string" && labelPattern
|
|
20
|
+
? value.replace(labelPattern, (_match, prefix, id) => `${prefix}${ids.get(id)}`) : value;
|
|
21
|
+
const walk = (before, after, path, beforePresent = true, afterPresent = true) => {
|
|
22
|
+
if (beforePresent === afterPresent && canonicalJson(before ?? null) === canonicalJson(after ?? null))
|
|
23
|
+
return;
|
|
24
|
+
if (input.rule === "draft-host-bindings-v1" && path === "baseline" && Array.isArray(before) && Array.isArray(after)) {
|
|
25
|
+
for (const prior of before)
|
|
26
|
+
if (!after.some(row => row.id === prior.id))
|
|
27
|
+
throw new Error("QA transformation cannot remove provider observations");
|
|
28
|
+
for (const [index, row] of after.entries()) {
|
|
29
|
+
const prior = before.find(item => item.id === row.id);
|
|
30
|
+
walk(prior, row, `baseline.${index}`, prior !== undefined, true);
|
|
31
|
+
}
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (before && after && typeof before === "object" && typeof after === "object" && Array.isArray(before) === Array.isArray(after)) {
|
|
35
|
+
for (const key of [...new Set([...Object.keys(before), ...Object.keys(after)])].sort())
|
|
36
|
+
walk(before[key], after[key], path ? `${path}.${key}` : key, Object.hasOwn(before, key), Object.hasOwn(after, key));
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
const assessment = /^(draftPayloadDigest|inventoryDigest|policyVersion|authorSessionId|sessionId|operationId)$/.test(path);
|
|
40
|
+
const depth = /^(plannerOperationId|plannerIdentity|minimumReasons(?:\.\d+)?)$/.test(path);
|
|
41
|
+
let draft = false;
|
|
42
|
+
if (input.rule === "draft-host-bindings-v1") {
|
|
43
|
+
const check = /^checks\.(\d+)\.(.+)$/.exec(path);
|
|
44
|
+
const originalCheck = check && original.checks[Number(check[1])];
|
|
45
|
+
const idReference = /^(checks\.\d+\.(id|dependsOnChecks\.\d+)|coverage\.\d+\.checkIds\.\d+|unresolved\.\d+\.checkIds\.\d+)$/.test(path);
|
|
46
|
+
draft = idReference && ids.get(before) === after;
|
|
47
|
+
if (check?.[2] === "obligation" && originalCheck && after === "mandatory") {
|
|
48
|
+
draft = original.requirements.some((requirement) => requirement.obligation === "mandatory" &&
|
|
49
|
+
original.coverage.some((row) => row.requirementId === requirement.id && row.checkIds.includes(originalCheck.id)));
|
|
50
|
+
}
|
|
51
|
+
if (check && /^requirementRefs\.\d+$/.test(check[2]) && !beforePresent && originalCheck) {
|
|
52
|
+
draft = original.coverage.some((row) => row.requirementId === after && row.checkIds.includes(originalCheck.id));
|
|
53
|
+
}
|
|
54
|
+
if (/^checks\.\d+\.hostKind$/.test(path) && !beforePresent)
|
|
55
|
+
draft = after === "source-scope" || after === "graph-maintenance";
|
|
56
|
+
if (/^checks\.\d+$/.test(path) && !beforePresent)
|
|
57
|
+
draft = after && (after.hostKind === "source-scope" || after.hostKind === "graph-maintenance") &&
|
|
58
|
+
after.origin === "invariant" && after.obligation === "mandatory" && after.requirementRefs.every((id) => original.requirements.some((requirement) => requirement.id === id));
|
|
59
|
+
if (/^baseline\.\d+$/.test(path) && !beforePresent)
|
|
60
|
+
draft = after && /^[a-f0-9]{64}$/.test(after.evidenceDigest) && typeof after.observedAt === "string";
|
|
61
|
+
if (/^baseline\.\d+\.(sourceDigest|observedAt|phase|evidenceDigest)$/.test(path))
|
|
62
|
+
draft = true;
|
|
63
|
+
if (/^baseline\.\d+\.checkId$/.test(path)) {
|
|
64
|
+
const row = transformed.baseline[Number(path.split(".")[1])];
|
|
65
|
+
const checks = transformed.checks.filter((item) => item.prerequisiteRefs.includes(row.id));
|
|
66
|
+
draft = ids.get(before) === after || checks.length === 1 && checks[0].id === after;
|
|
67
|
+
}
|
|
68
|
+
if (/^preparationEvidence\.\d+\.(operationId|sessionId|graphReceiptRefs(?:\.\d+)?|evidenceDigest)$/.test(path) ||
|
|
69
|
+
/^preparationEvidence\.\d+\.references\.\d+\.digest$/.test(path))
|
|
70
|
+
draft = true;
|
|
71
|
+
if (/^(checks\.\d+\.verification\.\d+\.cwd|preparationEvidence\.\d+\.references\.\d+\.path)$/.test(path))
|
|
72
|
+
draft = typeof before === "string" && input.workspacePaths?.[before] === after;
|
|
73
|
+
// The only permitted prose edit substitutes exact provider check identifiers.
|
|
74
|
+
if (/^(checks\.\d+\.(expectedBehavior|expectedEvidence\.\d+|expectedBaseline|applicability\.(predicate|requiredEvidence\.\d+)|verification\.\d+\.(expectedOutcome|steps\.\d+))|baseline\.\d+\.(procedure|relevance|expectedTransition|predicateEvidence\.\d+)|preparationEvidence\.\d+\.analysis|unresolved\.\d+\.(reason|nextAction))$/.test(path) &&
|
|
75
|
+
typeof before === "string" && before !== after && relabel(before) === after)
|
|
76
|
+
draft = true;
|
|
77
|
+
}
|
|
78
|
+
if (!(input.rule === "assessment-host-bindings-v1" ? assessment : input.rule === "depth-host-bindings-v1" ? depth : draft))
|
|
79
|
+
throw new Error(`QA host transformation cannot change provider requirements or semantic fields: ${path}`);
|
|
80
|
+
effects.push({ path, beforeDigest: hashValue(before, beforePresent), afterDigest: hashValue(after, afterPresent) });
|
|
81
|
+
if (effects.length > 10000)
|
|
82
|
+
throw new Error("QA host transformation exceeds effect budget");
|
|
83
|
+
};
|
|
84
|
+
walk(input.before, input.after, "");
|
|
85
|
+
if (input.rule === "depth-host-bindings-v1") {
|
|
86
|
+
const before = input.before;
|
|
87
|
+
const after = input.after;
|
|
88
|
+
const expected = [...before.minimumReasons, ...before.riskFactors.map(risk => risk.category).filter(category => !before.minimumReasons.includes(category))];
|
|
89
|
+
if (canonicalJson(expected) !== canonicalJson(after.minimumReasons))
|
|
90
|
+
throw new Error("Depth labels must be exactly derivable from provider risk factors");
|
|
91
|
+
}
|
|
92
|
+
return { version: 1, ruleVersion: input.rule, operation: input.operation, tupleDigest: input.tupleDigest,
|
|
93
|
+
inputDigest: structuredDigest(input.before), outputDigest: structuredDigest(input.after), effects,
|
|
94
|
+
authorityClass: "host-binding-and-observation", justification: "Bind native dispatch identities and retained host observations; preserve provider/user requirements" };
|
|
95
|
+
}
|
|
96
|
+
export function verifyQaHostTransformation(record, before, after, workspacePaths) {
|
|
97
|
+
const expected = qaHostTransformation({ rule: record.ruleVersion, operation: record.operation, tupleDigest: record.tupleDigest, before, after, workspacePaths });
|
|
98
|
+
if (canonicalJson(expected) !== canonicalJson(record))
|
|
99
|
+
throw new Error("QA host transformation replay mismatch");
|
|
100
|
+
}
|
package/dist/qaPreparation.d.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
|
+
import { type OutputContract, type SelectedDispatchTupleV2, type StructuredResponse } from "rafi-spec";
|
|
1
2
|
import type { QaContractCandidateV1, QaSemanticAssessmentV1, RequirementRef, QaVerificationContractV1 } from "rafi-spec";
|
|
2
3
|
import type { QaPreparationStore, LogicalPreparationBudget } from "./qaPreparationStore.js";
|
|
4
|
+
/** Compatibility re-exports; canonical contract ownership is rafi-spec. */
|
|
5
|
+
export { draftOutput, assessmentOutput, challengeOutput, depthOutput } from "rafi-spec";
|
|
3
6
|
export interface PreparationProviderResult {
|
|
7
|
+
output?: OutputContract;
|
|
8
|
+
structured?: StructuredResponse;
|
|
9
|
+
selectedDispatch?: SelectedDispatchTupleV2;
|
|
4
10
|
operationId?: string;
|
|
5
11
|
text: string;
|
|
6
12
|
sessionId: string;
|
|
@@ -14,7 +20,7 @@ export interface PreparationDependencies {
|
|
|
14
20
|
/** Provider-specific wording is allowed; contract ownership is not. */
|
|
15
21
|
provider?: "claude" | "codex";
|
|
16
22
|
/** Each dispatch is a fresh, confined conversation, with a persisted reservation. */
|
|
17
|
-
dispatch(phase: "planner" | "prepare" | "assess" | "challenge" | "repair", instruction: string, operationId: string, remainingMs: number): Promise<PreparationProviderResult>;
|
|
23
|
+
dispatch(phase: "planner" | "prepare" | "assess" | "challenge" | "repair", instruction: string, operationId: string, remainingMs: number, output: OutputContract): Promise<PreparationProviderResult>;
|
|
18
24
|
inputsCurrent(candidate: QaContractCandidateV1): boolean;
|
|
19
25
|
normalizeDraft?(draft: QaContractCandidateV1, response: PreparationProviderResult): QaContractCandidateV1;
|
|
20
26
|
hostFacts?: string;
|
|
@@ -29,22 +35,11 @@ export type PreparationOutcome = {
|
|
|
29
35
|
};
|
|
30
36
|
export declare const FILE_SCOPE_GUIDANCE = "File lists in plans and tickets, including likely_files and generated file-only diff expectations, are estimates of implementation, not exhaustive boundaries. Different named files or an extra relevant file do not by themselves create a material conflict or block the build. Retain and assess every original requirement; for file-list checks verify that changes serve the approved work rather than requiring an exact file list or a pre-edit snapshot proving no other files changed. Rafi owns tracker lifecycle updates. Generated instructions to mark a ticket done in a document are bookkeeping, not a Builder acceptance criterion or a material conflict; do not require the Builder to perform them, and do not require the host to amend the plan before building. Do not disregard explicit user-established security, privacy, access, or other safety restrictions, or expand the approved product functionality. Report genuine substantive or safety conflicts.";
|
|
31
37
|
export declare const ROLLBACK_VERIFICATION_GUIDANCE = "When an original requirement explicitly asks to verify that rollback restores behavior, specify concrete future verification evidence rather than only inspecting rollback prose. A rollback plan alone does not require a rollback execution or mutation-testing rehearsal. For an npm test project where current uncommitted product changes represent the change to rehearse, the Builder may run rafi qa-rehearse-rollback alone from the project root; it runs npm test before and after reverting a synthetic commit of all uncommitted product changes in an independent temporary repository. It also runs git diff --exit-code against the captured pre-change HEAD after the revert and tests; success proves the tracked files match that baseline. For a requirement that a particular string be absent after rollback, inspecting its absence in the captured baseline plus that successful whole-tree comparison is concrete evidence of absence in the reverted tracked file. The rehearsal cannot use installed dependencies or symbolic links and does not prove ticket isolation or that an actual existing ticket commit was reverted. Assess whether this evidence satisfies the original requirement; do not claim it does when the original explicitly requires the actual ticket commit. Do not assume copying files to external scratch, arbitrary scripts or mutation experiments are allowed. If actual rollback execution is explicitly required but no permitted method satisfies it, report the limitation. Never perform destructive rollback in the source workspace or claim a rehearsal has already happened.";
|
|
32
|
-
export declare const EXECUTABLE_CHECK_GUIDANCE = "Choose verification procedures compatible with the Builder's project permissions and workspace. Documentation is disabled unless the host-bound contract documentation.required is true. When it is false, do not invent documentation checks merely because a docs directory or API-looking path exists; an absent documentation surface is not a failure. When it is true, use only the contract's exact targets and criteria, and define an actionable absent-target outcome. Do not invent mandatory inline node -e or node --eval commands: the Builder policy may refuse them. For a JavaScript behavior or export check, prefer a project-local test file executed with the project's existing test command, or a permitted node --test invocation. Do not invent a mandatory test-first RED phase or other historical sequence merely because a plan requires tests before and after implementation: a passing baseline test is not a failing new test. When an original requirement actually requires a historical sequence, preserve it and specify concrete independently reviewable evidence paths or retained host events for each required step; a generic statement that sequencing evidence exists is not actionable, and a later passing test does not prove past events. If the project policy makes raw stdout redirection or scratch-log creation unavailable, do not turn log-file capture into a blocker: use the retained bounded tool output and host events instead, and never cite a scratch path unless that exact file was successfully created and inspected. The independent QA snapshot may have a detached HEAD even when the Builder source workspace is on a branch; do not use its branch name to infer the Builder source branch. Diff and scope checks must compare the current snapshot with the captured pre-implementation baseline, not blindly with repository HEAD: previously approved or completed ticket changes are part of that baseline and must not be reported as new work. A scope check must first enumerate every changed path outside the explicit bookkeeping/evidence exclusions, then compare that complete host-observed set with the approved product/ticket scope; do not pre-filter the list to expected filenames, because that would hide accidental extra product edits. Bounded evidence-only paths such as scratch/ are not product scope and must be excluded from product-path scope checks while their retained evidence remains subject to QA review. For a current-branch multi-ticket build, do not invent a mandatory branch-alone execution check merely to prove ticket independence; use baseline-aware source/test inspection and the permitted project tests unless the original requirement explicitly requires an actual isolated execution. For additive tickets in a current-branch multi-ticket build, \u201Cacceptable on its own\u201D is satisfied by showing that each ticket's own tests import and exercise only its own feature, pass with the shared baseline and required project tests, and do not depend on the sibling feature; do not require removing another additive feature or perform a destructive rollback experiment unless the approved requirement explicitly requires absence testing or an isolated branch. Preserve explicitly requested procedures and substantive requirements; if a required procedure cannot be run, identify the limitation rather than claiming it was executed. Independently assess whether proposed evidence actually tests the required behavior.";
|
|
38
|
+
export declare const EXECUTABLE_CHECK_GUIDANCE = "Choose verification procedures compatible with the Builder's project permissions and workspace. Keep applicability and verification instructions consistent: an unconditional check must always be reported applicable, including a check whose predicate confirms an absent feature, disabled documentation, or an unrequested skill. For such a check require passed evidence of the absence, never an instruction to report the check not-applicable. If execution itself depends on a condition, encode conditional applicability with a concrete predicate and evidence. The independent assessor must flag contradictory applicability instructions before readiness. Documentation is disabled unless the host-bound contract documentation.required is true. When it is false, do not invent documentation checks merely because a docs directory or API-looking path exists; an absent documentation surface is not a failure. When it is true, use only the contract's exact targets and criteria, and define an actionable absent-target outcome. Do not invent mandatory inline node -e or node --eval commands: the Builder policy may refuse them. For a JavaScript behavior or export check, prefer a project-local test file executed with the project's existing test command, or a permitted node --test invocation. Do not invent a mandatory test-first RED phase or other historical sequence merely because a plan requires tests before and after implementation: a passing baseline test is not a failing new test. When an original requirement actually requires a historical sequence, preserve it and specify concrete independently reviewable evidence paths or retained host events for each required step; a generic statement that sequencing evidence exists is not actionable, and a later passing test does not prove past events. If the project policy makes raw stdout redirection or scratch-log creation unavailable, do not turn log-file capture into a blocker: use the retained bounded tool output and host events instead, and never cite a scratch path unless that exact file was successfully created and inspected. The independent QA snapshot may have a detached HEAD even when the Builder source workspace is on a branch; do not use its branch name to infer the Builder source branch. Diff and scope checks must compare the current snapshot with the captured pre-implementation baseline, not blindly with repository HEAD: previously approved or completed ticket changes are part of that baseline and must not be reported as new work. A scope check must first enumerate every changed path outside the explicit bookkeeping/evidence exclusions, then compare that complete host-observed set with the approved product/ticket scope; do not pre-filter the list to expected filenames, because that would hide accidental extra product edits. Bounded evidence-only paths such as scratch/ are not product scope and must be excluded from product-path scope checks while their retained evidence remains subject to QA review. For a current-branch multi-ticket build, do not invent a mandatory branch-alone execution check merely to prove ticket independence; use baseline-aware source/test inspection and the permitted project tests unless the original requirement explicitly requires an actual isolated execution. For additive tickets in a current-branch multi-ticket build, \u201Cacceptable on its own\u201D is satisfied by showing that each ticket's own tests import and exercise only its own feature, pass with the shared baseline and required project tests, and do not depend on the sibling feature; do not require removing another additive feature or perform a destructive rollback experiment unless the approved requirement explicitly requires absence testing or an isolated branch. Preserve explicitly requested procedures and substantive requirements; if a required procedure cannot be run, identify the limitation rather than claiming it was executed. Independently assess whether proposed evidence actually tests the required behavior.";
|
|
33
39
|
/** Baseline bookkeeping must not block ordinary approved implementation work. */
|
|
34
40
|
export declare const APPROVED_PATH_BASELINE_GUIDANCE = "Baseline membership is host-owned bookkeeping, not a Builder requirement. An approved product path, export, or test file is not a material QA concern merely because the provider cannot prove whether it was present before implementation. Do not reconstruct or disclose the full baseline, and do not turn that uncertainty into an unresolved concern. Rafi performs the exact host comparison. Do not author a provider-executed check that asks for Rafi's host postimplementation path manifest, complete changed-path inventory, or unchanged-file comparison; that host-owned comparison is already performed outside the provider snapshot and must be represented by the retained host observation, not by raw manifest disclosure or a provider check. For current-branch additive tickets, a provider-authored or inferred request to test the branch with an already-completed sibling feature removed is not an applicable requirement when the approved scope says not to remove or reconstruct that sibling; verify the ticket's own behavior and tests against the shared baseline instead. Treat sibling-absence testing as binding only when the user explicitly approved that absence requirement. Escalate only an actual host-observed out-of-scope, destructive, protected-file, or requirement-relevant change.";
|
|
35
41
|
/** Ignore known non-authoritative annotations; the original provider response remains retained. */
|
|
36
42
|
export declare function normalizeAssessmentMetadata<T extends QaSemanticAssessmentV1>(assessment: T): T;
|
|
37
|
-
/** Accept only an exact known obligation label when a provider wraps it in a
|
|
38
|
-
* descriptive object. This is a representation repair, not a host decision:
|
|
39
|
-
* unknown or ambiguous objects remain unchanged and fail schema validation. */
|
|
40
|
-
export declare function normalizeAssessmentObligations<T extends QaSemanticAssessmentV1>(assessment: T, allowed: string[]): T;
|
|
41
|
-
/** A preparation assessor must not block on the expected absence of future
|
|
42
|
-
* Builder work. Keep the concern for auditability, but make it non-material
|
|
43
|
-
* only when every referenced check is postimplementation and the concern is
|
|
44
|
-
* explicitly asking the Builder to implement/rerun that check. Concerns tied
|
|
45
|
-
* to preimplementation checks, missing procedures, or other semantic gaps
|
|
46
|
-
* remain blocking. */
|
|
47
|
-
export declare function normalizeExpectedFutureAssessment<T extends QaSemanticAssessmentV1>(assessment: T, candidate: QaContractCandidateV1): T;
|
|
48
43
|
/**
|
|
49
44
|
* A mandatory requirement must have at least one mandatory verification check.
|
|
50
45
|
* Providers occasionally return a concrete check with the weaker advisory
|
|
@@ -53,11 +48,4 @@ export declare function normalizeExpectedFutureAssessment<T extends QaSemanticAs
|
|
|
53
48
|
* enforce readiness and independent-QA requirements for the promoted check.
|
|
54
49
|
*/
|
|
55
50
|
export declare function enforceMandatoryCoverage(candidate: QaContractCandidateV1): QaContractCandidateV1;
|
|
56
|
-
/**
|
|
57
|
-
* Codex occasionally appends a made-up requirement ID to an otherwise valid
|
|
58
|
-
* check. Remove only those extraneous annotations when the check still has an
|
|
59
|
-
* exact authoritative reference. Never invent or remap an ID, and leave a
|
|
60
|
-
* check with no valid references untouched so normal validation fails closed.
|
|
61
|
-
*/
|
|
62
|
-
export declare function removeExtraneousRequirementReferences(candidate: QaContractCandidateV1, inventory: RequirementRef[]): QaContractCandidateV1;
|
|
63
51
|
export declare function prepareVerificationContract(initial: QaContractCandidateV1, inventory: RequirementRef[], deps: PreparationDependencies): Promise<PreparationOutcome>;
|