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/qaPreparation.js
CHANGED
|
@@ -1,7 +1,147 @@
|
|
|
1
|
-
import { QA_RISK_MINIMUMS, qaContractCandidateV1Schema, qaSemanticAssessmentV1Schema, qaChallengeReceiptV1Schema, qaPreparationDepthDecisionV1Schema } from "rafi-spec";
|
|
2
|
-
import { randomUUID } from "node:crypto";
|
|
1
|
+
import { QA_RISK_MINIMUMS, parseProviderContract, qaContractCandidateV1Schema, qaPreparationEscalationProposalV1Schema, qaSemanticAssessmentV1Schema, qaChallengeReceiptV1Schema, qaPreparationDepthDecisionV1Schema as qaDepthDecisionSchema } from "rafi-spec";
|
|
2
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
3
|
+
import { Ajv } from "ajv";
|
|
4
|
+
const qaPreparationDepthDecisionV1Schema = qaSemanticAssessmentV1Schema;
|
|
3
5
|
import { depthObligations, validateDepthDecision } from "./qaPreparationPolicy.js";
|
|
4
|
-
import { canonicalContractJson, assembleContract, draftDigest, inventoryDigest, parsePreparationArtifact, validateCandidate } from "./qaVerificationContract.js";
|
|
6
|
+
import { canonicalContractJson, assembleContract, draftDigest, inventoryDigest, parsePreparationArtifact, validateAssessment, validateCandidate } from "./qaVerificationContract.js";
|
|
7
|
+
export 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.`;
|
|
8
|
+
export 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.`;
|
|
9
|
+
export 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, “acceptable on its own” 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.`;
|
|
10
|
+
/** Baseline bookkeeping must not block ordinary approved implementation work. */
|
|
11
|
+
export 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.`;
|
|
12
|
+
/** Ignore known non-authoritative annotations; the original provider response remains retained. */
|
|
13
|
+
export function normalizeAssessmentMetadata(assessment) {
|
|
14
|
+
const copy = { ...assessment };
|
|
15
|
+
delete copy.createdAt;
|
|
16
|
+
delete copy.policyVersion_note;
|
|
17
|
+
return copy;
|
|
18
|
+
}
|
|
19
|
+
/** Accept only an exact known obligation label when a provider wraps it in a
|
|
20
|
+
* descriptive object. This is a representation repair, not a host decision:
|
|
21
|
+
* unknown or ambiguous objects remain unchanged and fail schema validation. */
|
|
22
|
+
export function normalizeAssessmentObligations(assessment, allowed) {
|
|
23
|
+
const copy = structuredClone(assessment);
|
|
24
|
+
if (!Array.isArray(copy.assessedObligations))
|
|
25
|
+
return copy;
|
|
26
|
+
const known = new Set(allowed);
|
|
27
|
+
copy.assessedObligations = copy.assessedObligations.map(item => {
|
|
28
|
+
if (typeof item === "string")
|
|
29
|
+
return item;
|
|
30
|
+
if (!item || typeof item !== "object")
|
|
31
|
+
return item;
|
|
32
|
+
const matches = Object.values(item).filter(value => typeof value === "string" && known.has(value));
|
|
33
|
+
return matches.length === 1 ? matches[0] : item;
|
|
34
|
+
});
|
|
35
|
+
return copy;
|
|
36
|
+
}
|
|
37
|
+
/** A preparation assessor must not block on the expected absence of future
|
|
38
|
+
* Builder work. Keep the concern for auditability, but make it non-material
|
|
39
|
+
* only when every referenced check is postimplementation and the concern is
|
|
40
|
+
* explicitly asking the Builder to implement/rerun that check. Concerns tied
|
|
41
|
+
* to preimplementation checks, missing procedures, or other semantic gaps
|
|
42
|
+
* remain blocking. */
|
|
43
|
+
export function normalizeExpectedFutureAssessment(assessment, candidate) {
|
|
44
|
+
const copy = structuredClone(assessment);
|
|
45
|
+
const checks = new Map(candidate.checks.map(check => [check.id, check]));
|
|
46
|
+
copy.concerns = copy.concerns.map(concern => {
|
|
47
|
+
if (!concern.material || !concern.checkIds.length || !concern.checkIds.every(id => checks.get(id)?.timing === "postimplementation"))
|
|
48
|
+
return concern;
|
|
49
|
+
const expectedFutureAbsence = /\b(absent|missing|not (?:yet )?(?:implemented|present|available)|could not find|not found|does not exist|fails? with)\b/i.test(concern.reason)
|
|
50
|
+
&& /\b(?:before|pre[- ]?builder|not yet)\b/i.test(concern.reason)
|
|
51
|
+
&& /\b(?:implement|add|create|rerun|re-?run|builder|after implementation)\b/i.test(concern.nextAction);
|
|
52
|
+
return expectedFutureAbsence ? { ...concern, material: false } : concern;
|
|
53
|
+
});
|
|
54
|
+
return copy;
|
|
55
|
+
}
|
|
56
|
+
// The provider proposes verification work; the host owns contract identity,
|
|
57
|
+
// approved inputs, requirement text, depth, timestamps and evidence hashes.
|
|
58
|
+
const durableFields = qaContractCandidateV1Schema.properties;
|
|
59
|
+
const providerEvidence = { type: "object", additionalProperties: false, required: ["obligation", "references", "analysis"], properties: {
|
|
60
|
+
obligation: { type: "string", minLength: 1 }, analysis: { type: "string", minLength: 1 },
|
|
61
|
+
references: { type: "array", minItems: 1, items: { type: "object", additionalProperties: false, required: ["path", "locator"], properties: { path: { type: "string", minLength: 1 }, locator: { type: "string", minLength: 1 } } } },
|
|
62
|
+
} };
|
|
63
|
+
const providerBaseline = { type: "object", additionalProperties: false, required: ["id", "runtime", "procedure", "status", "relevance", "expectedTransition"], properties: {
|
|
64
|
+
id: { type: "string", minLength: 1 }, checkId: { type: "string", minLength: 1 }, applicability: { enum: ["applicable", "not-applicable", "unresolved"] }, predicateEvidence: { type: "array", items: { type: "string" } }, runtime: { type: "string", minLength: 1 }, procedure: { type: "string", minLength: 1 }, status: { enum: ["passed", "failed", "blocked", "not-run"] }, relevance: { type: "string", minLength: 1 }, expectedTransition: { type: "string", minLength: 1 },
|
|
65
|
+
} };
|
|
66
|
+
const providerDraftSchema = { type: "object", additionalProperties: false, required: ["checks", "coverage", "baseline", "preparationEvidence", "unresolved"], properties: {
|
|
67
|
+
checks: durableFields.checks, coverage: durableFields.coverage, baseline: { type: "array", items: providerBaseline }, preparationEvidence: { type: "array", minItems: 1, items: providerEvidence }, unresolved: durableFields.unresolved,
|
|
68
|
+
} };
|
|
69
|
+
const contractValidator = new Ajv({ allErrors: true });
|
|
70
|
+
const validateProviderDraft = contractValidator.compile(providerDraftSchema);
|
|
71
|
+
const validateAssessmentContract = contractValidator.compile(qaSemanticAssessmentV1Schema);
|
|
72
|
+
const validateChallengeContract = contractValidator.compile(qaChallengeReceiptV1Schema);
|
|
73
|
+
/** Providers sometimes echo host-owned evidence metadata into the draft. */
|
|
74
|
+
function normalizeProviderDraftMetadata(value) {
|
|
75
|
+
const copy = structuredClone(value);
|
|
76
|
+
for (const evidence of copy.preparationEvidence ?? []) {
|
|
77
|
+
const metadata = evidence;
|
|
78
|
+
delete metadata.operationId;
|
|
79
|
+
delete metadata.sessionId;
|
|
80
|
+
delete metadata.evidenceDigest;
|
|
81
|
+
delete metadata.graphReceiptRefs;
|
|
82
|
+
for (const reference of evidence.references ?? [])
|
|
83
|
+
delete reference.digest;
|
|
84
|
+
}
|
|
85
|
+
return copy;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* A mandatory requirement must have at least one mandatory verification check.
|
|
89
|
+
* Providers occasionally return a concrete check with the weaker advisory
|
|
90
|
+
* label. Promote the existing covering check at the host boundary; never
|
|
91
|
+
* invent a check, evidence, or requirement, and let the normal validator still
|
|
92
|
+
* enforce readiness and independent-QA requirements for the promoted check.
|
|
93
|
+
*/
|
|
94
|
+
export function enforceMandatoryCoverage(candidate) {
|
|
95
|
+
const copy = structuredClone(candidate);
|
|
96
|
+
const requirementIds = new Set(copy.requirements.map(requirement => requirement.id));
|
|
97
|
+
// Coverage is an explicit provider claim that a check verifies a
|
|
98
|
+
// requirement. Make that claim internally consistent by retaining the
|
|
99
|
+
// requirement on the check as well; do not add checks or coverage rows.
|
|
100
|
+
for (const row of copy.coverage) {
|
|
101
|
+
for (const checkId of row.checkIds) {
|
|
102
|
+
const check = copy.checks.find(item => item.id === checkId);
|
|
103
|
+
const hasUnknownReference = check?.requirementRefs.some(id => !requirementIds.has(id));
|
|
104
|
+
if (check && !hasUnknownReference && requirementIds.has(row.requirementId) && !check.requirementRefs.includes(row.requirementId))
|
|
105
|
+
check.requirementRefs.push(row.requirementId);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
for (const requirement of copy.requirements) {
|
|
109
|
+
if (requirement.obligation !== "mandatory")
|
|
110
|
+
continue;
|
|
111
|
+
const coverage = copy.coverage.find(row => row.requirementId === requirement.id);
|
|
112
|
+
const covering = coverage?.checkIds
|
|
113
|
+
.map(id => copy.checks.find(check => check.id === id))
|
|
114
|
+
.find(check => check?.requirementRefs.includes(requirement.id));
|
|
115
|
+
if (covering && covering.obligation !== "mandatory")
|
|
116
|
+
covering.obligation = "mandatory";
|
|
117
|
+
}
|
|
118
|
+
return copy;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Codex occasionally appends a made-up requirement ID to an otherwise valid
|
|
122
|
+
* check. Remove only those extraneous annotations when the check still has an
|
|
123
|
+
* exact authoritative reference. Never invent or remap an ID, and leave a
|
|
124
|
+
* check with no valid references untouched so normal validation fails closed.
|
|
125
|
+
*/
|
|
126
|
+
export function removeExtraneousRequirementReferences(candidate, inventory) {
|
|
127
|
+
const copy = structuredClone(candidate);
|
|
128
|
+
const allowed = new Set(inventory.map(requirement => requirement.id));
|
|
129
|
+
for (const check of copy.checks) {
|
|
130
|
+
const valid = check.requirementRefs.filter(id => allowed.has(id));
|
|
131
|
+
if (valid.length > 0 && valid.length !== check.requirementRefs.length)
|
|
132
|
+
check.requirementRefs = [...new Set(valid)];
|
|
133
|
+
}
|
|
134
|
+
return copy;
|
|
135
|
+
}
|
|
136
|
+
function providerBlocker(text, tag) {
|
|
137
|
+
// A refusal is not a malformed candidate and cannot be repaired by formatting.
|
|
138
|
+
if (text.includes(`${tag}_START`) && text.includes(`${tag}_END`) && /\{[\s\S]*\}/.test(text.slice(text.indexOf(`${tag}_START`) + tag.length + 6, text.indexOf(`${tag}_END`))))
|
|
139
|
+
return undefined;
|
|
140
|
+
if (!/\b(?:cannot|can't|unable|blocked|denied|not permitted|lack(?:s|ing) access)\b/i.test(text))
|
|
141
|
+
return undefined;
|
|
142
|
+
const explanation = text.replace(/\s+/g, " ").trim().slice(0, 500);
|
|
143
|
+
return `QA provider could not produce ${tag}: ${explanation}`;
|
|
144
|
+
}
|
|
5
145
|
export async function prepareVerificationContract(initial, inventory, deps) {
|
|
6
146
|
const store = deps.store;
|
|
7
147
|
const head = store.head(initial.runId, initial.workId, initial.admissionDigest);
|
|
@@ -29,13 +169,35 @@ export async function prepareVerificationContract(initial, inventory, deps) {
|
|
|
29
169
|
candidate.depthDecision = retainedDepth;
|
|
30
170
|
if (retained && canonicalContractJson(candidate.inputs) !== canonicalContractJson(initial.inputs))
|
|
31
171
|
return { status: "blocked", detail: "Retained draft inputs changed", nextAction: "Reconcile authoritative input changes" };
|
|
172
|
+
let correctionIssues = [];
|
|
32
173
|
const run = async (phase, kind, instruction, resultId) => {
|
|
174
|
+
const effectivePhase = phase === "prepare" && correctionIssues.length > 0 ? "repair" : phase;
|
|
175
|
+
if (phase === "assess")
|
|
176
|
+
instruction = `This is QA PREPARATION semantic assessment before Builder work, not final QA. Do not emit RAFI_QA_FAILURE_REPORT, RAFI_QA_COVERAGE, QA findings, or STEP_STATUS; return only the requested RAFI_QA_ASSESSMENT envelope.\n\n${instruction}`;
|
|
177
|
+
if (phase === "assess" && deps.provider === "codex")
|
|
178
|
+
instruction = `Codex role boundary: you are the independent PRE-BUILD QA ASSESSOR. Do not perform or summarize final QA, do not report Builder results, and do not emit RAFI_QA_FAILURE_REPORT_START/END. Your only output is the requested RAFI_QA_ASSESSMENT_START/END object describing whether the proposed contract is complete before Builder work.\n\n${instruction}`;
|
|
179
|
+
if (phase === "assess" && deps.provider === "claude")
|
|
180
|
+
instruction = `Claude response contract: every concerns entry MUST be an object exactly shaped as {"reason":"...","requirementIds":["exact-id"],"checkIds":["exact-check-id"],"material":true,"owner":"qa","nextAction":"..."}. Never use a legacy summary field, string concerns, or fields such as severity, title, message, evidence, category, or resolution. If there are no concerns, use concerns: [].\n\n${instruction}`;
|
|
181
|
+
if (phase === "prepare")
|
|
182
|
+
instruction = `This is a fresh draft response. Use only check IDs declared in this response; do not copy allocated check IDs or baseline/coverage references from an earlier draft or correction attempt. Every check reference in prose, procedures, expected evidence, and mappings must use the exact check.id declared in this response; never use descriptive aliases such as qa-subtract-export. Rafi will allocate stable IDs after validating this response.\n\n${instruction}`;
|
|
183
|
+
if (phase === "assess")
|
|
184
|
+
instruction = `The authoritative scope is the exact inventory and admitted slice in this prompt. Plan-level acceptance or test criteria outside the admitted slice are context only and must not be reported as missing inventory requirements.\n\n${instruction}`;
|
|
185
|
+
if (effectivePhase === "repair")
|
|
186
|
+
instruction = `Targeted QA contract repair only. Preserve valid checks, methods, bindings, and evidence; change only what the validation issues require. Do not redo investigation, expand scope, invent evidence, or change host-owned fields. Return the complete corrected five-field draft.\n\n${instruction}`;
|
|
187
|
+
if (["prepare", "assess", "challenge"].includes(phase) && deps.hostFacts)
|
|
188
|
+
instruction = `${deps.hostFacts}\n\n${instruction}`;
|
|
189
|
+
if (["prepare", "assess", "challenge"].includes(phase))
|
|
190
|
+
instruction = `${FILE_SCOPE_GUIDANCE}\n\n${instruction}`;
|
|
191
|
+
if (["prepare", "assess", "challenge"].includes(phase))
|
|
192
|
+
instruction = `${EXECUTABLE_CHECK_GUIDANCE}\n\n${APPROVED_PATH_BASELINE_GUIDANCE}\n\nHost scope rule for current-branch multi-ticket builds: never use git status, git diff HEAD, or a HEAD-based whole-tree comparison as ticket-scope proof because HEAD may predate a completed sibling ticket. Use the host-captured preimplementation baseline observation and approved product paths/ticket-owned files.\n\n${instruction}`;
|
|
193
|
+
if (["prepare", "assess", "challenge"].includes(phase))
|
|
194
|
+
instruction = `${ROLLBACK_VERIFICATION_GUIDANCE}\nThe Builder must invoke the exact launching CLI as: ${process.env.RAFI_REHEARSAL_COMMAND ?? "rafi qa-rehearse-rollback"}\n\n${instruction}`;
|
|
33
195
|
const operationId = `qa-preparation:${randomUUID()}`;
|
|
34
196
|
store.setState(initial.runId, initial.workId, initial.admissionDigest, ["assess", "challenge"].includes(phase) ? "validating" : "preparing", `Read-only ${phase} operation intended`, store.head(initial.runId, initial.workId, initial.admissionDigest).generation);
|
|
35
197
|
store.reserve(budget, operationId, kind, { phase, instruction, inputDigest: inventoryDigest(inventory) }, deps.now(), resultId);
|
|
36
198
|
const current = store.budget(initial.runId, initial.workId, initial.admissionDigest);
|
|
37
199
|
const dispatchedAt = deps.now();
|
|
38
|
-
const result = await deps.dispatch(
|
|
200
|
+
const result = await deps.dispatch(effectivePhase, instruction, operationId, Math.max(1, current.deadlineMs - deps.now()));
|
|
39
201
|
store.retainResult(budget, operationId, result, result.usage);
|
|
40
202
|
store.metric(initial.runId, initial.workId, initial.admissionDigest, `metric:preparation-operation:${operationId}`, kind === "repair" ? "format-repair" : "phase-observation", { phase: phase === "planner" ? "planning" : phase === "challenge" ? "challenge" : phase === "repair" ? "repair" : "preparation", durationMs: Math.max(0, deps.now() - dispatchedAt), costUsd: result.usage?.costUsd ?? null, inputTokens: result.usage?.inputTokens ?? null, outputTokens: result.usage?.outputTokens ?? null });
|
|
41
203
|
if (deps.now() >= current.deadlineMs)
|
|
@@ -44,49 +206,167 @@ export async function prepareVerificationContract(initial, inventory, deps) {
|
|
|
44
206
|
};
|
|
45
207
|
const parsed = async (result, tag) => {
|
|
46
208
|
let response = result;
|
|
209
|
+
const phaseBoundary = tag === "RAFI_QA_ASSESSMENT"
|
|
210
|
+
? "This is QA PREPARATION semantic assessment before Builder work, not final QA. Do not emit RAFI_QA_FAILURE_REPORT, RAFI_QA_COVERAGE, QA findings, or STEP_STATUS. Return only the requested RAFI_QA_ASSESSMENT envelope."
|
|
211
|
+
: tag === "RAFI_QA_CANDIDATE"
|
|
212
|
+
? "This is QA PREPARATION contract drafting before Builder work, not final QA. Do not emit RAFI_QA_FAILURE_REPORT, RAFI_QA_COVERAGE, or STEP_STATUS. Return only the requested RAFI_QA_CANDIDATE envelope."
|
|
213
|
+
: "";
|
|
47
214
|
for (let repair = 0;; repair++) {
|
|
48
215
|
try {
|
|
49
216
|
return parsePreparationArtifact(response.text, tag);
|
|
50
217
|
}
|
|
51
218
|
catch (error) {
|
|
219
|
+
// A provider can answer the preparation assessor with a final-QA
|
|
220
|
+
// artifact from the wrong role. That is a repairable protocol error,
|
|
221
|
+
// not a refusal to participate in preparation.
|
|
222
|
+
const blocker = tag === "RAFI_QA_ASSESSMENT" && response.text.includes("RAFI_QA_FAILURE_REPORT_START")
|
|
223
|
+
? undefined
|
|
224
|
+
: providerBlocker(response.text, tag);
|
|
225
|
+
if (blocker)
|
|
226
|
+
throw new Error(blocker);
|
|
227
|
+
// A provider can return a short progress/prose response without any
|
|
228
|
+
// JSON at all. Treat that as a bounded format defect unless it is an
|
|
229
|
+
// explicit refusal; the repair turn is still required to produce the
|
|
230
|
+
// exact envelope and is subject to the same strict parser below.
|
|
52
231
|
if (repair >= 2)
|
|
53
232
|
throw error;
|
|
54
|
-
response = await run("repair", "repair",
|
|
233
|
+
response = await run("repair", "repair", `${phaseBoundary}\nBounded format repair of the retained result. You may use read-only tools to inspect the frozen QA snapshot if needed, but do not edit files, redo the investigation, expand scope, invent evidence, or change host-owned fields. Return exactly one ${tag}_START JSON ${tag}_END envelope and no prose. If this is RAFI_QA_ASSESSMENT, it is still the pre-build assessor role: never convert the response into a final QA report or RAFI_QA_FAILURE_REPORT. If the retained response contains one JSON object without markers, copy that exact object between the markers; do not summarize it, add fields, or infer missing values. If it contains multiple objects or no complete object, return a corrected artifact only when the retained response itself unambiguously identifies the one requested object.\nRetained response:\n${response.text}`, result.operationId ?? result.sessionId);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
/**
|
|
238
|
+
* Validate provider-owned artifacts at their response boundary. A provider
|
|
239
|
+
* sometimes copies the preparation draft into an independent assessment;
|
|
240
|
+
* that is not an assessment contract and must be repaired as a bounded
|
|
241
|
+
* schema error rather than being allowed to fail much later in assembly.
|
|
242
|
+
*/
|
|
243
|
+
const parsedContract = async (result, tag, validate, name, normalize) => {
|
|
244
|
+
let response = result;
|
|
245
|
+
for (let repair = 0;; repair++) {
|
|
246
|
+
try {
|
|
247
|
+
const value = normalize ? normalize(await parsed(response, tag), response) : await parsed(response, tag);
|
|
248
|
+
if (!validate(value))
|
|
249
|
+
throw new Error(`${name} contract validation failed: ${(validate.errors ?? []).map(error => JSON.stringify(error)).join("; ")}`);
|
|
250
|
+
return { value, response };
|
|
251
|
+
}
|
|
252
|
+
catch (error) {
|
|
253
|
+
if (repair >= 2)
|
|
254
|
+
throw error;
|
|
255
|
+
const assessmentRepairContract = name === "QA semantic assessment"
|
|
256
|
+
? `Every concerns item must be an object with exactly these six fields and no others: {"reason":"...","requirementIds":[],"checkIds":[],"material":true,"owner":"qa","nextAction":"..."}. Replace legacy fields such as description, summary, severity, title, message, evidence, category, and resolution; do not merely wrap or rename an incomplete object. Use exact authoritative IDs only, and use concerns: [] when no supported concern remains.`
|
|
257
|
+
: "";
|
|
258
|
+
response = await run("repair", "repair", `Bounded schema repair for ${name}. You may use read-only tools to inspect the frozen QA snapshot if needed, but preserve the substantive independent assessment and return ONLY the exact ${tag}_START/END artifact matching the supplied schema. Do not edit files, add evidence, requirements, checks, scope, or host-owned identity. For RAFI_QA_ASSESSMENT, remain in the pre-build assessment role and never return a final QA failure report. ${assessmentRepairContract} Validation error: ${error instanceof Error ? error.message : String(error)}\nRetained response:\n${response.text}`, response.operationId ?? response.sessionId);
|
|
55
259
|
}
|
|
56
260
|
}
|
|
57
261
|
};
|
|
58
262
|
try {
|
|
59
263
|
if (!retained && !retainedDepth && decisionIssues.length) {
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
264
|
+
let plannerIssues = decisionIssues;
|
|
265
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
266
|
+
const response = await run("planner", attempt ? "repair" : "planning", `Read-only depth assessment of admitted scope. Select Standard or higher unless explicit Focused low-risk evidence is established. Return RAFI_QA_DEPTH_START/END with QaPreparationDepthDecisionV1. Schema: ${JSON.stringify(qaDepthDecisionSchema)}. Policy qa-preparation-v1. Host minimums: interactions/compatibility/state/external dependencies=3; authorization/security/data/migration/concurrency/recovery/deployment=4; unresolved architectural uncertainty=5. Do not change the admitted scope, inputs, requirements, or risk evidence. Every riskFactors.category must appear verbatim in minimumReasons. ${attempt ? `Correct these validation issues from the previous response: ${JSON.stringify(plannerIssues)}` : ""}\n${JSON.stringify(initial)}`);
|
|
267
|
+
let proposed;
|
|
268
|
+
try {
|
|
269
|
+
proposed = await parsed(response, "RAFI_QA_DEPTH");
|
|
270
|
+
}
|
|
271
|
+
catch (error) {
|
|
272
|
+
plannerIssues = [`QA planner response was not a valid RAFI_QA_DEPTH envelope: ${error instanceof Error ? error.message : String(error)}`];
|
|
273
|
+
if (attempt === 2)
|
|
274
|
+
throw new Error(`depth-decision-incomplete: ${plannerIssues.join("; ")}`);
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
proposed.plannerOperationId = response.operationId;
|
|
278
|
+
proposed.plannerIdentity = response.sessionId;
|
|
279
|
+
// minimumReasons is a host-validated restatement of the planner's
|
|
280
|
+
// riskFactors. Fill only omitted exact category labels; never alter
|
|
281
|
+
// the selected level, risk evidence, or rationale.
|
|
282
|
+
const requiredReasons = proposed.riskFactors.map(risk => risk.category);
|
|
283
|
+
proposed.minimumReasons = [...proposed.minimumReasons, ...requiredReasons.filter(category => !proposed.minimumReasons.includes(category))];
|
|
284
|
+
plannerIssues = validateDepthDecision(proposed);
|
|
285
|
+
if (!plannerIssues.length) {
|
|
286
|
+
candidate.depthDecision = proposed;
|
|
287
|
+
store.reviseDepth(budget, candidate.depthDecision);
|
|
288
|
+
break;
|
|
289
|
+
}
|
|
290
|
+
if (attempt === 2)
|
|
291
|
+
throw new Error(`depth-decision-incomplete: ${plannerIssues.join("; ")}`);
|
|
292
|
+
}
|
|
68
293
|
}
|
|
69
294
|
else
|
|
70
295
|
store.reviseDepth(budget, candidate.depthDecision);
|
|
71
296
|
store.retainProgress(budget, "depth-decision", candidate.depthDecision);
|
|
72
297
|
let authorSessionId = retained?.authorSessionId ?? "";
|
|
73
298
|
for (let pass = 0; pass < (candidate.depthDecision.level <= 2 ? 2 : 3) && (!retained || targetedConcerns.length > 0 || validateCandidate(candidate, inventory, store.equivalentResolver()).length > 0); pass++) {
|
|
74
|
-
const response = await run("prepare", "investigation", `QA preparation only. Inspect relevant source/tests; do not implement, install dependencies, or expand admitted product scope. Return RAFI_QA_CANDIDATE_START/END with
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
299
|
+
const response = await run("prepare", "investigation", `QA preparation only. Inspect relevant source/tests; do not implement, install dependencies, or expand admitted product scope. Return RAFI_QA_CANDIDATE_START/END with the provider draft schema: ${JSON.stringify(providerDraftSchema)}. Return ONLY the five draft fields in that schema: do not return contract identity, depth, inputs or requirements. Rafi owns and calculates timestamps, source and evidence hashes, operation/session identities and requirement text; you do not need shell access or hash calculations. Cite existing workspace-relative file paths and locations that you inspected. Use only the exact authoritative requirement IDs listed in the inventory below. Never invent, shorten, rename, derive, or copy a requirement ID from a filename, check ID, plan text, or sibling ticket. Every check.requirementRefs entry must be an exact ID from the inventory, and every coverage.requirementId must be an exact ID from the inventory; if a check has no applicable requirement, do not emit that check. Use the supplied requirement IDs and cover each original requirement, including rules and skills, with at least one mandatory check for each mandatory requirement. Each coverage row's checkIds must name checks whose requirementRefs array includes that exact row requirementId; every check must have at least one valid requirementRef. Verification cwd must be relative to the workspace; never copy an absolute disposable-snapshot path into cwd, commands, evidence, or prose. Use the stable relative location scratch/<filename> for bounded temporary logs. dependsOnChecks references other check IDs; prerequisiteRefs references only IDs of observations in baseline, not check IDs or host-preflight labels. Use empty arrays when none apply. Checks for future Builder work must use postimplementation timing; only checks already verified before Builder starts may use preimplementation or both timing, and every mandatory preimplementation/both check requires a matching passed baseline observation bound by checkId. Conditional observations also need predicate evidence. In this schema preimplementation means already verified before Builder starts, not a step Builder must perform before editing. For test-first or other Builder-executed sequencing, use postimplementation timing and require retained evidence proving the prescribed order; do not claim a future step has already passed. Future implementation and verification evidence is expected to be produced by Builder and final QA; specify it in the checks rather than treating its ordinary pre-build absence as a material unresolved concern. Do report genuine current blockers or uncertainties as unresolved. Scope checks must filter to approved product paths or ticket files; never require an exact whole-worktree status because Rafi-owned bookkeeping paths such as .tickets, .rafi, docs, configuration, and agent instructions are expected host changes. Supply one preparationEvidence entry for EACH required obligation, using its exact obligation label separately, with its own analysis and cited existing files; do not combine labels into a single entry. Supply concrete check methods/evidence and complete coverage. No disposition or waiver can be agent-authored. If risks exceed selected depth, return a separate RAFI_QA_ESCALATION_START/END JSON block {"version":1,"rationale":"...","riskFactors":[{"category":"...","rationale":"...","references":["..."]}]}; do not change the depth yourself.\nValidation issues to correct from the previous draft: ${JSON.stringify(correctionIssues)}\nPreviously retained independent concerns requiring targeted investigation and concrete resolution: ${JSON.stringify(targetedConcerns)}\nRequired obligations: ${depthObligations(candidate.depthDecision.level).join(", ")}\nThe authoritative candidate/input inventory below contains the exact allowed requirement ID set.\nAuthoritative candidate/input inventory:\n${JSON.stringify(candidate)}`);
|
|
300
|
+
const rawProposal = await parsed(response, "RAFI_QA_CANDIDATE");
|
|
301
|
+
const legacy = Object.hasOwn(rawProposal, "runId");
|
|
302
|
+
const proposal = legacy ? rawProposal : normalizeProviderDraftMetadata(rawProposal);
|
|
303
|
+
if (!proposal || typeof proposal !== "object" || Array.isArray(proposal))
|
|
304
|
+
throw new Error("QA preparation draft must be an object");
|
|
305
|
+
if (!legacy && !validateProviderDraft(proposal)) {
|
|
306
|
+
correctionIssues = (validateProviderDraft.errors ?? []).map(error => `${error.instancePath || "/"} ${error.message}`);
|
|
307
|
+
if (pass + 1 >= (candidate.depthDecision.level <= 2 ? 2 : 3))
|
|
308
|
+
throw new Error(`QA preparation draft correction exhausted: ${correctionIssues.join("; ")}`);
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
311
|
+
if (legacy) {
|
|
312
|
+
const depth = proposal.depthDecision;
|
|
313
|
+
const inputs = proposal.inputs;
|
|
314
|
+
// Legacy models sometimes remove Markdown code ticks from descriptive
|
|
315
|
+
// text. Compare those fields narrowly; never normalize references or
|
|
316
|
+
// risk categories and never publish the provider's copy.
|
|
317
|
+
const prose = (value) => value.replace(/`/g, "");
|
|
318
|
+
const sameDepthProse = !!depth && typeof depth.rationale === "string" && Array.isArray(depth.minimumReasons) && depth.minimumReasons.every(item => typeof item === "string") && Array.isArray(depth.riskFactors) && depth.riskFactors.every(item => item && typeof item.rationale === "string") && (!depth.lowRisk || [depth.lowRisk.narrowBehavior, depth.lowRisk.understoodDependencies, depth.lowRisk.adequateVerification].every(item => typeof item === "string")) && prose(depth.rationale) === prose(candidate.depthDecision.rationale)
|
|
319
|
+
&& canonicalContractJson(depth.minimumReasons.map(prose)) === canonicalContractJson(candidate.depthDecision.minimumReasons.map(prose))
|
|
320
|
+
&& canonicalContractJson(depth.riskFactors.map(risk => ({ ...risk, rationale: prose(risk.rationale) }))) === canonicalContractJson(candidate.depthDecision.riskFactors.map(risk => ({ ...risk, rationale: prose(risk.rationale) })))
|
|
321
|
+
&& canonicalContractJson(depth.lowRisk ? { narrowBehavior: prose(depth.lowRisk.narrowBehavior), understoodDependencies: prose(depth.lowRisk.understoodDependencies), adequateVerification: prose(depth.lowRisk.adequateVerification) } : null) === canonicalContractJson(candidate.depthDecision.lowRisk ? { narrowBehavior: prose(candidate.depthDecision.lowRisk.narrowBehavior), understoodDependencies: prose(candidate.depthDecision.lowRisk.understoodDependencies), adequateVerification: prose(candidate.depthDecision.lowRisk.adequateVerification) } : null);
|
|
322
|
+
const matchingReference = (actual, expected) => actual === expected || (!actual.startsWith("/") && !actual.split(/[\\/]/).includes("..") && expected.endsWith(`/${actual}`));
|
|
323
|
+
const matchingInputs = Array.isArray(inputs) && inputs.length === candidate.inputs.length && inputs.every((input, index) => {
|
|
324
|
+
const expected = candidate.inputs[index];
|
|
325
|
+
return expected && input && typeof input.reference === "string" && matchingReference(input.reference, expected.reference) && canonicalContractJson({ ...input, reference: expected.reference }) === canonicalContractJson(expected);
|
|
326
|
+
});
|
|
327
|
+
if (proposal.runId !== candidate.runId || proposal.workId !== candidate.workId || proposal.admissionDigest !== candidate.admissionDigest || proposal.contractId !== candidate.contractId || proposal.revision !== candidate.revision || !depth || depth.level !== candidate.depthDecision.level || depth.minimumLevel !== candidate.depthDecision.minimumLevel || depth.policyVersion !== candidate.depthDecision.policyVersion || depth.decisionId !== candidate.depthDecision.decisionId || !sameDepthProse || !matchingInputs)
|
|
328
|
+
throw new Error("Preparation changed host-owned identity/depth/input authority");
|
|
329
|
+
}
|
|
330
|
+
else if (Object.keys(proposal).some(key => !["checks", "coverage", "baseline", "preparationEvidence", "unresolved"].includes(key)))
|
|
331
|
+
throw new Error("QA preparation draft contains unexpected host-owned or unsupported fields");
|
|
332
|
+
if (![proposal.checks, proposal.coverage, proposal.baseline, proposal.preparationEvidence, proposal.unresolved].every(Array.isArray))
|
|
333
|
+
throw new Error("QA preparation draft is missing required proposal arrays");
|
|
334
|
+
if (!legacy && !deps.normalizeDraft)
|
|
335
|
+
throw new Error("QA preparation host evidence normalization is unavailable");
|
|
336
|
+
const draft = { ...structuredClone(candidate), checks: proposal.checks, coverage: proposal.coverage, baseline: proposal.baseline, preparationEvidence: proposal.preparationEvidence, unresolved: proposal.unresolved, ...(legacy && proposal.requirements ? { requirements: proposal.requirements } : {}) };
|
|
337
|
+
draft.createdAt = candidate.createdAt;
|
|
338
|
+
if (candidate.predecessorDigest === undefined)
|
|
339
|
+
delete draft.predecessorDigest;
|
|
340
|
+
else
|
|
341
|
+
draft.predecessorDigest = candidate.predecessorDigest;
|
|
342
|
+
if (Array.isArray(draft.requirements)) {
|
|
343
|
+
const authoritative = new Map(inventory.map(item => [item.id, item]));
|
|
344
|
+
const ids = draft.requirements.map(item => item.id);
|
|
345
|
+
if (ids.some(id => !authoritative.has(id)) || new Set(ids).size !== ids.length)
|
|
346
|
+
throw new Error("QA preparation draft contains unknown or duplicate requirement identities");
|
|
347
|
+
draft.requirements = draft.requirements.map(item => authoritative.has(item.id) ? structuredClone(authoritative.get(item.id)) : item);
|
|
348
|
+
const present = new Set(draft.requirements.map(item => item.id));
|
|
349
|
+
draft.requirements.push(...inventory.filter(item => !present.has(item.id)).map(item => structuredClone(item)));
|
|
350
|
+
}
|
|
351
|
+
try {
|
|
352
|
+
candidate = removeExtraneousRequirementReferences(enforceMandatoryCoverage(deps.normalizeDraft ? deps.normalizeDraft(draft, response) : draft), inventory);
|
|
353
|
+
}
|
|
354
|
+
catch (error) {
|
|
355
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
356
|
+
correctionIssues = [`Draft could not be normalized: ${detail}`];
|
|
357
|
+
if (pass + 1 >= (candidate.depthDecision.level <= 2 ? 2 : 3))
|
|
358
|
+
throw new Error(`QA preparation draft correction exhausted: ${detail}`);
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
79
361
|
authorSessionId = response.sessionId;
|
|
80
362
|
for (const evidence of candidate.preparationEvidence) {
|
|
81
363
|
evidence.operationId = response.operationId;
|
|
82
364
|
evidence.sessionId = response.sessionId;
|
|
83
365
|
}
|
|
84
366
|
if (response.text.includes("RAFI_QA_ESCALATION_START")) {
|
|
85
|
-
const request = parsePreparationArtifact(response.text, "RAFI_QA_ESCALATION");
|
|
86
|
-
if (!request.rationale?.trim() || !request.riskFactors?.length || request.riskFactors.some(risk => !(risk.category in QA_RISK_MINIMUMS) || !risk.references?.length))
|
|
87
|
-
throw new Error("Incomplete risk escalation evidence");
|
|
367
|
+
const request = parseProviderContract(qaPreparationEscalationProposalV1Schema, parsePreparationArtifact(response.text, "RAFI_QA_ESCALATION"), "QA preparation escalation");
|
|
88
368
|
const minimum = Math.max(candidate.depthDecision.level, ...request.riskFactors.map(risk => QA_RISK_MINIMUMS[risk.category]));
|
|
89
|
-
const revision = await run("planner", "escalation", `Revise only the QA depth for this admitted scope. Do not expand product scope. Host minimum ${minimum}; keep predecessorDecisionId ${candidate.depthDecision.decisionId}. Return RAFI_QA_DEPTH_START/END following ${JSON.stringify(
|
|
369
|
+
const revision = await run("planner", "escalation", `Revise only the QA depth for this admitted scope. Do not expand product scope. Host minimum ${minimum}; keep predecessorDecisionId ${candidate.depthDecision.decisionId}. Return RAFI_QA_DEPTH_START/END following ${JSON.stringify(qaDepthDecisionSchema)}. Previous decision: ${JSON.stringify(candidate.depthDecision)}; cited risk evidence: ${JSON.stringify(request)}`);
|
|
90
370
|
const decision = await parsed(revision, "RAFI_QA_DEPTH");
|
|
91
371
|
decision.plannerIdentity = revision.sessionId;
|
|
92
372
|
decision.plannerOperationId = revision.operationId;
|
|
@@ -103,7 +383,7 @@ export async function prepareVerificationContract(initial, inventory, deps) {
|
|
|
103
383
|
const structural = validateCandidate(candidate, inventory, store.equivalentResolver());
|
|
104
384
|
if (!structural.length)
|
|
105
385
|
break;
|
|
106
|
-
|
|
386
|
+
correctionIssues = structural;
|
|
107
387
|
}
|
|
108
388
|
store.retainProgress(budget, "draft", { candidate, authorSessionId });
|
|
109
389
|
const structural = validateCandidate(candidate, inventory, store.equivalentResolver());
|
|
@@ -111,11 +391,70 @@ export async function prepareVerificationContract(initial, inventory, deps) {
|
|
|
111
391
|
throw new Error(`Contract incomplete: ${structural.join("; ")}`);
|
|
112
392
|
const binding = { draftPayloadDigest: draftDigest(candidate), inventoryDigest: inventoryDigest(candidate.requirements), policyVersion: candidate.depthDecision.policyVersion, authorSessionId };
|
|
113
393
|
const retainedAssessment = store.progress(budget, "assessment");
|
|
114
|
-
const
|
|
115
|
-
|
|
394
|
+
const normalizeAssessment = (value, response, fallback) => {
|
|
395
|
+
const normalized = normalizeAssessmentObligations(normalizeAssessmentMetadata(value), depthObligations(candidate.depthDecision.level));
|
|
396
|
+
// These bindings are host-owned. A repair may omit them or emit a
|
|
397
|
+
// placeholder; restore only the exact current binding and retained
|
|
398
|
+
// obligation labels, never provider-invented content.
|
|
399
|
+
normalized.draftPayloadDigest = binding.draftPayloadDigest;
|
|
400
|
+
normalized.inventoryDigest = binding.inventoryDigest;
|
|
401
|
+
normalized.policyVersion = binding.policyVersion;
|
|
402
|
+
normalized.authorSessionId = binding.authorSessionId;
|
|
403
|
+
// Provider JSON does not own dispatch metadata. Bind the actual adapter
|
|
404
|
+
// receipt before schema validation so an omitted echo cannot block QA.
|
|
405
|
+
normalized.sessionId = response.sessionId;
|
|
406
|
+
if (response.operationId)
|
|
407
|
+
normalized.operationId = response.operationId;
|
|
408
|
+
if ((!normalized.assessedObligations || normalized.assessedObligations.length === 0) && fallback?.assessedObligations?.length)
|
|
409
|
+
normalized.assessedObligations = [...fallback.assessedObligations];
|
|
410
|
+
if ((!normalized.assessedRequirementIds || normalized.assessedRequirementIds.length === 0) && fallback?.assessedRequirementIds?.length)
|
|
411
|
+
normalized.assessedRequirementIds = [...fallback.assessedRequirementIds];
|
|
412
|
+
return normalized;
|
|
413
|
+
};
|
|
414
|
+
let assessmentResponse = retainedAssessment?.draftPayloadDigest === draftDigest(candidate) ? undefined : await run("assess", "assessment", `Independent semantic assessment in a fresh conversation. Assess each original requirement and selected-depth obligation; identify omissions, conflicts, practical verification gaps, accidental scope, and unsupported dispositions. Treat Rafi-owned bookkeeping paths (.tickets, .rafi, generated docs/configuration, and agent instructions) as expected host changes. Scope concerns must be evaluated against approved product paths and ticket requirements; do not require an exact full-worktree status listing. Future Builder actions are not preparation failures: when an approved requirement specifies test-first ordering, a migration, rollback rehearsal, or other implementation-time sequence, represent it as a mandatory postimplementation check requiring retained Builder evidence. Do not mark the absence of that future evidence before Builder starts as a material concern, and do not demand that preparation reproduce historical order. Only report a material concern when the contract cannot state an actionable postimplementation check or when an already-required preimplementation fact is missing. Return RAFI_QA_ASSESSMENT_START/END with QaSemanticAssessmentV1 (schema: ${JSON.stringify(qaPreparationDepthDecisionV1Schema)}). The concerns field is an array of objects, never strings; every concern object must have exactly these six fields and no others: {reason:string, requirementIds:string[], checkIds:string[], material:boolean, owner:string, nextAction:string}. Do not use legacy fields such as severity, evidence, title, message, category, or resolution. Use concerns: [] only when all issues are resolved. assessedRequirementIds MUST contain every ID in this exact list, even when a requirement is host-owned or has no concern: ${JSON.stringify(candidate.requirements.map(requirement => requirement.id))}. Put every individually assessed required obligation label in assessedObligations, using the exact labels below; do not combine labels or claim to have assessed anything you did not assess. Required obligation labels: ${JSON.stringify(depthObligations(candidate.depthDecision.level))}. Bind exactly ${JSON.stringify(binding)}.\nPrior independent concerns requiring concrete resolution evidence: ${JSON.stringify(targetedConcerns)}\nOriginal inventory:\n${JSON.stringify(inventory)}\nDraft:\n${JSON.stringify(candidate)}`);
|
|
415
|
+
let assessed = assessmentResponse
|
|
416
|
+
? await parsedContract(assessmentResponse, "RAFI_QA_ASSESSMENT", validateAssessmentContract, "QA semantic assessment", (value, response) => normalizeAssessment(value, response))
|
|
417
|
+
: { value: retainedAssessment, response: undefined };
|
|
418
|
+
// The author identity is host-owned. The provider must supply an independent
|
|
419
|
+
// session identity, but it cannot prove or safely author the builder's ID.
|
|
420
|
+
assessed.value.authorSessionId = binding.authorSessionId;
|
|
421
|
+
for (let repair = 0; assessmentResponse && repair < 3; repair++) {
|
|
422
|
+
const semanticIssues = validateAssessment(candidate, assessed.value, authorSessionId);
|
|
423
|
+
if (!semanticIssues.length)
|
|
424
|
+
break;
|
|
425
|
+
if (repair === 2)
|
|
426
|
+
throw new Error(semanticIssues.join("; "));
|
|
427
|
+
const preservedAuthorSessionId = assessed.value.authorSessionId;
|
|
428
|
+
const boundedRetainedAssessment = {
|
|
429
|
+
concerns: assessed.value.concerns,
|
|
430
|
+
assessedRequirementIds: assessed.value.assessedRequirementIds,
|
|
431
|
+
assessedObligations: assessed.value.assessedObligations,
|
|
432
|
+
draftPayloadDigest: assessed.value.draftPayloadDigest,
|
|
433
|
+
inventoryDigest: assessed.value.inventoryDigest,
|
|
434
|
+
policyVersion: assessed.value.policyVersion,
|
|
435
|
+
authorSessionId: assessed.value.authorSessionId,
|
|
436
|
+
};
|
|
437
|
+
const freshAssessment = semanticIssues.includes("Assessment lacks independent authorship");
|
|
438
|
+
assessmentResponse = await run(freshAssessment ? "assess" : "repair", freshAssessment ? "assessment" : "repair", `${freshAssessment ? "Fresh independent assessment required: use a new QA provider session, distinct from the preparation author session. Do not reuse or claim the preparation session." : "Bounded semantic repair for QA semantic assessment."} Preserve the independent review, but correct only these host validation issues: ${JSON.stringify(semanticIssues)}. Use only requirement IDs from the authoritative inventory and only check IDs declared in the supplied draft; never invent aliases such as qa-c1 or qa-c6, and never cite undefined retained evidence. If a concern is not supported by an actual requirement or declared check, remove or rewrite that concern. Return only RAFI_QA_ASSESSMENT_START/END with the complete corrected assessment. Binding: ${JSON.stringify(binding)}\nAuthoritative requirement IDs: ${JSON.stringify(candidate.requirements.map(requirement => requirement.id))}\nDeclared check IDs: ${JSON.stringify(candidate.checks.map(check => check.id))}\nBounded retained assessment fields to preserve or correct:\n${JSON.stringify(boundedRetainedAssessment)}`, assessed.response?.operationId ?? assessed.response?.sessionId);
|
|
439
|
+
const repaired = await parsedContract(assessmentResponse, "RAFI_QA_ASSESSMENT", validateAssessmentContract, "QA semantic assessment", (value, response) => normalizeAssessment(value, response, assessed.value));
|
|
440
|
+
// Repairs may omit this immutable binding while rewriting concerns. Preserve
|
|
441
|
+
// it only when the prior parsed assessment supplied it; a conflicting value
|
|
442
|
+
// remains visible to validateAssessment and still fails closed.
|
|
443
|
+
if (repaired.value.authorSessionId === undefined && preservedAuthorSessionId !== undefined)
|
|
444
|
+
repaired.value.authorSessionId = preservedAuthorSessionId;
|
|
445
|
+
repaired.value.authorSessionId = binding.authorSessionId;
|
|
446
|
+
assessed = repaired;
|
|
447
|
+
}
|
|
448
|
+
const assessment = normalizeExpectedFutureAssessment(normalizeAssessmentMetadata(assessed.value), candidate);
|
|
449
|
+
// These binding fields are host-owned. Providers frequently copy or
|
|
450
|
+
// truncate the inventory digest while describing their assessment; that
|
|
451
|
+
// must not invalidate an otherwise substantive independent review.
|
|
452
|
+
assessment.draftPayloadDigest = binding.draftPayloadDigest;
|
|
453
|
+
assessment.inventoryDigest = binding.inventoryDigest;
|
|
454
|
+
assessment.policyVersion = binding.policyVersion;
|
|
116
455
|
if (assessmentResponse) {
|
|
117
|
-
assessment.sessionId =
|
|
118
|
-
assessment.operationId =
|
|
456
|
+
assessment.sessionId = assessed.response.sessionId;
|
|
457
|
+
assessment.operationId = assessed.response.operationId;
|
|
119
458
|
}
|
|
120
459
|
store.retainProgress(budget, "assessment", assessment);
|
|
121
460
|
let challenge = store.progress(budget, "challenge");
|
|
@@ -123,9 +462,10 @@ export async function prepareVerificationContract(initial, inventory, deps) {
|
|
|
123
462
|
challenge = undefined;
|
|
124
463
|
if (candidate.depthDecision.level === 5 && !challenge) {
|
|
125
464
|
const response = await run("challenge", "challenge", `Independent approach challenge, fresh assessor conversation. Planning only. Return RAFI_QA_CHALLENGE_START/END with QaChallengeReceiptV1 (schema: ${JSON.stringify(qaChallengeReceiptV1Schema)}): independently critique the author's retained approach-proposal evidence (do not author a replacement), independent contract conclusions and separate approachConcerns for scope, invariants, failure-boundaries, verification. Unresolved material concerns prevent readiness. Binding: ${JSON.stringify(binding)}. Previously retained concerns requiring concrete resolution: ${JSON.stringify(targetedConcerns)}.\n${JSON.stringify(candidate)}`);
|
|
126
|
-
|
|
127
|
-
challenge
|
|
128
|
-
challenge.
|
|
465
|
+
const challenged = await parsedContract(response, "RAFI_QA_CHALLENGE", validateChallengeContract, "QA approach challenge", (value, response) => normalizeAssessment(value, response));
|
|
466
|
+
challenge = normalizeAssessmentMetadata(challenged.value);
|
|
467
|
+
challenge.sessionId = challenged.response.sessionId;
|
|
468
|
+
challenge.operationId = challenged.response.operationId;
|
|
129
469
|
store.retainProgress(budget, "challenge", challenge);
|
|
130
470
|
}
|
|
131
471
|
const contract = assembleContract(candidate, inventory, assessment, authorSessionId, challenge, store.equivalentResolver());
|
|
@@ -144,7 +484,8 @@ export async function prepareVerificationContract(initial, inventory, deps) {
|
|
|
144
484
|
const status = current.reservations.some(reservation => !reservation.resultDigest) ? "uncertain" : "incomplete";
|
|
145
485
|
store.setState(initial.runId, initial.workId, initial.admissionDigest, status, detail, store.head(initial.runId, initial.workId, initial.admissionDigest).generation);
|
|
146
486
|
store.metric(initial.runId, initial.workId, initial.admissionDigest, `metric:preparation-failure:${current.id}:${current.reservations.length}`, "preparation-failure", { status: "blocked" });
|
|
147
|
-
|
|
487
|
+
const failureDigest = createHash("sha256").update(JSON.stringify({ status, detail })).digest("hex");
|
|
488
|
+
store.event(initial.runId, initial.workId, `preparation-failure:${current.id}:${current.reservations.length}:${failureDigest}`, "preparation-failure", { status, detail });
|
|
148
489
|
return { status, detail, nextAction: status === "uncertain" ? "Reconcile retained provider dispatch; do not replay uncertain work" : "Resolve remaining concerns or record authorized operational extension, then retry" };
|
|
149
490
|
}
|
|
150
491
|
}
|
|
@@ -33,7 +33,11 @@ export function admittedPlanInput(projectDir, ticket) {
|
|
|
33
33
|
throw new Error("Admitted plan slice is absent from its exact approved revision");
|
|
34
34
|
const input = { id: "approved-plan", kind: "plan", reference: path, digest: plan.content_digest, authority: "approved-scope", revision: String(plan.revision), availability: "available" };
|
|
35
35
|
const requirements = [];
|
|
36
|
-
|
|
36
|
+
// Plan-level acceptance and test-plan entries describe the whole plan, not
|
|
37
|
+
// this one ticket. Including them in every ticket's inventory makes an
|
|
38
|
+
// independent slice appear responsible for sibling work. The ticket and
|
|
39
|
+
// admitted slice are the scoped authorities for this contract.
|
|
40
|
+
for (const [field, entries, location] of [["acceptance", slice.acceptance, `slices.${slice.slice_ref}.`], ["required_tests", slice.required_tests, `slices.${slice.slice_ref}.`]]) {
|
|
37
41
|
const occurrences = new Map();
|
|
38
42
|
entries.forEach((statement, index) => {
|
|
39
43
|
const fingerprint = contractDigest("plan-requirement", { field, statement }), occurrence = occurrences.get(fingerprint) ?? 0;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type QaPreparationConfigV1, type QaPreparationDepthDecisionV1, type QaPreparationLevel } from "rafi-spec";
|
|
2
2
|
export declare const DEPTH_OBLIGATIONS: readonly [readonly ["behavior-mapping", "prerequisites", "scope-boundaries"], readonly ["implementation-inspection", "test-inspection", "edge-cases", "verification-sequence"], readonly ["dependency-map", "failure-cases", "state-transitions", "regression-compatibility"], readonly ["consequential-invariants", "adversarial-cases", "concurrency-retry", "rollback-recovery"], readonly ["approach-proposal", "independent-challenge", "coordinated-verification"]];
|
|
3
3
|
export declare function depthObligations(level: QaPreparationLevel): string[];
|
|
4
|
-
|
|
4
|
+
/** Resolve public boolean configuration while temporarily accepting old mode objects. */
|
|
5
|
+
export declare function resolveQaPreparationConfig(value: unknown, shadow?: boolean, wallTimeMs?: unknown): QaPreparationConfigV1;
|
|
5
6
|
export declare function validateDepthDecision(value: unknown, predecessor?: QaPreparationDepthDecisionV1): string[];
|