ai-foreman 1.7.27 → 1.7.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/claude.js +1 -1
- package/dist/adapters/types.d.ts +1 -0
- package/dist/buildApproval.d.ts +15 -0
- package/dist/buildApproval.js +43 -0
- package/dist/cli/start.d.ts +2 -1
- package/dist/cli/start.js +35 -5
- package/dist/cli/tickets.d.ts +7 -0
- package/dist/cli/tickets.js +18 -3
- package/dist/config.js +13 -4
- package/dist/foreman.d.ts +11 -2
- package/dist/foreman.js +22 -10
- package/dist/hostGraphApplicability.d.ts +45 -0
- package/dist/hostGraphApplicability.js +99 -0
- package/dist/hostQaAugmentation.d.ts +18 -0
- package/dist/hostQaAugmentation.js +64 -0
- package/dist/hostQaComparison.d.ts +50 -0
- package/dist/hostQaComparison.js +196 -0
- package/dist/observability.d.ts +2 -0
- package/dist/observability.js +2 -1
- package/dist/permissions/policy.js +11 -0
- package/dist/qaBuildGate.js +3 -2
- package/dist/qaBuilderCoverage.js +11 -2
- package/dist/qaContractCoverage.js +12 -3
- package/dist/qaEffectiveConfig.d.ts +1 -0
- package/dist/qaEffectiveConfig.js +6 -1
- package/dist/qaPreparation.js +21 -2
- package/dist/qaPreparationStore.d.ts +4 -0
- package/dist/qaPreparationStore.js +42 -6
- package/dist/qaReview.js +38 -3
- package/dist/qaToolRecovery.d.ts +12 -0
- package/dist/qaToolRecovery.js +44 -0
- package/dist/qaVerificationContract.d.ts +9 -0
- package/dist/qaVerificationContract.js +76 -1
- package/dist/supervisedStart.d.ts +1 -1
- package/dist/supervisedStart.js +65 -9
- package/dist/supervisor.d.ts +18 -2
- package/dist/supervisor.js +74 -17
- package/dist/ticketPopulation.js +2 -0
- package/dist/workflowDb.js +39 -5
- package/package.json +3 -3
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { applyHostQaComparison } from "./hostQaComparison.js";
|
|
2
|
+
import { applyHostGraphApplicability } from "./hostGraphApplicability.js";
|
|
3
|
+
function commandObservationFor(check, observations) {
|
|
4
|
+
const command = check.verification.find(method => method.kind === "command");
|
|
5
|
+
const argv = command?.argv ?? [];
|
|
6
|
+
const kind = argv[0] === "npm" && argv[1] === "test" ? "npm-test" : argv[0] === "node" && argv[1] === "--test" ? "node-test" : undefined;
|
|
7
|
+
if (!kind)
|
|
8
|
+
return undefined;
|
|
9
|
+
return observations.find(observation => observation.kind === kind && observation.outcome === "completed" && observation.exitCode === 0 && typeof observation.spanId === "string");
|
|
10
|
+
}
|
|
11
|
+
function applyHostCommandObservations(coverage, contract, observations) {
|
|
12
|
+
const parsed = observations.filter((observation) => Boolean(observation && typeof observation === "object"));
|
|
13
|
+
const copy = structuredClone(coverage);
|
|
14
|
+
for (const check of contract.checks) {
|
|
15
|
+
const observation = commandObservationFor(check, parsed);
|
|
16
|
+
if (!observation)
|
|
17
|
+
continue;
|
|
18
|
+
const row = copy.checks.find(item => item.checkId === check.id);
|
|
19
|
+
if (!row || row.outcome === "failed")
|
|
20
|
+
continue;
|
|
21
|
+
row.outcome = "passed";
|
|
22
|
+
row.evidence = [...(row.evidence ?? []), {
|
|
23
|
+
procedure: "Rafi host-retained command observation",
|
|
24
|
+
cwd: ".",
|
|
25
|
+
runtime: "Rafi host",
|
|
26
|
+
sourceDigest: copy.sourceDigest,
|
|
27
|
+
reference: `host-command-span:${observation.spanId}`,
|
|
28
|
+
}];
|
|
29
|
+
}
|
|
30
|
+
return copy;
|
|
31
|
+
}
|
|
32
|
+
function applyBaselineAuthorities(coverage, contract) {
|
|
33
|
+
const copy = structuredClone(coverage);
|
|
34
|
+
for (const check of contract.checks) {
|
|
35
|
+
if (check.timing !== "preimplementation")
|
|
36
|
+
continue;
|
|
37
|
+
const baseline = contract.baseline.find(observation => observation.checkId === check.id && observation.status === "passed");
|
|
38
|
+
const row = copy.checks.find(item => item.checkId === check.id);
|
|
39
|
+
if (!baseline || !row || row.applicability !== "applicable" || row.outcome !== "passed")
|
|
40
|
+
continue;
|
|
41
|
+
if (row.evidence?.some(evidence => evidence.reference === baseline.evidenceDigest))
|
|
42
|
+
continue;
|
|
43
|
+
row.evidence = [...(row.evidence ?? []), {
|
|
44
|
+
procedure: "Rafi host-retained preimplementation baseline authority",
|
|
45
|
+
cwd: ".",
|
|
46
|
+
runtime: "Rafi host",
|
|
47
|
+
sourceDigest: copy.sourceDigest,
|
|
48
|
+
reference: baseline.evidenceDigest,
|
|
49
|
+
}];
|
|
50
|
+
}
|
|
51
|
+
return copy;
|
|
52
|
+
}
|
|
53
|
+
/** Applies every host-owned coverage decision in one canonical order. */
|
|
54
|
+
export function applyHostQaAugmentations(input) {
|
|
55
|
+
let coverage = input.coverage;
|
|
56
|
+
coverage = applyBaselineAuthorities(coverage, input.contract);
|
|
57
|
+
if (input.commandObservations?.length)
|
|
58
|
+
coverage = applyHostCommandObservations(coverage, input.contract, input.commandObservations);
|
|
59
|
+
if (input.hostComparison)
|
|
60
|
+
coverage = applyHostQaComparison(coverage, input.contract, input.hostComparison.result, input.hostComparison.resultDigest, input.projectDir);
|
|
61
|
+
if (input.hostGraph)
|
|
62
|
+
coverage = applyHostGraphApplicability(coverage, input.contract, input.hostGraph.result, input.hostGraph.resultDigest, input.projectDir);
|
|
63
|
+
return coverage;
|
|
64
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { QaContractCoverageV1, QaVerificationContractV1 } from "rafi-spec";
|
|
3
|
+
import type { TicketDef } from "./tickets/ticketSchema.js";
|
|
4
|
+
import type { WorkflowDb } from "./workflowDb.js";
|
|
5
|
+
export declare const hostQaComparisonV1Schema: z.ZodObject<{
|
|
6
|
+
version: z.ZodLiteral<1>;
|
|
7
|
+
runId: z.ZodString;
|
|
8
|
+
ticketId: z.ZodString;
|
|
9
|
+
assignmentId: z.ZodString;
|
|
10
|
+
contractDigest: z.ZodString;
|
|
11
|
+
contractRevision: z.ZodNumber;
|
|
12
|
+
reviewAttemptId: z.ZodString;
|
|
13
|
+
reviewBasisDigest: z.ZodString;
|
|
14
|
+
baselineSourceDigest: z.ZodString;
|
|
15
|
+
finalSourceDigest: z.ZodString;
|
|
16
|
+
scopeBasisDigest: z.ZodString;
|
|
17
|
+
status: z.ZodEnum<{
|
|
18
|
+
failed: "failed";
|
|
19
|
+
passed: "passed";
|
|
20
|
+
blocked: "blocked";
|
|
21
|
+
}>;
|
|
22
|
+
changedPaths: z.ZodArray<z.ZodString>;
|
|
23
|
+
unauthorizedPaths: z.ZodArray<z.ZodString>;
|
|
24
|
+
deletedPaths: z.ZodArray<z.ZodString>;
|
|
25
|
+
protectedPaths: z.ZodArray<z.ZodString>;
|
|
26
|
+
reason: z.ZodString;
|
|
27
|
+
comparedAt: z.ZodString;
|
|
28
|
+
}, z.core.$strip>;
|
|
29
|
+
export type HostQaComparisonV1 = z.infer<typeof hostQaComparisonV1Schema>;
|
|
30
|
+
export declare function findHostQaCheck(contract: QaVerificationContractV1): QaVerificationContractV1["checks"][number] | undefined;
|
|
31
|
+
/**
|
|
32
|
+
* Performs the private host comparison and returns the immutable artifact plus
|
|
33
|
+
* the coverage row replacement. Provider output is never used to decide scope.
|
|
34
|
+
*/
|
|
35
|
+
export declare function createHostQaComparison(input: {
|
|
36
|
+
db: Pick<WorkflowDb, "operations" | "qaPreparationStore">;
|
|
37
|
+
projectDir: string;
|
|
38
|
+
worktree: string;
|
|
39
|
+
runId: string;
|
|
40
|
+
ticket: TicketDef;
|
|
41
|
+
contract: QaVerificationContractV1;
|
|
42
|
+
reviewAttemptId: string;
|
|
43
|
+
reviewBasisDigest: string;
|
|
44
|
+
}): {
|
|
45
|
+
result: HostQaComparisonV1;
|
|
46
|
+
resultDigest: string;
|
|
47
|
+
checkId?: string;
|
|
48
|
+
};
|
|
49
|
+
export declare function applyHostQaComparison(coverage: QaContractCoverageV1, contract: QaVerificationContractV1, result: HostQaComparisonV1, resultDigest: string, projectDir: string): QaContractCoverageV1;
|
|
50
|
+
export declare function hostComparisonDigestFromCoverage(coverage: QaContractCoverageV1, checkId: string): string | undefined;
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { captureBuildSource } from "./buildAssignment.js";
|
|
3
|
+
import { contractDigest } from "./qaVerificationContract.js";
|
|
4
|
+
const digest = z.string().regex(/^[a-f0-9]{64}$/);
|
|
5
|
+
const path = z.string().min(1).max(1024);
|
|
6
|
+
export const hostQaComparisonV1Schema = z.object({
|
|
7
|
+
version: z.literal(1),
|
|
8
|
+
runId: z.string().min(1),
|
|
9
|
+
ticketId: z.string().min(1),
|
|
10
|
+
assignmentId: z.string().min(1),
|
|
11
|
+
contractDigest: digest,
|
|
12
|
+
contractRevision: z.number().int().positive(),
|
|
13
|
+
reviewAttemptId: z.string().min(1),
|
|
14
|
+
reviewBasisDigest: digest,
|
|
15
|
+
baselineSourceDigest: digest,
|
|
16
|
+
finalSourceDigest: digest,
|
|
17
|
+
scopeBasisDigest: digest,
|
|
18
|
+
status: z.enum(["passed", "failed", "blocked"]),
|
|
19
|
+
changedPaths: z.array(path).max(256),
|
|
20
|
+
unauthorizedPaths: z.array(path).max(256),
|
|
21
|
+
deletedPaths: z.array(path).max(256),
|
|
22
|
+
protectedPaths: z.array(path).max(256),
|
|
23
|
+
reason: z.string().max(2048),
|
|
24
|
+
comparedAt: z.string().datetime(),
|
|
25
|
+
});
|
|
26
|
+
function identity(item) {
|
|
27
|
+
if (!item)
|
|
28
|
+
return "missing";
|
|
29
|
+
return JSON.stringify({
|
|
30
|
+
headObject: item.headObject,
|
|
31
|
+
indexObject: item.indexObject,
|
|
32
|
+
worktreeObject: item.worktreeObject,
|
|
33
|
+
untracked: item.untracked,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
function matchesScope(candidate, patterns) {
|
|
37
|
+
return patterns.some(pattern => {
|
|
38
|
+
const normalized = pattern.replaceAll("\\", "/").replace(/^\.\//, "");
|
|
39
|
+
if (normalized.endsWith("/**"))
|
|
40
|
+
return candidate === normalized.slice(0, -3) || candidate.startsWith(normalized.slice(0, -2));
|
|
41
|
+
if (normalized.includes("*")) {
|
|
42
|
+
const expression = new RegExp(`^${normalized.split("*").map(part => part.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join(".*")}$`);
|
|
43
|
+
return expression.test(candidate);
|
|
44
|
+
}
|
|
45
|
+
return candidate === normalized;
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
function isHostQaCheck(check) {
|
|
49
|
+
const text = `${check.expectedBehavior} ${check.expectedEvidence.join(" ")} ${check.applicability.requiredEvidence.join(" ")}`;
|
|
50
|
+
const hostScopeEvidence = check.applicability.requiredEvidence.some(evidence => /host[- ]owned.*scope comparison/i.test(evidence));
|
|
51
|
+
const scopeSemantics = /scope/i.test(text) && /comparison/i.test(text) && /baseline|protected|approved scope|product changes/i.test(text);
|
|
52
|
+
const changedProductPaths = /changed product paths/i.test(text) && /host[- ]retained|baseline|bookkeeping|evidence exclusions/i.test(text);
|
|
53
|
+
return !/documentation/i.test(text)
|
|
54
|
+
&& (hostScopeEvidence || changedProductPaths || (check.applicability.decisionOwner === "host" && (/Rafi compares the final product paths/i.test(check.expectedBehavior) || scopeSemantics)));
|
|
55
|
+
}
|
|
56
|
+
export function findHostQaCheck(contract) {
|
|
57
|
+
return contract.checks.find(isHostQaCheck);
|
|
58
|
+
}
|
|
59
|
+
function findHostQaChecks(contract) {
|
|
60
|
+
return contract.checks.filter(isHostQaCheck);
|
|
61
|
+
}
|
|
62
|
+
function findHostDocumentationCheck(contract) {
|
|
63
|
+
return contract.checks.find(check => check.applicability.decisionOwner === "host"
|
|
64
|
+
&& /documentation/i.test(check.expectedBehavior)
|
|
65
|
+
&& /documentation/i.test(check.expectedEvidence.join(" ")));
|
|
66
|
+
}
|
|
67
|
+
function findAssignment(db, runId, ticketId, baselineDigest) {
|
|
68
|
+
const operations = db.operations(runId);
|
|
69
|
+
return operations
|
|
70
|
+
.filter(operation => operation.intent?.ticketId === ticketId && operation.intent?.before?.digest === baselineDigest
|
|
71
|
+
&& Array.isArray(operation.intent.before.pathInventory) && Array.isArray(operation.result?.after?.pathInventory))
|
|
72
|
+
.at(-1);
|
|
73
|
+
}
|
|
74
|
+
function baselineDigest(contract) {
|
|
75
|
+
return contract.inputs.find(input => input.id === "preimplementation-source")?.digest;
|
|
76
|
+
}
|
|
77
|
+
function resultEvidence(resultDigest, projectDir, sourceDigest, status, reason) {
|
|
78
|
+
return {
|
|
79
|
+
procedure: "Rafi host-owned comparison of the retained preimplementation inventory and final Builder source",
|
|
80
|
+
cwd: projectDir,
|
|
81
|
+
runtime: "Rafi host",
|
|
82
|
+
sourceDigest,
|
|
83
|
+
reference: `host-qa-comparison:${resultDigest}`,
|
|
84
|
+
...(status === "passed" ? {} : { limitation: reason }),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Performs the private host comparison and returns the immutable artifact plus
|
|
89
|
+
* the coverage row replacement. Provider output is never used to decide scope.
|
|
90
|
+
*/
|
|
91
|
+
export function createHostQaComparison(input) {
|
|
92
|
+
const check = findHostQaCheck(input.contract);
|
|
93
|
+
if (!check)
|
|
94
|
+
throw new Error("Host QA comparison contract check is missing");
|
|
95
|
+
const expectedBaseline = baselineDigest(input.contract);
|
|
96
|
+
if (!expectedBaseline)
|
|
97
|
+
throw new Error("Host QA comparison lacks the admitted preimplementation baseline");
|
|
98
|
+
const assignment = findAssignment(input.db, input.runId, input.ticket.id, expectedBaseline);
|
|
99
|
+
const finalFirst = captureBuildSource(input.worktree);
|
|
100
|
+
const finalSecond = captureBuildSource(input.worktree);
|
|
101
|
+
if (finalFirst.digest !== finalSecond.digest)
|
|
102
|
+
throw new Error("Host QA comparison source changed during final capture");
|
|
103
|
+
const final = finalSecond;
|
|
104
|
+
const before = assignment?.intent.before;
|
|
105
|
+
const assignmentId = assignment?.idempotencyKey ?? "missing-assignment";
|
|
106
|
+
const beforePaths = new Map((before?.pathInventory ?? []).map(item => [item.path, item]));
|
|
107
|
+
const afterPaths = new Map(final.pathInventory.map(item => [item.path, item]));
|
|
108
|
+
const allPaths = [...new Set([...beforePaths.keys(), ...afterPaths.keys()])].sort();
|
|
109
|
+
const changedPaths = allPaths.filter(item => identity(beforePaths.get(item)) !== identity(afterPaths.get(item)));
|
|
110
|
+
const deletedPaths = [...beforePaths.keys()].filter(item => !afterPaths.has(item)).sort();
|
|
111
|
+
const protectedPaths = changedPaths.filter(item => item === ".git" || item.startsWith(".git/") || item === ".rafi" || item.startsWith(".rafi/") || item === ".foreman" || item.startsWith(".foreman/")).sort();
|
|
112
|
+
const scopePatterns = input.ticket.likely_files.map(item => item.replaceAll("\\", "/"));
|
|
113
|
+
const unauthorizedPaths = scopePatterns.length ? changedPaths.filter(item => !matchesScope(item, scopePatterns)) : changedPaths;
|
|
114
|
+
const scopeBasisDigest = contractDigest("host-qa-scope-basis", { ticketId: input.ticket.id, ticket: input.ticket, contractDigest: input.contract.contentDigest, assignmentId });
|
|
115
|
+
const missingBaseline = !assignment || !before || before.digest !== expectedBaseline;
|
|
116
|
+
const status = missingBaseline || protectedPaths.length || deletedPaths.length || unauthorizedPaths.length ? (missingBaseline ? "blocked" : "failed") : "passed";
|
|
117
|
+
const reason = missingBaseline
|
|
118
|
+
? "The admitted Builder assignment baseline was not retained or does not match the contract baseline."
|
|
119
|
+
: protectedPaths.length
|
|
120
|
+
? `Protected paths changed: ${protectedPaths.slice(0, 8).join(", ")}`
|
|
121
|
+
: deletedPaths.length
|
|
122
|
+
? `Product paths were deleted: ${deletedPaths.slice(0, 8).join(", ")}`
|
|
123
|
+
: unauthorizedPaths.length
|
|
124
|
+
? `Changed paths are outside the host-resolved ticket scope: ${unauthorizedPaths.slice(0, 8).join(", ")}`
|
|
125
|
+
: "Host-observed product paths match the retained baseline and host-resolved ticket scope.";
|
|
126
|
+
const result = hostQaComparisonV1Schema.parse({ version: 1, runId: input.runId, ticketId: input.ticket.id, assignmentId, contractDigest: input.contract.contentDigest, contractRevision: input.contract.revision, reviewAttemptId: input.reviewAttemptId, reviewBasisDigest: input.reviewBasisDigest, baselineSourceDigest: expectedBaseline, finalSourceDigest: final.digest, scopeBasisDigest, status, changedPaths, unauthorizedPaths, deletedPaths, protectedPaths, reason, comparedAt: new Date().toISOString() });
|
|
127
|
+
const store = input.db.qaPreparationStore();
|
|
128
|
+
const existing = store.artifacts("host-qa-comparison").find(item => {
|
|
129
|
+
const candidate = hostQaComparisonV1Schema.safeParse(item.value);
|
|
130
|
+
if (!candidate.success)
|
|
131
|
+
return false;
|
|
132
|
+
const prior = candidate.data;
|
|
133
|
+
return prior.runId === result.runId && prior.ticketId === result.ticketId && prior.assignmentId === result.assignmentId
|
|
134
|
+
&& prior.contractDigest === result.contractDigest && prior.contractRevision === result.contractRevision
|
|
135
|
+
&& prior.reviewAttemptId === result.reviewAttemptId && prior.reviewBasisDigest === result.reviewBasisDigest
|
|
136
|
+
&& prior.baselineSourceDigest === result.baselineSourceDigest && prior.finalSourceDigest === result.finalSourceDigest
|
|
137
|
+
&& prior.scopeBasisDigest === result.scopeBasisDigest && prior.status === result.status
|
|
138
|
+
&& JSON.stringify(prior.changedPaths) === JSON.stringify(result.changedPaths)
|
|
139
|
+
&& JSON.stringify(prior.unauthorizedPaths) === JSON.stringify(result.unauthorizedPaths)
|
|
140
|
+
&& JSON.stringify(prior.deletedPaths) === JSON.stringify(result.deletedPaths)
|
|
141
|
+
&& JSON.stringify(prior.protectedPaths) === JSON.stringify(result.protectedPaths)
|
|
142
|
+
&& prior.reason === result.reason;
|
|
143
|
+
});
|
|
144
|
+
const resultDigest = existing?.digest ?? store.putArtifact("host-qa-comparison", result);
|
|
145
|
+
return { result: existing ? hostQaComparisonV1Schema.parse(existing.value) : result, resultDigest, checkId: check.id };
|
|
146
|
+
}
|
|
147
|
+
export function applyHostQaComparison(coverage, contract, result, resultDigest, projectDir) {
|
|
148
|
+
const parsed = hostQaComparisonV1Schema.parse(result);
|
|
149
|
+
if (parsed.contractDigest !== contract.contentDigest || parsed.contractRevision !== contract.revision || parsed.runId !== contract.runId || parsed.ticketId !== contract.workId || parsed.finalSourceDigest !== coverage.sourceDigest || parsed.reviewAttemptId !== coverage.attemptId || parsed.reviewBasisDigest !== coverage.inputBasisDigest)
|
|
150
|
+
throw new Error("Host QA comparison is not bound to the current contract, review, or source");
|
|
151
|
+
const checks = findHostQaChecks(contract);
|
|
152
|
+
if (!checks.length)
|
|
153
|
+
throw new Error("Host QA comparison contract check is missing");
|
|
154
|
+
const copy = structuredClone(coverage);
|
|
155
|
+
for (const check of checks) {
|
|
156
|
+
const row = {
|
|
157
|
+
checkId: check.id,
|
|
158
|
+
applicability: "applicable",
|
|
159
|
+
predicateEvidence: ["Rafi host-owned scope comparison executed against the retained baseline."],
|
|
160
|
+
outcome: parsed.status === "passed" ? "passed" : parsed.status === "failed" ? "failed" : "blocked",
|
|
161
|
+
evidence: [resultEvidence(resultDigest, projectDir, parsed.finalSourceDigest, parsed.status, parsed.reason)],
|
|
162
|
+
};
|
|
163
|
+
const index = copy.checks.findIndex(item => item.checkId === check.id);
|
|
164
|
+
if (index < 0)
|
|
165
|
+
copy.checks.push(row);
|
|
166
|
+
else
|
|
167
|
+
copy.checks[index] = row;
|
|
168
|
+
}
|
|
169
|
+
const documentationCheck = findHostDocumentationCheck(contract);
|
|
170
|
+
if (documentationCheck && contract.documentation?.required === false) {
|
|
171
|
+
const documentationRow = {
|
|
172
|
+
checkId: documentationCheck.id,
|
|
173
|
+
applicability: "applicable",
|
|
174
|
+
predicateEvidence: ["The admitted contract explicitly sets documentation.required to false."],
|
|
175
|
+
outcome: "passed",
|
|
176
|
+
evidence: [{
|
|
177
|
+
procedure: "Rafi host-owned evaluation of the admitted documentation policy",
|
|
178
|
+
cwd: projectDir,
|
|
179
|
+
runtime: "Rafi host",
|
|
180
|
+
sourceDigest: parsed.finalSourceDigest,
|
|
181
|
+
reference: `host-qa-documentation:${contract.contentDigest}`,
|
|
182
|
+
}],
|
|
183
|
+
};
|
|
184
|
+
const documentationIndex = copy.checks.findIndex(item => item.checkId === documentationCheck.id);
|
|
185
|
+
if (documentationIndex < 0)
|
|
186
|
+
copy.checks.push(documentationRow);
|
|
187
|
+
else
|
|
188
|
+
copy.checks[documentationIndex] = documentationRow;
|
|
189
|
+
}
|
|
190
|
+
return copy;
|
|
191
|
+
}
|
|
192
|
+
export function hostComparisonDigestFromCoverage(coverage, checkId) {
|
|
193
|
+
const reference = coverage.checks.find(row => row.checkId === checkId)?.evidence.find(item => item.reference.startsWith("host-qa-comparison:"))?.reference;
|
|
194
|
+
const value = reference?.slice("host-qa-comparison:".length);
|
|
195
|
+
return value && /^[a-f0-9]{64}$/.test(value) ? value : undefined;
|
|
196
|
+
}
|
package/dist/observability.d.ts
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import type { ConfigurableAgentRole, DiagnosticSourceResultV1, ManagerRunSummaryV1, RunCurrentStateV1, RunRollupV1, RunSpanV1 } from "rafi-spec";
|
|
2
2
|
export declare const OBSERVABILITY_DB_FILE = ".rafi/observability.sqlite3";
|
|
3
3
|
export declare const OBSERVABILITY_SCHEMA_VERSION: 2;
|
|
4
|
+
export type ObservabilityMode = "off" | "summary" | "trace";
|
|
4
5
|
export interface ObservabilityConfig {
|
|
5
6
|
enabled: boolean;
|
|
7
|
+
mode: ObservabilityMode;
|
|
6
8
|
sample_interval_seconds: number;
|
|
7
9
|
detail_retention_days: number;
|
|
8
10
|
log_retention_days: number;
|
package/dist/observability.js
CHANGED
|
@@ -10,6 +10,7 @@ export const OBSERVABILITY_DB_FILE = ".rafi/observability.sqlite3";
|
|
|
10
10
|
export const OBSERVABILITY_SCHEMA_VERSION = 2;
|
|
11
11
|
export const DEFAULT_OBSERVABILITY_CONFIG = {
|
|
12
12
|
enabled: true,
|
|
13
|
+
mode: "summary",
|
|
13
14
|
sample_interval_seconds: 60,
|
|
14
15
|
detail_retention_days: 30,
|
|
15
16
|
log_retention_days: 30,
|
|
@@ -49,7 +50,7 @@ export class ObservabilityStore {
|
|
|
49
50
|
this.db.pragma("foreign_keys = ON");
|
|
50
51
|
this.migrate();
|
|
51
52
|
this.backfillBuildSnapshots();
|
|
52
|
-
this.summaryOnly = this.meta("summary_only") === "1";
|
|
53
|
+
this.summaryOnly = this.config.mode === "summary" || this.meta("summary_only") === "1";
|
|
53
54
|
}
|
|
54
55
|
close() { if (this.db.open)
|
|
55
56
|
this.db.close(); }
|
|
@@ -65,6 +65,17 @@ export class PermissionPolicy {
|
|
|
65
65
|
if (hitEscalate) {
|
|
66
66
|
return { decision: "escalate", reason: `command matches "${hitEscalate}"` };
|
|
67
67
|
}
|
|
68
|
+
// Permit only the bounded exit-status wrapper needed to retain explicit
|
|
69
|
+
// evidence for an already allow-listed project test. The base command is
|
|
70
|
+
// still parsed by the restricted grammar; this does not grant arbitrary
|
|
71
|
+
// shell sequencing, expansion, or output redirection.
|
|
72
|
+
const exitCapture = /^\s*(.+?)\s+2>&1\s*;\s*echo\s+(?:"EXIT_CODE=\$\?"|'EXIT_CODE=\$\?'|EXIT_CODE=\$\?)\s*$/.exec(command);
|
|
73
|
+
if (exitCapture) {
|
|
74
|
+
const reason = classifySupportedShellForm(exitCapture[1], this.cwd, executionCwd, this.config.allowBash);
|
|
75
|
+
return reason
|
|
76
|
+
? { decision: "escalate", reason: `exit-code capture base command rejected: ${reason}` }
|
|
77
|
+
: { decision: "allow", reason: "bounded exit-code capture for an allowed project test" };
|
|
78
|
+
}
|
|
68
79
|
const entry = process.env.RAFI_REHEARSAL_CLI_ENTRY;
|
|
69
80
|
const shellQuote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
|
|
70
81
|
const launchingRehearsal = entry && isAbsolute(entry) && !/[\r\n\0]/.test(entry)
|
package/dist/qaBuildGate.js
CHANGED
|
@@ -14,7 +14,7 @@ import { WorkflowDb } from "./workflowDb.js";
|
|
|
14
14
|
import { createRoleBuilder, readOnlyPermissionConfig } from "./agentRun.js";
|
|
15
15
|
import { captureBuildSource, assertBuildAssignmentReconciled } from "./buildAssignment.js";
|
|
16
16
|
import { createDisposableQaSnapshotAsync } from "./qaSnapshot.js";
|
|
17
|
-
import { allocateStableCheckIds, authoritativeInventory, bindUniqueBaselineCheckIds, contractDigest, mapSnapshotWorkingDirectories, normalizeSnapshotPaths, renderVerificationContract } from "./qaVerificationContract.js";
|
|
17
|
+
import { allocateStableCheckIds, authoritativeInventory, bindUniqueBaselineCheckIds, contractDigest, ensureHostScopeCheck, mapSnapshotWorkingDirectories, normalizeSnapshotPaths, renderVerificationContract, rewriteProviderHostScopeChecks } from "./qaVerificationContract.js";
|
|
18
18
|
import { resolveEffectiveQaConfiguration } from "./qaEffectiveConfig.js";
|
|
19
19
|
import { prepareVerificationContract } from "./qaPreparation.js";
|
|
20
20
|
import { qaHostGraphFact } from "./qaHostFacts.js";
|
|
@@ -23,6 +23,7 @@ import { BUILDER_WORK_COVERAGE_INSTRUCTIONS } from "./qaContractCoverage.js";
|
|
|
23
23
|
import { renderBuildWorkContext } from "./buildWorkContext.js";
|
|
24
24
|
import { compareContractInputs } from "./qaContractFreshness.js";
|
|
25
25
|
import { readBuildRuns } from "./buildRuns.js";
|
|
26
|
+
import { normalizeGraphQaChecks } from "./hostGraphApplicability.js";
|
|
26
27
|
/** A received response missing only continuity metadata is a bounded format defect. */
|
|
27
28
|
export function preparationTurnPayload(turn) {
|
|
28
29
|
const formatErrors = turn.continuityErrors ?? [];
|
|
@@ -129,7 +130,7 @@ async function ensureBuilderContractWithProvenance(projectDir, runId, workId, wo
|
|
|
129
130
|
provider: builder.agent,
|
|
130
131
|
store, now: Date.now, hostFacts: `${qaHostGraphFact(projectDir)}\n${deliveryFact}\nHost-owned baseline fact: Rafi captured the complete preimplementation per-file path manifest and will perform the exact unchanged-file and scope comparison. Its digest is ${preimplementationPathManifestDigest}. Do not require the provider to receive or reproduce the raw manifest; cite the retained host-preimplementation-source observation and assess the host comparison result instead. Do not author a provider-executed check for Rafi's host postimplementation path manifest, complete changed-path inventory, or unchanged-file comparison; Rafi performs that comparison outside the provider snapshot. Ordinary approved product paths, exports, and tests are not a material QA concern merely because the provider cannot prove whether they were present before implementation; Rafi owns that comparison. Do not reconstruct or disclose the full baseline. For current-branch additive tickets, a provider-authored or inferred request to test the branch with an already-completed sibling feature removed is not an applicable requirement when the approved scope says not to remove or reconstruct that sibling; verify the ticket's own behavior and tests against the shared baseline instead. Treat sibling-absence testing as binding only when the user explicitly approved that absence requirement. Escalate only an actual host-observed out-of-scope, destructive, protected-file, or requirement-relevant change.`,
|
|
131
132
|
normalizeDraft: (draft, response) => {
|
|
132
|
-
const normalized = bindUniqueBaselineCheckIds(
|
|
133
|
+
const normalized = allocateStableCheckIds(bindUniqueBaselineCheckIds(rewriteProviderHostScopeChecks(normalizeGraphQaChecks(ensureHostScopeCheck(draft, hostBaseline.id)), hostBaseline.id)));
|
|
133
134
|
if (!normalized.baseline.some(observation => observation.id === hostBaseline.id))
|
|
134
135
|
normalized.baseline.unshift(structuredClone(hostBaseline));
|
|
135
136
|
for (const check of normalized.checks)
|
|
@@ -21,7 +21,7 @@ async function collectBuilderContractCoverageOwned(projectDir, runId, workId, wo
|
|
|
21
21
|
const session = actualContractSession(adapter, workspace, projectDir);
|
|
22
22
|
const contract = assertContractReceipt(store, runId, workId, admission.requirementsDigest, session, adapter);
|
|
23
23
|
const source = captureBuildSource(workspace);
|
|
24
|
-
|
|
24
|
+
let binding = { phase: "builder", sourceDigest: source.digest, inputBasisDigest: contractDigest("builder-input-basis", { operationId, instruction, contractDigest: contract.contentDigest }), attemptId: operationId, sessionId: session.sessionId };
|
|
25
25
|
const eventId = `builder-coverage:${operationId}`;
|
|
26
26
|
const existing = db.operation(eventId);
|
|
27
27
|
if (existing) {
|
|
@@ -44,8 +44,17 @@ async function collectBuilderContractCoverageOwned(projectDir, runId, workId, wo
|
|
|
44
44
|
db.updateOperation(eventId, "uncertain", { result: { responseDigest } });
|
|
45
45
|
if (response.isError || response.failure)
|
|
46
46
|
throw new Error("Builder coverage provider failed; retained response requires reconciliation");
|
|
47
|
-
|
|
47
|
+
const afterSession = actualContractSession(adapter, workspace, projectDir);
|
|
48
|
+
const sameScopedSession = afterSession.provider === session.provider
|
|
49
|
+
&& afterSession.workspace === session.workspace
|
|
50
|
+
&& afterSession.configRoot === session.configRoot
|
|
51
|
+
&& (afterSession.sessionId === session.sessionId || afterSession.compactionSequence > session.compactionSequence);
|
|
52
|
+
if (!sameScopedSession || captureBuildSource(workspace).digest !== source.digest)
|
|
48
53
|
throw new Error("Builder coverage changed session or product source");
|
|
54
|
+
// A provider may compact while producing this response-only evidence.
|
|
55
|
+
// Bind the retained coverage to the host-observed post-compaction session
|
|
56
|
+
// while continuing to reject unscoped session changes and source edits.
|
|
57
|
+
binding = { ...binding, sessionId: afterSession.sessionId };
|
|
49
58
|
const coverage = bindCoverageHostFields(parseContractCoverage(response.text), contract, binding);
|
|
50
59
|
bindHostOwnedBuilderFacts(db, coverage, contract, hostObservations, initialSourceDigest, source.digest, workId, projectDir);
|
|
51
60
|
const errors = validateContractCoverage(contract, coverage, binding, false);
|
|
@@ -2,7 +2,7 @@ import { Ajv } from "ajv";
|
|
|
2
2
|
import { qaContractCoverageV1Schema } from "rafi-spec";
|
|
3
3
|
const coverageSchema = new Ajv({ allErrors: true }).compile(qaContractCoverageV1Schema);
|
|
4
4
|
import { parsePreparationArtifact } from "./qaVerificationContract.js";
|
|
5
|
-
export const COVERAGE_INSTRUCTIONS = `Return RAFI_QA_COVERAGE_START, one QaContractCoverageV1 JSON object, RAFI_QA_COVERAGE_END before the final status marker. The exact schema is ${JSON.stringify(qaContractCoverageV1Schema)}. Put every binding field at the TOP LEVEL, not inside a binding object, and copy the host-supplied binding values exactly. For each check use checkId (not id), applicability as applicable/not-applicable/unresolved, predicateEvidence as an array of strings, outcome as passed/failed/blocked/not-run, and evidence as an array of objects with procedure, cwd, runtime, sourceDigest and reference. For postimplementation evidence sourceDigest is the exact host-supplied sourceDigest, not an individual file hash. Unconditional checks cannot be marked not-applicable or unresolved; for an absence or non-impact check, mark it applicable and provide bounded inspection evidence showing that the absent change or impact was assessed. For independent QA coverage of an applicable passed preimplementation-only check, inspect its matching passed contract.baseline observation and set at least one evidence reference to that observation's exact evidenceDigest; the historical observation is bound to its original source, not the current source. Assess its relevance rather than inventing or claiming a new baseline. Before emitting qa_pass, run every applicable mandatory check that is not backed by a passed retained baseline; do not leave such a check as not-run merely because an earlier turn omitted it. If an exact required command is blocked by the provider shell policy, use a semantically equivalent permitted read-only procedure, record the substitution and limitation truthfully, and assess the same predicate; do not mark the check blocked merely because the literal command syntax was denied. If a required command is permitted and read-only, run it now and retain its bounded result. Do not add unsupported fields. Include every check with truthful applicability and concrete evidence. Missing, blocked, not-run, unresolved applicability, or failed mandatory coverage cannot authorize normal completion. Builder coverage is a claim; final QA independently verifies it.`;
|
|
5
|
+
export const COVERAGE_INSTRUCTIONS = `Return RAFI_QA_COVERAGE_START, one QaContractCoverageV1 JSON object, RAFI_QA_COVERAGE_END before the final status marker. The exact schema is ${JSON.stringify(qaContractCoverageV1Schema)}. Put every binding field at the TOP LEVEL, not inside a binding object, and copy the host-supplied binding values exactly. For each check use checkId (not id), applicability as applicable/not-applicable/unresolved, predicateEvidence as an array of strings, outcome as passed/failed/blocked/not-run, and evidence as an array of objects with procedure, cwd, runtime, sourceDigest and reference. For postimplementation evidence sourceDigest is the exact host-supplied sourceDigest, not an individual file hash. Unconditional checks cannot be marked not-applicable or unresolved; for an absence or non-impact check, mark it applicable and provide bounded inspection evidence showing that the absent change or impact was assessed. For independent QA coverage of an applicable passed preimplementation-only check, inspect its matching passed contract.baseline observation and set at least one evidence reference to that observation's exact evidenceDigest; the historical observation is bound to its original source, not the current source. Assess its relevance rather than inventing or claiming a new baseline. Graphify is optional evidence: do not install, refresh, scan, or claim Graphify execution unless the host says Rafi-managed Graphify is adopted and enabled. Host-owned Graphify applicability is authoritative; disabled, unadopted, or unavailable Graphify is not a QA blocker, while adopted-enabled Graphify is applicable and cannot be waived by provider output. Other host-owned checks whose applicability decisionOwner is host are the exception to provider execution: include the check truthfully as applicable/not-run with evidence that the host-owned comparison is pending, and do not block or fail QA over it; Rafi will execute the comparison and replace that row before accepting the pass. Before emitting qa_pass, run every other applicable mandatory check that is not backed by a passed retained baseline; do not leave such a provider-owned check as not-run merely because an earlier turn omitted it. If an exact required command is blocked by the provider shell policy, use a semantically equivalent permitted read-only procedure, record the substitution and limitation truthfully, and assess the same predicate; do not mark the check blocked merely because the literal command syntax was denied. If a required command is permitted and read-only, run it now and retain its bounded result. For command exit status, use the host-retained tool event's exitCode and bounded output when available; that is authoritative evidence, and you must not create an echo wrapper, compound command, or other shell scaffold solely to print a numeric status. Do not add unsupported fields. Include every check with truthful applicability and concrete evidence. Missing, blocked, not-run, unresolved applicability, or failed provider-owned mandatory coverage cannot authorize normal completion. Builder coverage is a claim; final QA independently verifies it.`;
|
|
6
6
|
// The source and attempt binding do not exist until the implementation turn has finished.
|
|
7
7
|
export const BUILDER_WORK_COVERAGE_INSTRUCTIONS = `Carry out the applicable contract checks and report your work and evidence honestly in your normal final response. When a check requires historical ordering, retain bounded output for each required step and an explicit ordering record while performing it; a later passing test cannot prove an earlier failing step. Do not emit RAFI_QA_COVERAGE JSON during this implementation turn or stop for coverage binding values. After the implementation turn, Rafi will capture the completed source and request a separate response-only coverage record with the exact host-owned bindings.`;
|
|
8
8
|
export function parseContractCoverage(text) { return parsePreparationArtifact(text, "RAFI_QA_COVERAGE"); }
|
|
@@ -20,10 +20,15 @@ export function bindCoverageHostFields(coverage, contract, binding) {
|
|
|
20
20
|
// Optional schema fields may be emitted as JSON null by providers. Null is
|
|
21
21
|
// not a meaningful limitation, so omit only that representation variant;
|
|
22
22
|
// non-string values elsewhere remain validation errors.
|
|
23
|
-
for (const check of copy.checks ?? [])
|
|
23
|
+
for (const check of copy.checks ?? []) {
|
|
24
|
+
// Some providers mirror the applicability token into outcome. Canonicalize
|
|
25
|
+
// that harmless representation mistake at the host boundary.
|
|
26
|
+
if (check.outcome === "not-applicable")
|
|
27
|
+
check.outcome = "not-run";
|
|
24
28
|
for (const evidence of check.evidence ?? [])
|
|
25
29
|
if (evidence.limitation === null)
|
|
26
30
|
delete evidence.limitation;
|
|
31
|
+
}
|
|
27
32
|
copy.version = 1;
|
|
28
33
|
copy.runId = contract.runId;
|
|
29
34
|
copy.workId = contract.workId;
|
|
@@ -61,7 +66,11 @@ export function validateContractCoverage(contract, coverage, binding, requirePas
|
|
|
61
66
|
issues.push(`Unconditional applicability cannot be waived ${row.checkId}`);
|
|
62
67
|
if (row.applicability === "not-applicable" && !row.predicateEvidence?.length)
|
|
63
68
|
issues.push(`Not-applicable requires predicate evidence ${row.checkId}`);
|
|
64
|
-
|
|
69
|
+
// Host-owned conditional predicates are resolved by the host augmentation
|
|
70
|
+
// after provider coverage is collected (for example Graphify adoption).
|
|
71
|
+
// Requiring provider predicate evidence here would make the host-owned,
|
|
72
|
+
// fail-open path block before the authoritative host observation exists.
|
|
73
|
+
if (check.applicability.kind === "conditional" && check.applicability.decisionOwner !== "host" && row.applicability === "applicable" && !row.predicateEvidence?.length)
|
|
65
74
|
issues.push(`Conditional applicability requires predicate evidence ${row.checkId}`);
|
|
66
75
|
if (requirePass && check.obligation === "mandatory" && (row.applicability === "unresolved" || row.applicability === "applicable" && row.outcome !== "passed"))
|
|
67
76
|
issues.push(`Mandatory check incomplete ${row.checkId}: run its permitted verification now and resubmit passed evidence, or report a truthful blocked result for host recovery`);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ResolvedAgentSettings } from "rafi-spec";
|
|
2
2
|
export declare const FINAL_QA_ROLE_SUFFIX = "You are an independent QA reviewer. Do not edit source, tickets, configuration, or project documentation. You may run tests and create only harmless ignored caches or coverage output.";
|
|
3
|
+
export declare const QA_COMMAND_DISCIPLINE: string;
|
|
3
4
|
export declare function finalQaRoleInstructions(rules: string): string;
|
|
4
5
|
export declare function loadQaPreparationPolicy(projectDir: string): import("special-agents").QaPreparationConfigV1;
|
|
5
6
|
/** Resolve only explicit rule/skill content; never project secrets or mutable .rafi state. */
|
|
@@ -6,7 +6,12 @@ import { loadTicketSetupConfigWithDefaults, loadRafiConfigObject } from "./ticke
|
|
|
6
6
|
import { resolveQaPreparationConfig } from "./qaPreparationPolicy.js";
|
|
7
7
|
import { contractDigest } from "./qaVerificationContract.js";
|
|
8
8
|
export const FINAL_QA_ROLE_SUFFIX = "You are an independent QA reviewer. Do not edit source, tickets, configuration, or project documentation. You may run tests and create only harmless ignored caches or coverage output.";
|
|
9
|
-
export
|
|
9
|
+
export const QA_COMMAND_DISCIPLINE = [
|
|
10
|
+
"QA command discipline: issue one shell command per Bash call. Prefer the Read tool for source inspection and the configured project test command for verification.",
|
|
11
|
+
"Do not use shell sequencing or scaffolding such as ;, ||, redirects, command substitution, echo exit markers, temporary baseline copies, cp, cmp, or mkdir. Do not reconstruct or compare preimplementation files; Rafi owns baseline, path-scope, unchanged-file, and byte-identity comparisons.",
|
|
12
|
+
"If a command is denied by policy, do not retry the same denied command or wrap it in another compound command. Use a permitted equivalent that tests the same product behavior and record any limitation truthfully.",
|
|
13
|
+
].join(" ");
|
|
14
|
+
export function finalQaRoleInstructions(rules) { return `${rules}\n\n${QA_COMMAND_DISCIPLINE}\n\n${FINAL_QA_ROLE_SUFFIX}`; }
|
|
10
15
|
export function loadQaPreparationPolicy(projectDir) {
|
|
11
16
|
const config = loadRafiConfigObject(projectDir);
|
|
12
17
|
return resolveQaPreparationConfig(config?.qa_preparation, config?.qa_preparation_shadow === true, config?.qa_preparation_wall_time_ms);
|
package/dist/qaPreparation.js
CHANGED
|
@@ -44,12 +44,26 @@ export function normalizeExpectedFutureAssessment(assessment, candidate) {
|
|
|
44
44
|
const copy = structuredClone(assessment);
|
|
45
45
|
const checks = new Map(candidate.checks.map(check => [check.id, check]));
|
|
46
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 };
|
|
47
59
|
if (!concern.material || !concern.checkIds.length || !concern.checkIds.every(id => checks.get(id)?.timing === "postimplementation"))
|
|
48
60
|
return concern;
|
|
49
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)
|
|
50
62
|
&& /\b(?:before|pre[- ]?builder|not yet)\b/i.test(concern.reason)
|
|
51
63
|
&& /\b(?:implement|add|create|rerun|re-?run|builder|after implementation)\b/i.test(concern.nextAction);
|
|
52
|
-
|
|
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;
|
|
53
67
|
});
|
|
54
68
|
return copy;
|
|
55
69
|
}
|
|
@@ -411,7 +425,7 @@ export async function prepareVerificationContract(initial, inventory, deps) {
|
|
|
411
425
|
normalized.assessedRequirementIds = [...fallback.assessedRequirementIds];
|
|
412
426
|
return normalized;
|
|
413
427
|
};
|
|
414
|
-
let assessmentResponse = retainedAssessment?.draftPayloadDigest === draftDigest(candidate) ? undefined : await run("assess", "assessment", `Independent semantic assessment in a fresh conversation. Assess each original requirement and selected-depth obligation; identify omissions, conflicts, practical verification gaps, accidental scope, and unsupported dispositions. Treat Rafi-owned bookkeeping paths (.tickets, .rafi, generated docs/configuration, and agent instructions) as expected host changes. Scope concerns must be evaluated against approved product paths and ticket requirements; do not require an exact full-worktree status listing. Future Builder actions are not preparation failures: when an approved requirement specifies test-first ordering, a migration, rollback rehearsal, or other implementation-time sequence, represent it as a mandatory postimplementation check requiring retained Builder evidence. Do not mark the absence of that future evidence before Builder starts as a material concern, and do not demand that preparation reproduce historical order. Only report a material concern when the contract cannot state an actionable postimplementation check or when an already-required preimplementation fact is missing. Return RAFI_QA_ASSESSMENT_START/END with QaSemanticAssessmentV1 (schema: ${JSON.stringify(qaPreparationDepthDecisionV1Schema)}). The concerns field is an array of objects, never strings; every concern object must have exactly these six fields and no others: {reason:string, requirementIds:string[], checkIds:string[], material:boolean, owner:string, nextAction:string}. Do not use legacy fields such as severity, evidence, title, message, category, or resolution. Use concerns: [] only when all issues are resolved. assessedRequirementIds MUST contain every ID in this exact list, even when a requirement is host-owned or has no concern: ${JSON.stringify(candidate.requirements.map(requirement => requirement.id))}. Put every individually assessed required obligation label in assessedObligations, using the exact labels below; do not combine labels or claim to have assessed anything you did not assess. Required obligation labels: ${JSON.stringify(depthObligations(candidate.depthDecision.level))}. Bind exactly ${JSON.stringify(binding)}.\nPrior independent concerns requiring concrete resolution evidence: ${JSON.stringify(targetedConcerns)}\nOriginal inventory:\n${JSON.stringify(inventory)}\nDraft:\n${JSON.stringify(candidate)}`);
|
|
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 RAFI_QA_ASSESSMENT_START/END with QaSemanticAssessmentV1 (schema: ${JSON.stringify(qaPreparationDepthDecisionV1Schema)}). The concerns field is an array of objects, never strings; every concern object must have exactly these six fields and no others: {reason:string, requirementIds:string[], checkIds:string[], material:boolean, owner:string, nextAction:string}. Do not use legacy fields such as severity, evidence, title, message, category, or resolution. Use concerns: [] only when all issues are resolved. assessedRequirementIds MUST contain every ID in this exact list, even when a requirement is host-owned or has no concern: ${JSON.stringify(candidate.requirements.map(requirement => requirement.id))}. Put every individually assessed required obligation label in assessedObligations, using the exact labels below; do not combine labels or claim to have assessed anything you did not assess. Required obligation labels: ${JSON.stringify(depthObligations(candidate.depthDecision.level))}. Bind exactly ${JSON.stringify(binding)}.\nPrior independent concerns requiring concrete resolution evidence: ${JSON.stringify(targetedConcerns)}\nOriginal inventory:\n${JSON.stringify(inventory)}\nDraft:\n${JSON.stringify(candidate)}`);
|
|
415
429
|
let assessed = assessmentResponse
|
|
416
430
|
? await parsedContract(assessmentResponse, "RAFI_QA_ASSESSMENT", validateAssessmentContract, "QA semantic assessment", (value, response) => normalizeAssessment(value, response))
|
|
417
431
|
: { value: retainedAssessment, response: undefined };
|
|
@@ -419,6 +433,11 @@ export async function prepareVerificationContract(initial, inventory, deps) {
|
|
|
419
433
|
// session identity, but it cannot prove or safely author the builder's ID.
|
|
420
434
|
assessed.value.authorSessionId = binding.authorSessionId;
|
|
421
435
|
for (let repair = 0; assessmentResponse && repair < 3; repair++) {
|
|
436
|
+
// Apply host-owned current-branch applicability normalization before
|
|
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);
|
|
422
441
|
const semanticIssues = validateAssessment(candidate, assessed.value, authorSessionId);
|
|
423
442
|
if (!semanticIssues.length)
|
|
424
443
|
break;
|
|
@@ -140,6 +140,10 @@ export declare class QaPreparationStore {
|
|
|
140
140
|
extendBudget(budget: LogicalPreparationBudget, decisionId: string): void;
|
|
141
141
|
putArtifact(kind: string, value: unknown): string;
|
|
142
142
|
artifact<T>(digest: string, kind: string): T;
|
|
143
|
+
artifacts(kind: string): Array<{
|
|
144
|
+
digest: string;
|
|
145
|
+
value: unknown;
|
|
146
|
+
}>;
|
|
143
147
|
head(runId: string, workId: string, admissionDigest: string): {
|
|
144
148
|
digest?: string;
|
|
145
149
|
generation: number;
|