ai-foreman 1.7.28 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/adapters/claude.d.ts +23 -12
- package/dist/adapters/claude.js +228 -24
- package/dist/adapters/codex.d.ts +17 -7
- package/dist/adapters/codex.js +195 -17
- package/dist/adapters/recovering.d.ts +2 -0
- package/dist/adapters/recovering.js +5 -1
- package/dist/adapters/structured.d.ts +71 -0
- package/dist/adapters/structured.js +213 -0
- package/dist/adapters/types.d.ts +86 -9
- package/dist/agentRun.d.ts +2 -0
- package/dist/agentRun.js +2 -1
- package/dist/boundedTurnEvidence.d.ts +18 -0
- package/dist/boundedTurnEvidence.js +76 -0
- package/dist/branch/currentGuard.d.ts +2 -0
- package/dist/branch/currentGuard.js +2 -0
- package/dist/branch/planner.js +1 -4
- package/dist/branch/runner.d.ts +2 -1
- package/dist/branch/runner.js +10 -7
- package/dist/buildAdmission.js +4 -0
- package/dist/buildApproval.d.ts +30 -0
- package/dist/buildApproval.js +68 -0
- package/dist/buildAssignment.d.ts +1 -0
- package/dist/buildAssignment.js +82 -23
- package/dist/buildInterventions.d.ts +2 -2
- package/dist/buildInterventions.js +11 -4
- package/dist/buildLaunchGate.js +3 -2
- package/dist/builderGuidanceFollowup.js +17 -6
- package/dist/cli/manager.js +22 -20
- package/dist/cli/start.js +32 -24
- package/dist/cli/tickets.js +40 -24
- package/dist/continuity.d.ts +10 -14
- package/dist/continuity.js +198 -179
- package/dist/continuityOrigin.d.ts +13 -0
- package/dist/continuityOrigin.js +91 -0
- package/dist/foreman.d.ts +20 -5
- package/dist/foreman.js +57 -187
- package/dist/graph/boundary.d.ts +1 -1
- package/dist/graph/boundary.js +1 -1
- package/dist/graph/host.js +8 -19
- package/dist/graph/manager.d.ts +1 -1
- package/dist/graph/manager.js +1 -1
- package/dist/graph/preparation.d.ts +38 -0
- package/dist/graph/preparation.js +7 -6
- package/dist/graph/semantic.d.ts +1 -0
- package/dist/graph/semantic.js +4 -1
- package/dist/graph/session.js +7 -2
- package/dist/graph/turn.d.ts +2 -3
- package/dist/graph/turn.js +28 -40
- package/dist/handoffProtocol.js +2 -2
- package/dist/handoffs.d.ts +3 -1
- package/dist/handoffs.js +90 -59
- package/dist/hostGraphApplicability.d.ts +46 -0
- package/dist/hostGraphApplicability.js +97 -0
- package/dist/hostQaAugmentation.d.ts +18 -0
- package/dist/hostQaAugmentation.js +81 -0
- package/dist/hostQaComparison.d.ts +21 -2
- package/dist/hostQaComparison.js +43 -28
- package/dist/hostSourcePreservation.d.ts +38 -0
- package/dist/hostSourcePreservation.js +72 -0
- package/dist/interviews.d.ts +7 -0
- package/dist/interviews.js +18 -1
- package/dist/legacyCommunicationDisplay.d.ts +2 -0
- package/dist/legacyCommunicationDisplay.js +2 -0
- package/dist/managerPacket.js +3 -3
- package/dist/markers.d.ts +4 -3
- package/dist/markers.js +4 -20
- package/dist/probeIpc.d.ts +14 -0
- package/dist/probeIpc.js +29 -0
- package/dist/qaBuildGate.d.ts +21 -0
- package/dist/qaBuildGate.js +48 -12
- package/dist/qaBuilderCoverage.js +22 -13
- package/dist/qaBuilderEvidence.d.ts +16 -1
- package/dist/qaBuilderEvidence.js +48 -8
- package/dist/qaContractCoverage.d.ts +2 -2
- package/dist/qaContractCoverage.js +11 -7
- package/dist/qaContractDelivery.js +34 -20
- package/dist/qaCoverageAssembly.d.ts +22 -0
- package/dist/qaCoverageAssembly.js +16 -0
- package/dist/qaDeliveryJournal.d.ts +2 -0
- package/dist/qaDeliveryJournal.js +19 -0
- package/dist/qaEffectiveConfig.d.ts +1 -0
- package/dist/qaEffectiveConfig.js +6 -1
- package/dist/qaFailureDelivery.js +97 -85
- package/dist/qaHostTransformation.d.ts +26 -0
- package/dist/qaHostTransformation.js +100 -0
- package/dist/qaPreparation.d.ts +8 -20
- package/dist/qaPreparation.js +133 -229
- package/dist/qaPreparationInputs.js +19 -0
- package/dist/qaPreparationStore.js +34 -6
- package/dist/qaPrerequisites.d.ts +2 -1
- package/dist/qaPrerequisites.js +21 -1
- package/dist/qaProtocolV2.d.ts +15 -0
- package/dist/qaRecovery.js +21 -4
- package/dist/qaReview.js +232 -394
- package/dist/qaReviewEvidence.d.ts +7 -3
- package/dist/qaReviewEvidence.js +15 -4
- package/dist/qaRuntime.js +1 -1
- package/dist/qaSnapshot.js +71 -21
- package/dist/qaStructuredResponse.d.ts +10 -0
- package/dist/qaStructuredResponse.js +57 -0
- package/dist/qaTimeline.js +5 -3
- package/dist/qaToolRecovery.d.ts +12 -0
- package/dist/qaToolRecovery.js +44 -0
- package/dist/qaVerificationContract.d.ts +3 -2
- package/dist/qaVerificationContract.js +58 -34
- package/dist/readinessGate.js +5 -4
- package/dist/runtimeReadiness.js +23 -15
- package/dist/statusReporter.d.ts +2 -0
- package/dist/statusReporter.js +2 -0
- package/dist/structuredCutover.d.ts +31 -0
- package/dist/structuredCutover.js +510 -0
- package/dist/structuredCutoverFiles.d.ts +10 -0
- package/dist/structuredCutoverFiles.js +181 -0
- package/dist/structuredDispatchAuthority.d.ts +22 -0
- package/dist/structuredDispatchAuthority.js +110 -0
- package/dist/structuredDispatchRecord.d.ts +29 -0
- package/dist/structuredDispatchRecord.js +36 -0
- package/dist/structuredLiveBudget.d.ts +2 -0
- package/dist/structuredLiveBudget.js +23 -0
- package/dist/structuredOperation.d.ts +18 -0
- package/dist/structuredOperation.js +257 -0
- package/dist/supervisedStart.d.ts +1 -1
- package/dist/supervisedStart.js +78 -31
- package/dist/supervisor.d.ts +18 -2
- package/dist/supervisor.js +74 -17
- package/dist/ticketPlanning.d.ts +1 -373
- package/dist/ticketPlanning.js +2 -26
- package/dist/ticketPopulation.d.ts +0 -3
- package/dist/ticketPopulation.js +4 -21
- package/dist/ticketPopulationPageAuthority.d.ts +13 -0
- package/dist/ticketPopulationPageAuthority.js +30 -0
- package/dist/ticketPopulationPaging.d.ts +27 -0
- package/dist/ticketPopulationPaging.js +105 -0
- package/dist/tickets/renderMarkdown.d.ts +14 -0
- package/dist/tickets/renderMarkdown.js +39 -5
- package/dist/tickets/ticketLoader.js +10 -0
- package/dist/tickets/ticketSchema.d.ts +107 -0
- package/dist/tickets/ticketSchema.js +4 -1
- package/dist/workflowDb.d.ts +100 -2
- package/dist/workflowDb.js +781 -35
- package/dist/workflowReader.d.ts +1 -0
- package/dist/workflowReader.js +9 -0
- package/package.json +15 -7
package/dist/qaPreparation.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { qaHostTransformation } from "./qaHostTransformation.js";
|
|
2
|
+
import { historicProviderWireContract, validateStructuredCandidate, QA_RISK_MINIMUMS, parseProviderContract, qaSemanticAssessmentV1Schema, qaChallengeReceiptV1Schema, qaPreparationEscalationProposalV1Schema, qaPreparationDepthDecisionV1Schema as qaDepthDecisionSchema, providerDraftSchema, depthProposalSchema, draftOutput, assessmentOutput, challengeOutput, depthOutput } from "rafi-spec";
|
|
2
3
|
import { createHash, randomUUID } from "node:crypto";
|
|
3
4
|
import { Ajv } from "ajv";
|
|
4
|
-
const qaPreparationDepthDecisionV1Schema = qaSemanticAssessmentV1Schema;
|
|
5
5
|
import { depthObligations, validateDepthDecision } from "./qaPreparationPolicy.js";
|
|
6
|
-
import { canonicalContractJson, assembleContract, draftDigest, inventoryDigest,
|
|
6
|
+
import { canonicalContractJson, assembleContract, draftDigest, inventoryDigest, validateAssessment, validateCandidate } from "./qaVerificationContract.js";
|
|
7
|
+
/** Compatibility re-exports; canonical contract ownership is rafi-spec. */
|
|
8
|
+
export { draftOutput, assessmentOutput, challengeOutput, depthOutput } from "rafi-spec";
|
|
7
9
|
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
10
|
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.`;
|
|
11
|
+
export const EXECUTABLE_CHECK_GUIDANCE = `Choose verification procedures compatible with the Builder's project permissions and workspace. Keep applicability and verification instructions consistent: an unconditional check must always be reported applicable, including a check whose predicate confirms an absent feature, disabled documentation, or an unrequested skill. For such a check require passed evidence of the absence, never an instruction to report the check not-applicable. If execution itself depends on a condition, encode conditional applicability with a concrete predicate and evidence. The independent assessor must flag contradictory applicability instructions before readiness. Documentation is disabled unless the host-bound contract documentation.required is true. When it is false, do not invent documentation checks merely because a docs directory or API-looking path exists; an absent documentation surface is not a failure. When it is true, use only the contract's exact targets and criteria, and define an actionable absent-target outcome. Do not invent mandatory inline node -e or node --eval commands: the Builder policy may refuse them. For a JavaScript behavior or export check, prefer a project-local test file executed with the project's existing test command, or a permitted node --test invocation. Do not invent a mandatory test-first RED phase or other historical sequence merely because a plan requires tests before and after implementation: a passing baseline test is not a failing new test. When an original requirement actually requires a historical sequence, preserve it and specify concrete independently reviewable evidence paths or retained host events for each required step; a generic statement that sequencing evidence exists is not actionable, and a later passing test does not prove past events. If the project policy makes raw stdout redirection or scratch-log creation unavailable, do not turn log-file capture into a blocker: use the retained bounded tool output and host events instead, and never cite a scratch path unless that exact file was successfully created and inspected. The independent QA snapshot may have a detached HEAD even when the Builder source workspace is on a branch; do not use its branch name to infer the Builder source branch. Diff and scope checks must compare the current snapshot with the captured pre-implementation baseline, not blindly with repository HEAD: previously approved or completed ticket changes are part of that baseline and must not be reported as new work. A scope check must first enumerate every changed path outside the explicit bookkeeping/evidence exclusions, then compare that complete host-observed set with the approved product/ticket scope; do not pre-filter the list to expected filenames, because that would hide accidental extra product edits. Bounded evidence-only paths such as scratch/ are not product scope and must be excluded from product-path scope checks while their retained evidence remains subject to QA review. For a current-branch multi-ticket build, do not invent a mandatory branch-alone execution check merely to prove ticket independence; use baseline-aware source/test inspection and the permitted project tests unless the original requirement explicitly requires an actual isolated execution. For additive tickets in a current-branch multi-ticket build, “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
12
|
/** Baseline bookkeeping must not block ordinary approved implementation work. */
|
|
11
13
|
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
14
|
/** Ignore known non-authoritative annotations; the original provider response remains retained. */
|
|
@@ -16,74 +18,28 @@ export function normalizeAssessmentMetadata(assessment) {
|
|
|
16
18
|
delete copy.policyVersion_note;
|
|
17
19
|
return copy;
|
|
18
20
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
return
|
|
34
|
-
}
|
|
35
|
-
return
|
|
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;
|
|
21
|
+
function classifyMaterialConcern(concern) {
|
|
22
|
+
// A semantic defect in the approved contract cannot be delegated to Builder
|
|
23
|
+
// merely because the eventual evidence is postimplementation. Resolve the
|
|
24
|
+
// authority first; only an otherwise valid future check belongs to Builder.
|
|
25
|
+
if (concern.category === "contract-gap" || concern.category === "verification-gap") {
|
|
26
|
+
return { route: "plan-revision", action: "Revise and approve the affected plan or ticket verification intent before Builder dispatch." };
|
|
27
|
+
}
|
|
28
|
+
if (concern.timing === "postimplementation") {
|
|
29
|
+
return { route: "builder", action: "Retain this as an approved postimplementation check; do not repair it during preparation." };
|
|
30
|
+
}
|
|
31
|
+
if (concern.category === "scope-conflict") {
|
|
32
|
+
return { route: "human", action: "Resolve the scope conflict through an explicit human decision or approved plan revision." };
|
|
33
|
+
}
|
|
34
|
+
if (concern.category === "evidence-gap") {
|
|
35
|
+
return { route: "draft", action: "Repair the QA draft with retained source evidence, then obtain a fresh independent assessment." };
|
|
36
|
+
}
|
|
37
|
+
return { route: "host", action: "Record the missing host fact and resume only after it is available." };
|
|
55
38
|
}
|
|
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
39
|
const contractValidator = new Ajv({ allErrors: true });
|
|
70
40
|
const validateProviderDraft = contractValidator.compile(providerDraftSchema);
|
|
71
41
|
const validateAssessmentContract = contractValidator.compile(qaSemanticAssessmentV1Schema);
|
|
72
42
|
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
43
|
/**
|
|
88
44
|
* A mandatory requirement must have at least one mandatory verification check.
|
|
89
45
|
* Providers occasionally return a concrete check with the weaker advisory
|
|
@@ -117,31 +73,6 @@ export function enforceMandatoryCoverage(candidate) {
|
|
|
117
73
|
}
|
|
118
74
|
return copy;
|
|
119
75
|
}
|
|
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
|
-
}
|
|
145
76
|
export async function prepareVerificationContract(initial, inventory, deps) {
|
|
146
77
|
const store = deps.store;
|
|
147
78
|
const head = store.head(initial.runId, initial.workId, initial.admissionDigest);
|
|
@@ -170,20 +101,17 @@ export async function prepareVerificationContract(initial, inventory, deps) {
|
|
|
170
101
|
if (retained && canonicalContractJson(candidate.inputs) !== canonicalContractJson(initial.inputs))
|
|
171
102
|
return { status: "blocked", detail: "Retained draft inputs changed", nextAction: "Reconcile authoritative input changes" };
|
|
172
103
|
let correctionIssues = [];
|
|
173
|
-
const run = async (phase, kind, instruction, resultId) => {
|
|
104
|
+
const run = async (phase, kind, instruction, resultId, artifact) => {
|
|
105
|
+
const output = artifact ?? (phase === "planner" ? depthOutput : phase === "assess" ? assessmentOutput : phase === "challenge" ? challengeOutput : draftOutput);
|
|
174
106
|
const effectivePhase = phase === "prepare" && correctionIssues.length > 0 ? "repair" : phase;
|
|
175
107
|
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
|
|
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}`;
|
|
108
|
+
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 preparation-assessment result.\n\n${instruction}`;
|
|
181
109
|
if (phase === "prepare")
|
|
182
110
|
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
111
|
if (phase === "assess")
|
|
184
112
|
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
113
|
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
|
|
114
|
+
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 artifact for the supplied operation schema.\n\n${instruction}`;
|
|
187
115
|
if (["prepare", "assess", "challenge"].includes(phase) && deps.hostFacts)
|
|
188
116
|
instruction = `${deps.hostFacts}\n\n${instruction}`;
|
|
189
117
|
if (["prepare", "assess", "challenge"].includes(phase))
|
|
@@ -194,86 +122,46 @@ export async function prepareVerificationContract(initial, inventory, deps) {
|
|
|
194
122
|
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}`;
|
|
195
123
|
const operationId = `qa-preparation:${randomUUID()}`;
|
|
196
124
|
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);
|
|
197
|
-
store.reserve(budget, operationId, kind, { phase, instruction, inputDigest: inventoryDigest(inventory) }, deps.now(), resultId);
|
|
125
|
+
store.reserve(budget, operationId, kind, { phase, instruction, inputDigest: inventoryDigest(inventory), protocolVersion: output.protocolVersion, operation: output.operation, schemaVersion: output.schemaVersion, schemaDigest: output.schemaDigest }, deps.now(), resultId);
|
|
198
126
|
const current = store.budget(initial.runId, initial.workId, initial.admissionDigest);
|
|
199
127
|
const dispatchedAt = deps.now();
|
|
200
|
-
const result = await deps.dispatch(effectivePhase, instruction, operationId, Math.max(1, current.deadlineMs - deps.now()));
|
|
128
|
+
const result = await deps.dispatch(effectivePhase, instruction, operationId, Math.max(1, current.deadlineMs - deps.now()), output);
|
|
201
129
|
store.retainResult(budget, operationId, result, result.usage);
|
|
202
|
-
store.metric(initial.runId, initial.workId, initial.admissionDigest, `metric:preparation-operation:${operationId}`,
|
|
130
|
+
store.metric(initial.runId, initial.workId, initial.admissionDigest, `metric:preparation-operation:${operationId}`, "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 });
|
|
203
131
|
if (deps.now() >= current.deadlineMs)
|
|
204
132
|
throw new Error("Preparation deadline expired; partial evidence retained");
|
|
205
|
-
return { ...result, operationId };
|
|
133
|
+
return { ...result, operationId, output };
|
|
206
134
|
};
|
|
207
|
-
const parsed = async (result,
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
return parsePreparationArtifact(response.text, tag);
|
|
217
|
-
}
|
|
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.
|
|
231
|
-
if (repair >= 2)
|
|
232
|
-
throw error;
|
|
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
|
-
}
|
|
135
|
+
const parsed = async (result, _tag) => {
|
|
136
|
+
if (!result.output)
|
|
137
|
+
throw new Error("Preparation result has no selected output contract");
|
|
138
|
+
if (!result.selectedDispatch)
|
|
139
|
+
throw new Error("Preparation result has no sealed v2 dispatch tuple");
|
|
140
|
+
const response = validateStructuredCandidate(result.output, result.structured, historicProviderWireContract(result.output, result.selectedDispatch), result.selectedDispatch);
|
|
141
|
+
if (response.result.kind !== "completed")
|
|
142
|
+
throw new Error(response.result.kind === "blocked" ? response.result.reason : `Preparation returned ${response.result.kind}`);
|
|
143
|
+
return response.result.payload;
|
|
236
144
|
};
|
|
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
145
|
const parsedContract = async (result, tag, validate, name, normalize) => {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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);
|
|
259
|
-
}
|
|
146
|
+
const payload = await parsed(result, tag);
|
|
147
|
+
const value = normalize ? normalize(payload, result) : payload;
|
|
148
|
+
if (normalize) {
|
|
149
|
+
const record = qaHostTransformation({ rule: "assessment-host-bindings-v1", operation: result.output.operation,
|
|
150
|
+
tupleDigest: result.selectedDispatch.digest, before: payload, after: value });
|
|
151
|
+
store.putArtifact("host-normalization", { ...record,
|
|
152
|
+
inputRef: store.putArtifact("host-transformation-input", payload), outputRef: store.putArtifact("host-transformation-output", value) });
|
|
260
153
|
}
|
|
154
|
+
if (!validate(value))
|
|
155
|
+
throw new Error(`${name} contract validation failed: ${JSON.stringify(validate.errors)}`);
|
|
156
|
+
return { value, response: result };
|
|
261
157
|
};
|
|
262
158
|
try {
|
|
263
159
|
if (!retained && !retainedDepth && decisionIssues.length) {
|
|
264
160
|
let plannerIssues = decisionIssues;
|
|
265
161
|
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
|
|
267
|
-
|
|
268
|
-
|
|
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
|
-
}
|
|
162
|
+
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 the native depth payload with QaPreparationDepthDecisionV1. Schema: ${JSON.stringify(depthProposalSchema)}. 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)}`);
|
|
163
|
+
const proposed = await parsed(response, "RAFI_QA_DEPTH");
|
|
164
|
+
const depthBefore = structuredClone(proposed);
|
|
277
165
|
proposed.plannerOperationId = response.operationId;
|
|
278
166
|
proposed.plannerIdentity = response.sessionId;
|
|
279
167
|
// minimumReasons is a host-validated restatement of the planner's
|
|
@@ -281,6 +169,9 @@ export async function prepareVerificationContract(initial, inventory, deps) {
|
|
|
281
169
|
// the selected level, risk evidence, or rationale.
|
|
282
170
|
const requiredReasons = proposed.riskFactors.map(risk => risk.category);
|
|
283
171
|
proposed.minimumReasons = [...proposed.minimumReasons, ...requiredReasons.filter(category => !proposed.minimumReasons.includes(category))];
|
|
172
|
+
store.putArtifact("host-normalization", { ...qaHostTransformation({ rule: "depth-host-bindings-v1", operation: response.output.operation,
|
|
173
|
+
tupleDigest: response.selectedDispatch.digest, before: depthBefore, after: proposed }),
|
|
174
|
+
inputRef: store.putArtifact("host-transformation-input", depthBefore), outputRef: store.putArtifact("host-transformation-output", proposed) });
|
|
284
175
|
plannerIssues = validateDepthDecision(proposed);
|
|
285
176
|
if (!plannerIssues.length) {
|
|
286
177
|
candidate.depthDecision = proposed;
|
|
@@ -296,44 +187,38 @@ export async function prepareVerificationContract(initial, inventory, deps) {
|
|
|
296
187
|
store.retainProgress(budget, "depth-decision", candidate.depthDecision);
|
|
297
188
|
let authorSessionId = retained?.authorSessionId ?? "";
|
|
298
189
|
for (let pass = 0; pass < (candidate.depthDecision.level <= 2 ? 2 : 3) && (!retained || targetedConcerns.length > 0 || validateCandidate(candidate, inventory, store.equivalentResolver()).length > 0); pass++) {
|
|
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
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
190
|
+
const response = await run("prepare", "investigation", `QA preparation only. Inspect relevant source/tests; do not implement, install dependencies, or expand admitted product scope. Return the native preparation-draft payload matching the supplied native 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. Your completed operation result has only kind, summary, and payload: never add any field such as escalation beside result.payload. Choose exactly one payload shape: (1) a normal {"kind":"draft","checks":[],"coverage":[],"baseline":[],"preparationEvidence":[],"unresolved":[]} payload, or (2) {"kind":"escalation","escalation":{"version":1,"rationale":"...","riskFactors":[{"category":"...","rationale":"...","references":["..."]}]}} when risk exceeds selected depth. Do not combine a draft and an escalation. 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.\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)}`);
|
|
191
|
+
const payload = await parsed(response, "RAFI_QA_CANDIDATE");
|
|
192
|
+
if (payload.kind === "escalation") {
|
|
193
|
+
const request = parseProviderContract(qaPreparationEscalationProposalV1Schema, payload.escalation, "QA preparation escalation");
|
|
194
|
+
const minimum = Math.max(candidate.depthDecision.level, ...request.riskFactors.map(risk => QA_RISK_MINIMUMS[risk.category]));
|
|
195
|
+
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 the native depth payload following ${JSON.stringify(qaDepthDecisionSchema)}. Previous decision: ${JSON.stringify(candidate.depthDecision)}; cited risk evidence: ${JSON.stringify(request)}`);
|
|
196
|
+
const decision = await parsed(revision, "RAFI_QA_DEPTH");
|
|
197
|
+
const before = structuredClone(decision);
|
|
198
|
+
decision.plannerIdentity = revision.sessionId;
|
|
199
|
+
decision.plannerOperationId = revision.operationId;
|
|
200
|
+
store.putArtifact("host-normalization", { ...qaHostTransformation({ rule: "depth-host-bindings-v1", operation: revision.output.operation,
|
|
201
|
+
tupleDigest: revision.selectedDispatch.digest, before, after: decision }),
|
|
202
|
+
inputRef: store.putArtifact("host-transformation-input", before), outputRef: store.putArtifact("host-transformation-output", decision) });
|
|
203
|
+
const errors = validateDepthDecision(decision, candidate.depthDecision);
|
|
204
|
+
if (decision.level < minimum || request.riskFactors.some(risk => !decision.riskFactors.some(actual => actual.category === risk.category)))
|
|
205
|
+
errors.push("Planner revision does not address evidenced risk minimum");
|
|
206
|
+
if (errors.length)
|
|
207
|
+
throw new Error(`depth-decision-incomplete: ${errors.join("; ")}`);
|
|
208
|
+
candidate.depthDecision = decision;
|
|
209
|
+
store.reviseDepth(budget, decision);
|
|
210
|
+
store.retainProgress(budget, "depth-decision", decision);
|
|
309
211
|
continue;
|
|
310
212
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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)
|
|
213
|
+
const { kind: _kind, ...rawProposal } = payload;
|
|
214
|
+
// Only provider-owned fields can cross the native contract. Historical
|
|
215
|
+
// full-candidate envelopes are read by historical readers, never here.
|
|
216
|
+
const proposal = rawProposal;
|
|
217
|
+
if (!validateProviderDraft(proposal))
|
|
218
|
+
throw new Error(`Invalid native draft: ${JSON.stringify(validateProviderDraft.errors)}`);
|
|
219
|
+
if (!deps.normalizeDraft)
|
|
335
220
|
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
|
|
221
|
+
const draft = { ...structuredClone(candidate), checks: proposal.checks, coverage: proposal.coverage, baseline: proposal.baseline, preparationEvidence: proposal.preparationEvidence, unresolved: proposal.unresolved };
|
|
337
222
|
draft.createdAt = candidate.createdAt;
|
|
338
223
|
if (candidate.predecessorDigest === undefined)
|
|
339
224
|
delete draft.predecessorDigest;
|
|
@@ -349,7 +234,13 @@ export async function prepareVerificationContract(initial, inventory, deps) {
|
|
|
349
234
|
draft.requirements.push(...inventory.filter(item => !present.has(item.id)).map(item => structuredClone(item)));
|
|
350
235
|
}
|
|
351
236
|
try {
|
|
352
|
-
|
|
237
|
+
const before = structuredClone(draft);
|
|
238
|
+
const normalized = enforceMandatoryCoverage(deps.normalizeDraft ? deps.normalizeDraft(draft, response) : draft);
|
|
239
|
+
const record = qaHostTransformation({ rule: "draft-host-bindings-v1", operation: response.output.operation,
|
|
240
|
+
tupleDigest: response.selectedDispatch.digest, before, after: normalized, workspacePaths: response.workspacePaths });
|
|
241
|
+
store.putArtifact("host-normalization", { ...record, workspacePaths: response.workspacePaths,
|
|
242
|
+
inputRef: store.putArtifact("host-transformation-input", before), outputRef: store.putArtifact("host-transformation-output", normalized) });
|
|
243
|
+
candidate = normalized;
|
|
353
244
|
}
|
|
354
245
|
catch (error) {
|
|
355
246
|
const detail = error instanceof Error ? error.message : String(error);
|
|
@@ -363,23 +254,6 @@ export async function prepareVerificationContract(initial, inventory, deps) {
|
|
|
363
254
|
evidence.operationId = response.operationId;
|
|
364
255
|
evidence.sessionId = response.sessionId;
|
|
365
256
|
}
|
|
366
|
-
if (response.text.includes("RAFI_QA_ESCALATION_START")) {
|
|
367
|
-
const request = parseProviderContract(qaPreparationEscalationProposalV1Schema, parsePreparationArtifact(response.text, "RAFI_QA_ESCALATION"), "QA preparation escalation");
|
|
368
|
-
const minimum = Math.max(candidate.depthDecision.level, ...request.riskFactors.map(risk => QA_RISK_MINIMUMS[risk.category]));
|
|
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)}`);
|
|
370
|
-
const decision = await parsed(revision, "RAFI_QA_DEPTH");
|
|
371
|
-
decision.plannerIdentity = revision.sessionId;
|
|
372
|
-
decision.plannerOperationId = revision.operationId;
|
|
373
|
-
const errors = validateDepthDecision(decision, candidate.depthDecision);
|
|
374
|
-
if (decision.level < minimum || request.riskFactors.some(risk => !decision.riskFactors.some(actual => actual.category === risk.category)))
|
|
375
|
-
errors.push("Planner revision does not address evidenced risk minimum");
|
|
376
|
-
if (errors.length)
|
|
377
|
-
throw new Error(`depth-decision-incomplete: ${errors.join("; ")}`);
|
|
378
|
-
candidate.depthDecision = decision;
|
|
379
|
-
store.reviseDepth(budget, decision);
|
|
380
|
-
store.retainProgress(budget, "depth-decision", decision);
|
|
381
|
-
continue;
|
|
382
|
-
}
|
|
383
257
|
const structural = validateCandidate(candidate, inventory, store.equivalentResolver());
|
|
384
258
|
if (!structural.length)
|
|
385
259
|
break;
|
|
@@ -391,8 +265,8 @@ export async function prepareVerificationContract(initial, inventory, deps) {
|
|
|
391
265
|
throw new Error(`Contract incomplete: ${structural.join("; ")}`);
|
|
392
266
|
const binding = { draftPayloadDigest: draftDigest(candidate), inventoryDigest: inventoryDigest(candidate.requirements), policyVersion: candidate.depthDecision.policyVersion, authorSessionId };
|
|
393
267
|
const retainedAssessment = store.progress(budget, "assessment");
|
|
394
|
-
const normalizeAssessment = (value, response
|
|
395
|
-
const normalized =
|
|
268
|
+
const normalizeAssessment = (value, response) => {
|
|
269
|
+
const normalized = structuredClone(value);
|
|
396
270
|
// These bindings are host-owned. A repair may omit them or emit a
|
|
397
271
|
// placeholder; restore only the exact current binding and retained
|
|
398
272
|
// obligation labels, never provider-invented content.
|
|
@@ -405,13 +279,9 @@ export async function prepareVerificationContract(initial, inventory, deps) {
|
|
|
405
279
|
normalized.sessionId = response.sessionId;
|
|
406
280
|
if (response.operationId)
|
|
407
281
|
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
282
|
return normalized;
|
|
413
283
|
};
|
|
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
|
|
284
|
+
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. If a concern's only next action is to inspect, compare, run, or otherwise produce evidence for a check that is already declared with postimplementation timing, set that concern material:false; it is an actionable future check, not a preparation blocker. Only report a material concern when the contract cannot state an actionable postimplementation check or when an already-required preimplementation fact is missing. Return the native preparation-assessment payload matching the supplied native preparation-assessment schema. The concerns field is an array of objects, never strings; every concern object must have exactly these eight fields and no others: {reason:string, requirementIds:string[], checkIds:string[], material:boolean, owner:string, nextAction:string, category:"evidence-gap|contract-gap|scope-conflict|verification-gap", timing:"preimplementation|postimplementation|both"}. Do not use legacy fields such as severity, evidence, title, message, 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))}. The host binds dispatch, source, policy and author identities; do not echo them in the payload.\nPrior independent concerns requiring concrete resolution evidence: ${JSON.stringify(targetedConcerns)}\nOriginal inventory:\n${JSON.stringify(inventory)}\nDraft:\n${JSON.stringify(candidate)}`);
|
|
415
285
|
let assessed = assessmentResponse
|
|
416
286
|
? await parsedContract(assessmentResponse, "RAFI_QA_ASSESSMENT", validateAssessmentContract, "QA semantic assessment", (value, response) => normalizeAssessment(value, response))
|
|
417
287
|
: { value: retainedAssessment, response: undefined };
|
|
@@ -419,9 +289,43 @@ export async function prepareVerificationContract(initial, inventory, deps) {
|
|
|
419
289
|
// session identity, but it cannot prove or safely author the builder's ID.
|
|
420
290
|
assessed.value.authorSessionId = binding.authorSessionId;
|
|
421
291
|
for (let repair = 0; assessmentResponse && repair < 3; repair++) {
|
|
292
|
+
// Native concerns retain their declared materiality; prose cannot downgrade it.
|
|
422
293
|
const semanticIssues = validateAssessment(candidate, assessed.value, authorSessionId);
|
|
423
294
|
if (!semanticIssues.length)
|
|
424
295
|
break;
|
|
296
|
+
const materialConcerns = assessed.value.concerns.filter(concern => concern.material);
|
|
297
|
+
if (materialConcerns.length) {
|
|
298
|
+
const routes = materialConcerns.map(concern => ({
|
|
299
|
+
concern: {
|
|
300
|
+
category: concern.category ?? "unknown",
|
|
301
|
+
timing: concern.timing ?? "unknown",
|
|
302
|
+
requirementIds: [...concern.requirementIds],
|
|
303
|
+
checkIds: [...concern.checkIds],
|
|
304
|
+
reason: concern.reason,
|
|
305
|
+
},
|
|
306
|
+
...classifyMaterialConcern(concern),
|
|
307
|
+
}));
|
|
308
|
+
const classificationRef = store.putArtifact("preparation-concern-routing-v1", {
|
|
309
|
+
version: 1,
|
|
310
|
+
runId: initial.runId,
|
|
311
|
+
workId: initial.workId,
|
|
312
|
+
admissionDigest: initial.admissionDigest,
|
|
313
|
+
targetArtifactDigest: draftDigest(candidate),
|
|
314
|
+
originatingAssessmentReceipt: assessed.response?.operationId ?? assessed.value.operationId,
|
|
315
|
+
routes,
|
|
316
|
+
});
|
|
317
|
+
store.retainProgress(budget, "assessment", assessed.value);
|
|
318
|
+
const nextAction = routes.map(route => route.action).join(" ");
|
|
319
|
+
const detail = `Material concern requires host-owned routing (${routes.map(route => route.route).join(", ")}); assessment repair is not authorized: ${materialConcerns.map(concern => concern.reason).join(" | ").slice(0, 1600)}`;
|
|
320
|
+
store.setState(initial.runId, initial.workId, initial.admissionDigest, "blocked", detail, store.head(initial.runId, initial.workId, initial.admissionDigest).generation);
|
|
321
|
+
store.event(initial.runId, initial.workId, `preparation-concern-routing:${classificationRef}`, "preparation-concern-routing", {
|
|
322
|
+
classificationRef,
|
|
323
|
+
routes,
|
|
324
|
+
targetArtifactDigest: draftDigest(candidate),
|
|
325
|
+
originatingAssessmentReceipt: assessed.response?.operationId ?? assessed.value.operationId,
|
|
326
|
+
});
|
|
327
|
+
return { status: "blocked", detail, nextAction };
|
|
328
|
+
}
|
|
425
329
|
if (repair === 2)
|
|
426
330
|
throw new Error(semanticIssues.join("; "));
|
|
427
331
|
const preservedAuthorSessionId = assessed.value.authorSessionId;
|
|
@@ -435,8 +339,8 @@ export async function prepareVerificationContract(initial, inventory, deps) {
|
|
|
435
339
|
authorSessionId: assessed.value.authorSessionId,
|
|
436
340
|
};
|
|
437
341
|
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
|
|
439
|
-
const repaired = await parsedContract(assessmentResponse, "RAFI_QA_ASSESSMENT", validateAssessmentContract, "QA semantic assessment", (value, response) => normalizeAssessment(value, response
|
|
342
|
+
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 the native preparation-assessment payload 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, assessmentOutput);
|
|
343
|
+
const repaired = await parsedContract(assessmentResponse, "RAFI_QA_ASSESSMENT", validateAssessmentContract, "QA semantic assessment", (value, response) => normalizeAssessment(value, response));
|
|
440
344
|
// Repairs may omit this immutable binding while rewriting concerns. Preserve
|
|
441
345
|
// it only when the prior parsed assessment supplied it; a conflicting value
|
|
442
346
|
// remains visible to validateAssessment and still fails closed.
|
|
@@ -445,7 +349,7 @@ export async function prepareVerificationContract(initial, inventory, deps) {
|
|
|
445
349
|
repaired.value.authorSessionId = binding.authorSessionId;
|
|
446
350
|
assessed = repaired;
|
|
447
351
|
}
|
|
448
|
-
const assessment =
|
|
352
|
+
const assessment = assessed.value;
|
|
449
353
|
// These binding fields are host-owned. Providers frequently copy or
|
|
450
354
|
// truncate the inventory digest while describing their assessment; that
|
|
451
355
|
// must not invalidate an otherwise substantive independent review.
|
|
@@ -461,7 +365,7 @@ export async function prepareVerificationContract(initial, inventory, deps) {
|
|
|
461
365
|
if (challenge?.draftPayloadDigest !== draftDigest(candidate))
|
|
462
366
|
challenge = undefined;
|
|
463
367
|
if (candidate.depthDecision.level === 5 && !challenge) {
|
|
464
|
-
const response = await run("challenge", "challenge", `Independent approach challenge, fresh assessor conversation. Planning only. Return
|
|
368
|
+
const response = await run("challenge", "challenge", `Independent approach challenge, fresh assessor conversation. Planning only. Return the native preparation-challenge payload matching the supplied native preparation-challenge schema: 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)}`);
|
|
465
369
|
const challenged = await parsedContract(response, "RAFI_QA_CHALLENGE", validateChallengeContract, "QA approach challenge", (value, response) => normalizeAssessment(value, response));
|
|
466
370
|
challenge = normalizeAssessmentMetadata(challenged.value);
|
|
467
371
|
challenge.sessionId = challenged.response.sessionId;
|