ai-foreman 1.7.26 → 1.7.28
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/dist/adapters/codex.d.ts +12 -0
- package/dist/adapters/codex.js +50 -1
- package/dist/adapters/recovering.d.ts +4 -0
- package/dist/adapters/recovering.js +10 -0
- package/dist/adapters/sessionFailure.d.ts +6 -0
- package/dist/adapters/sessionFailure.js +19 -0
- package/dist/adapters/types.d.ts +6 -0
- package/dist/branch/currentGuard.d.ts +8 -0
- package/dist/branch/currentGuard.js +5 -0
- package/dist/branch/runner.js +6 -2
- package/dist/buildApproval.js +2 -1
- package/dist/buildRuns.d.ts +12 -0
- package/dist/buildRuns.js +50 -0
- package/dist/buildWorkContext.js +1 -1
- package/dist/buildWorkMigration.d.ts +10 -0
- package/dist/buildWorkMigration.js +18 -2
- package/dist/cli/start.d.ts +10 -1
- package/dist/cli/start.js +186 -31
- package/dist/cli/state.js +39 -1
- package/dist/cli/tickets.d.ts +37 -0
- package/dist/cli/tickets.js +367 -12
- package/dist/config.js +14 -4
- package/dist/continuity.d.ts +1 -0
- package/dist/continuity.js +5 -0
- package/dist/foreman.js +89 -8
- package/dist/graph/preparation.js +6 -3
- package/dist/graph/semantic.js +2 -1
- package/dist/graph/turn.js +8 -1
- package/dist/handoffProtocol.js +2 -1
- package/dist/handoffs.js +7 -19
- package/dist/hostQaComparison.d.ts +50 -0
- package/dist/hostQaComparison.js +195 -0
- package/dist/log.d.ts +1 -1
- package/dist/managerPacket.js +1 -1
- package/dist/observability.d.ts +2 -0
- package/dist/observability.js +2 -1
- package/dist/permissions/policy.js +32 -0
- package/dist/planChanges.js +1 -1
- package/dist/projectDiagnostics.js +30 -0
- package/dist/providerQuestions.js +4 -24
- package/dist/qaBuildGate.d.ts +6 -1
- package/dist/qaBuildGate.js +76 -16
- package/dist/qaBuilderCoverage.d.ts +2 -1
- package/dist/qaBuilderCoverage.js +58 -7
- package/dist/qaBuilderEvidence.d.ts +18 -0
- package/dist/qaBuilderEvidence.js +60 -0
- package/dist/qaContractCoverage.d.ts +12 -1
- package/dist/qaContractCoverage.js +34 -2
- package/dist/qaContractDelivery.d.ts +2 -2
- package/dist/qaContractDelivery.js +88 -10
- package/dist/qaEffectiveConfig.js +2 -1
- package/dist/qaFailureDelivery.d.ts +23 -0
- package/dist/qaFailureDelivery.js +87 -6
- package/dist/qaHostFacts.d.ts +2 -0
- package/dist/qaHostFacts.js +11 -0
- package/dist/qaPreparation.d.ts +38 -1
- package/dist/qaPreparation.js +372 -31
- package/dist/qaPreparationInputs.js +5 -1
- package/dist/qaPreparationPolicy.d.ts +2 -1
- package/dist/qaPreparationPolicy.js +39 -9
- package/dist/qaPreparationStore.d.ts +4 -0
- package/dist/qaPreparationStore.js +8 -0
- package/dist/qaRecovery.d.ts +7 -0
- package/dist/qaRecovery.js +73 -1
- package/dist/qaReview.js +58 -9
- package/dist/qaReviewEvidence.d.ts +6 -0
- package/dist/qaReviewEvidence.js +37 -0
- package/dist/qaRollbackRehearsal.d.ts +2 -0
- package/dist/qaRollbackRehearsal.js +78 -0
- package/dist/qaSnapshot.d.ts +3 -1
- package/dist/qaSnapshot.js +3 -2
- package/dist/qaVerificationContract.d.ts +16 -0
- package/dist/qaVerificationContract.js +183 -17
- package/dist/sources/sourceRegistry.js +16 -5
- package/dist/stateTransfer.d.ts +14 -0
- package/dist/stateTransfer.js +51 -0
- package/dist/statusReporter.d.ts +8 -0
- package/dist/statusReporter.js +5 -0
- package/dist/supervisedStart.js +23 -0
- package/dist/ticketPlanning.d.ts +373 -0
- package/dist/ticketPlanning.js +34 -3
- package/dist/ticketPopulation.d.ts +28 -3
- package/dist/ticketPopulation.js +127 -15
- package/dist/tickets/details.d.ts +2 -1
- package/dist/tickets/details.js +7 -3
- package/dist/tickets/eligibility.d.ts +6 -0
- package/dist/tickets/eligibility.js +12 -1
- package/dist/tickets/riskWaivers.d.ts +3 -0
- package/dist/tickets/riskWaivers.js +12 -0
- package/dist/tickets/stateDb.d.ts +10 -0
- package/dist/tickets/stateDb.js +23 -0
- package/dist/tickets/ticketLoader.d.ts +9 -0
- package/dist/tickets/ticketLoader.js +15 -2
- package/dist/tickets/ticketSchema.d.ts +14 -1
- package/dist/tickets/ticketSchema.js +2 -1
- package/dist/tickets/validate.js +11 -1
- package/dist/workflowDb.js +24 -3
- package/package.json +9 -4
package/dist/planChanges.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const MATERIAL_KEYS = new Set(["scope", "summary", "acceptance", "acceptance_criteria", "depends_on", "dependencies", "delivery", "delivery_strategy", "branch", "branch_mode", "completion", "completion_mode"]);
|
|
1
|
+
const MATERIAL_KEYS = new Set(["scope", "summary", "acceptance", "acceptance_criteria", "documentation", "depends_on", "dependencies", "delivery", "delivery_strategy", "branch", "branch_mode", "completion", "completion_mode"]);
|
|
2
2
|
const BOOKKEEPING_KEYS = new Set(["status", "evidence", "updated_at", "updatedAt", "timestamp", "notes"]);
|
|
3
3
|
/** Structural classification; agent prose is intentionally ignored. */
|
|
4
4
|
export function classifyPlanChange(before, after) {
|
|
@@ -5,6 +5,8 @@ import { execFileSync } from "node:child_process";
|
|
|
5
5
|
import { readInterviewRecords } from "./interviews.js";
|
|
6
6
|
import { cmdQueue, cmdValidate } from "./tickets/commands.js";
|
|
7
7
|
import { getAllTicketDetails } from "./tickets/details.js";
|
|
8
|
+
import { loadRafiConfigObject } from "./tickets/setupConfig.js";
|
|
9
|
+
import { resolveQaPreparationConfig } from "./qaPreparationPolicy.js";
|
|
8
10
|
function collectProjectState(root) {
|
|
9
11
|
const state = { configPresent: existsSync(join(root, "rafi-config.yaml")), dirtyPaths: [], planningSessions: [], errors: [] };
|
|
10
12
|
const sessions = readInterviewRecords(root);
|
|
@@ -22,6 +24,21 @@ function collectProjectState(root) {
|
|
|
22
24
|
state.errors.push(String(error));
|
|
23
25
|
}
|
|
24
26
|
}
|
|
27
|
+
if (state.configPresent) {
|
|
28
|
+
try {
|
|
29
|
+
const config = loadRafiConfigObject(root);
|
|
30
|
+
const legacy = Boolean(config && config.qa_preparation && typeof config.qa_preparation === "object" && !Array.isArray(config.qa_preparation));
|
|
31
|
+
const policy = resolveQaPreparationConfig(config?.qa_preparation, config?.qa_preparation_shadow === true, config?.qa_preparation_wall_time_ms);
|
|
32
|
+
state.qaPreparation = {
|
|
33
|
+
mode: policy.mode,
|
|
34
|
+
configuration: legacy ? "legacy" : "current",
|
|
35
|
+
detail: legacy ? "Legacy QA preparation configuration will be rewritten to public booleans by `rafi compile <project>`." : policy.mode === "legacy" ? "QA preparation is disabled; enable qa_preparation: true to require QA requirements before Builder work." : policy.mode === "shadow" ? "QA preparation is collecting non-blocking rollout evidence." : "QA preparation is enabled and requires a ticket contract before Builder work.",
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
state.qaPreparation = { mode: "legacy", configuration: "invalid", detail: `QA preparation configuration is invalid: ${error instanceof Error ? error.message : String(error)}` };
|
|
40
|
+
}
|
|
41
|
+
}
|
|
25
42
|
return state;
|
|
26
43
|
}
|
|
27
44
|
import { BUILD_LEASE_STALE_MS, readBuildRuns } from "./buildRuns.js";
|
|
@@ -67,6 +84,19 @@ export function collectManagerProjectDiagnostics(projectDir, options = {}) {
|
|
|
67
84
|
const currentFocus = options.currentFocusRunId ?? initialFocus;
|
|
68
85
|
if (currentFocus && !ids.has(currentFocus))
|
|
69
86
|
throw new Error(`build run not found: ${currentFocus}`);
|
|
87
|
+
if (projectState.qaPreparation && currentFocus) {
|
|
88
|
+
const preparation = workflow.qaPreparation(currentFocus);
|
|
89
|
+
const states = preparation.works.reduce((counts, work) => {
|
|
90
|
+
if (work.state === "ready")
|
|
91
|
+
counts.ready++;
|
|
92
|
+
else if (["blocked", "incomplete", "uncertain"].includes(work.state))
|
|
93
|
+
counts.blocked++;
|
|
94
|
+
else
|
|
95
|
+
counts.pending++;
|
|
96
|
+
return counts;
|
|
97
|
+
}, { ready: 0, pending: 0, blocked: 0 });
|
|
98
|
+
projectState.qaPreparation = { ...projectState.qaPreparation, mode: preparation.mode, runId: currentFocus, contracts: states };
|
|
99
|
+
}
|
|
70
100
|
const summaries = [...ids].map(runId => mergeSummary({ runId, build: buildById.get(runId), stored: storedById.get(runId), rollup: rollupById.get(runId), workflow: workflowById.get(runId), activeRunId, leaseRunId: lease?.runId, now, schemaVersion: observability.schemaVersion() }))
|
|
71
101
|
.sort((a, b) => b.updatedAt.localeCompare(a.updatedAt) || b.createdAt.localeCompare(a.createdAt) || a.runId.localeCompare(b.runId));
|
|
72
102
|
const detailIds = [...new Set([currentFocus, activeRunId, ...(options.referencedRunIds ?? [])].filter((id) => Boolean(id && buildById.has(id))))].slice(0, 5);
|
|
@@ -2,6 +2,7 @@ import { buildScopeRevision } from "./buildApproval.js";
|
|
|
2
2
|
import { durableHumanDecision, HumanDecisionRequired } from "./humanDecision.js";
|
|
3
3
|
import { isCancel, multiselect, select, text } from "@clack/prompts";
|
|
4
4
|
import { currentActivity, pauseActivityForInput } from "./activity.js";
|
|
5
|
+
import { providerQuestionV1Schema } from "rafi-spec";
|
|
5
6
|
const ASK_USER_QUESTION_TOOL = "AskUserQuestion";
|
|
6
7
|
const CUSTOM_VALUE = "__rafi_custom_response__";
|
|
7
8
|
export const GRILL_ME_STOP_CHOICE = "Stop questions and make the plan now";
|
|
@@ -101,31 +102,10 @@ function parseQuestions(input) {
|
|
|
101
102
|
if (!Array.isArray(rawQuestions))
|
|
102
103
|
return [];
|
|
103
104
|
return rawQuestions.flatMap((raw) => {
|
|
104
|
-
|
|
105
|
+
const result = providerQuestionV1Schema.safeParse(raw);
|
|
106
|
+
if (!result.success)
|
|
105
107
|
return [];
|
|
106
|
-
|
|
107
|
-
const question = typeof record.question === "string" ? record.question.trim() : "";
|
|
108
|
-
if (!question)
|
|
109
|
-
return [];
|
|
110
|
-
const header = typeof record.header === "string" && record.header.trim()
|
|
111
|
-
? record.header.trim()
|
|
112
|
-
: undefined;
|
|
113
|
-
const options = Array.isArray(record.options)
|
|
114
|
-
? record.options.flatMap((option) => {
|
|
115
|
-
if (!option || typeof option !== "object" || Array.isArray(option))
|
|
116
|
-
return [];
|
|
117
|
-
const optionRecord = option;
|
|
118
|
-
const label = typeof optionRecord.label === "string" ? optionRecord.label.trim() : "";
|
|
119
|
-
if (!label)
|
|
120
|
-
return [];
|
|
121
|
-
return [{
|
|
122
|
-
label,
|
|
123
|
-
description: typeof optionRecord.description === "string" ? optionRecord.description : undefined,
|
|
124
|
-
preview: typeof optionRecord.preview === "string" ? optionRecord.preview : undefined,
|
|
125
|
-
}];
|
|
126
|
-
})
|
|
127
|
-
: [];
|
|
128
|
-
return [{ question, header, options, multiSelect: record.multiSelect === true }];
|
|
108
|
+
return [result.data];
|
|
129
109
|
});
|
|
130
110
|
}
|
|
131
111
|
async function askOneQuestion(question, prompts, signal) {
|
package/dist/qaBuildGate.d.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
-
import type { BuilderAdapter } from "./adapters/types.js";
|
|
1
|
+
import type { BuilderAdapter, TurnResult } from "./adapters/types.js";
|
|
2
2
|
import type { QaSessionHandle } from "./qaReview.js";
|
|
3
|
+
/** A received response missing only continuity metadata is a bounded format defect. */
|
|
4
|
+
export declare function preparationTurnPayload(turn: TurnResult): {
|
|
5
|
+
text: string;
|
|
6
|
+
formatErrors: string[];
|
|
7
|
+
};
|
|
3
8
|
/** Shared by ordinary, branch, synthetic, remediation and Manager followups. */
|
|
4
9
|
export declare function ensureBuilderContract(projectDir: string, runId: string, workId: string, workspace: string, builder: BuilderAdapter, qaFactory?: (cwd: string) => Promise<QaSessionHandle>): Promise<string>;
|
package/dist/qaBuildGate.js
CHANGED
|
@@ -14,13 +14,23 @@ import { WorkflowDb } from "./workflowDb.js";
|
|
|
14
14
|
import { createRoleBuilder, readOnlyPermissionConfig } from "./agentRun.js";
|
|
15
15
|
import { captureBuildSource, assertBuildAssignmentReconciled } from "./buildAssignment.js";
|
|
16
16
|
import { createDisposableQaSnapshotAsync } from "./qaSnapshot.js";
|
|
17
|
-
import { allocateStableCheckIds, authoritativeInventory, contractDigest, renderVerificationContract } from "./qaVerificationContract.js";
|
|
17
|
+
import { allocateStableCheckIds, authoritativeInventory, bindUniqueBaselineCheckIds, contractDigest, mapSnapshotWorkingDirectories, normalizeSnapshotPaths, renderVerificationContract, rewriteProviderHostScopeChecks } from "./qaVerificationContract.js";
|
|
18
18
|
import { resolveEffectiveQaConfiguration } from "./qaEffectiveConfig.js";
|
|
19
19
|
import { prepareVerificationContract } from "./qaPreparation.js";
|
|
20
|
+
import { qaHostGraphFact } from "./qaHostFacts.js";
|
|
20
21
|
import { actualContractSession, assertContractReceipt, deliverVerificationContract } from "./qaContractDelivery.js";
|
|
21
|
-
import {
|
|
22
|
+
import { BUILDER_WORK_COVERAGE_INSTRUCTIONS } from "./qaContractCoverage.js";
|
|
22
23
|
import { renderBuildWorkContext } from "./buildWorkContext.js";
|
|
23
24
|
import { compareContractInputs } from "./qaContractFreshness.js";
|
|
25
|
+
import { readBuildRuns } from "./buildRuns.js";
|
|
26
|
+
/** A received response missing only continuity metadata is a bounded format defect. */
|
|
27
|
+
export function preparationTurnPayload(turn) {
|
|
28
|
+
const formatErrors = turn.continuityErrors ?? [];
|
|
29
|
+
return {
|
|
30
|
+
text: formatErrors.length ? turn.rawResponse ?? turn.cleanedResponse ?? turn.text : turn.text,
|
|
31
|
+
formatErrors,
|
|
32
|
+
};
|
|
33
|
+
}
|
|
24
34
|
/** Shared by ordinary, branch, synthetic, remediation and Manager followups. */
|
|
25
35
|
export async function ensureBuilderContract(projectDir, runId, workId, workspace, builder, qaFactory) {
|
|
26
36
|
return withGraphDerivedAccess([], () => ensureBuilderContractWithProvenance(projectDir, runId, workId, workspace, builder, qaFactory));
|
|
@@ -33,8 +43,6 @@ async function ensureBuilderContractWithProvenance(projectDir, runId, workId, wo
|
|
|
33
43
|
const policy = store.policy(runId) ?? store.freezePolicy(runId, resolveQaPreparationConfig(undefined));
|
|
34
44
|
if (policy.mode === "legacy")
|
|
35
45
|
return "";
|
|
36
|
-
if (policy.mode === "enforce" && !builder.contractCapabilities?.().nativeCompactionBarrier)
|
|
37
|
-
throw new Error(`unsupported-capability: ${builder.agent} enforcement requires proven native-compaction delivery continuity`);
|
|
38
46
|
assertBuildAssignmentReconciled(projectDir, runId, workspace);
|
|
39
47
|
const admission = db.assertAdmittedWork(runId, workId);
|
|
40
48
|
const ticket = db.workDefinitions(runId).find(work => work.workId === workId)?.definition;
|
|
@@ -66,6 +74,24 @@ async function ensureBuilderContractWithProvenance(projectDir, runId, workId, wo
|
|
|
66
74
|
inventory.push({ id: `req-rules-${contractDigest("rule-identity", { locator, statement }).slice(0, 24)}`, inputRef: "effective-rules", locator, statement, digest: contractDigest("statement", statement), obligation: "mandatory", origin: "explicit", authority: "approved" });
|
|
67
75
|
}
|
|
68
76
|
const source = captureBuildSource(workspace);
|
|
77
|
+
const preimplementationPathManifestDigest = contractDigest("preimplementation-path-manifest", source.pathInventory.map(path => ({
|
|
78
|
+
path: path.path,
|
|
79
|
+
staged: path.staged,
|
|
80
|
+
unstaged: path.unstaged,
|
|
81
|
+
...(path.headObject ? { headObject: path.headObject } : {}),
|
|
82
|
+
...(path.indexObject ? { indexObject: path.indexObject } : {}),
|
|
83
|
+
...(path.worktreeObject ? { worktreeObject: path.worktreeObject } : {}),
|
|
84
|
+
...(path.untracked ? { untracked: path.untracked } : {}),
|
|
85
|
+
})));
|
|
86
|
+
const baselineEvidence = store.putArtifact("baseline-observation", {
|
|
87
|
+
kind: "host-captured-preimplementation-source",
|
|
88
|
+
sourceDigest: source.digest,
|
|
89
|
+
contentDigest: source.contentDigest,
|
|
90
|
+
head: source.head,
|
|
91
|
+
captureMode: source.captureMode,
|
|
92
|
+
capturedAt: source.capturedAt,
|
|
93
|
+
});
|
|
94
|
+
inputs.push({ id: "preimplementation-source", kind: "baseline", reference: "builder.preimplementation-source", digest: source.digest, authority: "inspected-context", revision: source.head, availability: "available" });
|
|
69
95
|
const head = store.head(runId, workId, admission.requirementsDigest);
|
|
70
96
|
const priorBudget = store.budget(runId, workId, admission.requirementsDigest);
|
|
71
97
|
if (priorBudget)
|
|
@@ -87,14 +113,30 @@ async function ensureBuilderContractWithProvenance(projectDir, runId, workId, wo
|
|
|
87
113
|
amendmentPredecessor = old.contentDigest;
|
|
88
114
|
}
|
|
89
115
|
}
|
|
116
|
+
const hostBaseline = { id: "host-preimplementation-source", sourceDigest: source.digest, phase: "preimplementation-host-capture", runtime: "Rafi host", procedure: "Host-captured source identity and per-file path manifest before Builder edits", observedAt: source.capturedAt, status: "passed", evidenceDigest: baselineEvidence, relevance: `Authoritative comparison basis for unchanged-file and scope checks; exact host-owned path-manifest digest is ${preimplementationPathManifestDigest}`, expectedTransition: "Rafi compares the complete postimplementation path inventory with this host-owned baseline; providers do not need the raw manifest" };
|
|
90
117
|
const initial = { version: 1, contractId: head.digest ? store.contract(head.digest).contractId : `contract:${contractDigest("contract-family", { runId, workId, admission: admission.requirementsDigest })}`, revision: head.digest ? store.contract(head.digest).revision + (amendmentPredecessor ? 1 : 0) : 1,
|
|
91
118
|
...(amendmentPredecessor ? { predecessorDigest: amendmentPredecessor } : {}),
|
|
92
119
|
runId, workId, admissionDigest: admission.requirementsDigest, depthDecision: ticket.qa_preparation,
|
|
93
|
-
|
|
120
|
+
documentation: structuredClone(ticket.documentation ?? { required: false, targets: [], criteria: [] }),
|
|
121
|
+
inputs, baseline: [hostBaseline], requirements: inventory, checks: [], coverage: [], preparationEvidence: [], unresolved: [], createdAt: new Date(priorBudget?.startMs ?? Date.now()).toISOString() };
|
|
122
|
+
const buildRun = readBuildRuns(projectDir).find(run => run.runId === runId);
|
|
123
|
+
const deliveryFact = buildRun?.branchMode === "current"
|
|
124
|
+
? "Host-owned delivery-mode fact: this admitted build uses current-branch semantics. Do not require an isolated ticket branch, a multiply-only branch, branch identity, or a branch-alone test merely to prove additive ticket independence. HEAD may predate already completed sibling tickets, so never use git status, git diff HEAD, or a HEAD-based whole-tree comparison as the ticket-scope proof. Evaluate independence with the host-captured preimplementation baseline observation, approved product-path scope, ticket-owned tests, and permitted project tests. An isolated execution remains required only if the original approved requirement explicitly says so."
|
|
125
|
+
: buildRun?.branchMode === "per-ticket"
|
|
126
|
+
? "Host-owned delivery-mode fact: this admitted build uses isolated branch-per-ticket semantics. Verify branch/worktree identity when the contract requires it; do not infer it from the independent QA snapshot."
|
|
127
|
+
: "Host-owned delivery-mode fact: delivery mode was not available from the authoritative build record; do not invent branch-isolation requirements without an explicit approved requirement.";
|
|
94
128
|
const result = await prepareVerificationContract(initial, inventory, {
|
|
95
|
-
|
|
129
|
+
provider: builder.agent,
|
|
130
|
+
store, now: Date.now, hostFacts: `${qaHostGraphFact(projectDir)}\n${deliveryFact}\nHost-owned baseline fact: Rafi captured the complete preimplementation per-file path manifest and will perform the exact unchanged-file and scope comparison. Its digest is ${preimplementationPathManifestDigest}. Do not require the provider to receive or reproduce the raw manifest; cite the retained host-preimplementation-source observation and assess the host comparison result instead. Do not author a provider-executed check for Rafi's host postimplementation path manifest, complete changed-path inventory, or unchanged-file comparison; Rafi performs that comparison outside the provider snapshot. Ordinary approved product paths, exports, and tests are not a material QA concern merely because the provider cannot prove whether they were present before implementation; Rafi owns that comparison. Do not reconstruct or disclose the full baseline. 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.`,
|
|
96
131
|
normalizeDraft: (draft, response) => {
|
|
97
|
-
const normalized = allocateStableCheckIds(draft);
|
|
132
|
+
const normalized = rewriteProviderHostScopeChecks(bindUniqueBaselineCheckIds(allocateStableCheckIds(draft)), hostBaseline.id);
|
|
133
|
+
if (!normalized.baseline.some(observation => observation.id === hostBaseline.id))
|
|
134
|
+
normalized.baseline.unshift(structuredClone(hostBaseline));
|
|
135
|
+
for (const check of normalized.checks)
|
|
136
|
+
for (const method of check.verification) {
|
|
137
|
+
if (response.workspacePaths && Object.hasOwn(response.workspacePaths, method.cwd))
|
|
138
|
+
method.cwd = response.workspacePaths[method.cwd];
|
|
139
|
+
}
|
|
98
140
|
for (const observation of normalized.baseline) {
|
|
99
141
|
const original = structuredClone(observation);
|
|
100
142
|
observation.sourceDigest = source.digest;
|
|
@@ -110,10 +152,22 @@ async function ensureBuilderContractWithProvenance(projectDir, runId, workId, wo
|
|
|
110
152
|
evidence.graphReceiptRefs = [...response.graphReceiptRefs];
|
|
111
153
|
evidence.evidenceDigest = store.putArtifact("investigation-evidence", response);
|
|
112
154
|
for (const reference of evidence.references) {
|
|
113
|
-
|
|
155
|
+
let path;
|
|
156
|
+
try {
|
|
157
|
+
path = realpathSync(resolve(workspace, reference.path));
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
throw new Error(`Inspected source reference ${JSON.stringify(reference.path)} is missing or inaccessible; cite an existing file actually inspected in the frozen workspace`);
|
|
161
|
+
}
|
|
162
|
+
const root = realpathSync(workspace);
|
|
114
163
|
if (!path.startsWith(root + sep))
|
|
115
164
|
throw new Error("Inspected source reference escapes actual workspace");
|
|
116
|
-
|
|
165
|
+
try {
|
|
166
|
+
reference.digest = createHash("sha256").update(readFileSync(path)).digest("hex");
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
throw new Error(`Inspected source reference ${JSON.stringify(reference.path)} is not a readable file`);
|
|
170
|
+
}
|
|
117
171
|
}
|
|
118
172
|
}
|
|
119
173
|
return normalized;
|
|
@@ -121,8 +175,11 @@ async function ensureBuilderContractWithProvenance(projectDir, runId, workId, wo
|
|
|
121
175
|
inputsCurrent: candidate => {
|
|
122
176
|
if (db.assertAdmittedWork(runId, workId).requirementsDigest !== admission.requirementsDigest)
|
|
123
177
|
return false;
|
|
178
|
+
const currentSource = captureBuildSource(workspace);
|
|
179
|
+
if (currentSource.contentDigest !== source.contentDigest)
|
|
180
|
+
return false;
|
|
124
181
|
const current = resolveEffectiveQaConfiguration(projectDir, { make: state?.qa?.settings?.make ?? builder.agent });
|
|
125
|
-
const compared = compareContractInputs(candidate, inputs.map(input => input.id === "effective-rules" ? { ...input, digest: current.digest } : input.id === "project-checklist" ? { ...input, digest: contractDigest("input-checklist", current.checklist) } : input));
|
|
182
|
+
const compared = compareContractInputs(candidate, inputs.map(input => input.id === "effective-rules" ? { ...input, digest: current.digest } : input.id === "project-checklist" ? { ...input, digest: contractDigest("input-checklist", current.checklist) } : input.id === "preimplementation-source" ? { ...input, digest: currentSource.digest, revision: currentSource.head } : input));
|
|
126
183
|
return !compared.changed.length && !compared.unavailable.length;
|
|
127
184
|
},
|
|
128
185
|
dispatch: async (phase, instruction, operationId, remainingMs) => new OperationDeadline("QA preparation operation", remainingMs).run(async () => {
|
|
@@ -137,13 +194,14 @@ async function ensureBuilderContractWithProvenance(projectDir, runId, workId, wo
|
|
|
137
194
|
if (phase === "planner") {
|
|
138
195
|
const role = await createRoleBuilder({ projectDir: workspace, configRoot: projectDir, role: "planner", label: "QA depth assessment", sandboxMode: "read-only", permissionConfig: readOnlyPermissionConfig(), yes: true, allowSwitch: false });
|
|
139
196
|
try {
|
|
140
|
-
const turn = await sendPreparationGraphTurn(role.builder,
|
|
197
|
+
const turn = await sendPreparationGraphTurn(role.builder, `This is a read-only QA planning subturn, not Builder work. Do not implement anything, edit files, run project tests, or describe a TDD implementation plan. Your response must begin with RAFI_QA_DEPTH_START and contain only the requested depth JSON, ending with RAFI_QA_DEPTH_END.\n${instruction}\nFrozen admitted work definition: ${JSON.stringify(ticket)}\nOriginal approved plan context: ${JSON.stringify(planInput?.plan ?? null)}\nFrozen effective configuration: ${JSON.stringify(effective)}`, { phase, projectDir, workspace, sourceRef: before.digest, runId, workId, operationId, remainingMs: remaining() });
|
|
141
198
|
store.event(runId, workId, `${operationId}:raw`, "preparation-provider-return", turn);
|
|
142
199
|
if (captureBuildSource(workspace).contentDigest !== before.contentDigest)
|
|
143
200
|
throw new Error("Product source changed during confined planning; reconcile retained dispatch");
|
|
144
|
-
|
|
201
|
+
const payload = preparationTurnPayload(turn);
|
|
202
|
+
if ((turn.isError && !payload.formatErrors.length) || turn.failure || !role.builder.sessionId())
|
|
145
203
|
throw new Error(`planner-unavailable: ${turn.text}`);
|
|
146
|
-
return { text:
|
|
204
|
+
return { text: payload.text, graphReceiptRefs: turn.graphReceiptRefs, sessionId: role.builder.sessionId(), usage: { inputTokens: turn.usage?.scope === "turn-delta" ? turn.usage.inputTokens : turn.inputTokens, outputTokens: turn.usage?.scope === "turn-delta" ? turn.usage.outputTokens : turn.outputTokens, costUsd: turn.costAuthoritative ? turn.costUsd : undefined } };
|
|
147
205
|
}
|
|
148
206
|
finally {
|
|
149
207
|
await role.builder.close();
|
|
@@ -162,11 +220,13 @@ async function ensureBuilderContractWithProvenance(projectDir, runId, workId, wo
|
|
|
162
220
|
adapter = (await createRoleBuilder({ projectDir: snapshot.path, configRoot: projectDir, role: "qa", agent: state?.qa?.settings?.make ?? builder.agent, preloadedSkillContent: effective.skills.map(skill => ({ name: skill.name, content: skill.content })), label: "QA preparation", sandboxMode: "read-only", permissionConfig: readOnlyPermissionConfig(), yes: true, allowSwitch: false })).builder;
|
|
163
221
|
const turn = await sendPreparationGraphTurn(adapter, `${instruction}\nOriginal approved plan context: ${JSON.stringify(planInput?.plan ?? null)}\nFrozen admitted work definition: ${JSON.stringify(ticket)}\nHost early prerequisite observations (host availability is not provider access; classify readiness versus final-only dependencies): ${JSON.stringify({ ...prerequisites, evidenceDigest: prerequisiteDigest })}\nFrozen effective configuration: ${JSON.stringify(effective)}\nOperation: ${operationId}; remaining wall time: ${remainingMs}ms; source: ${source.digest}`, { phase, projectDir, workspace: snapshot.path, accessWorkspace: workspace, sourceRef: snapshot.frozenState.digest, runId, workId, operationId, remainingMs: remaining() });
|
|
164
222
|
store.event(runId, workId, `${operationId}:raw`, "preparation-provider-return", turn);
|
|
165
|
-
|
|
223
|
+
const payload = preparationTurnPayload(turn);
|
|
224
|
+
payload.text = normalizeSnapshotPaths(payload.text);
|
|
225
|
+
if ((turn.isError && !payload.formatErrors.length) || turn.failure || !adapter.sessionId())
|
|
166
226
|
throw new Error(`Preparation provider unavailable: ${turn.text}`);
|
|
167
227
|
if (captureBuildSource(workspace).contentDigest !== before.contentDigest)
|
|
168
228
|
throw new Error("Source changed during preparation; retained dispatch requires reconciliation");
|
|
169
|
-
return { text:
|
|
229
|
+
return { text: payload.text, graphReceiptRefs: turn.graphReceiptRefs, sessionId: adapter.sessionId(), workspacePaths: mapSnapshotWorkingDirectories(payload.text, snapshot.path), usage: { inputTokens: turn.usage?.scope === "turn-delta" ? turn.usage.inputTokens : turn.inputTokens, outputTokens: turn.usage?.scope === "turn-delta" ? turn.usage.outputTokens : turn.outputTokens, costUsd: turn.costAuthoritative ? turn.costUsd : undefined } };
|
|
170
230
|
}
|
|
171
231
|
finally {
|
|
172
232
|
try {
|
|
@@ -208,7 +268,7 @@ async function ensureBuilderContractWithProvenance(projectDir, runId, workId, wo
|
|
|
208
268
|
catch {
|
|
209
269
|
await deliverVerificationContract(result.contract, store, builder, workspace, projectDir, renderBuildWorkContext(ticket, effective.checklist));
|
|
210
270
|
}
|
|
211
|
-
return `${renderVerificationContract(result.contract)}\n${
|
|
271
|
+
return `${renderVerificationContract(result.contract)}\n${BUILDER_WORK_COVERAGE_INSTRUCTIONS}`;
|
|
212
272
|
}
|
|
213
273
|
finally {
|
|
214
274
|
db.close();
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { BuilderAdapter, TurnResult } from "./adapters/types.js";
|
|
2
2
|
import { WorkflowDb } from "./workflowDb.js";
|
|
3
|
+
import { type HostBuilderToolObservation } from "./qaBuilderEvidence.js";
|
|
3
4
|
/** Claims are collected after source capture so the provider can name the actual completed source. */
|
|
4
|
-
export declare function collectBuilderContractCoverage(projectDir: string, runId: string, workId: string, workspace: string, adapter: BuilderAdapter, operationId: string, instruction: string, implementation: TurnResult): Promise<void>;
|
|
5
|
+
export declare function collectBuilderContractCoverage(projectDir: string, runId: string, workId: string, workspace: string, adapter: BuilderAdapter, operationId: string, instruction: string, implementation: TurnResult, hostObservations?: HostBuilderToolObservation[], initialSourceDigest?: string): Promise<void>;
|
|
5
6
|
export declare function assertBuilderContractCoverage(db: WorkflowDb, runId: string, workId: string, operationId: string, sourceDigest: string): void;
|
|
@@ -2,14 +2,16 @@ import { dispatchWithGraphAccess, withGraphSessionAccess } from "./graph/session
|
|
|
2
2
|
import { WorkflowDb } from "./workflowDb.js";
|
|
3
3
|
import { actualContractSession, assertContractReceipt } from "./qaContractDelivery.js";
|
|
4
4
|
import { captureBuildSource } from "./buildAssignment.js";
|
|
5
|
-
import { COVERAGE_INSTRUCTIONS, parseContractCoverage, validateContractCoverage } from "./qaContractCoverage.js";
|
|
5
|
+
import { COVERAGE_INSTRUCTIONS, bindCoverageHostFields, parseContractCoverage, validateContractCoverage } from "./qaContractCoverage.js";
|
|
6
6
|
import { contractDigest } from "./qaVerificationContract.js";
|
|
7
7
|
import { OperationDeadline } from "./util/deadline.js";
|
|
8
|
+
import { hostTestSequenceEvidence } from "./qaBuilderEvidence.js";
|
|
9
|
+
import { qaHostGraphFact } from "./qaHostFacts.js";
|
|
8
10
|
/** Claims are collected after source capture so the provider can name the actual completed source. */
|
|
9
|
-
export async function collectBuilderContractCoverage(projectDir, runId, workId, workspace, adapter, operationId, instruction, implementation) {
|
|
10
|
-
return withGraphSessionAccess(projectDir, adapter, () => collectBuilderContractCoverageOwned(projectDir, runId, workId, workspace, adapter, operationId, instruction, implementation));
|
|
11
|
+
export async function collectBuilderContractCoverage(projectDir, runId, workId, workspace, adapter, operationId, instruction, implementation, hostObservations = [], initialSourceDigest) {
|
|
12
|
+
return withGraphSessionAccess(projectDir, adapter, () => collectBuilderContractCoverageOwned(projectDir, runId, workId, workspace, adapter, operationId, instruction, implementation, hostObservations, initialSourceDigest));
|
|
11
13
|
}
|
|
12
|
-
async function collectBuilderContractCoverageOwned(projectDir, runId, workId, workspace, adapter, operationId, instruction, implementation) {
|
|
14
|
+
async function collectBuilderContractCoverageOwned(projectDir, runId, workId, workspace, adapter, operationId, instruction, implementation, hostObservations, initialSourceDigest) {
|
|
13
15
|
const db = new WorkflowDb(projectDir);
|
|
14
16
|
try {
|
|
15
17
|
const store = db.qaPreparationStore();
|
|
@@ -19,7 +21,7 @@ async function collectBuilderContractCoverageOwned(projectDir, runId, workId, wo
|
|
|
19
21
|
const session = actualContractSession(adapter, workspace, projectDir);
|
|
20
22
|
const contract = assertContractReceipt(store, runId, workId, admission.requirementsDigest, session, adapter);
|
|
21
23
|
const source = captureBuildSource(workspace);
|
|
22
|
-
|
|
24
|
+
let binding = { phase: "builder", sourceDigest: source.digest, inputBasisDigest: contractDigest("builder-input-basis", { operationId, instruction, contractDigest: contract.contentDigest }), attemptId: operationId, sessionId: session.sessionId };
|
|
23
25
|
const eventId = `builder-coverage:${operationId}`;
|
|
24
26
|
const existing = db.operation(eventId);
|
|
25
27
|
if (existing) {
|
|
@@ -42,9 +44,19 @@ async function collectBuilderContractCoverageOwned(projectDir, runId, workId, wo
|
|
|
42
44
|
db.updateOperation(eventId, "uncertain", { result: { responseDigest } });
|
|
43
45
|
if (response.isError || response.failure)
|
|
44
46
|
throw new Error("Builder coverage provider failed; retained response requires reconciliation");
|
|
45
|
-
|
|
47
|
+
const afterSession = actualContractSession(adapter, workspace, projectDir);
|
|
48
|
+
const sameScopedSession = afterSession.provider === session.provider
|
|
49
|
+
&& afterSession.workspace === session.workspace
|
|
50
|
+
&& afterSession.configRoot === session.configRoot
|
|
51
|
+
&& (afterSession.sessionId === session.sessionId || afterSession.compactionSequence > session.compactionSequence);
|
|
52
|
+
if (!sameScopedSession || captureBuildSource(workspace).digest !== source.digest)
|
|
46
53
|
throw new Error("Builder coverage changed session or product source");
|
|
47
|
-
|
|
54
|
+
// A provider may compact while producing this response-only evidence.
|
|
55
|
+
// Bind the retained coverage to the host-observed post-compaction session
|
|
56
|
+
// while continuing to reject unscoped session changes and source edits.
|
|
57
|
+
binding = { ...binding, sessionId: afterSession.sessionId };
|
|
58
|
+
const coverage = bindCoverageHostFields(parseContractCoverage(response.text), contract, binding);
|
|
59
|
+
bindHostOwnedBuilderFacts(db, coverage, contract, hostObservations, initialSourceDigest, source.digest, workId, projectDir);
|
|
48
60
|
const errors = validateContractCoverage(contract, coverage, binding, false);
|
|
49
61
|
if (errors.length)
|
|
50
62
|
throw new Error(`Builder coverage invalid: ${errors.join("; ")}`);
|
|
@@ -59,6 +71,45 @@ async function collectBuilderContractCoverageOwned(projectDir, runId, workId, wo
|
|
|
59
71
|
db.close();
|
|
60
72
|
}
|
|
61
73
|
}
|
|
74
|
+
function checkText(check) {
|
|
75
|
+
return JSON.stringify(check);
|
|
76
|
+
}
|
|
77
|
+
function bindHostOwnedBuilderFacts(db, coverage, contract, observations, _initialSourceDigest, finalSourceDigest, workId, projectDir) {
|
|
78
|
+
const graphFact = qaHostGraphFact(projectDir);
|
|
79
|
+
const graphDisabled = /"enabled":false/.test(graphFact);
|
|
80
|
+
for (const row of coverage.checks ?? []) {
|
|
81
|
+
const check = contract.checks.find(candidate => candidate.id === row.checkId);
|
|
82
|
+
if (!check)
|
|
83
|
+
continue;
|
|
84
|
+
const text = checkText(check);
|
|
85
|
+
if (check.timing === "preimplementation" && row.outcome === "not-run") {
|
|
86
|
+
const baseline = contract.baseline.find(observation => observation.checkId === check.id && observation.status === "passed");
|
|
87
|
+
if (baseline) {
|
|
88
|
+
row.outcome = "passed";
|
|
89
|
+
row.evidence = [...(row.evidence ?? []), { procedure: "Rafi retained preimplementation baseline observation", cwd: ".", runtime: "rafi host", sourceDigest: baseline.sourceDigest, reference: baseline.evidenceDigest }];
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (graphDisabled && /graph/i.test(text) && /disabled|not.?adopt|no graph/i.test(text)) {
|
|
93
|
+
const factDigest = db.putEvidence("qa", graphFact);
|
|
94
|
+
if (row.outcome === "not-run")
|
|
95
|
+
row.outcome = "passed";
|
|
96
|
+
if (!row.evidence?.some(evidence => evidence.reference === `host-graph-fact:${factDigest}`))
|
|
97
|
+
row.evidence = [...(row.evidence ?? []), { procedure: "Rafi host-owned graph configuration observation", cwd: ".", runtime: "rafi host", sourceDigest: finalSourceDigest, reference: `host-graph-fact:${factDigest}` }];
|
|
98
|
+
}
|
|
99
|
+
if (row.outcome !== "passed")
|
|
100
|
+
continue;
|
|
101
|
+
const historical = /red|test.?first|ordering|historical/i.test(text);
|
|
102
|
+
if (!historical)
|
|
103
|
+
continue;
|
|
104
|
+
const testPath = check.verification.find(method => method.kind === "command" && method.argv?.includes("--test"))?.argv?.at(-1);
|
|
105
|
+
const sequence = hostTestSequenceEvidence(observations, finalSourceDigest, testPath);
|
|
106
|
+
if (!sequence)
|
|
107
|
+
continue;
|
|
108
|
+
const evidence = JSON.stringify({ version: 1, kind: "host-builder-test-sequence", workId, checkId: row.checkId, sequence });
|
|
109
|
+
const evidenceDigest = db.putEvidence("qa", evidence);
|
|
110
|
+
row.evidence = [...(row.evidence ?? []), { procedure: "Rafi host-observed test sequence", cwd: ".", runtime: "provider tool lifecycle plus host source capture", sourceDigest: finalSourceDigest, reference: `host-test-sequence:${evidenceDigest}` }];
|
|
111
|
+
}
|
|
112
|
+
}
|
|
62
113
|
export function assertBuilderContractCoverage(db, runId, workId, operationId, sourceDigest) {
|
|
63
114
|
const store = db.qaPreparationStore();
|
|
64
115
|
if (store.policy(runId)?.mode !== "enforce")
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { BuilderAdapter } from "./adapters/types.js";
|
|
2
|
+
export interface HostBuilderToolObservation {
|
|
3
|
+
kind: "node-test" | "npm-test";
|
|
4
|
+
startedAt: string;
|
|
5
|
+
completedAt: string;
|
|
6
|
+
outcome: string;
|
|
7
|
+
exitCode?: number;
|
|
8
|
+
outputDigest?: string;
|
|
9
|
+
sourceDigest: string;
|
|
10
|
+
testPath?: string;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Observe only bounded test-command metadata while the provider is working.
|
|
14
|
+
* The provider remains subject to its normal shell policy; this is a host-side
|
|
15
|
+
* observation of what actually completed, not an execution or permission grant.
|
|
16
|
+
*/
|
|
17
|
+
export declare function observeHostBuilderTests(adapter: BuilderAdapter, workspace: string, observations: HostBuilderToolObservation[]): () => void;
|
|
18
|
+
export declare function hostTestSequenceEvidence(observations: HostBuilderToolObservation[], finalSourceDigest: string, testPath?: string): HostBuilderToolObservation[] | undefined;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { captureBuildSource } from "./buildAssignment.js";
|
|
2
|
+
function commandDetails(input) {
|
|
3
|
+
const command = typeof input?.command === "string"
|
|
4
|
+
? input.command
|
|
5
|
+
: undefined;
|
|
6
|
+
if (!command)
|
|
7
|
+
return undefined;
|
|
8
|
+
const nodeTest = /\bnode\s+--test(?:\s+([^\s;&|>]+))?/.exec(command);
|
|
9
|
+
if (nodeTest)
|
|
10
|
+
return { kind: "node-test", ...(nodeTest[1] ? { testPath: nodeTest[1] } : {}) };
|
|
11
|
+
if (/\bnpm\s+test\b/.test(command))
|
|
12
|
+
return { kind: "npm-test" };
|
|
13
|
+
return undefined;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Observe only bounded test-command metadata while the provider is working.
|
|
17
|
+
* The provider remains subject to its normal shell policy; this is a host-side
|
|
18
|
+
* observation of what actually completed, not an execution or permission grant.
|
|
19
|
+
*/
|
|
20
|
+
export function observeHostBuilderTests(adapter, workspace, observations) {
|
|
21
|
+
const open = new Map();
|
|
22
|
+
const stop = adapter.observeEvents?.((event) => {
|
|
23
|
+
if (event.kind !== "tool")
|
|
24
|
+
return;
|
|
25
|
+
const details = commandDetails(event.input);
|
|
26
|
+
if (event.lifecycle === "started" && details && event.callId) {
|
|
27
|
+
open.set(event.callId, { ...details, startedAt: event.observedAt ?? new Date().toISOString() });
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
if (event.lifecycle !== "completed" || !event.callId || !event.completionKnown)
|
|
31
|
+
return;
|
|
32
|
+
const prior = open.get(event.callId);
|
|
33
|
+
if (!prior)
|
|
34
|
+
return;
|
|
35
|
+
open.delete(event.callId);
|
|
36
|
+
if (observations.length >= 40)
|
|
37
|
+
return;
|
|
38
|
+
observations.push({
|
|
39
|
+
kind: prior.kind,
|
|
40
|
+
startedAt: prior.startedAt,
|
|
41
|
+
completedAt: event.observedAt ?? new Date().toISOString(),
|
|
42
|
+
// Some adapters label every finished tool call "completed" even when
|
|
43
|
+
// the process exited non-zero. Exit status is the host-owned authority
|
|
44
|
+
// for the red/green distinction whenever it is available.
|
|
45
|
+
outcome: event.exitCode !== undefined ? event.exitCode === 0 ? "completed" : "failed" : event.status ?? "completed",
|
|
46
|
+
...(event.exitCode === undefined ? {} : { exitCode: event.exitCode }),
|
|
47
|
+
...(event.outputDigest && /^[a-f0-9]{64}$/.test(event.outputDigest) ? { outputDigest: event.outputDigest } : {}),
|
|
48
|
+
sourceDigest: captureBuildSource(workspace).digest,
|
|
49
|
+
...(prior.testPath ? { testPath: prior.testPath } : {}),
|
|
50
|
+
});
|
|
51
|
+
}) ?? (() => { });
|
|
52
|
+
return () => { stop(); open.clear(); };
|
|
53
|
+
}
|
|
54
|
+
export function hostTestSequenceEvidence(observations, finalSourceDigest, testPath) {
|
|
55
|
+
const relevant = observations.filter(observation => (observation.kind === "node-test" || observation.kind === "npm-test") && (!testPath || observation.testPath === testPath));
|
|
56
|
+
const red = relevant.find(observation => observation.outcome !== "completed" && observation.outcome !== "denied");
|
|
57
|
+
const redIndex = red ? relevant.indexOf(red) : -1;
|
|
58
|
+
const green = red ? relevant.slice(redIndex + 1).find(observation => observation.sourceDigest === finalSourceDigest && observation.outcome === "completed" && observation.sourceDigest !== red.sourceDigest) : undefined;
|
|
59
|
+
return red && green ? [red, green] : undefined;
|
|
60
|
+
}
|
|
@@ -1,6 +1,17 @@
|
|
|
1
1
|
import type { QaContractCoverageV1, QaVerificationContractV1 } from "rafi-spec";
|
|
2
|
-
export declare const COVERAGE_INSTRUCTIONS
|
|
2
|
+
export declare const COVERAGE_INSTRUCTIONS: string;
|
|
3
|
+
export declare const BUILDER_WORK_COVERAGE_INSTRUCTIONS = "Carry out the applicable contract checks and report your work and evidence honestly in your normal final response. When a check requires historical ordering, retain bounded output for each required step and an explicit ordering record while performing it; a later passing test cannot prove an earlier failing step. Do not emit RAFI_QA_COVERAGE JSON during this implementation turn or stop for coverage binding values. After the implementation turn, Rafi will capture the completed source and request a separate response-only coverage record with the exact host-owned bindings.";
|
|
3
4
|
export declare function parseContractCoverage(text: string): QaContractCoverageV1;
|
|
5
|
+
/** Source identity is host-owned; providers may copy a stale Builder digest. */
|
|
6
|
+
export declare function bindCoverageSourceDigest(coverage: QaContractCoverageV1, sourceDigest: string): QaContractCoverageV1;
|
|
7
|
+
/** All coverage identity fields are host-owned; provider output supplies only claims and evidence text. */
|
|
8
|
+
export declare function bindCoverageHostFields(coverage: QaContractCoverageV1, contract: QaVerificationContractV1, binding: {
|
|
9
|
+
phase: "builder" | "qa";
|
|
10
|
+
sourceDigest: string;
|
|
11
|
+
inputBasisDigest: string;
|
|
12
|
+
attemptId: string;
|
|
13
|
+
sessionId: string;
|
|
14
|
+
}): QaContractCoverageV1;
|
|
4
15
|
export declare function validateContractCoverage(contract: QaVerificationContractV1, coverage: QaContractCoverageV1, binding: {
|
|
5
16
|
phase: "builder" | "qa";
|
|
6
17
|
sourceDigest: string;
|
|
@@ -2,8 +2,40 @@ import { Ajv } from "ajv";
|
|
|
2
2
|
import { qaContractCoverageV1Schema } from "rafi-spec";
|
|
3
3
|
const coverageSchema = new Ajv({ allErrors: true }).compile(qaContractCoverageV1Schema);
|
|
4
4
|
import { parsePreparationArtifact } from "./qaVerificationContract.js";
|
|
5
|
-
export const COVERAGE_INSTRUCTIONS = `Return RAFI_QA_COVERAGE_START, one QaContractCoverageV1 JSON object, RAFI_QA_COVERAGE_END before the final status marker.
|
|
5
|
+
export const COVERAGE_INSTRUCTIONS = `Return RAFI_QA_COVERAGE_START, one QaContractCoverageV1 JSON object, RAFI_QA_COVERAGE_END before the final status marker. The exact schema is ${JSON.stringify(qaContractCoverageV1Schema)}. Put every binding field at the TOP LEVEL, not inside a binding object, and copy the host-supplied binding values exactly. For each check use checkId (not id), applicability as applicable/not-applicable/unresolved, predicateEvidence as an array of strings, outcome as passed/failed/blocked/not-run, and evidence as an array of objects with procedure, cwd, runtime, sourceDigest and reference. For postimplementation evidence sourceDigest is the exact host-supplied sourceDigest, not an individual file hash. Unconditional checks cannot be marked not-applicable or unresolved; for an absence or non-impact check, mark it applicable and provide bounded inspection evidence showing that the absent change or impact was assessed. For independent QA coverage of an applicable passed preimplementation-only check, inspect its matching passed contract.baseline observation and set at least one evidence reference to that observation's exact evidenceDigest; the historical observation is bound to its original source, not the current source. Assess its relevance rather than inventing or claiming a new baseline. Host-owned checks whose applicability decisionOwner is host are the exception to provider execution: include the check truthfully as applicable/not-run with evidence that the host-owned comparison is pending, and do not block or fail QA over it; Rafi will execute the comparison and replace that row before accepting the pass. Before emitting qa_pass, run every other applicable mandatory check that is not backed by a passed retained baseline; do not leave such a provider-owned check as not-run merely because an earlier turn omitted it. If an exact required command is blocked by the provider shell policy, use a semantically equivalent permitted read-only procedure, record the substitution and limitation truthfully, and assess the same predicate; do not mark the check blocked merely because the literal command syntax was denied. If a required command is permitted and read-only, run it now and retain its bounded result. Do not add unsupported fields. Include every check with truthful applicability and concrete evidence. Missing, blocked, not-run, unresolved applicability, or failed provider-owned mandatory coverage cannot authorize normal completion. Builder coverage is a claim; final QA independently verifies it.`;
|
|
6
|
+
// The source and attempt binding do not exist until the implementation turn has finished.
|
|
7
|
+
export const BUILDER_WORK_COVERAGE_INSTRUCTIONS = `Carry out the applicable contract checks and report your work and evidence honestly in your normal final response. When a check requires historical ordering, retain bounded output for each required step and an explicit ordering record while performing it; a later passing test cannot prove an earlier failing step. Do not emit RAFI_QA_COVERAGE JSON during this implementation turn or stop for coverage binding values. After the implementation turn, Rafi will capture the completed source and request a separate response-only coverage record with the exact host-owned bindings.`;
|
|
6
8
|
export function parseContractCoverage(text) { return parsePreparationArtifact(text, "RAFI_QA_COVERAGE"); }
|
|
9
|
+
/** Source identity is host-owned; providers may copy a stale Builder digest. */
|
|
10
|
+
export function bindCoverageSourceDigest(coverage, sourceDigest) {
|
|
11
|
+
const copy = structuredClone(coverage);
|
|
12
|
+
for (const check of copy.checks ?? [])
|
|
13
|
+
for (const evidence of check.evidence ?? [])
|
|
14
|
+
evidence.sourceDigest = sourceDigest;
|
|
15
|
+
return copy;
|
|
16
|
+
}
|
|
17
|
+
/** All coverage identity fields are host-owned; provider output supplies only claims and evidence text. */
|
|
18
|
+
export function bindCoverageHostFields(coverage, contract, binding) {
|
|
19
|
+
const copy = bindCoverageSourceDigest(coverage, binding.sourceDigest);
|
|
20
|
+
// Optional schema fields may be emitted as JSON null by providers. Null is
|
|
21
|
+
// not a meaningful limitation, so omit only that representation variant;
|
|
22
|
+
// non-string values elsewhere remain validation errors.
|
|
23
|
+
for (const check of copy.checks ?? [])
|
|
24
|
+
for (const evidence of check.evidence ?? [])
|
|
25
|
+
if (evidence.limitation === null)
|
|
26
|
+
delete evidence.limitation;
|
|
27
|
+
copy.version = 1;
|
|
28
|
+
copy.runId = contract.runId;
|
|
29
|
+
copy.workId = contract.workId;
|
|
30
|
+
copy.revision = contract.revision;
|
|
31
|
+
copy.contractDigest = contract.contentDigest;
|
|
32
|
+
copy.phase = binding.phase;
|
|
33
|
+
copy.sourceDigest = binding.sourceDigest;
|
|
34
|
+
copy.inputBasisDigest = binding.inputBasisDigest;
|
|
35
|
+
copy.attemptId = binding.attemptId;
|
|
36
|
+
copy.sessionId = binding.sessionId;
|
|
37
|
+
return copy;
|
|
38
|
+
}
|
|
7
39
|
export function validateContractCoverage(contract, coverage, binding, requirePass = true) {
|
|
8
40
|
if (!coverageSchema(coverage))
|
|
9
41
|
return (coverageSchema.errors ?? []).map(error => `${error.instancePath} ${error.message}`);
|
|
@@ -32,7 +64,7 @@ export function validateContractCoverage(contract, coverage, binding, requirePas
|
|
|
32
64
|
if (check.applicability.kind === "conditional" && row.applicability === "applicable" && !row.predicateEvidence?.length)
|
|
33
65
|
issues.push(`Conditional applicability requires predicate evidence ${row.checkId}`);
|
|
34
66
|
if (requirePass && check.obligation === "mandatory" && (row.applicability === "unresolved" || row.applicability === "applicable" && row.outcome !== "passed"))
|
|
35
|
-
issues.push(`Mandatory check incomplete ${row.checkId}`);
|
|
67
|
+
issues.push(`Mandatory check incomplete ${row.checkId}: run its permitted verification now and resubmit passed evidence, or report a truthful blocked result for host recovery`);
|
|
36
68
|
if (!Array.isArray(row.evidence))
|
|
37
69
|
issues.push(`Missing evidence array ${row.checkId}`);
|
|
38
70
|
if (row.outcome === "passed" && row.applicability === "applicable") {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type QaVerificationContractV1, type QaContractDeliveryReceiptV1 } from "rafi-spec";
|
|
2
2
|
import type { BuilderAdapter } from "./adapters/types.js";
|
|
3
3
|
import type { QaPreparationStore } from "./qaPreparationStore.js";
|
|
4
4
|
export interface ContractSessionBinding {
|
|
@@ -16,7 +16,7 @@ export declare function materializeContract(contract: QaVerificationContractV1,
|
|
|
16
16
|
resourceDigest: string;
|
|
17
17
|
};
|
|
18
18
|
/** Complete retained review inputs, accessible inside the confined QA snapshot. */
|
|
19
|
-
export declare function finalReviewContractContext(contract: QaVerificationContractV1, store: QaPreparationStore, workspace: string, builderClaims?: unknown[]): string;
|
|
19
|
+
export declare function finalReviewContractContext(contract: QaVerificationContractV1, store: QaPreparationStore, workspace: string, builderClaims?: unknown[], commandObservations?: unknown[]): string;
|
|
20
20
|
export declare function deliverVerificationContract(contract: QaVerificationContractV1, store: QaPreparationStore, adapter: BuilderAdapter, workspace: string, configRoot: string, context: string): Promise<QaContractDeliveryReceiptV1>;
|
|
21
21
|
/** Byte bounded, lossless Unicode transport. Concatenation reconstructs the exact original input. */
|
|
22
22
|
export declare function segmentContractTransport(content: string, maximumBytes?: number): string[];
|