ai-foreman 1.7.29 → 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 +227 -23
- 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 +85 -9
- package/dist/agentRun.d.ts +2 -0
- package/dist/agentRun.js +2 -1
- package/dist/boundedTurnEvidence.d.ts +18 -0
- package/dist/boundedTurnEvidence.js +76 -0
- package/dist/branch/currentGuard.d.ts +2 -0
- package/dist/branch/currentGuard.js +2 -0
- package/dist/branch/planner.js +1 -4
- package/dist/branch/runner.d.ts +2 -1
- package/dist/branch/runner.js +10 -7
- package/dist/buildAdmission.js +4 -0
- package/dist/buildApproval.d.ts +15 -0
- package/dist/buildApproval.js +25 -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 +22 -26
- package/dist/cli/tickets.d.ts +0 -7
- package/dist/cli/tickets.js +41 -40
- 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 +10 -4
- package/dist/foreman.js +51 -193
- 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 +2 -1
- package/dist/hostGraphApplicability.js +17 -19
- package/dist/hostQaAugmentation.js +21 -4
- package/dist/hostQaComparison.d.ts +21 -2
- package/dist/hostQaComparison.js +43 -29
- package/dist/hostSourcePreservation.d.ts +38 -0
- package/dist/hostSourcePreservation.js +72 -0
- package/dist/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 +47 -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 +4 -9
- package/dist/qaContractDelivery.js +34 -20
- package/dist/qaCoverageAssembly.d.ts +22 -0
- package/dist/qaCoverageAssembly.js +16 -0
- package/dist/qaDeliveryJournal.d.ts +2 -0
- package/dist/qaDeliveryJournal.js +19 -0
- package/dist/qaFailureDelivery.js +97 -85
- package/dist/qaHostTransformation.d.ts +26 -0
- package/dist/qaHostTransformation.js +100 -0
- package/dist/qaPreparation.d.ts +8 -20
- package/dist/qaPreparation.js +133 -248
- package/dist/qaPreparationInputs.js +19 -0
- 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 +220 -395
- 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/qaVerificationContract.d.ts +1 -2
- package/dist/qaVerificationContract.js +24 -35
- 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.js +35 -21
- 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 +763 -39
- 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,88 +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
|
-
// A provider can report a stale check identity copied into descriptive
|
|
48
|
-
// prose from an earlier draft. The host has already validated the
|
|
49
|
-
// structured check/coverage/dependency references; when the assessor also
|
|
50
|
-
// says the referenced acceptance item is already covered, this is a
|
|
51
|
-
// non-material labeling defect rather than an unresolved contract gap.
|
|
52
|
-
// Keep the concern in the retained assessment, but do not block Builder.
|
|
53
|
-
const staleCoveredReference = /not in the declared check IDs or its dependsOnChecks/i.test(concern.reason)
|
|
54
|
-
&& /reference can be removed|already covered/i.test(`${concern.reason} ${concern.nextAction}`)
|
|
55
|
-
&& concern.checkIds.length > 0
|
|
56
|
-
&& concern.checkIds.every(id => checks.has(id));
|
|
57
|
-
if (concern.material && staleCoveredReference)
|
|
58
|
-
return { ...concern, material: false };
|
|
59
|
-
if (!concern.material || !concern.checkIds.length || !concern.checkIds.every(id => checks.get(id)?.timing === "postimplementation"))
|
|
60
|
-
return concern;
|
|
61
|
-
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)
|
|
62
|
-
&& /\b(?:before|pre[- ]?builder|not yet)\b/i.test(concern.reason)
|
|
63
|
-
&& /\b(?:implement|add|create|rerun|re-?run|builder|after implementation)\b/i.test(concern.nextAction);
|
|
64
|
-
const inventedSiblingAbsence = /\b(?:no|without|absen[ct]|missing)\s+(?:the\s+)?[a-z0-9_-]+\s+export\b|\b(?:sibling|other)\s+(?:feature|slice)\s+(?:absen[ct]|removed|present)\b/i.test(concern.reason)
|
|
65
|
-
&& /\b(?:own|imports? only|no [a-z0-9_-]+ symbol|drop|remove)\b/i.test(concern.nextAction);
|
|
66
|
-
return expectedFutureAbsence || inventedSiblingAbsence ? { ...concern, material: false } : concern;
|
|
67
|
-
});
|
|
68
|
-
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." };
|
|
69
38
|
}
|
|
70
|
-
// The provider proposes verification work; the host owns contract identity,
|
|
71
|
-
// approved inputs, requirement text, depth, timestamps and evidence hashes.
|
|
72
|
-
const durableFields = qaContractCandidateV1Schema.properties;
|
|
73
|
-
const providerEvidence = { type: "object", additionalProperties: false, required: ["obligation", "references", "analysis"], properties: {
|
|
74
|
-
obligation: { type: "string", minLength: 1 }, analysis: { type: "string", minLength: 1 },
|
|
75
|
-
references: { type: "array", minItems: 1, items: { type: "object", additionalProperties: false, required: ["path", "locator"], properties: { path: { type: "string", minLength: 1 }, locator: { type: "string", minLength: 1 } } } },
|
|
76
|
-
} };
|
|
77
|
-
const providerBaseline = { type: "object", additionalProperties: false, required: ["id", "runtime", "procedure", "status", "relevance", "expectedTransition"], properties: {
|
|
78
|
-
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 },
|
|
79
|
-
} };
|
|
80
|
-
const providerDraftSchema = { type: "object", additionalProperties: false, required: ["checks", "coverage", "baseline", "preparationEvidence", "unresolved"], properties: {
|
|
81
|
-
checks: durableFields.checks, coverage: durableFields.coverage, baseline: { type: "array", items: providerBaseline }, preparationEvidence: { type: "array", minItems: 1, items: providerEvidence }, unresolved: durableFields.unresolved,
|
|
82
|
-
} };
|
|
83
39
|
const contractValidator = new Ajv({ allErrors: true });
|
|
84
40
|
const validateProviderDraft = contractValidator.compile(providerDraftSchema);
|
|
85
41
|
const validateAssessmentContract = contractValidator.compile(qaSemanticAssessmentV1Schema);
|
|
86
42
|
const validateChallengeContract = contractValidator.compile(qaChallengeReceiptV1Schema);
|
|
87
|
-
/** Providers sometimes echo host-owned evidence metadata into the draft. */
|
|
88
|
-
function normalizeProviderDraftMetadata(value) {
|
|
89
|
-
const copy = structuredClone(value);
|
|
90
|
-
for (const evidence of copy.preparationEvidence ?? []) {
|
|
91
|
-
const metadata = evidence;
|
|
92
|
-
delete metadata.operationId;
|
|
93
|
-
delete metadata.sessionId;
|
|
94
|
-
delete metadata.evidenceDigest;
|
|
95
|
-
delete metadata.graphReceiptRefs;
|
|
96
|
-
for (const reference of evidence.references ?? [])
|
|
97
|
-
delete reference.digest;
|
|
98
|
-
}
|
|
99
|
-
return copy;
|
|
100
|
-
}
|
|
101
43
|
/**
|
|
102
44
|
* A mandatory requirement must have at least one mandatory verification check.
|
|
103
45
|
* Providers occasionally return a concrete check with the weaker advisory
|
|
@@ -131,31 +73,6 @@ export function enforceMandatoryCoverage(candidate) {
|
|
|
131
73
|
}
|
|
132
74
|
return copy;
|
|
133
75
|
}
|
|
134
|
-
/**
|
|
135
|
-
* Codex occasionally appends a made-up requirement ID to an otherwise valid
|
|
136
|
-
* check. Remove only those extraneous annotations when the check still has an
|
|
137
|
-
* exact authoritative reference. Never invent or remap an ID, and leave a
|
|
138
|
-
* check with no valid references untouched so normal validation fails closed.
|
|
139
|
-
*/
|
|
140
|
-
export function removeExtraneousRequirementReferences(candidate, inventory) {
|
|
141
|
-
const copy = structuredClone(candidate);
|
|
142
|
-
const allowed = new Set(inventory.map(requirement => requirement.id));
|
|
143
|
-
for (const check of copy.checks) {
|
|
144
|
-
const valid = check.requirementRefs.filter(id => allowed.has(id));
|
|
145
|
-
if (valid.length > 0 && valid.length !== check.requirementRefs.length)
|
|
146
|
-
check.requirementRefs = [...new Set(valid)];
|
|
147
|
-
}
|
|
148
|
-
return copy;
|
|
149
|
-
}
|
|
150
|
-
function providerBlocker(text, tag) {
|
|
151
|
-
// A refusal is not a malformed candidate and cannot be repaired by formatting.
|
|
152
|
-
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`))))
|
|
153
|
-
return undefined;
|
|
154
|
-
if (!/\b(?:cannot|can't|unable|blocked|denied|not permitted|lack(?:s|ing) access)\b/i.test(text))
|
|
155
|
-
return undefined;
|
|
156
|
-
const explanation = text.replace(/\s+/g, " ").trim().slice(0, 500);
|
|
157
|
-
return `QA provider could not produce ${tag}: ${explanation}`;
|
|
158
|
-
}
|
|
159
76
|
export async function prepareVerificationContract(initial, inventory, deps) {
|
|
160
77
|
const store = deps.store;
|
|
161
78
|
const head = store.head(initial.runId, initial.workId, initial.admissionDigest);
|
|
@@ -184,20 +101,17 @@ export async function prepareVerificationContract(initial, inventory, deps) {
|
|
|
184
101
|
if (retained && canonicalContractJson(candidate.inputs) !== canonicalContractJson(initial.inputs))
|
|
185
102
|
return { status: "blocked", detail: "Retained draft inputs changed", nextAction: "Reconcile authoritative input changes" };
|
|
186
103
|
let correctionIssues = [];
|
|
187
|
-
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);
|
|
188
106
|
const effectivePhase = phase === "prepare" && correctionIssues.length > 0 ? "repair" : phase;
|
|
189
107
|
if (phase === "assess")
|
|
190
|
-
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
|
|
191
|
-
if (phase === "assess" && deps.provider === "codex")
|
|
192
|
-
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}`;
|
|
193
|
-
if (phase === "assess" && deps.provider === "claude")
|
|
194
|
-
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}`;
|
|
195
109
|
if (phase === "prepare")
|
|
196
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}`;
|
|
197
111
|
if (phase === "assess")
|
|
198
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}`;
|
|
199
113
|
if (effectivePhase === "repair")
|
|
200
|
-
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}`;
|
|
201
115
|
if (["prepare", "assess", "challenge"].includes(phase) && deps.hostFacts)
|
|
202
116
|
instruction = `${deps.hostFacts}\n\n${instruction}`;
|
|
203
117
|
if (["prepare", "assess", "challenge"].includes(phase))
|
|
@@ -208,86 +122,46 @@ export async function prepareVerificationContract(initial, inventory, deps) {
|
|
|
208
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}`;
|
|
209
123
|
const operationId = `qa-preparation:${randomUUID()}`;
|
|
210
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);
|
|
211
|
-
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);
|
|
212
126
|
const current = store.budget(initial.runId, initial.workId, initial.admissionDigest);
|
|
213
127
|
const dispatchedAt = deps.now();
|
|
214
|
-
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);
|
|
215
129
|
store.retainResult(budget, operationId, result, result.usage);
|
|
216
|
-
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 });
|
|
217
131
|
if (deps.now() >= current.deadlineMs)
|
|
218
132
|
throw new Error("Preparation deadline expired; partial evidence retained");
|
|
219
|
-
return { ...result, operationId };
|
|
133
|
+
return { ...result, operationId, output };
|
|
220
134
|
};
|
|
221
|
-
const parsed = async (result,
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
return parsePreparationArtifact(response.text, tag);
|
|
231
|
-
}
|
|
232
|
-
catch (error) {
|
|
233
|
-
// A provider can answer the preparation assessor with a final-QA
|
|
234
|
-
// artifact from the wrong role. That is a repairable protocol error,
|
|
235
|
-
// not a refusal to participate in preparation.
|
|
236
|
-
const blocker = tag === "RAFI_QA_ASSESSMENT" && response.text.includes("RAFI_QA_FAILURE_REPORT_START")
|
|
237
|
-
? undefined
|
|
238
|
-
: providerBlocker(response.text, tag);
|
|
239
|
-
if (blocker)
|
|
240
|
-
throw new Error(blocker);
|
|
241
|
-
// A provider can return a short progress/prose response without any
|
|
242
|
-
// JSON at all. Treat that as a bounded format defect unless it is an
|
|
243
|
-
// explicit refusal; the repair turn is still required to produce the
|
|
244
|
-
// exact envelope and is subject to the same strict parser below.
|
|
245
|
-
if (repair >= 2)
|
|
246
|
-
throw error;
|
|
247
|
-
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);
|
|
248
|
-
}
|
|
249
|
-
}
|
|
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;
|
|
250
144
|
};
|
|
251
|
-
/**
|
|
252
|
-
* Validate provider-owned artifacts at their response boundary. A provider
|
|
253
|
-
* sometimes copies the preparation draft into an independent assessment;
|
|
254
|
-
* that is not an assessment contract and must be repaired as a bounded
|
|
255
|
-
* schema error rather than being allowed to fail much later in assembly.
|
|
256
|
-
*/
|
|
257
145
|
const parsedContract = async (result, tag, validate, name, normalize) => {
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
}
|
|
266
|
-
catch (error) {
|
|
267
|
-
if (repair >= 2)
|
|
268
|
-
throw error;
|
|
269
|
-
const assessmentRepairContract = name === "QA semantic assessment"
|
|
270
|
-
? `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.`
|
|
271
|
-
: "";
|
|
272
|
-
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);
|
|
273
|
-
}
|
|
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) });
|
|
274
153
|
}
|
|
154
|
+
if (!validate(value))
|
|
155
|
+
throw new Error(`${name} contract validation failed: ${JSON.stringify(validate.errors)}`);
|
|
156
|
+
return { value, response: result };
|
|
275
157
|
};
|
|
276
158
|
try {
|
|
277
159
|
if (!retained && !retainedDepth && decisionIssues.length) {
|
|
278
160
|
let plannerIssues = decisionIssues;
|
|
279
161
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
280
|
-
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
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
proposed = await parsed(response, "RAFI_QA_DEPTH");
|
|
284
|
-
}
|
|
285
|
-
catch (error) {
|
|
286
|
-
plannerIssues = [`QA planner response was not a valid RAFI_QA_DEPTH envelope: ${error instanceof Error ? error.message : String(error)}`];
|
|
287
|
-
if (attempt === 2)
|
|
288
|
-
throw new Error(`depth-decision-incomplete: ${plannerIssues.join("; ")}`);
|
|
289
|
-
continue;
|
|
290
|
-
}
|
|
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);
|
|
291
165
|
proposed.plannerOperationId = response.operationId;
|
|
292
166
|
proposed.plannerIdentity = response.sessionId;
|
|
293
167
|
// minimumReasons is a host-validated restatement of the planner's
|
|
@@ -295,6 +169,9 @@ export async function prepareVerificationContract(initial, inventory, deps) {
|
|
|
295
169
|
// the selected level, risk evidence, or rationale.
|
|
296
170
|
const requiredReasons = proposed.riskFactors.map(risk => risk.category);
|
|
297
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) });
|
|
298
175
|
plannerIssues = validateDepthDecision(proposed);
|
|
299
176
|
if (!plannerIssues.length) {
|
|
300
177
|
candidate.depthDecision = proposed;
|
|
@@ -310,44 +187,38 @@ export async function prepareVerificationContract(initial, inventory, deps) {
|
|
|
310
187
|
store.retainProgress(budget, "depth-decision", candidate.depthDecision);
|
|
311
188
|
let authorSessionId = retained?.authorSessionId ?? "";
|
|
312
189
|
for (let pass = 0; pass < (candidate.depthDecision.level <= 2 ? 2 : 3) && (!retained || targetedConcerns.length > 0 || validateCandidate(candidate, inventory, store.equivalentResolver()).length > 0); pass++) {
|
|
313
|
-
const response = await run("prepare", "investigation", `QA preparation only. Inspect relevant source/tests; do not implement, install dependencies, or expand admitted product scope. Return
|
|
314
|
-
const
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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);
|
|
323
211
|
continue;
|
|
324
212
|
}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
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)
|
|
333
|
-
&& canonicalContractJson(depth.minimumReasons.map(prose)) === canonicalContractJson(candidate.depthDecision.minimumReasons.map(prose))
|
|
334
|
-
&& canonicalContractJson(depth.riskFactors.map(risk => ({ ...risk, rationale: prose(risk.rationale) }))) === canonicalContractJson(candidate.depthDecision.riskFactors.map(risk => ({ ...risk, rationale: prose(risk.rationale) })))
|
|
335
|
-
&& 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);
|
|
336
|
-
const matchingReference = (actual, expected) => actual === expected || (!actual.startsWith("/") && !actual.split(/[\\/]/).includes("..") && expected.endsWith(`/${actual}`));
|
|
337
|
-
const matchingInputs = Array.isArray(inputs) && inputs.length === candidate.inputs.length && inputs.every((input, index) => {
|
|
338
|
-
const expected = candidate.inputs[index];
|
|
339
|
-
return expected && input && typeof input.reference === "string" && matchingReference(input.reference, expected.reference) && canonicalContractJson({ ...input, reference: expected.reference }) === canonicalContractJson(expected);
|
|
340
|
-
});
|
|
341
|
-
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)
|
|
342
|
-
throw new Error("Preparation changed host-owned identity/depth/input authority");
|
|
343
|
-
}
|
|
344
|
-
else if (Object.keys(proposal).some(key => !["checks", "coverage", "baseline", "preparationEvidence", "unresolved"].includes(key)))
|
|
345
|
-
throw new Error("QA preparation draft contains unexpected host-owned or unsupported fields");
|
|
346
|
-
if (![proposal.checks, proposal.coverage, proposal.baseline, proposal.preparationEvidence, proposal.unresolved].every(Array.isArray))
|
|
347
|
-
throw new Error("QA preparation draft is missing required proposal arrays");
|
|
348
|
-
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)
|
|
349
220
|
throw new Error("QA preparation host evidence normalization is unavailable");
|
|
350
|
-
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 };
|
|
351
222
|
draft.createdAt = candidate.createdAt;
|
|
352
223
|
if (candidate.predecessorDigest === undefined)
|
|
353
224
|
delete draft.predecessorDigest;
|
|
@@ -363,7 +234,13 @@ export async function prepareVerificationContract(initial, inventory, deps) {
|
|
|
363
234
|
draft.requirements.push(...inventory.filter(item => !present.has(item.id)).map(item => structuredClone(item)));
|
|
364
235
|
}
|
|
365
236
|
try {
|
|
366
|
-
|
|
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;
|
|
367
244
|
}
|
|
368
245
|
catch (error) {
|
|
369
246
|
const detail = error instanceof Error ? error.message : String(error);
|
|
@@ -377,23 +254,6 @@ export async function prepareVerificationContract(initial, inventory, deps) {
|
|
|
377
254
|
evidence.operationId = response.operationId;
|
|
378
255
|
evidence.sessionId = response.sessionId;
|
|
379
256
|
}
|
|
380
|
-
if (response.text.includes("RAFI_QA_ESCALATION_START")) {
|
|
381
|
-
const request = parseProviderContract(qaPreparationEscalationProposalV1Schema, parsePreparationArtifact(response.text, "RAFI_QA_ESCALATION"), "QA preparation escalation");
|
|
382
|
-
const minimum = Math.max(candidate.depthDecision.level, ...request.riskFactors.map(risk => QA_RISK_MINIMUMS[risk.category]));
|
|
383
|
-
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)}`);
|
|
384
|
-
const decision = await parsed(revision, "RAFI_QA_DEPTH");
|
|
385
|
-
decision.plannerIdentity = revision.sessionId;
|
|
386
|
-
decision.plannerOperationId = revision.operationId;
|
|
387
|
-
const errors = validateDepthDecision(decision, candidate.depthDecision);
|
|
388
|
-
if (decision.level < minimum || request.riskFactors.some(risk => !decision.riskFactors.some(actual => actual.category === risk.category)))
|
|
389
|
-
errors.push("Planner revision does not address evidenced risk minimum");
|
|
390
|
-
if (errors.length)
|
|
391
|
-
throw new Error(`depth-decision-incomplete: ${errors.join("; ")}`);
|
|
392
|
-
candidate.depthDecision = decision;
|
|
393
|
-
store.reviseDepth(budget, decision);
|
|
394
|
-
store.retainProgress(budget, "depth-decision", decision);
|
|
395
|
-
continue;
|
|
396
|
-
}
|
|
397
257
|
const structural = validateCandidate(candidate, inventory, store.equivalentResolver());
|
|
398
258
|
if (!structural.length)
|
|
399
259
|
break;
|
|
@@ -405,8 +265,8 @@ export async function prepareVerificationContract(initial, inventory, deps) {
|
|
|
405
265
|
throw new Error(`Contract incomplete: ${structural.join("; ")}`);
|
|
406
266
|
const binding = { draftPayloadDigest: draftDigest(candidate), inventoryDigest: inventoryDigest(candidate.requirements), policyVersion: candidate.depthDecision.policyVersion, authorSessionId };
|
|
407
267
|
const retainedAssessment = store.progress(budget, "assessment");
|
|
408
|
-
const normalizeAssessment = (value, response
|
|
409
|
-
const normalized =
|
|
268
|
+
const normalizeAssessment = (value, response) => {
|
|
269
|
+
const normalized = structuredClone(value);
|
|
410
270
|
// These bindings are host-owned. A repair may omit them or emit a
|
|
411
271
|
// placeholder; restore only the exact current binding and retained
|
|
412
272
|
// obligation labels, never provider-invented content.
|
|
@@ -419,13 +279,9 @@ export async function prepareVerificationContract(initial, inventory, deps) {
|
|
|
419
279
|
normalized.sessionId = response.sessionId;
|
|
420
280
|
if (response.operationId)
|
|
421
281
|
normalized.operationId = response.operationId;
|
|
422
|
-
if ((!normalized.assessedObligations || normalized.assessedObligations.length === 0) && fallback?.assessedObligations?.length)
|
|
423
|
-
normalized.assessedObligations = [...fallback.assessedObligations];
|
|
424
|
-
if ((!normalized.assessedRequirementIds || normalized.assessedRequirementIds.length === 0) && fallback?.assessedRequirementIds?.length)
|
|
425
|
-
normalized.assessedRequirementIds = [...fallback.assessedRequirementIds];
|
|
426
282
|
return normalized;
|
|
427
283
|
};
|
|
428
|
-
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
|
|
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)}`);
|
|
429
285
|
let assessed = assessmentResponse
|
|
430
286
|
? await parsedContract(assessmentResponse, "RAFI_QA_ASSESSMENT", validateAssessmentContract, "QA semantic assessment", (value, response) => normalizeAssessment(value, response))
|
|
431
287
|
: { value: retainedAssessment, response: undefined };
|
|
@@ -433,14 +289,43 @@ export async function prepareVerificationContract(initial, inventory, deps) {
|
|
|
433
289
|
// session identity, but it cannot prove or safely author the builder's ID.
|
|
434
290
|
assessed.value.authorSessionId = binding.authorSessionId;
|
|
435
291
|
for (let repair = 0; assessmentResponse && repair < 3; repair++) {
|
|
436
|
-
//
|
|
437
|
-
// semantic validation. Otherwise an assessor's material sibling-absence
|
|
438
|
-
// concern is rejected before the host can downgrade that non-binding
|
|
439
|
-
// concern to an auditable, non-material observation.
|
|
440
|
-
assessed.value = normalizeExpectedFutureAssessment(assessed.value, candidate);
|
|
292
|
+
// Native concerns retain their declared materiality; prose cannot downgrade it.
|
|
441
293
|
const semanticIssues = validateAssessment(candidate, assessed.value, authorSessionId);
|
|
442
294
|
if (!semanticIssues.length)
|
|
443
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
|
+
}
|
|
444
329
|
if (repair === 2)
|
|
445
330
|
throw new Error(semanticIssues.join("; "));
|
|
446
331
|
const preservedAuthorSessionId = assessed.value.authorSessionId;
|
|
@@ -454,8 +339,8 @@ export async function prepareVerificationContract(initial, inventory, deps) {
|
|
|
454
339
|
authorSessionId: assessed.value.authorSessionId,
|
|
455
340
|
};
|
|
456
341
|
const freshAssessment = semanticIssues.includes("Assessment lacks independent authorship");
|
|
457
|
-
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
|
|
458
|
-
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));
|
|
459
344
|
// Repairs may omit this immutable binding while rewriting concerns. Preserve
|
|
460
345
|
// it only when the prior parsed assessment supplied it; a conflicting value
|
|
461
346
|
// remains visible to validateAssessment and still fails closed.
|
|
@@ -464,7 +349,7 @@ export async function prepareVerificationContract(initial, inventory, deps) {
|
|
|
464
349
|
repaired.value.authorSessionId = binding.authorSessionId;
|
|
465
350
|
assessed = repaired;
|
|
466
351
|
}
|
|
467
|
-
const assessment =
|
|
352
|
+
const assessment = assessed.value;
|
|
468
353
|
// These binding fields are host-owned. Providers frequently copy or
|
|
469
354
|
// truncate the inventory digest while describing their assessment; that
|
|
470
355
|
// must not invalidate an otherwise substantive independent review.
|
|
@@ -480,7 +365,7 @@ export async function prepareVerificationContract(initial, inventory, deps) {
|
|
|
480
365
|
if (challenge?.draftPayloadDigest !== draftDigest(candidate))
|
|
481
366
|
challenge = undefined;
|
|
482
367
|
if (candidate.depthDecision.level === 5 && !challenge) {
|
|
483
|
-
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)}`);
|
|
484
369
|
const challenged = await parsedContract(response, "RAFI_QA_CHALLENGE", validateChallengeContract, "QA approach challenge", (value, response) => normalizeAssessment(value, response));
|
|
485
370
|
challenge = normalizeAssessmentMetadata(challenged.value);
|
|
486
371
|
challenge.sessionId = challenged.response.sessionId;
|