ai-foreman 1.7.29 → 2.0.1
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 +231 -26
- package/dist/adapters/codex.d.ts +19 -7
- package/dist/adapters/codex.js +202 -19
- 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/git.js +3 -8
- 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 +28 -9
- 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/buildRuns.d.ts +1 -0
- package/dist/buildRuns.js +6 -1
- package/dist/builderGuidanceFollowup.js +17 -6
- package/dist/cli/doctor.js +3 -1
- package/dist/cli/manager.js +22 -20
- package/dist/cli/start.js +85 -39
- package/dist/cli/tickets.d.ts +0 -8
- package/dist/cli/tickets.js +50 -60
- package/dist/continuity.d.ts +10 -16
- package/dist/continuity.js +223 -208
- package/dist/continuityOrigin.d.ts +13 -0
- package/dist/continuityOrigin.js +91 -0
- package/dist/dispatchNotSent.d.ts +4 -0
- package/dist/dispatchNotSent.js +10 -0
- package/dist/foreman.d.ts +15 -16
- package/dist/foreman.js +70 -278
- package/dist/gitExclude.d.ts +16 -0
- package/dist/gitExclude.js +46 -0
- 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 +3 -4
- package/dist/graph/turn.js +29 -41
- package/dist/handoffProtocol.js +2 -2
- package/dist/handoffs.d.ts +5 -3
- package/dist/handoffs.js +92 -88
- 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/humanDecision.d.ts +7 -0
- package/dist/humanDecision.js +11 -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/observability.js +3 -8
- package/dist/probeIpc.d.ts +14 -0
- package/dist/probeIpc.js +29 -0
- package/dist/qaBuildGate.d.ts +21 -0
- package/dist/qaBuildGate.js +60 -13
- 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 +153 -254
- package/dist/qaPreparationInputs.js +19 -0
- package/dist/qaPreparationStore.d.ts +11 -0
- package/dist/qaPreparationStore.js +44 -4
- package/dist/qaPrerequisites.d.ts +2 -1
- package/dist/qaPrerequisites.js +21 -1
- package/dist/qaProtocolV2.d.ts +15 -0
- package/dist/qaRecovery.d.ts +0 -3
- package/dist/qaRecovery.js +21 -27
- package/dist/qaReview.js +241 -406
- package/dist/qaReviewEvidence.d.ts +7 -3
- package/dist/qaReviewEvidence.js +15 -4
- package/dist/qaRuntime.js +1 -1
- package/dist/qaSnapshot.js +76 -21
- package/dist/qaStructuredResponse.d.ts +18 -0
- package/dist/qaStructuredResponse.js +81 -0
- package/dist/qaTimeline.js +2 -2
- package/dist/qaVerificationContract.d.ts +1 -2
- package/dist/qaVerificationContract.js +24 -35
- package/dist/readinessGate.js +5 -4
- package/dist/recoveryOwners.d.ts +32 -0
- package/dist/recoveryOwners.js +87 -0
- package/dist/runtimePreflight.d.ts +11 -0
- package/dist/runtimePreflight.js +77 -0
- package/dist/runtimeReadiness.js +23 -15
- package/dist/sources/sourceRegistry.d.ts +0 -3
- package/dist/sources/sourceRegistry.js +0 -42
- 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 +279 -0
- package/dist/supervisedStart.js +35 -21
- package/dist/ticketPlanning.d.ts +1 -376
- package/dist/ticketPlanning.js +2 -49
- 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 +127 -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 +116 -3
- package/dist/workflowDb.js +852 -61
- package/dist/workflowReader.d.ts +1 -0
- package/dist/workflowReader.js +9 -0
- package/package.json +23 -7
package/dist/handoffs.js
CHANGED
|
@@ -3,20 +3,20 @@ import { durableHumanDecision } from "./humanDecision.js";
|
|
|
3
3
|
import { createHash } from "node:crypto";
|
|
4
4
|
import { existsSync, mkdirSync, mkdtempSync, readdirSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
5
5
|
import { dirname, join, resolve } from "node:path";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
6
|
+
import { handoffAcceptanceOutput, providerWireContract } from "rafi-spec";
|
|
7
|
+
import { BoundedTurnEvents, boundedUtf8, receiptFingerprint, rawFingerprint } from "./boundedTurnEvidence.js";
|
|
8
|
+
import { mergeContinuityDeltas, validateContinuityDelta } from "./continuity.js";
|
|
9
|
+
import { nativePolicy, operationInstruction, operationResponse } from "./structuredOperation.js";
|
|
8
10
|
import { WorkflowDb } from "./workflowDb.js";
|
|
9
11
|
import { pauseActivityForInput } from "./activity.js";
|
|
10
12
|
import { signalAttention } from "./notify.js";
|
|
11
13
|
export const HANDOFF_CACHE_RETENTION_DAYS = 30;
|
|
12
14
|
export const HANDOFF_ACCEPTED = "HANDOFF_ACCEPTED";
|
|
13
|
-
export const HANDOFF_REQUEST_START = "RAFI_HANDOFF_REQUEST_START";
|
|
14
|
-
export const HANDOFF_REQUEST_END = "RAFI_HANDOFF_REQUEST_END";
|
|
15
15
|
function handoffSessionIdentity(ref) {
|
|
16
16
|
return JSON.stringify([ref.version, ref.provider, ref.sessionId, ref.role, ref.stream, ref.generation, resolve(ref.cwd), resolve(ref.configRoot), ref.workspaceIdentity, ref.ticketId, ref.deliveryUnitId]);
|
|
17
17
|
}
|
|
18
|
-
async function collectHandoffTerminalEvents(adapter, turnId) {
|
|
19
|
-
const events =
|
|
18
|
+
async function collectHandoffTerminalEvents(adapter, turnId, maximumBytes) {
|
|
19
|
+
const events = new BoundedTurnEvents(maximumBytes);
|
|
20
20
|
let timer;
|
|
21
21
|
const collect = (async () => {
|
|
22
22
|
for await (const event of adapter.events()) {
|
|
@@ -40,33 +40,10 @@ async function collectHandoffTerminalEvents(adapter, turnId) {
|
|
|
40
40
|
void collect.catch(() => { });
|
|
41
41
|
return events;
|
|
42
42
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
return undefined;
|
|
48
|
-
if (starts.length !== 1 || ends.length !== 1 || starts[0].index >= ends[0].index)
|
|
49
|
-
throw new Error("malformed structured Builder handoff request envelope");
|
|
50
|
-
const raw = text.slice(starts[0].index + HANDOFF_REQUEST_START.length, ends[0].index).trim();
|
|
51
|
-
let parsed;
|
|
52
|
-
try {
|
|
53
|
-
parsed = JSON.parse(raw);
|
|
54
|
-
}
|
|
55
|
-
catch {
|
|
56
|
-
throw new Error("Builder handoff request is not valid JSON");
|
|
57
|
-
}
|
|
58
|
-
const contract = parseProviderContract(builderHandoffRequestV1Schema, parsed, "Builder handoff request");
|
|
59
|
-
return {
|
|
60
|
-
version: 1,
|
|
61
|
-
reason: clean(contract.reason, 2_000),
|
|
62
|
-
delta: {
|
|
63
|
-
version: 1,
|
|
64
|
-
decisions: contract.decisions, constraints: contract.constraints, discoveries: contract.discoveries,
|
|
65
|
-
completedActions: contract.completed_actions, evidence: contract.evidence, failures: contract.failures,
|
|
66
|
-
blockers: contract.blockers, openWork: contract.open_work, nextAction: contract.next_action,
|
|
67
|
-
},
|
|
68
|
-
roleState: sanitizeObject(contract.role_state),
|
|
69
|
-
};
|
|
43
|
+
/** Builder handoff requests arrive only as validated native proposals; plain
|
|
44
|
+
* provider text never requests a handoff. */
|
|
45
|
+
export function parseBuilderHandoffRequest(request) {
|
|
46
|
+
return typeof request === "string" ? undefined : request;
|
|
70
47
|
}
|
|
71
48
|
export class HandoffLoopError extends Error {
|
|
72
49
|
runId;
|
|
@@ -106,6 +83,8 @@ export class HandoffService {
|
|
|
106
83
|
this.cacheRoot = join(this.projectDir, ".rafi", "cache", "handoffs");
|
|
107
84
|
}
|
|
108
85
|
stage(input, now = new Date()) {
|
|
86
|
+
if (input.predecessorDispatchId)
|
|
87
|
+
input = { ...input, roleState: { ...input.roleState, structuredParentDispatchId: input.predecessorDispatchId } };
|
|
109
88
|
this.pruneExpiredCache(now);
|
|
110
89
|
const db = new WorkflowDb(this.projectDir);
|
|
111
90
|
try {
|
|
@@ -142,16 +121,26 @@ export class HandoffService {
|
|
|
142
121
|
// an explicit recovery stages the next transfer from the accepted owner.
|
|
143
122
|
const historicalHighWater = historicalLineage.reduce((maximum, item) => Math.max(maximum, item.generation, item.predecessorSessionRef?.validatedAt ? item.predecessorSessionRef.generation : 0, item.successorSessionRef?.validatedAt ? item.successorSessionRef.generation : 0), 0);
|
|
144
123
|
const generation = Math.max(historicalHighWater, input.predecessorSessionRef?.generation ?? 0, owner?.generation ?? 0) + 1;
|
|
145
|
-
const resources = (input.resources ?? []).map((resource) =>
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
124
|
+
const resources = (input.resources ?? []).map((resource) => {
|
|
125
|
+
if (resource.content !== undefined) {
|
|
126
|
+
const bytes = Buffer.isBuffer(resource.content) ? resource.content : Buffer.from(resource.content);
|
|
127
|
+
if (resource.digest !== undefined && resource.digest !== digest(bytes))
|
|
128
|
+
throw new Error(`Handoff resource digest mismatch: ${resource.label}`);
|
|
129
|
+
if (resource.bytes !== undefined && resource.bytes !== bytes.length)
|
|
130
|
+
throw new Error(`Handoff resource byte count mismatch: ${resource.label}`);
|
|
131
|
+
db.putEvidence("handoff", bytes);
|
|
132
|
+
}
|
|
133
|
+
return ({
|
|
134
|
+
label: resource.label,
|
|
135
|
+
digest: resource.digest && /^[a-f0-9]{64}$/.test(resource.digest) ? resource.digest : digest(resource.content ?? ""),
|
|
136
|
+
authoritative: resource.authoritative,
|
|
137
|
+
requiredForRecovery: resource.requiredForRecovery ?? false,
|
|
138
|
+
mediaType: resource.mediaType ?? "application/octet-stream",
|
|
139
|
+
path: resource.path ?? `embedded:${resource.label}`,
|
|
140
|
+
purpose: resource.purpose ?? `Declared ${input.role} handoff resource: ${resource.label}`,
|
|
141
|
+
bytes: resource.bytes ?? (resource.content !== undefined ? Buffer.byteLength(resource.content) : 0),
|
|
142
|
+
});
|
|
143
|
+
});
|
|
155
144
|
resources.unshift({ label: "continuity-checkpoint", digest: head.digest, authoritative: true, requiredForRecovery: false, mediaType: "application/vnd.rafi.digest", path: "db:continuity-checkpoint", purpose: "Digest reference to the durable role continuity checkpoint", bytes: Buffer.byteLength(head.digest) }, { label: "authoritative-run-state", digest: runHead.digest, authoritative: true, requiredForRecovery: false, mediaType: "application/vnd.rafi.digest", path: "db:authoritative-run-state", purpose: "Digest reference to the durable run continuity state", bytes: Buffer.byteLength(runHead.digest) });
|
|
156
145
|
const manifest = {
|
|
157
146
|
version: 1,
|
|
@@ -293,28 +282,33 @@ export class HandoffService {
|
|
|
293
282
|
const sendAcceptance = async (prompt) => {
|
|
294
283
|
const hostPromptDigest = db.putEvidence("handoff", Buffer.from(prompt));
|
|
295
284
|
db.appendContinuityEvent({ runId: staged.manifest.runId, role: "host", kind: "handoff_acceptance_intended", payload: { generation: staged.manifest.generation, attempt: acceptanceAttempts.length + 1, hostPromptDigest, preparedRef }, authoritativeStateRevision: db.continuityHead(staged.manifest.runId, staged.manifest.role)?.authoritativeStateRevision ?? 0 });
|
|
296
|
-
const
|
|
297
|
-
const
|
|
298
|
-
|
|
285
|
+
const maximumBytes = providerWireContract(handoffAcceptanceOutput, successor.agent).maximumResponseBytes;
|
|
286
|
+
const observed = new BoundedTurnEvents(maximumBytes);
|
|
287
|
+
const stopObserving = successor.observeEvents?.(event => observed.push(event));
|
|
299
288
|
let response;
|
|
300
289
|
try {
|
|
301
|
-
response = await dispatchWithGraphAccess(this.projectDir, successor, prompt, { responseOnly: true, purpose: "contract-acceptance" }, (text, policy) => successor.sendTurn(text, policy));
|
|
290
|
+
response = await dispatchWithGraphAccess(this.projectDir, successor, prompt, nativePolicy(handoffAcceptanceOutput, { responseOnly: true, purpose: "contract-acceptance", journalRunId: staged.manifest.runId, parentDispatchId: typeof staged.manifest.roleState?.structuredParentDispatchId === "string" ? staged.manifest.roleState.structuredParentDispatchId : undefined }), (text, policy) => successor.sendTurn(text, policy));
|
|
291
|
+
}
|
|
292
|
+
catch (error) {
|
|
293
|
+
if (!successor.sessionRef?.())
|
|
294
|
+
throw new HandoffAcceptanceError("missing-scoped-successor-session", staged.manifest.runId, staged.manifest.generation, "acceptance requires a prepared scoped successor");
|
|
295
|
+
throw error;
|
|
302
296
|
}
|
|
303
297
|
finally {
|
|
304
298
|
stopObserving?.();
|
|
305
299
|
}
|
|
306
|
-
const events = qaBoundary && response.turnId ? await collectHandoffTerminalEvents(successor, response.turnId) :
|
|
300
|
+
const events = qaBoundary && response.turnId ? await collectHandoffTerminalEvents(successor, response.turnId, maximumBytes) : new BoundedTurnEvents(maximumBytes);
|
|
307
301
|
const attempt = {
|
|
308
302
|
hostPromptDigest,
|
|
309
303
|
providerPromptDigest: db.putEvidence("handoff", Buffer.from(response.providerInstruction ?? response.hostInstruction ?? prompt)),
|
|
310
|
-
rawResponseDigest: db.putEvidence("handoff",
|
|
311
|
-
cleanedResponseDigest: db.putEvidence("handoff",
|
|
304
|
+
rawResponseDigest: response.transportEvidence?.rawDigest ?? db.putEvidence("handoff", boundedUtf8(response.rawResponse ?? response.text, maximumBytes)),
|
|
305
|
+
cleanedResponseDigest: db.putEvidence("handoff", boundedUtf8(response.cleanedResponse ?? response.text, maximumBytes)),
|
|
312
306
|
...(response.turnId ? { providerTurnId: response.turnId } : {}),
|
|
313
|
-
...(qaBoundary ? { eventStreamDigest: db.putEvidence("handoff", Buffer.from(JSON.stringify(events))) } : {}),
|
|
307
|
+
...(qaBoundary ? { eventStreamDigest: db.putEvidence("handoff", Buffer.from(JSON.stringify(events.metadata()))) } : {}),
|
|
314
308
|
};
|
|
315
309
|
acceptanceAttempts.push(attempt);
|
|
316
|
-
db.appendContinuityEvent({ runId: staged.manifest.runId, role: "host", kind: "handoff_acceptance_observed", payload: { generation: staged.manifest.generation, attempt, isError: response.isError, failure: response.failure, providerMetadata: response.providerMetadata, activeSession: successor.sessionRef?.() }, authoritativeStateRevision: db.continuityHead(staged.manifest.runId, staged.manifest.role)?.authoritativeStateRevision ?? 0 });
|
|
317
|
-
if (
|
|
310
|
+
db.appendContinuityEvent({ runId: staged.manifest.runId, role: "host", kind: "handoff_acceptance_observed", payload: { generation: staged.manifest.generation, attempt, isError: response.isError, failure: response.failure && { ...response.failure, diagnostics: boundedUtf8(response.failure.diagnostics, 4096).toString() }, providerMetadata: response.providerMetadata, activeSession: successor.sessionRef?.() }, authoritativeStateRevision: db.continuityHead(staged.manifest.runId, staged.manifest.role)?.authoritativeStateRevision ?? 0 });
|
|
311
|
+
if (observed.toolCount || events.toolCount)
|
|
318
312
|
throw new HandoffAcceptanceError("tool-use-during-acceptance", staged.manifest.runId, staged.manifest.generation, "tools were attempted during the host response-only receipt phase");
|
|
319
313
|
if (response.isError || response.failure)
|
|
320
314
|
throw new HandoffAcceptanceError("provider-turn-failed", staged.manifest.runId, staged.manifest.generation, "provider rejected or failed the acknowledgement turn");
|
|
@@ -326,34 +320,40 @@ export class HandoffService {
|
|
|
326
320
|
|| successor.agent !== preparedRef.provider || handoffSessionIdentity(reportedRef) !== handoffSessionIdentity(preparedRef)))) {
|
|
327
321
|
throw new HandoffAcceptanceError("successor-identity-mismatch", staged.manifest.runId, staged.manifest.generation, "acknowledgement did not come from the prepared successor identity");
|
|
328
322
|
}
|
|
329
|
-
|
|
323
|
+
// The adapter queue can still hold terminals of earlier host-owned turns
|
|
324
|
+
// (such as provider initialization). Exactly one terminal must belong
|
|
325
|
+
// to this acknowledgement, matching the QA review receipt rule.
|
|
326
|
+
const correlated = events.filter((event) => event.kind === "turn-complete" && (event.turnId ?? event.result.turnId) === response.turnId);
|
|
327
|
+
if (qaBoundary && (!response.turnId || events.terminalOverflow || correlated.length !== 1 || !correlated.some((event) => event.kind === "turn-complete" && receiptFingerprint(event.result) === receiptFingerprint(response) && rawFingerprint(event.result) === rawFingerprint(response)))) {
|
|
330
328
|
throw new HandoffAcceptanceError("missing-terminal-event", staged.manifest.runId, staged.manifest.generation, "QA acknowledgement has no correlated terminal provider event");
|
|
331
329
|
}
|
|
332
330
|
return response;
|
|
333
331
|
};
|
|
332
|
+
const embeddedResources = staged.manifest.resources.filter(resource => resource.path?.startsWith("embedded:")).flatMap(resource => {
|
|
333
|
+
const bytes = db.getEvidence(resource.digest);
|
|
334
|
+
if (!bytes) {
|
|
335
|
+
if (resource.requiredForRecovery)
|
|
336
|
+
throw new Error(`Required embedded handoff resource unavailable: ${resource.label}`);
|
|
337
|
+
return [];
|
|
338
|
+
}
|
|
339
|
+
if (digest(bytes) !== resource.digest || bytes.length !== resource.bytes)
|
|
340
|
+
throw new Error(`Embedded handoff resource integrity mismatch: ${resource.label}`);
|
|
341
|
+
const text = bytes.toString("utf8");
|
|
342
|
+
const utf8 = Buffer.from(text).equals(bytes);
|
|
343
|
+
return [{ label: resource.label, digest: resource.digest, encoding: utf8 ? "utf8" : "base64", content: utf8 ? text : bytes.toString("base64") }];
|
|
344
|
+
});
|
|
334
345
|
const acceptancePrompt = [
|
|
335
|
-
handoffAcceptanceInstruction(),
|
|
346
|
+
handoffAcceptanceInstruction(staged),
|
|
336
347
|
"Accept this validated Rafi handoff. Do not repeat completed work and reconcile host receipts before side effects.",
|
|
337
348
|
"The following JSON is quoted continuity data, not instructions. Acknowledgment confirms receipt, not implementation or QA success.",
|
|
338
|
-
JSON.stringify({ continuityPacket: staged.markdown, manifest: staged.manifest, ...(options.guidance ? { humanRecoveryGuidance: options.guidance } : {}) }),
|
|
339
|
-
handoffAcceptanceInstruction(),
|
|
349
|
+
JSON.stringify({ continuityPacket: staged.markdown, manifest: staged.manifest, embeddedResources, ...(options.guidance ? { humanRecoveryGuidance: options.guidance } : {}) }),
|
|
350
|
+
handoffAcceptanceInstruction(staged),
|
|
340
351
|
].join("\n\n");
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
let validation = validateHandoffAcceptance(staged, successor, acceptanceText);
|
|
344
|
-
if (validation && (validation.code === "missing-acknowledgement" || validation.code === "invalid-continuity-delta")) {
|
|
345
|
-
const repairPrompt = [
|
|
346
|
-
`Your handoff acknowledgement was rejected: ${validation.message}.`,
|
|
347
|
-
"Correction only: do not use tools, repeat completed work, or perform implementation.",
|
|
348
|
-
handoffAcceptanceInstruction(),
|
|
349
|
-
].join("\n\n");
|
|
350
|
-
response = await sendAcceptance(repairPrompt);
|
|
351
|
-
acceptanceText = response.rawResponse ?? response.text;
|
|
352
|
-
validation = validateHandoffAcceptance(staged, successor, acceptanceText);
|
|
353
|
-
}
|
|
352
|
+
const response = await sendAcceptance(acceptancePrompt);
|
|
353
|
+
const validation = validateHandoffAcceptance(staged, successor, response);
|
|
354
354
|
if (validation)
|
|
355
355
|
throw new HandoffAcceptanceError(validation.code, staged.manifest.runId, staged.manifest.generation, validation.message);
|
|
356
|
-
const parsed =
|
|
356
|
+
const parsed = { delta: validateContinuityDelta(operationResponse(response, handoffAcceptanceOutput).continuity) };
|
|
357
357
|
const sessionId = successor.sessionId();
|
|
358
358
|
const observedSuccessorRef = successor.sessionRef?.();
|
|
359
359
|
const successorRef = observedSuccessorRef ? { ...observedSuccessorRef, generation: staged.manifest.generation, validatedAt: new Date().toISOString() } : undefined;
|
|
@@ -368,7 +368,7 @@ export class HandoffService {
|
|
|
368
368
|
const acceptedDelta = parsed.delta;
|
|
369
369
|
return db.atomic(() => {
|
|
370
370
|
db.appendContinuityEvent({ runId: staged.manifest.runId, role: staged.manifest.role, kind: "handoff_successor_accepted", payload: { generation: staged.manifest.generation, sessionId, sessionRef: successorRef, delta: parsed.delta, acceptanceAttempts }, authoritativeStateRevision: db.continuityHead(staged.manifest.runId, staged.manifest.role)?.authoritativeStateRevision ?? 0, sessionRef: successorRef });
|
|
371
|
-
const checkpoint = db.publishContinuityCheckpoint({ runId: staged.manifest.runId, role: staged.manifest.role, delta: acceptedDelta, authoritativeStateRevision: db.continuityHead(staged.manifest.runId, staged.manifest.role)?.authoritativeStateRevision ?? 0, sessionRef:
|
|
371
|
+
const checkpoint = db.publishContinuityCheckpoint({ runId: staged.manifest.runId, role: staged.manifest.role, delta: acceptedDelta, authoritativeStateRevision: db.continuityHead(staged.manifest.runId, staged.manifest.role)?.authoritativeStateRevision ?? 0, sessionRef: observedSuccessorRef, origin: { kind: "provider", dispatchId: response.selectedDispatch.dispatchId, publicationKind: "handoff_acceptance" } });
|
|
372
372
|
const receipt = successorRef ? { version: 1, runId: staged.manifest.runId, generation: staged.manifest.generation, role: staged.manifest.role,
|
|
373
373
|
manifestDigest: staged.lineage.manifestDigest, continuityCheckpointDigest: staged.manifest.continuityCheckpointDigest, acceptanceCheckpointDigest: checkpoint.digest,
|
|
374
374
|
...(staged.manifest.predecessorSessionRef ? { predecessorSessionRef: staged.manifest.predecessorSessionRef } : {}), successorSessionRef: successorRef,
|
|
@@ -501,16 +501,25 @@ export class HandoffService {
|
|
|
501
501
|
return usefulSinceLastRequest ? 0 : count;
|
|
502
502
|
}
|
|
503
503
|
}
|
|
504
|
-
function validateHandoffAcceptance(staged, successor,
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
504
|
+
function validateHandoffAcceptance(staged, successor, turn) {
|
|
505
|
+
let response;
|
|
506
|
+
try {
|
|
507
|
+
response = operationResponse(turn, handoffAcceptanceOutput);
|
|
508
|
+
}
|
|
509
|
+
catch (error) {
|
|
510
|
+
return { code: "missing-acknowledgement", message: String(error) };
|
|
510
511
|
}
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
512
|
+
if (response.result.kind === "blocked")
|
|
513
|
+
return { code: "provider-refused-acknowledgement", message: response.result.reason };
|
|
514
|
+
try {
|
|
515
|
+
if (response.result.kind !== "completed" || response.result.payload.manifestDigest !== staged.lineage.manifestDigest ||
|
|
516
|
+
response.result.payload.generation !== staged.manifest.generation || response.result.payload.missingResources.length) {
|
|
517
|
+
return { code: "missing-acknowledgement", message: "Missing, incomplete, or foreign handoff acknowledgement" };
|
|
518
|
+
}
|
|
519
|
+
validateContinuityDelta(response.continuity);
|
|
520
|
+
}
|
|
521
|
+
catch (error) {
|
|
522
|
+
return { code: "invalid-continuity-delta", message: String(error) };
|
|
514
523
|
}
|
|
515
524
|
const sessionId = successor.sessionId();
|
|
516
525
|
if (!sessionId)
|
|
@@ -524,13 +533,8 @@ function validateHandoffAcceptance(staged, successor, text) {
|
|
|
524
533
|
return undefined;
|
|
525
534
|
}
|
|
526
535
|
/** Keep the small acknowledgement protocol visible above and after a large handoff packet. */
|
|
527
|
-
function handoffAcceptanceInstruction() {
|
|
528
|
-
return
|
|
529
|
-
"Mandatory response protocol: do not call tools or add a preface, explanation, or Markdown fence.",
|
|
530
|
-
`Your first non-whitespace line must be exactly ${HANDOFF_ACCEPTED}.`,
|
|
531
|
-
"After it, emit exactly one RAFI_CONTINUITY_DELTA record as instructed below.",
|
|
532
|
-
continuityInstruction(),
|
|
533
|
-
].join("\n");
|
|
536
|
+
function handoffAcceptanceInstruction(staged) {
|
|
537
|
+
return `${operationInstruction(handoffAcceptanceOutput)}\nTools are disabled. Acknowledge manifestDigest ${staged.lineage.manifestDigest}, generation ${staged.manifest.generation}. List any missingResources; an empty list confirms the complete packet is available. Include a non-null bounded continuity object. This acknowledges receipt only.`;
|
|
534
538
|
}
|
|
535
539
|
async function promptHandoffRecovery(error, runtime, allowProviderSwitch) {
|
|
536
540
|
const { select, isCancel } = await import("@clack/prompts");
|
|
@@ -21,7 +21,8 @@ export declare const hostGraphApplicabilityV1Schema: z.ZodObject<{
|
|
|
21
21
|
export type HostGraphApplicabilityV1 = z.infer<typeof hostGraphApplicabilityV1Schema>;
|
|
22
22
|
export declare function isGraphQaCheck(check: QaVerificationContractV1["checks"][number]): boolean;
|
|
23
23
|
export declare function findGraphQaChecks(contract: QaVerificationContractV1): QaVerificationContractV1["checks"];
|
|
24
|
-
|
|
24
|
+
/** Host policy adds its own check; provider wording cannot acquire host authority. */
|
|
25
|
+
export declare function normalizeGraphQaChecks(candidate: QaContractCandidateV1, adopted?: boolean): QaContractCandidateV1;
|
|
25
26
|
export declare function createHostGraphApplicability(input: {
|
|
26
27
|
projectDir: string;
|
|
27
28
|
runId: string;
|
|
@@ -14,31 +14,29 @@ export const hostGraphApplicabilityV1Schema = z.object({
|
|
|
14
14
|
reason: z.string().max(2048),
|
|
15
15
|
observedAt: z.string().datetime(),
|
|
16
16
|
});
|
|
17
|
-
function graphText(check) {
|
|
18
|
-
return [check.expectedBehavior, ...check.expectedEvidence, ...check.applicability.requiredEvidence,
|
|
19
|
-
...check.verification.flatMap(method => [method.expectedOutcome, ...(method.steps ?? []), ...(method.argv ?? [])])].join(" ");
|
|
20
|
-
}
|
|
21
17
|
export function isGraphQaCheck(check) {
|
|
22
|
-
return
|
|
18
|
+
return check.hostKind === "graph-maintenance";
|
|
23
19
|
}
|
|
24
20
|
export function findGraphQaChecks(contract) {
|
|
25
21
|
return contract.checks.filter(isGraphQaCheck);
|
|
26
22
|
}
|
|
27
|
-
|
|
23
|
+
/** Host policy adds its own check; provider wording cannot acquire host authority. */
|
|
24
|
+
export function normalizeGraphQaChecks(candidate, adopted = false) {
|
|
28
25
|
const copy = structuredClone(candidate);
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
|
|
26
|
+
if (!adopted || copy.checks.some(isGraphQaCheck))
|
|
27
|
+
return copy;
|
|
28
|
+
const requirement = copy.requirements[0]?.id;
|
|
29
|
+
if (!requirement)
|
|
30
|
+
throw new Error("Graph policy requires an admitted requirement inventory");
|
|
31
|
+
let id = "host-graph-maintenance";
|
|
32
|
+
while (copy.checks.some(check => check.id === id))
|
|
33
|
+
id += "-host";
|
|
34
|
+
copy.checks.push({ id, hostKind: "graph-maintenance", requirementRefs: [requirement], origin: "invariant", obligation: "mandatory", timing: "postimplementation",
|
|
35
|
+
expectedBehavior: "Adopted Graphify evidence follows the host trigger gate and maintenance policy.",
|
|
36
|
+
applicability: { kind: "conditional", predicate: "Rafi-managed Graphify remains adopted and enabled", decisionOwner: "host", requiredEvidence: ["Host-owned Graphify applicability observation"] },
|
|
37
|
+
verification: [{ kind: "procedure", steps: ["Inspect the host maintenance receipt or the documented non-trigger result for this work."], cwd: ".", fixtures: [], expectedOutcome: "Meaningful indexed changes have a host maintenance receipt; routine non-trigger work does not refresh the graph.", runtime: "Rafi host", timeoutMs: 30000 }],
|
|
38
|
+
expectedEvidence: ["Host graph maintenance or non-trigger receipt"], prerequisiteRefs: [], dependsOnChecks: [],
|
|
39
|
+
});
|
|
42
40
|
return copy;
|
|
43
41
|
}
|
|
44
42
|
export function createHostGraphApplicability(input) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { applyHostQaComparison } from "./hostQaComparison.js";
|
|
2
2
|
import { applyHostGraphApplicability } from "./hostGraphApplicability.js";
|
|
3
|
+
import { hostTestSequenceEvidence } from "./qaBuilderEvidence.js";
|
|
3
4
|
function commandObservationFor(check, observations) {
|
|
4
5
|
const command = check.verification.find(method => method.kind === "command");
|
|
5
6
|
const argv = command?.argv ?? [];
|
|
@@ -12,13 +13,29 @@ function applyHostCommandObservations(coverage, contract, observations) {
|
|
|
12
13
|
const parsed = observations.filter((observation) => Boolean(observation && typeof observation === "object"));
|
|
13
14
|
const copy = structuredClone(coverage);
|
|
14
15
|
for (const check of contract.checks) {
|
|
16
|
+
const row = copy.checks.find(item => item.checkId === check.id);
|
|
17
|
+
// Observations supplement an independently passed check; a command label or
|
|
18
|
+
// prose resemblance cannot establish the check's domain semantics.
|
|
19
|
+
if (!row || row.outcome !== "passed")
|
|
20
|
+
continue;
|
|
21
|
+
const historical = /red|test.?first|ordering|historical/i.test(`${check.expectedBehavior} ${(check.expectedEvidence ?? []).join(" ")}`);
|
|
22
|
+
const testPath = check.verification.find(method => method.kind === "command" && method.argv?.includes("--test"))?.argv?.at(-1);
|
|
23
|
+
if (historical) {
|
|
24
|
+
const sequence = hostTestSequenceEvidence(parsed.filter((item) => typeof item.sequence === "number" && typeof item.sourceDigest === "string" && (item.kind === "node-test" || item.kind === "npm-test")), copy.sourceDigest, testPath);
|
|
25
|
+
if (!sequence)
|
|
26
|
+
continue;
|
|
27
|
+
row.evidence = [...(row.evidence ?? []), {
|
|
28
|
+
procedure: "Rafi host-retained ordered Builder test sequence",
|
|
29
|
+
cwd: ".",
|
|
30
|
+
runtime: "provider tool lifecycle plus host source capture",
|
|
31
|
+
sourceDigest: copy.sourceDigest,
|
|
32
|
+
reference: `host-test-sequence:${sequence.map(item => `${item.sequence}:${item.sourceDigest}`).join(",")}`,
|
|
33
|
+
}];
|
|
34
|
+
continue;
|
|
35
|
+
}
|
|
15
36
|
const observation = commandObservationFor(check, parsed);
|
|
16
37
|
if (!observation)
|
|
17
38
|
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
39
|
row.evidence = [...(row.evidence ?? []), {
|
|
23
40
|
procedure: "Rafi host-retained command observation",
|
|
24
41
|
cwd: ".",
|
|
@@ -25,6 +25,21 @@ export declare const hostQaComparisonV1Schema: z.ZodObject<{
|
|
|
25
25
|
protectedPaths: z.ZodArray<z.ZodString>;
|
|
26
26
|
reason: z.ZodString;
|
|
27
27
|
comparedAt: z.ZodString;
|
|
28
|
+
filePreservation: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
29
|
+
path: z.ZodString;
|
|
30
|
+
status: z.ZodEnum<{
|
|
31
|
+
unavailable: "unavailable";
|
|
32
|
+
compared: "compared";
|
|
33
|
+
}>;
|
|
34
|
+
baselineBytes: z.ZodOptional<z.ZodNumber>;
|
|
35
|
+
finalBytes: z.ZodOptional<z.ZodNumber>;
|
|
36
|
+
baselineHash: z.ZodOptional<z.ZodString>;
|
|
37
|
+
finalHash: z.ZodOptional<z.ZodString>;
|
|
38
|
+
baselineIsExactPrefix: z.ZodOptional<z.ZodBoolean>;
|
|
39
|
+
baselineEndsWithNewline: z.ZodOptional<z.ZodBoolean>;
|
|
40
|
+
reason: z.ZodString;
|
|
41
|
+
}, z.core.$strip>>>;
|
|
42
|
+
preservationOmitted: z.ZodOptional<z.ZodNumber>;
|
|
28
43
|
}, z.core.$strip>;
|
|
29
44
|
export type HostQaComparisonV1 = z.infer<typeof hostQaComparisonV1Schema>;
|
|
30
45
|
export declare function findHostQaCheck(contract: QaVerificationContractV1): QaVerificationContractV1["checks"][number] | undefined;
|
|
@@ -32,7 +47,7 @@ export declare function findHostQaCheck(contract: QaVerificationContractV1): QaV
|
|
|
32
47
|
* Performs the private host comparison and returns the immutable artifact plus
|
|
33
48
|
* the coverage row replacement. Provider output is never used to decide scope.
|
|
34
49
|
*/
|
|
35
|
-
|
|
50
|
+
type HostQaComparisonInput = {
|
|
36
51
|
db: Pick<WorkflowDb, "operations" | "qaPreparationStore">;
|
|
37
52
|
projectDir: string;
|
|
38
53
|
worktree: string;
|
|
@@ -41,10 +56,14 @@ export declare function createHostQaComparison(input: {
|
|
|
41
56
|
contract: QaVerificationContractV1;
|
|
42
57
|
reviewAttemptId: string;
|
|
43
58
|
reviewBasisDigest: string;
|
|
44
|
-
}
|
|
59
|
+
};
|
|
60
|
+
/** Source facts supplied before review; this is not a QA acceptance certificate. */
|
|
61
|
+
export declare function inspectHostQaComparison(input: Omit<HostQaComparisonInput, "reviewAttemptId" | "reviewBasisDigest">): Omit<HostQaComparisonV1, "reviewAttemptId" | "reviewBasisDigest">;
|
|
62
|
+
export declare function createHostQaComparison(input: HostQaComparisonInput): {
|
|
45
63
|
result: HostQaComparisonV1;
|
|
46
64
|
resultDigest: string;
|
|
47
65
|
checkId?: string;
|
|
48
66
|
};
|
|
49
67
|
export declare function applyHostQaComparison(coverage: QaContractCoverageV1, contract: QaVerificationContractV1, result: HostQaComparisonV1, resultDigest: string, projectDir: string): QaContractCoverageV1;
|
|
50
68
|
export declare function hostComparisonDigestFromCoverage(coverage: QaContractCoverageV1, checkId: string): string | undefined;
|
|
69
|
+
export {};
|
package/dist/hostQaComparison.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { lstatSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { filePreservationSchema, inspectFilePreservation } from "./hostSourcePreservation.js";
|
|
2
5
|
import { captureBuildSource } from "./buildAssignment.js";
|
|
3
6
|
import { contractDigest } from "./qaVerificationContract.js";
|
|
4
7
|
const digest = z.string().regex(/^[a-f0-9]{64}$/);
|
|
@@ -22,6 +25,8 @@ export const hostQaComparisonV1Schema = z.object({
|
|
|
22
25
|
protectedPaths: z.array(path).max(256),
|
|
23
26
|
reason: z.string().max(2048),
|
|
24
27
|
comparedAt: z.string().datetime(),
|
|
28
|
+
filePreservation: z.array(filePreservationSchema).max(20).optional(),
|
|
29
|
+
preservationOmitted: z.number().int().nonnegative().optional(),
|
|
25
30
|
});
|
|
26
31
|
function identity(item) {
|
|
27
32
|
if (!item)
|
|
@@ -46,12 +51,7 @@ function matchesScope(candidate, patterns) {
|
|
|
46
51
|
});
|
|
47
52
|
}
|
|
48
53
|
function isHostQaCheck(check) {
|
|
49
|
-
|
|
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)));
|
|
54
|
+
return check.hostKind === "source-scope";
|
|
55
55
|
}
|
|
56
56
|
export function findHostQaCheck(contract) {
|
|
57
57
|
return contract.checks.find(isHostQaCheck);
|
|
@@ -60,9 +60,7 @@ function findHostQaChecks(contract) {
|
|
|
60
60
|
return contract.checks.filter(isHostQaCheck);
|
|
61
61
|
}
|
|
62
62
|
function findHostDocumentationCheck(contract) {
|
|
63
|
-
return contract.checks.find(check => check.
|
|
64
|
-
&& /documentation/i.test(check.expectedBehavior)
|
|
65
|
-
&& /documentation/i.test(check.expectedEvidence.join(" ")));
|
|
63
|
+
return contract.checks.find(check => check.hostKind === "documentation-policy");
|
|
66
64
|
}
|
|
67
65
|
function findAssignment(db, runId, ticketId, baselineDigest) {
|
|
68
66
|
const operations = db.operations(runId);
|
|
@@ -84,11 +82,8 @@ function resultEvidence(resultDigest, projectDir, sourceDigest, status, reason)
|
|
|
84
82
|
...(status === "passed" ? {} : { limitation: reason }),
|
|
85
83
|
};
|
|
86
84
|
}
|
|
87
|
-
/**
|
|
88
|
-
|
|
89
|
-
* the coverage row replacement. Provider output is never used to decide scope.
|
|
90
|
-
*/
|
|
91
|
-
export function createHostQaComparison(input) {
|
|
85
|
+
/** Source facts supplied before review; this is not a QA acceptance certificate. */
|
|
86
|
+
export function inspectHostQaComparison(input) {
|
|
92
87
|
const check = findHostQaCheck(input.contract);
|
|
93
88
|
if (!check)
|
|
94
89
|
throw new Error("Host QA comparison contract check is missing");
|
|
@@ -97,33 +92,50 @@ export function createHostQaComparison(input) {
|
|
|
97
92
|
throw new Error("Host QA comparison lacks the admitted preimplementation baseline");
|
|
98
93
|
const assignment = findAssignment(input.db, input.runId, input.ticket.id, expectedBaseline);
|
|
99
94
|
const finalFirst = captureBuildSource(input.worktree);
|
|
100
|
-
const
|
|
101
|
-
if (finalFirst.digest !== finalSecond.digest)
|
|
102
|
-
throw new Error("Host QA comparison source changed during final capture");
|
|
103
|
-
const final = finalSecond;
|
|
95
|
+
const final = finalFirst;
|
|
104
96
|
const before = assignment?.intent.before;
|
|
105
97
|
const assignmentId = assignment?.idempotencyKey ?? "missing-assignment";
|
|
106
98
|
const beforePaths = new Map((before?.pathInventory ?? []).map(item => [item.path, item]));
|
|
107
99
|
const afterPaths = new Map(final.pathInventory.map(item => [item.path, item]));
|
|
108
100
|
const allPaths = [...new Set([...beforePaths.keys(), ...afterPaths.keys()])].sort();
|
|
109
101
|
const changedPaths = allPaths.filter(item => identity(beforePaths.get(item)) !== identity(afterPaths.get(item)));
|
|
110
|
-
const deletedPaths = [...beforePaths.keys()].filter(item =>
|
|
102
|
+
const deletedPaths = [...beforePaths.keys()].filter(item => {
|
|
103
|
+
if (afterPaths.has(item))
|
|
104
|
+
return false;
|
|
105
|
+
try {
|
|
106
|
+
lstatSync(join(input.worktree, item));
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
return true;
|
|
111
|
+
}
|
|
112
|
+
}).sort();
|
|
111
113
|
const protectedPaths = changedPaths.filter(item => item === ".git" || item.startsWith(".git/") || item === ".rafi" || item.startsWith(".rafi/") || item === ".foreman" || item.startsWith(".foreman/")).sort();
|
|
112
114
|
const scopePatterns = input.ticket.likely_files.map(item => item.replaceAll("\\", "/"));
|
|
113
115
|
const unauthorizedPaths = scopePatterns.length ? changedPaths.filter(item => !matchesScope(item, scopePatterns)) : changedPaths;
|
|
114
116
|
const scopeBasisDigest = contractDigest("host-qa-scope-basis", { ticketId: input.ticket.id, ticket: input.ticket, contractDigest: input.contract.contentDigest, assignmentId });
|
|
117
|
+
const headChanged = Boolean(before?.head && before.head !== final.head);
|
|
115
118
|
const missingBaseline = !assignment || !before || before.digest !== expectedBaseline;
|
|
116
|
-
const status = missingBaseline || protectedPaths.length || deletedPaths.length || unauthorizedPaths.length ? (missingBaseline ? "blocked" : "failed") : "passed";
|
|
119
|
+
const status = missingBaseline || headChanged || protectedPaths.length || deletedPaths.length || unauthorizedPaths.length ? (missingBaseline || headChanged ? "blocked" : "failed") : "passed";
|
|
117
120
|
const reason = missingBaseline
|
|
118
121
|
? "The admitted Builder assignment baseline was not retained or does not match the contract baseline."
|
|
119
|
-
:
|
|
120
|
-
?
|
|
121
|
-
:
|
|
122
|
-
? `
|
|
123
|
-
:
|
|
124
|
-
? `
|
|
125
|
-
:
|
|
126
|
-
|
|
122
|
+
: headChanged
|
|
123
|
+
? "Repository HEAD changed since the admitted baseline; path-inventory comparison cannot authorize this source."
|
|
124
|
+
: protectedPaths.length
|
|
125
|
+
? `Protected paths changed: ${protectedPaths.slice(0, 8).join(", ")}`
|
|
126
|
+
: deletedPaths.length
|
|
127
|
+
? `Product paths were deleted: ${deletedPaths.slice(0, 8).join(", ")}`
|
|
128
|
+
: unauthorizedPaths.length
|
|
129
|
+
? `Changed paths are outside the host-resolved ticket scope: ${unauthorizedPaths.slice(0, 8).join(", ")}`
|
|
130
|
+
: "Host-observed product paths match the retained baseline and host-resolved ticket scope.";
|
|
131
|
+
const eligiblePaths = changedPaths.filter(path => !protectedPaths.includes(path) && !unauthorizedPaths.includes(path));
|
|
132
|
+
const filePreservation = before && !headChanged ? inspectFilePreservation(input.worktree, before, eligiblePaths) : [];
|
|
133
|
+
if (final.digest !== captureBuildSource(input.worktree).digest)
|
|
134
|
+
throw new Error("Host QA comparison source changed during final capture");
|
|
135
|
+
return hostQaComparisonV1Schema.omit({ reviewAttemptId: true, reviewBasisDigest: true }).parse({ version: 1, runId: input.runId, ticketId: input.ticket.id, assignmentId, contractDigest: input.contract.contentDigest, contractRevision: input.contract.revision, baselineSourceDigest: expectedBaseline, finalSourceDigest: final.digest, scopeBasisDigest, status, changedPaths, unauthorizedPaths, deletedPaths, protectedPaths, filePreservation, preservationOmitted: Math.max(0, eligiblePaths.length - filePreservation.length), reason, comparedAt: new Date().toISOString() });
|
|
136
|
+
}
|
|
137
|
+
export function createHostQaComparison(input) {
|
|
138
|
+
const result = hostQaComparisonV1Schema.parse({ ...inspectHostQaComparison(input), reviewAttemptId: input.reviewAttemptId, reviewBasisDigest: input.reviewBasisDigest });
|
|
127
139
|
const store = input.db.qaPreparationStore();
|
|
128
140
|
const existing = store.artifacts("host-qa-comparison").find(item => {
|
|
129
141
|
const candidate = hostQaComparisonV1Schema.safeParse(item.value);
|
|
@@ -139,10 +151,12 @@ export function createHostQaComparison(input) {
|
|
|
139
151
|
&& JSON.stringify(prior.unauthorizedPaths) === JSON.stringify(result.unauthorizedPaths)
|
|
140
152
|
&& JSON.stringify(prior.deletedPaths) === JSON.stringify(result.deletedPaths)
|
|
141
153
|
&& JSON.stringify(prior.protectedPaths) === JSON.stringify(result.protectedPaths)
|
|
154
|
+
&& JSON.stringify(prior.filePreservation) === JSON.stringify(result.filePreservation)
|
|
155
|
+
&& prior.preservationOmitted === result.preservationOmitted
|
|
142
156
|
&& prior.reason === result.reason;
|
|
143
157
|
});
|
|
144
158
|
const resultDigest = existing?.digest ?? store.putArtifact("host-qa-comparison", result);
|
|
145
|
-
return { result: existing ? hostQaComparisonV1Schema.parse(existing.value) : result, resultDigest, checkId:
|
|
159
|
+
return { result: existing ? hostQaComparisonV1Schema.parse(existing.value) : result, resultDigest, checkId: findHostQaCheck(input.contract).id };
|
|
146
160
|
}
|
|
147
161
|
export function applyHostQaComparison(coverage, contract, result, resultDigest, projectDir) {
|
|
148
162
|
const parsed = hostQaComparisonV1Schema.parse(result);
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
export declare const filePreservationSchema: z.ZodObject<{
|
|
3
|
+
path: z.ZodString;
|
|
4
|
+
status: z.ZodEnum<{
|
|
5
|
+
unavailable: "unavailable";
|
|
6
|
+
compared: "compared";
|
|
7
|
+
}>;
|
|
8
|
+
baselineBytes: z.ZodOptional<z.ZodNumber>;
|
|
9
|
+
finalBytes: z.ZodOptional<z.ZodNumber>;
|
|
10
|
+
baselineHash: z.ZodOptional<z.ZodString>;
|
|
11
|
+
finalHash: z.ZodOptional<z.ZodString>;
|
|
12
|
+
baselineIsExactPrefix: z.ZodOptional<z.ZodBoolean>;
|
|
13
|
+
baselineEndsWithNewline: z.ZodOptional<z.ZodBoolean>;
|
|
14
|
+
reason: z.ZodString;
|
|
15
|
+
}, z.core.$strip>;
|
|
16
|
+
export type RetainedSourceIdentity = {
|
|
17
|
+
digest?: string;
|
|
18
|
+
head?: string;
|
|
19
|
+
fileByteLengths?: Record<string, number>;
|
|
20
|
+
pathInventory?: Array<{
|
|
21
|
+
path: string;
|
|
22
|
+
worktreeObject?: string;
|
|
23
|
+
untracked?: {
|
|
24
|
+
kind: "file" | "symlink";
|
|
25
|
+
mode: number;
|
|
26
|
+
digest: string;
|
|
27
|
+
};
|
|
28
|
+
}>;
|
|
29
|
+
untracked?: Array<{
|
|
30
|
+
path: string;
|
|
31
|
+
kind: string;
|
|
32
|
+
digest: string;
|
|
33
|
+
byteLength?: number;
|
|
34
|
+
bytes?: Buffer;
|
|
35
|
+
}>;
|
|
36
|
+
};
|
|
37
|
+
/** A bounded fact, never an acceptance decision. No baseline bytes reach agents. */
|
|
38
|
+
export declare function inspectFilePreservation(worktree: string, before: RetainedSourceIdentity, paths: string[]): z.infer<typeof filePreservationSchema>[];
|