ai-foreman 1.7.26 → 1.7.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/codex.d.ts +12 -0
- package/dist/adapters/codex.js +50 -1
- package/dist/adapters/recovering.d.ts +4 -0
- package/dist/adapters/recovering.js +10 -0
- package/dist/adapters/sessionFailure.d.ts +6 -0
- package/dist/adapters/sessionFailure.js +19 -0
- package/dist/adapters/types.d.ts +6 -0
- package/dist/branch/currentGuard.d.ts +8 -0
- package/dist/branch/currentGuard.js +5 -0
- package/dist/branch/runner.js +6 -2
- package/dist/buildApproval.js +2 -1
- package/dist/buildRuns.d.ts +12 -0
- package/dist/buildRuns.js +50 -0
- package/dist/buildWorkContext.js +1 -1
- package/dist/buildWorkMigration.d.ts +10 -0
- package/dist/buildWorkMigration.js +18 -2
- package/dist/cli/start.d.ts +10 -1
- package/dist/cli/start.js +186 -31
- package/dist/cli/state.js +39 -1
- package/dist/cli/tickets.d.ts +37 -0
- package/dist/cli/tickets.js +367 -12
- package/dist/config.js +14 -4
- package/dist/continuity.d.ts +1 -0
- package/dist/continuity.js +5 -0
- package/dist/foreman.js +89 -8
- package/dist/graph/preparation.js +6 -3
- package/dist/graph/semantic.js +2 -1
- package/dist/graph/turn.js +8 -1
- package/dist/handoffProtocol.js +2 -1
- package/dist/handoffs.js +7 -19
- package/dist/hostQaComparison.d.ts +50 -0
- package/dist/hostQaComparison.js +195 -0
- package/dist/log.d.ts +1 -1
- package/dist/managerPacket.js +1 -1
- package/dist/observability.d.ts +2 -0
- package/dist/observability.js +2 -1
- package/dist/permissions/policy.js +32 -0
- package/dist/planChanges.js +1 -1
- package/dist/projectDiagnostics.js +30 -0
- package/dist/providerQuestions.js +4 -24
- package/dist/qaBuildGate.d.ts +6 -1
- package/dist/qaBuildGate.js +76 -16
- package/dist/qaBuilderCoverage.d.ts +2 -1
- package/dist/qaBuilderCoverage.js +58 -7
- package/dist/qaBuilderEvidence.d.ts +18 -0
- package/dist/qaBuilderEvidence.js +60 -0
- package/dist/qaContractCoverage.d.ts +12 -1
- package/dist/qaContractCoverage.js +34 -2
- package/dist/qaContractDelivery.d.ts +2 -2
- package/dist/qaContractDelivery.js +88 -10
- package/dist/qaEffectiveConfig.js +2 -1
- package/dist/qaFailureDelivery.d.ts +23 -0
- package/dist/qaFailureDelivery.js +87 -6
- package/dist/qaHostFacts.d.ts +2 -0
- package/dist/qaHostFacts.js +11 -0
- package/dist/qaPreparation.d.ts +38 -1
- package/dist/qaPreparation.js +372 -31
- package/dist/qaPreparationInputs.js +5 -1
- package/dist/qaPreparationPolicy.d.ts +2 -1
- package/dist/qaPreparationPolicy.js +39 -9
- package/dist/qaPreparationStore.d.ts +4 -0
- package/dist/qaPreparationStore.js +8 -0
- package/dist/qaRecovery.d.ts +7 -0
- package/dist/qaRecovery.js +73 -1
- package/dist/qaReview.js +58 -9
- package/dist/qaReviewEvidence.d.ts +6 -0
- package/dist/qaReviewEvidence.js +37 -0
- package/dist/qaRollbackRehearsal.d.ts +2 -0
- package/dist/qaRollbackRehearsal.js +78 -0
- package/dist/qaSnapshot.d.ts +3 -1
- package/dist/qaSnapshot.js +3 -2
- package/dist/qaVerificationContract.d.ts +16 -0
- package/dist/qaVerificationContract.js +183 -17
- package/dist/sources/sourceRegistry.js +16 -5
- package/dist/stateTransfer.d.ts +14 -0
- package/dist/stateTransfer.js +51 -0
- package/dist/statusReporter.d.ts +8 -0
- package/dist/statusReporter.js +5 -0
- package/dist/supervisedStart.js +23 -0
- package/dist/ticketPlanning.d.ts +373 -0
- package/dist/ticketPlanning.js +34 -3
- package/dist/ticketPopulation.d.ts +28 -3
- package/dist/ticketPopulation.js +127 -15
- package/dist/tickets/details.d.ts +2 -1
- package/dist/tickets/details.js +7 -3
- package/dist/tickets/eligibility.d.ts +6 -0
- package/dist/tickets/eligibility.js +12 -1
- package/dist/tickets/riskWaivers.d.ts +3 -0
- package/dist/tickets/riskWaivers.js +12 -0
- package/dist/tickets/stateDb.d.ts +10 -0
- package/dist/tickets/stateDb.js +23 -0
- package/dist/tickets/ticketLoader.d.ts +9 -0
- package/dist/tickets/ticketLoader.js +15 -2
- package/dist/tickets/ticketSchema.d.ts +14 -1
- package/dist/tickets/ticketSchema.js +2 -1
- package/dist/tickets/validate.js +11 -1
- package/dist/workflowDb.js +24 -3
- package/package.json +9 -4
|
@@ -9,10 +9,34 @@ export const DEPTH_OBLIGATIONS = [
|
|
|
9
9
|
["approach-proposal", "independent-challenge", "coordinated-verification"],
|
|
10
10
|
];
|
|
11
11
|
export function depthObligations(level) { return DEPTH_OBLIGATIONS.slice(0, level).flat(); }
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
/** Resolve public boolean configuration while temporarily accepting old mode objects. */
|
|
13
|
+
export function resolveQaPreparationConfig(value, shadow = false, wallTimeMs) {
|
|
14
|
+
// Preparation may require several fresh provider conversations (draft,
|
|
15
|
+
// independent assessment, challenge, and bounded repairs). Keep this
|
|
16
|
+
// ceiling separate from provider turn deadlines and give the logical
|
|
17
|
+
// workflow enough time to reconcile retained work without replaying an
|
|
18
|
+
// uncertain dispatch.
|
|
19
|
+
const defaults = { mode: "legacy", policyVersion: QA_PREPARATION_POLICY_VERSION, wallTimeMs: [600_000, 1_200_000, 2_400_000, 3_600_000, 5_400_000] };
|
|
20
|
+
const resolveWallTime = (candidate) => {
|
|
21
|
+
if (candidate === undefined)
|
|
22
|
+
return undefined;
|
|
23
|
+
if (!Array.isArray(candidate) || candidate.length !== 5 || candidate.some((v, i) => !Number.isSafeInteger(v) || v < defaults.wallTimeMs[i]))
|
|
24
|
+
throw new Error("QA preparation ceilings must specify five integer limits at least as large as policy defaults");
|
|
25
|
+
return candidate;
|
|
26
|
+
};
|
|
27
|
+
const publicWallTime = resolveWallTime(wallTimeMs);
|
|
28
|
+
if (value === undefined) {
|
|
29
|
+
if (shadow)
|
|
30
|
+
throw new Error("qa_preparation_shadow requires qa_preparation: true");
|
|
31
|
+
return { ...defaults, ...(publicWallTime ? { wallTimeMs: publicWallTime } : {}) };
|
|
32
|
+
}
|
|
33
|
+
if (typeof value === "boolean") {
|
|
34
|
+
if (shadow && !value)
|
|
35
|
+
throw new Error("qa_preparation_shadow requires qa_preparation: true");
|
|
36
|
+
return { ...defaults, ...(publicWallTime ? { wallTimeMs: publicWallTime } : {}), mode: !value ? "legacy" : shadow ? "shadow" : "enforce" };
|
|
37
|
+
}
|
|
38
|
+
if (shadow)
|
|
39
|
+
throw new Error("qa_preparation_shadow cannot be combined with legacy qa_preparation object configuration");
|
|
16
40
|
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
17
41
|
throw new Error("qa_preparation must be an object");
|
|
18
42
|
const raw = value;
|
|
@@ -22,9 +46,10 @@ export function resolveQaPreparationConfig(value) {
|
|
|
22
46
|
throw new Error("qa_preparation.mode must be legacy, shadow, or enforce");
|
|
23
47
|
if (raw.policyVersion !== undefined && raw.policyVersion !== defaults.policyVersion)
|
|
24
48
|
throw new Error("Unsupported QA preparation policy version");
|
|
25
|
-
if (
|
|
26
|
-
throw new Error("
|
|
27
|
-
|
|
49
|
+
if (wallTimeMs !== undefined)
|
|
50
|
+
throw new Error("qa_preparation_wall_time_ms cannot be combined with legacy qa_preparation object configuration");
|
|
51
|
+
const legacyWallTime = resolveWallTime(raw.wallTimeMs);
|
|
52
|
+
return { ...defaults, mode: raw.mode, ...(legacyWallTime ? { wallTimeMs: legacyWallTime } : {}) };
|
|
28
53
|
}
|
|
29
54
|
export function validateDepthDecision(value, predecessor) {
|
|
30
55
|
if (!validateDecision(value))
|
|
@@ -36,8 +61,13 @@ export function validateDepthDecision(value, predecessor) {
|
|
|
36
61
|
issues.push("Invalid planner timestamp");
|
|
37
62
|
if (decision.level === 1 && (!decision.lowRisk || decision.riskFactors.length))
|
|
38
63
|
issues.push("Focused requires explicit low-risk assessment and no consequential risks");
|
|
39
|
-
|
|
40
|
-
|
|
64
|
+
// The minimum describes the risk floor, not the level the planner selected.
|
|
65
|
+
// A planner may choose extra preparation while recognizing that Focused is sufficient.
|
|
66
|
+
const effectiveMinimum = decision.lowRisk && !decision.riskFactors.length ? 1 : minimum;
|
|
67
|
+
// Older plans described Standard as the minimum when selecting it, even with
|
|
68
|
+
// low-risk evidence. Continue accepting those conservative recorded plans.
|
|
69
|
+
const legacyStandardMinimum = effectiveMinimum === 1 && decision.level >= 2 && decision.minimumLevel === 2;
|
|
70
|
+
if ((!legacyStandardMinimum && decision.minimumLevel !== effectiveMinimum) || decision.level < effectiveMinimum)
|
|
41
71
|
issues.push(`Planner decision is below or misstates host minimum ${effectiveMinimum}`);
|
|
42
72
|
if (decision.riskFactors.some(risk => !decision.minimumReasons.includes(risk.category)))
|
|
43
73
|
issues.push("Minimum reasons must enumerate evidenced risk categories");
|
|
@@ -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;
|
|
@@ -324,6 +324,14 @@ export class QaPreparationStore {
|
|
|
324
324
|
throw new Error("Corrupt preparation artifact");
|
|
325
325
|
return value;
|
|
326
326
|
}
|
|
327
|
+
artifacts(kind) {
|
|
328
|
+
return this.db.prepare("SELECT digest,record_json FROM qa_contract_artifacts WHERE kind=? ORDER BY digest").all(kind).map(row => {
|
|
329
|
+
const value = this.decode(row.record_json);
|
|
330
|
+
if (contractDigest(`artifact-${kind}`, value) !== row.digest)
|
|
331
|
+
throw new Error("Corrupt preparation artifact");
|
|
332
|
+
return { digest: row.digest, value };
|
|
333
|
+
});
|
|
334
|
+
}
|
|
327
335
|
head(runId, workId, admissionDigest) {
|
|
328
336
|
const row = this.db.prepare("SELECT digest,generation,state,detail FROM qa_contract_heads WHERE run_id=? AND work_id=? AND admission_digest=?").get(runId, workId, admissionDigest);
|
|
329
337
|
return row ? { ...row, digest: row.digest ?? undefined, detail: row.detail && this.projectDir && !graphDerivedAllowed(this.db, this.projectDir, bytesDigest(row.detail)) ? "Graph-derived preparation detail withheld; source-based recovery required" : row.detail ?? undefined } : { generation: 0, state: "preparation-required" };
|
package/dist/qaRecovery.d.ts
CHANGED
|
@@ -86,6 +86,13 @@ export interface QaRecoveryPacketInput {
|
|
|
86
86
|
}>;
|
|
87
87
|
reportJson?: string;
|
|
88
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* Keeps an unresolved packet auditable without making a successor reread every
|
|
91
|
+
* failed attempt. Sealed history remains in the packet and in its digest
|
|
92
|
+
* lineage; only the current review/source closure and this digest-indexed
|
|
93
|
+
* history manifest are required for recovery.
|
|
94
|
+
*/
|
|
95
|
+
export declare function canonicalizeQaRecoveryRequirements(packet: QaRecoveryPacket): QaRecoveryPacket;
|
|
89
96
|
export declare function loadQaRecoveryPacket(directory: string): QaRecoveryPacket;
|
|
90
97
|
export declare function inspectLegacyQaRecoveryPacket(directory: string): {
|
|
91
98
|
directory: string;
|
package/dist/qaRecovery.js
CHANGED
|
@@ -2,7 +2,7 @@ import { createHash, randomUUID } from "node:crypto";
|
|
|
2
2
|
import { execFileSync } from "node:child_process";
|
|
3
3
|
import { chmodSync, closeSync, cpSync, existsSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, readlinkSync, readdirSync, realpathSync, renameSync, writeFileSync, } from "node:fs";
|
|
4
4
|
import { basename, dirname, join, relative, resolve, sep } from "node:path";
|
|
5
|
-
import { parseQaFailureReport } from "rafi-spec";
|
|
5
|
+
import { parseProviderContract, parseQaFailureReport, qaRecoveryAcknowledgementV1Schema } from "rafi-spec";
|
|
6
6
|
import { calculateFrozenQaStateDigest, captureFrozenQaSource } from "./qaSnapshot.js";
|
|
7
7
|
import { WorkflowDb } from "./workflowDb.js";
|
|
8
8
|
export const QA_RECOVERY_ROOT = ".foreman/qa-report-recovery";
|
|
@@ -17,6 +17,69 @@ export class LegacyQaRecoveryPacketError extends Error {
|
|
|
17
17
|
this.name = "LegacyQaRecoveryPacketError";
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* Keeps an unresolved packet auditable without making a successor reread every
|
|
22
|
+
* failed attempt. Sealed history remains in the packet and in its digest
|
|
23
|
+
* lineage; only the current review/source closure and this digest-indexed
|
|
24
|
+
* history manifest are required for recovery.
|
|
25
|
+
*/
|
|
26
|
+
export function canonicalizeQaRecoveryRequirements(packet) {
|
|
27
|
+
const loaded = loadQaRecoveryPacket(packet.directory);
|
|
28
|
+
if (loaded.manifest.packetDigest !== packet.manifest.packetDigest)
|
|
29
|
+
throw new Error("cannot canonicalize a stale QA recovery packet revision");
|
|
30
|
+
// Ordinary first-attempt packets stay byte-for-byte on their established
|
|
31
|
+
// publication path. The bundle is introduced only once retry/turn history
|
|
32
|
+
// would otherwise make the next successor inspect an accumulating archive.
|
|
33
|
+
const hasBundle = loaded.manifest.resources.some((resource) => resource.path.startsWith("canonical-recovery-bundle/"));
|
|
34
|
+
const hasRetryHistory = loaded.manifest.resources.some((resource) => /^(?:reviews\/|turns\/|prompts\/|qa-responses\/|validation\/|recovery-history\/)/.test(resource.path));
|
|
35
|
+
if (!hasBundle && !hasRetryHistory)
|
|
36
|
+
return loaded;
|
|
37
|
+
const currentSourcePrefix = currentReviewedStatePrefix(loaded);
|
|
38
|
+
const currentReviewPrefix = `reviews/r${loaded.manifest.reviewAttempt}/context/`;
|
|
39
|
+
const currentTransition = `review-transitions/${loaded.manifest.reviewAttemptId}.json`;
|
|
40
|
+
const baseContext = new Set([
|
|
41
|
+
"ticket", "qa-review-prompt", "report-schema", "builder-result", "builder-response-history",
|
|
42
|
+
"original-host-prompt", "original-provider-prompt", "original-raw-response", "original-cleaned-response",
|
|
43
|
+
"original-validation", "tool-events", "continuity", "settings", "context-usage",
|
|
44
|
+
"uncertain-turn-instruction", "uncertain-turn-detail", "review-binding",
|
|
45
|
+
]);
|
|
46
|
+
const isCurrent = (resource) => {
|
|
47
|
+
if (resource.path === "report.json")
|
|
48
|
+
return true;
|
|
49
|
+
if (resource.path.startsWith(`${currentSourcePrefix}/`) || resource.path.startsWith(currentReviewPrefix))
|
|
50
|
+
return true;
|
|
51
|
+
if (resource.path === currentTransition)
|
|
52
|
+
return true;
|
|
53
|
+
const context = /^context\/([^./]+)\.(?:json|txt)$/.exec(resource.path)?.[1];
|
|
54
|
+
return Boolean(context && baseContext.has(context));
|
|
55
|
+
};
|
|
56
|
+
const history = loaded.manifest.resources
|
|
57
|
+
.filter((resource) => !resource.path.startsWith("canonical-recovery-bundle/") && !isCurrent(resource))
|
|
58
|
+
.map(({ path, digest, bytes, mediaType, purpose, integrityPolicy }) => ({ path, digest, bytes, mediaType, purpose, integrityPolicy }));
|
|
59
|
+
const current = loaded.manifest.resources
|
|
60
|
+
.filter((resource) => !resource.path.startsWith("canonical-recovery-bundle/") && isCurrent(resource))
|
|
61
|
+
.map(({ path, digest, bytes, mediaType, purpose }) => ({ path, digest, bytes, mediaType, purpose }));
|
|
62
|
+
const bundleBytes = encodeValue({
|
|
63
|
+
version: 1,
|
|
64
|
+
purpose: "Bounded canonical QA recovery closure; all omitted records remain sealed packet history and are verified by the packet digest and revision lineage",
|
|
65
|
+
runId: loaded.manifest.runId, ticketId: loaded.manifest.ticketId,
|
|
66
|
+
reviewAttempt: loaded.manifest.reviewAttempt, reviewAttemptId: loaded.manifest.reviewAttemptId,
|
|
67
|
+
reviewedStateDigest: loaded.manifest.reviewedStateDigest,
|
|
68
|
+
currentRequiredResources: current,
|
|
69
|
+
retainedHistory: history,
|
|
70
|
+
}, false);
|
|
71
|
+
const priorBundle = loaded.manifest.resources.filter((resource) => resource.path.startsWith("canonical-recovery-bundle/")).at(-1);
|
|
72
|
+
const unchanged = priorBundle && priorBundle.digest === hash(bundleBytes)
|
|
73
|
+
&& loaded.manifest.resources.every((resource) => resource.requiredForRecovery === (isCurrent(resource) || resource.path === priorBundle.path));
|
|
74
|
+
if (unchanged)
|
|
75
|
+
return loaded;
|
|
76
|
+
const bundle = storeSealed(loaded.directory, `canonical-recovery-bundle/r${loaded.manifest.revision + 1}.json`, bundleBytes, "Canonical current recovery closure and digest-indexed sealed history", "application/json", true);
|
|
77
|
+
const resources = loaded.manifest.resources
|
|
78
|
+
.map((resource) => ({ ...resource, requiredForRecovery: isCurrent(resource) }))
|
|
79
|
+
.concat(bundle)
|
|
80
|
+
.sort(resourceOrder);
|
|
81
|
+
return revise(loaded, { resources });
|
|
82
|
+
}
|
|
20
83
|
export function loadQaRecoveryPacket(directory) {
|
|
21
84
|
const root = resolve(directory);
|
|
22
85
|
const manifestPath = join(root, "manifest.json");
|
|
@@ -446,6 +509,15 @@ export function validateQaRecoveryAcknowledgement(text, packet, options = {}) {
|
|
|
446
509
|
const errors = [];
|
|
447
510
|
if (lines[0] !== expected)
|
|
448
511
|
errors.push("missing or malformed exact recovery-packet acknowledgement");
|
|
512
|
+
const acknowledgement = /^RAFI_QA_RECOVERY_ACK packet="([a-f0-9]{64})" reviewed_state="([a-f0-9]{64})" required_resources_read="(all)"$/.exec(lines[0] ?? "");
|
|
513
|
+
if (acknowledgement) {
|
|
514
|
+
try {
|
|
515
|
+
parseProviderContract(qaRecoveryAcknowledgementV1Schema, { packet: acknowledgement[1], reviewed_state: acknowledgement[2], required_resources_read: acknowledgement[3] }, "RAFI_QA_RECOVERY_ACK");
|
|
516
|
+
}
|
|
517
|
+
catch (error) {
|
|
518
|
+
errors.push(error instanceof Error ? error.message : String(error));
|
|
519
|
+
}
|
|
520
|
+
}
|
|
449
521
|
if (options.continuityAlreadyValidated) {
|
|
450
522
|
if (lines.length !== 1)
|
|
451
523
|
errors.push("recovery acknowledgement must not contain additional text");
|
package/dist/qaReview.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { withGraphDerivedAccess } from "./graph/derived.js";
|
|
2
2
|
import { dispatchWithGraphAccess } from "./graph/session.js";
|
|
3
3
|
import { finalQaRoleInstructions, resolveEffectiveQaConfiguration } from "./qaEffectiveConfig.js";
|
|
4
|
-
import { COVERAGE_INSTRUCTIONS, parseContractCoverage, validateContractCoverage } from "./qaContractCoverage.js";
|
|
4
|
+
import { COVERAGE_INSTRUCTIONS, bindCoverageHostFields, parseContractCoverage, validateContractCoverage } from "./qaContractCoverage.js";
|
|
5
5
|
import { contractDigest, renderVerificationContract } from "./qaVerificationContract.js";
|
|
6
6
|
import { finalReviewContractContext } from "./qaContractDelivery.js";
|
|
7
|
+
import { qaCommandOutputExcerptsAllowed, retainedQaCommandObservations } from "./qaReviewEvidence.js";
|
|
8
|
+
import { qaHostGraphFact } from "./qaHostFacts.js";
|
|
7
9
|
import { graphContext, parseGraphRequest, prepareGraphInstruction, sendGraphTurn } from "./graph/turn.js";
|
|
8
10
|
import { BuildControlBoundary } from "./buildInterventions.js";
|
|
9
11
|
import { formatRecoveryCommand } from "./recoveryGuidance.js";
|
|
@@ -24,6 +26,7 @@ import { canonicalSessionPath } from "./sessionIdentity.js";
|
|
|
24
26
|
import { loadProjectAutonomyConfig, resolveAutonomyPolicy } from "./recoveryPolicy.js";
|
|
25
27
|
import { appendQaRecoveryAttempt, appendQaRecoveryResource, appendQaRecoveryReviewedState, compareQaRecoveryReviewedState, createQaRecoveryPacket, ensureQaRecoveryExcluded, loadQaRecoveryPacket, materializeQaRecoveryContext, qaRecoveryInventory, qaReportDigest, renderQaRecoveryAcknowledgementInstruction, updateQaRecoveryPosition, updateQaRecoveryReviewIdentity, validateManualQaReport, validateQaRecoveryAcknowledgement, } from "./qaRecovery.js";
|
|
26
28
|
import { canonicalJson, createQaFindingRefs, qaDigest } from "./qaProtocolV2.js";
|
|
29
|
+
import { applyHostQaComparison, createHostQaComparison, findHostQaCheck } from "./hostQaComparison.js";
|
|
27
30
|
function ensureProtocolPausePacket(opts, head, stage, detail, frozenState) {
|
|
28
31
|
const db = new WorkflowDb(opts.recovery.projectDir);
|
|
29
32
|
try {
|
|
@@ -229,7 +232,10 @@ async function runIsolatedQaOwned(opts) {
|
|
|
229
232
|
const maxBuilderFixes = Math.min(opts.maxCycles, durableLimit);
|
|
230
233
|
let automaticFixes = persistedQaFixCount(opts);
|
|
231
234
|
let reviewAttempt = Math.max(opts.state.reviews, ...durableReviews.map((item) => item.reviewNumber), 0);
|
|
232
|
-
|
|
235
|
+
// Remediation generations are scoped to a ticket head. The surrounding
|
|
236
|
+
// build state is shared across tickets, so using its counter here can leak
|
|
237
|
+
// T-001's fixes into T-002 and make a valid review fail binding validation.
|
|
238
|
+
let remediationGeneration = Math.max(durableRemediationGeneration, ...durableReviews.map((item) => item.remediationGeneration), ...durableRemediations.map((item) => item.generation), 0);
|
|
233
239
|
const resumedHeadDb = new WorkflowDb(opts.recovery.projectDir);
|
|
234
240
|
resumedHeadDb.reconcileInstructionDeliveries(opts.recovery.runId, opts.ticket.id);
|
|
235
241
|
opts.qaRemediationAuthorization ??= resumedHeadDb.consumeInstructionControls(opts.recovery.runId, opts.ticket.id, "attempts");
|
|
@@ -549,6 +555,10 @@ function deliverQaFailureOrLegacyFix(opts, request, causingAttemptId) {
|
|
|
549
555
|
const reportDigest = request.reportDigest;
|
|
550
556
|
if (!attempt)
|
|
551
557
|
throw new Error(`QA failure delivery is missing review attempt ${causingAttemptId}`);
|
|
558
|
+
const admission = headDb.assertAdmittedWork(opts.recovery.runId, opts.ticket.id);
|
|
559
|
+
const preparation = headDb.qaPreparationStore();
|
|
560
|
+
const contractHead = preparation.head(opts.recovery.runId, opts.ticket.id, admission.requirementsDigest);
|
|
561
|
+
const contract = contractHead.state === "ready" && contractHead.digest ? preparation.contract(contractHead.digest) : undefined;
|
|
552
562
|
const input = {
|
|
553
563
|
projectDir: opts.recovery.projectDir,
|
|
554
564
|
runId: opts.recovery.runId,
|
|
@@ -560,6 +570,7 @@ function deliverQaFailureOrLegacyFix(opts, request, causingAttemptId) {
|
|
|
560
570
|
reviewNumber: attempt.reviewNumber,
|
|
561
571
|
reviewedSourceStateDigest: attempt.sourceDigest,
|
|
562
572
|
reviewBasisDigest: headDb.qaTicketHead(opts.recovery.runId, opts.ticket.id).reviewBasisDigest ?? "",
|
|
573
|
+
...(contract ? { contractDigest: contract.contentDigest } : {}),
|
|
563
574
|
remediationGeneration: attempt.remediationGeneration,
|
|
564
575
|
latestBuilderResult: request.latestBuilderResult,
|
|
565
576
|
history: request.history,
|
|
@@ -980,7 +991,14 @@ async function oneReview(opts, identity) {
|
|
|
980
991
|
return pauseQaReview(opts, "invalid-pass-contract", `invalid QA pass response: ${contract.errors.join("; ")}`, reviewedSnapshot.frozenState);
|
|
981
992
|
if (missingPrerequisites.length)
|
|
982
993
|
return pauseQaReview(opts, "verification-not-run", "Required verification remains not_run; prerequisite recovery and fresh QA required", reviewedSnapshot.frozenState);
|
|
983
|
-
|
|
994
|
+
let certificate;
|
|
995
|
+
try {
|
|
996
|
+
certificate = finishV2Pass(opts, qa, status.summary ?? "qa_pass");
|
|
997
|
+
}
|
|
998
|
+
catch (error) {
|
|
999
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1000
|
+
return retryFreshQaReview(opts, "invalid-qa-coverage", `QA pass evidence was not parseable or complete: ${detail}`, reviewedSnapshot.frozenState);
|
|
1001
|
+
}
|
|
984
1002
|
markQaRecoveryResolved(opts);
|
|
985
1003
|
durableReviewFinished = true;
|
|
986
1004
|
opts.evidence?.({ cycle, reviewAttempt: activeBinding.reviewNumber, remediationGeneration, attemptId: activeBinding.reviewAttemptId, outcome: "passed", detail: status.summary ?? "qa_pass", durationMs: Math.max(0, performance.now() - started) });
|
|
@@ -1034,7 +1052,14 @@ async function oneReview(opts, identity) {
|
|
|
1034
1052
|
if (contract.valid && contract.status === "qa_pass") {
|
|
1035
1053
|
if (missingPrerequisites.length)
|
|
1036
1054
|
return pauseQaReview(opts, "verification-not-run", "Required verification remains not_run; prerequisite recovery and fresh QA required", reviewedSnapshot.frozenState);
|
|
1037
|
-
|
|
1055
|
+
let certificate;
|
|
1056
|
+
try {
|
|
1057
|
+
certificate = finishV2Pass(opts, qa, contract.fields.summary ?? "qa_pass");
|
|
1058
|
+
}
|
|
1059
|
+
catch (error) {
|
|
1060
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
1061
|
+
return retryFreshQaReview(opts, "invalid-qa-coverage", `Recovered QA pass evidence was not parseable or complete: ${detail}`, reviewedSnapshot.frozenState);
|
|
1062
|
+
}
|
|
1038
1063
|
markQaRecoveryResolved(opts);
|
|
1039
1064
|
durableReviewFinished = true;
|
|
1040
1065
|
opts.evidence?.({ cycle, reviewAttempt: activeBinding.reviewNumber, remediationGeneration, attemptId: activeBinding.reviewAttemptId, outcome: "passed", detail: contract.fields.summary ?? "qa_pass", durationMs: Math.max(0, performance.now() - started) });
|
|
@@ -1577,7 +1602,7 @@ function validateBoundaryReceipt(receipt, packet, successorHandle) {
|
|
|
1577
1602
|
receiptDb.close();
|
|
1578
1603
|
}
|
|
1579
1604
|
const byLabel = new Map(receipt.resources.map((resource) => [resource.label, resource]));
|
|
1580
|
-
for (const expected of packet.manifest.resources) {
|
|
1605
|
+
for (const expected of packet.manifest.resources.filter((resource) => resource.requiredForRecovery)) {
|
|
1581
1606
|
const actual = byLabel.get(expected.path);
|
|
1582
1607
|
if (!actual || actual.authoritative !== true || actual.digest !== expected.digest || actual.path !== expected.path || actual.purpose !== expected.purpose
|
|
1583
1608
|
|| actual.bytes !== expected.bytes || actual.mediaType !== expected.mediaType || actual.requiredForRecovery !== expected.requiredForRecovery) {
|
|
@@ -1629,13 +1654,30 @@ function beginV2Review(opts, source, reviewedSnapshotPath, instruction, handle,
|
|
|
1629
1654
|
const expected = contract.inputs.find(input => input.kind === "checklist");
|
|
1630
1655
|
if (expected?.digest !== contractDigest("input-checklist", checklist))
|
|
1631
1656
|
throw new Error("Final QA checklist differs from delivered contract");
|
|
1657
|
+
const needsHistoricalEvidence = contract.checks.some(check => /red|test.?first|ordering|historical/i.test(JSON.stringify(check)));
|
|
1632
1658
|
const builderClaims = db.operations(opts.recovery.runId).flatMap(operation => {
|
|
1633
1659
|
const intent = operation.intent;
|
|
1634
1660
|
const result = operation.result;
|
|
1635
1661
|
return operation.kind === "builder-coverage" && operation.status === "confirmed" && intent.workId === opts.ticket.id && intent.contractDigest === contract.contentDigest && result?.binding?.sourceDigest === source.digest && result.coverageDigest
|
|
1636
|
-
? [{
|
|
1662
|
+
? [{
|
|
1663
|
+
operationId: operation.idempotencyKey,
|
|
1664
|
+
digest: result.coverageDigest,
|
|
1665
|
+
coverage: preparationStore.artifact(result.coverageDigest, "builder-coverage"),
|
|
1666
|
+
...(needsHistoricalEvidence && result.implementationDigest ? {
|
|
1667
|
+
// The response is already host-retained by finishBuildAssignment.
|
|
1668
|
+
// Expose only this bounded, contract-relevant excerpt to the
|
|
1669
|
+
// independent reviewer; do not retain or disclose all Builder
|
|
1670
|
+
// output globally.
|
|
1671
|
+
implementationResponse: {
|
|
1672
|
+
digest: result.implementationDigest,
|
|
1673
|
+
response: db.getEvidence(result.implementationDigest)?.toString() ?? "unavailable",
|
|
1674
|
+
},
|
|
1675
|
+
} : {}),
|
|
1676
|
+
}] : [];
|
|
1637
1677
|
});
|
|
1638
|
-
|
|
1678
|
+
const commandObservations = retainedQaCommandObservations(opts.recovery.projectDir, opts.recovery.runId, opts.ticket.id, qaCommandOutputExcerptsAllowed(opts.ticket.id));
|
|
1679
|
+
instruction += `\n${qaHostGraphFact(opts.recovery.projectDir)}\n`;
|
|
1680
|
+
instruction += finalReviewContractContext(contract, preparationStore, reviewedSnapshotPath, builderClaims, commandObservations);
|
|
1639
1681
|
instruction += `\n${renderVerificationContract(contract)}\n${COVERAGE_INSTRUCTIONS}\nCoverage binding: ${JSON.stringify({ phase: "qa", runId: opts.recovery.runId, workId: opts.ticket.id, contractDigest: contract.contentDigest, revision: contract.revision, sourceDigest: source.digest, attemptId: attempt.attemptId, sessionId: handle.sessionIdentity().sessionId })}`;
|
|
1640
1682
|
}
|
|
1641
1683
|
const originalInstruction = instruction;
|
|
@@ -1963,8 +2005,15 @@ function finishV2Pass(opts, adapter, detail) {
|
|
|
1963
2005
|
if (!contractHead.digest || contractHead.state !== "ready")
|
|
1964
2006
|
throw new Error("Final coverage requires the current ready contract");
|
|
1965
2007
|
const contract = db.qaPreparationStore().contract(contractHead.digest);
|
|
1966
|
-
const
|
|
1967
|
-
const
|
|
2008
|
+
const coverageBinding = { phase: "qa", sourceDigest: binding.sourceStateDigest, inputBasisDigest: binding.reviewBasisDigest, attemptId: binding.reviewAttemptId, sessionId: binding.sessionRef.sessionId };
|
|
2009
|
+
const providerCoverage = bindCoverageHostFields(parseContractCoverage(binding.lastResponse ?? ""), contract, coverageBinding);
|
|
2010
|
+
const hostComparison = findHostQaCheck(contract)
|
|
2011
|
+
? createHostQaComparison({ db, projectDir: binding.projectDir, worktree: opts.builderWorktree, runId: binding.runId, ticket: opts.ticket, contract, reviewAttemptId: binding.reviewAttemptId, reviewBasisDigest: binding.reviewBasisDigest })
|
|
2012
|
+
: undefined;
|
|
2013
|
+
const coverage = hostComparison
|
|
2014
|
+
? applyHostQaComparison(providerCoverage, contract, hostComparison.result, hostComparison.resultDigest, binding.projectDir)
|
|
2015
|
+
: providerCoverage;
|
|
2016
|
+
const errors = validateContractCoverage(contract, coverage, coverageBinding);
|
|
1968
2017
|
if (errors.length)
|
|
1969
2018
|
throw new Error(`Final QA coverage incomplete: ${errors.join("; ")}`);
|
|
1970
2019
|
const coverageDigest = db.qaPreparationStore().putArtifact("final-coverage", coverage);
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { RunSpanV1 } from "rafi-spec";
|
|
2
|
+
/** Explicit authorization is scoped to one nonempty ticket ID. */
|
|
3
|
+
export declare function qaCommandOutputExcerptsAllowed(workId: string, configuredTicket?: string | undefined): boolean;
|
|
4
|
+
/** Excerpts are opt-in; command text is never forwarded. */
|
|
5
|
+
export declare function selectQaCommandObservations(spans: RunSpanV1[], runId: string, workId: string, includeOutputExcerpts?: boolean): unknown[];
|
|
6
|
+
export declare function retainedQaCommandObservations(projectDir: string, runId: string, workId: string, includeOutputExcerpts?: boolean): unknown[];
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { ObservabilityReader } from "./observability.js";
|
|
2
|
+
/** Explicit authorization is scoped to one nonempty ticket ID. */
|
|
3
|
+
export function qaCommandOutputExcerptsAllowed(workId, configuredTicket = process.env.RAFI_QA_COMMAND_OUTPUT_EXCERPTS) {
|
|
4
|
+
return workId.length > 0 && configuredTicket === workId;
|
|
5
|
+
}
|
|
6
|
+
/** Excerpts are opt-in; command text is never forwarded. */
|
|
7
|
+
export function selectQaCommandObservations(spans, runId, workId, includeOutputExcerpts = false) {
|
|
8
|
+
const turns = new Set(spans.filter(span => span.runId === runId && span.kind === "provider_turn" && span.role === "builder" && span.ticketId === workId).map(span => span.spanId));
|
|
9
|
+
return spans.filter(span => span.runId === runId && span.kind === "tool" && span.role === "builder" && span.parentSpanId && turns.has(span.parentSpanId) && span.completionKnown && span.endedAt)
|
|
10
|
+
.flatMap(span => {
|
|
11
|
+
const attributes = span.attributes ?? {};
|
|
12
|
+
const input = attributes.input;
|
|
13
|
+
const command = typeof input?.command === "string" ? input.command : "";
|
|
14
|
+
const kind = /\bqa-rehearse-rollback\b/.test(command) ? "rollback-rehearsal" : /\bnpm\s+test\b|\bnode\s+--test\b/.test(command) ? "test" : undefined;
|
|
15
|
+
if (!kind)
|
|
16
|
+
return [];
|
|
17
|
+
const outcome = ["completed", "failed", "cancelled", "timed_out"].includes(span.outcome ?? "") ? span.outcome : "unknown";
|
|
18
|
+
const exitCode = typeof attributes.exitCode === "number" && Number.isSafeInteger(attributes.exitCode) ? attributes.exitCode : undefined;
|
|
19
|
+
const outputDigest = typeof attributes.outputDigest === "string" && /^[a-f0-9]{64}$/.test(attributes.outputDigest) ? attributes.outputDigest : undefined;
|
|
20
|
+
const outputExcerpt = includeOutputExcerpts && typeof attributes.outputSummary === "string" ? attributes.outputSummary.slice(0, 1000) : undefined;
|
|
21
|
+
return [{ spanId: span.spanId, providerTurnId: span.providerTurnId, startedAt: span.startedAt, endedAt: span.endedAt, kind, outcome, exitCode, outputDigest, ...(outputExcerpt === undefined ? {} : { outputExcerpt, excerptMayBeTruncated: true }) }];
|
|
22
|
+
}).slice(-40);
|
|
23
|
+
}
|
|
24
|
+
export function retainedQaCommandObservations(projectDir, runId, workId, includeOutputExcerpts = false) {
|
|
25
|
+
try {
|
|
26
|
+
const reader = new ObservabilityReader(projectDir);
|
|
27
|
+
try {
|
|
28
|
+
return selectQaCommandObservations(reader.spans(runId), runId, workId, includeOutputExcerpts);
|
|
29
|
+
}
|
|
30
|
+
finally {
|
|
31
|
+
reader.close();
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
return [];
|
|
36
|
+
} // Diagnostics are optional; never prevent independent review.
|
|
37
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { captureFrozenQaSource, createDisposableQaSnapshot } from "./qaSnapshot.js";
|
|
4
|
+
import { canonicalToolPath, isWithinProject } from "./permissions/shellForms.js";
|
|
5
|
+
/** Rehearse the current uncommitted product changes against HEAD, never the source Git index. */
|
|
6
|
+
export function rehearseRollback(projectDir) {
|
|
7
|
+
const unsafeGitEnvironment = Object.keys(process.env).filter(key => /^GIT_(?:DIR|WORK_TREE|INDEX_FILE|OBJECT_DIRECTORY|ALTERNATE_OBJECT_DIRECTORIES|COMMON_DIR|EXEC_PATH|TEMPLATE_DIR|SHALLOW_FILE|QUARANTINE_PATH|NAMESPACE)$/.test(key));
|
|
8
|
+
if (unsafeGitEnvironment.length)
|
|
9
|
+
throw new Error(`Rollback rehearsal cannot use Git repository overrides: ${unsafeGitEnvironment.join(", ")}`);
|
|
10
|
+
// Provider sandboxes may inject Git configuration. Never apply those
|
|
11
|
+
// settings to the independent clone, and restore the caller's environment.
|
|
12
|
+
const configKeys = Object.keys(process.env).filter(key => /^GIT_CONFIG(?:$|_)/.test(key));
|
|
13
|
+
const savedConfig = new Map(configKeys.map(key => [key, process.env[key]]));
|
|
14
|
+
for (const key of configKeys)
|
|
15
|
+
delete process.env[key];
|
|
16
|
+
process.env.GIT_CONFIG_NOSYSTEM = "1";
|
|
17
|
+
process.env.GIT_CONFIG_GLOBAL = "/dev/null";
|
|
18
|
+
try {
|
|
19
|
+
return rehearseWithCleanGitConfig(projectDir);
|
|
20
|
+
}
|
|
21
|
+
finally {
|
|
22
|
+
for (const key of Object.keys(process.env).filter(key => /^GIT_CONFIG(?:$|_)/.test(key)))
|
|
23
|
+
delete process.env[key];
|
|
24
|
+
for (const [key, value] of savedConfig)
|
|
25
|
+
if (value !== undefined)
|
|
26
|
+
process.env[key] = value;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function rehearseWithCleanGitConfig(projectDir) {
|
|
30
|
+
const topLevel = spawnSync("git", ["rev-parse", "--show-toplevel"], { cwd: projectDir, encoding: "utf8", timeout: 30_000 });
|
|
31
|
+
if (topLevel.error || topLevel.status !== 0)
|
|
32
|
+
throw new Error("Rollback rehearsal requires a Git project");
|
|
33
|
+
if (canonicalToolPath(projectDir) !== canonicalToolPath(topLevel.stdout.trim()))
|
|
34
|
+
throw new Error("Run rollback rehearsal from the Git project root");
|
|
35
|
+
if (isWithinProject(projectDir, tmpdir()))
|
|
36
|
+
throw new Error("Rollback rehearsal requires a temporary directory outside the source project");
|
|
37
|
+
const snapshot = createDisposableQaSnapshot(projectDir, { projectDependencies: false });
|
|
38
|
+
const output = [];
|
|
39
|
+
const childEnv = { ...process.env, GIT_CONFIG_NOSYSTEM: "1", GIT_CONFIG_GLOBAL: "/dev/null", GIT_TERMINAL_PROMPT: "0", npm_config_offline: "true", npm_config_update_notifier: "false", npm_config_audit: "false", npm_config_fund: "false" };
|
|
40
|
+
// Inherited node:test context otherwise makes a nested test runner silently
|
|
41
|
+
// skip all test files while returning success.
|
|
42
|
+
delete childEnv.NODE_TEST_CONTEXT;
|
|
43
|
+
const run = (program, args) => {
|
|
44
|
+
const result = spawnSync(program, args, {
|
|
45
|
+
cwd: snapshot.path, encoding: "utf8", timeout: 300_000, maxBuffer: 8 * 1024 * 1024,
|
|
46
|
+
env: childEnv,
|
|
47
|
+
});
|
|
48
|
+
if (!args.includes("--stage"))
|
|
49
|
+
output.push(`$ ${[program, ...args].join(" ")}\n${result.stdout ?? ""}${result.stderr ?? ""}`);
|
|
50
|
+
if (result.error || result.status !== 0)
|
|
51
|
+
throw new Error(`Rollback rehearsal failed: ${program} ${args.join(" ")} (${result.error?.message ?? `exit ${result.status}`})\n${output.join("\n")}`);
|
|
52
|
+
return result.stdout;
|
|
53
|
+
};
|
|
54
|
+
try {
|
|
55
|
+
const source = snapshot.frozenState;
|
|
56
|
+
if (source.untracked.some(item => item.kind === "symlink"))
|
|
57
|
+
throw new Error("Rollback rehearsal cannot run with untracked symbolic links");
|
|
58
|
+
run("git", ["-c", "core.hooksPath=/dev/null", "add", "-A", "--", ".", ":(exclude).foreman/**", ":(exclude).rafi/**"]);
|
|
59
|
+
const entries = run("git", ["ls-files", "--stage", "-z"]);
|
|
60
|
+
if (entries.split("\0").some(entry => entry.startsWith("120000 ")))
|
|
61
|
+
throw new Error("Rollback rehearsal cannot run with tracked symbolic links");
|
|
62
|
+
const changed = run("git", ["diff", "--cached", "--name-only"]);
|
|
63
|
+
if (!changed.trim())
|
|
64
|
+
throw new Error("Rollback rehearsal requires uncommitted product changes relative to HEAD");
|
|
65
|
+
run("git", ["-c", "core.hooksPath=/dev/null", "-c", "user.name=Rafi rehearsal", "-c", "user.email=rafi-rehearsal@example.invalid", "-c", "commit.gpgsign=false", "commit", "-m", "Temporary rollback rehearsal"]);
|
|
66
|
+
run("npm", ["test"]);
|
|
67
|
+
run("git", ["-c", "core.hooksPath=/dev/null", "-c", "user.name=Rafi rehearsal", "-c", "user.email=rafi-rehearsal@example.invalid", "-c", "commit.gpgsign=false", "revert", "--no-edit", "HEAD"]);
|
|
68
|
+
run("npm", ["test"]);
|
|
69
|
+
run("git", ["diff", "--exit-code", source.head]);
|
|
70
|
+
const after = captureFrozenQaSource(projectDir);
|
|
71
|
+
if (after.digest !== source.digest)
|
|
72
|
+
throw new Error("Source project changed during rollback rehearsal; inspect it before continuing");
|
|
73
|
+
return `Rollback rehearsal passed in an independent temporary repository. Verification: npm test before revert exited 0; git revert exited 0; npm test after revert exited 0; git diff --exit-code against captured HEAD exited 0. Its tracked files match the captured HEAD baseline: ${source.head}. The synthetic commit contains all uncommitted product changes, not necessarily only one ticket. Source digest: ${source.digest}.\n${output.join("\n")}`;
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
snapshot.remove();
|
|
77
|
+
}
|
|
78
|
+
}
|
package/dist/qaSnapshot.d.ts
CHANGED
|
@@ -80,7 +80,9 @@ export declare function captureFrozenQaSource(worktree: string): FrozenQaSourceS
|
|
|
80
80
|
export declare function captureFrozenQaSourceAsync(worktree: string, progress?: QaSnapshotProgress): Promise<FrozenQaSourceState>;
|
|
81
81
|
export declare function captureStableFrozenQaSource(read: (attempt: number, pass: 1 | 2) => FrozenQaSourceState, now?: Date): FrozenQaSourceState;
|
|
82
82
|
export declare function captureStableFrozenQaSourceAsync(read: (attempt: number, pass: 1 | 2) => Promise<FrozenQaSourceState>, now?: Date): Promise<FrozenQaSourceState>;
|
|
83
|
-
export declare function createDisposableQaSnapshot(builderWorktree: string
|
|
83
|
+
export declare function createDisposableQaSnapshot(builderWorktree: string, options?: {
|
|
84
|
+
projectDependencies?: boolean;
|
|
85
|
+
}): DisposableQaSnapshot;
|
|
84
86
|
export declare function createDisposableQaSnapshotAsync(builderWorktree: string, progress?: QaSnapshotProgress): Promise<AsyncDisposableQaSnapshot>;
|
|
85
87
|
export declare function frozenQaStateManifest(state: FrozenQaSourceState): QaChangeManifest;
|
|
86
88
|
export declare function changeManifest(worktree: string): QaChangeManifest;
|
package/dist/qaSnapshot.js
CHANGED
|
@@ -45,7 +45,7 @@ export async function captureStableFrozenQaSourceAsync(read, now = new Date()) {
|
|
|
45
45
|
}
|
|
46
46
|
throw new QaSourceInstabilityError(MAX_CAPTURE_ATTEMPTS);
|
|
47
47
|
}
|
|
48
|
-
export function createDisposableQaSnapshot(builderWorktree) {
|
|
48
|
+
export function createDisposableQaSnapshot(builderWorktree, options = {}) {
|
|
49
49
|
const source = resolve(builderWorktree);
|
|
50
50
|
const root = gitText(source, ["rev-parse", "--show-toplevel"]);
|
|
51
51
|
const frozenState = captureFrozenQaSource(source);
|
|
@@ -54,7 +54,8 @@ export function createDisposableQaSnapshot(builderWorktree) {
|
|
|
54
54
|
cloneRepositorySync(root, review, frozenState.head);
|
|
55
55
|
try {
|
|
56
56
|
applyFrozenSync(review, frozenState);
|
|
57
|
-
|
|
57
|
+
if (options.projectDependencies !== false)
|
|
58
|
+
projectDependencyTrees(root, review);
|
|
58
59
|
mkdirSync(join(tempRoot, "scratch"), { mode: 0o700 });
|
|
59
60
|
const manifest = manifestFromFrozen(frozenState);
|
|
60
61
|
const snapshot = {
|
|
@@ -8,8 +8,24 @@ export declare function draftDigest(candidate: QaContractCandidateV1): string;
|
|
|
8
8
|
/** Allocate identities from meaning, replacing proposed agent IDs throughout the draft. */
|
|
9
9
|
export declare function allocateStableCheckIds(candidate: QaContractCandidateV1): QaContractCandidateV1;
|
|
10
10
|
export declare function inventoryDigest(requirements: RequirementRef[]): string;
|
|
11
|
+
/** Derive a missing baseline binding only from an explicit, unique check prerequisite reference. */
|
|
12
|
+
export declare function bindUniqueBaselineCheckIds(candidate: QaContractCandidateV1): QaContractCandidateV1;
|
|
13
|
+
/**
|
|
14
|
+
* The host owns the before/after path comparison. Providers may still draft a
|
|
15
|
+
* check that asks them to enumerate every changed file, which turns host
|
|
16
|
+
* bookkeeping into a Builder obligation. Replace only that explicit shape;
|
|
17
|
+
* ordinary product-scope checks remain provider-authored and unchanged.
|
|
18
|
+
*/
|
|
19
|
+
export declare function rewriteProviderHostScopeChecks(candidate: QaContractCandidateV1, baselineId: string): QaContractCandidateV1;
|
|
11
20
|
export declare function authoritativeInventory(ticket: TicketDef, checklist: string[], inputs: ContractInputRef[]): RequirementRef[];
|
|
12
21
|
export declare function parsePreparationArtifact<T>(text: string, tag: string, maximum?: number): T;
|
|
22
|
+
/** Capture safe absolute snapshot paths before the disposable snapshot is removed. */
|
|
23
|
+
export declare function mapSnapshotWorkingDirectories(text: string, snapshotPath: string): Record<string, string>;
|
|
24
|
+
/** Providers sometimes repeat an absolute disposable-snapshot path from an earlier
|
|
25
|
+
* conversation. That path is host-owned and must not enter the durable contract;
|
|
26
|
+
* normalize only the bounded QA snapshot naming convention to stable relative
|
|
27
|
+
* locations before parsing the provider draft. */
|
|
28
|
+
export declare function normalizeSnapshotPaths(text: string): string;
|
|
13
29
|
export declare function validateCandidate(candidate: QaContractCandidateV1, inventory: RequirementRef[], resolveEquivalent?: EquivalentAuthorityResolver): string[];
|
|
14
30
|
export declare function validateAssessment(candidate: QaContractCandidateV1, assessment: QaSemanticAssessmentV1, authorSessionId: string): string[];
|
|
15
31
|
export declare function assembleContract(candidate: QaContractCandidateV1, inventory: RequirementRef[], assessment: QaSemanticAssessmentV1, authorSessionId: string, challenge?: QaChallengeReceiptV1, resolveEquivalent?: EquivalentAuthorityResolver): QaVerificationContractV1;
|