ai-foreman 1.7.28 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. package/README.md +1 -1
  2. package/dist/adapters/claude.d.ts +23 -12
  3. package/dist/adapters/claude.js +228 -24
  4. package/dist/adapters/codex.d.ts +17 -7
  5. package/dist/adapters/codex.js +195 -17
  6. package/dist/adapters/recovering.d.ts +2 -0
  7. package/dist/adapters/recovering.js +5 -1
  8. package/dist/adapters/structured.d.ts +71 -0
  9. package/dist/adapters/structured.js +213 -0
  10. package/dist/adapters/types.d.ts +86 -9
  11. package/dist/agentRun.d.ts +2 -0
  12. package/dist/agentRun.js +2 -1
  13. package/dist/boundedTurnEvidence.d.ts +18 -0
  14. package/dist/boundedTurnEvidence.js +76 -0
  15. package/dist/branch/currentGuard.d.ts +2 -0
  16. package/dist/branch/currentGuard.js +2 -0
  17. package/dist/branch/planner.js +1 -4
  18. package/dist/branch/runner.d.ts +2 -1
  19. package/dist/branch/runner.js +10 -7
  20. package/dist/buildAdmission.js +4 -0
  21. package/dist/buildApproval.d.ts +30 -0
  22. package/dist/buildApproval.js +68 -0
  23. package/dist/buildAssignment.d.ts +1 -0
  24. package/dist/buildAssignment.js +82 -23
  25. package/dist/buildInterventions.d.ts +2 -2
  26. package/dist/buildInterventions.js +11 -4
  27. package/dist/buildLaunchGate.js +3 -2
  28. package/dist/builderGuidanceFollowup.js +17 -6
  29. package/dist/cli/manager.js +22 -20
  30. package/dist/cli/start.js +32 -24
  31. package/dist/cli/tickets.js +40 -24
  32. package/dist/continuity.d.ts +10 -14
  33. package/dist/continuity.js +198 -179
  34. package/dist/continuityOrigin.d.ts +13 -0
  35. package/dist/continuityOrigin.js +91 -0
  36. package/dist/foreman.d.ts +20 -5
  37. package/dist/foreman.js +57 -187
  38. package/dist/graph/boundary.d.ts +1 -1
  39. package/dist/graph/boundary.js +1 -1
  40. package/dist/graph/host.js +8 -19
  41. package/dist/graph/manager.d.ts +1 -1
  42. package/dist/graph/manager.js +1 -1
  43. package/dist/graph/preparation.d.ts +38 -0
  44. package/dist/graph/preparation.js +7 -6
  45. package/dist/graph/semantic.d.ts +1 -0
  46. package/dist/graph/semantic.js +4 -1
  47. package/dist/graph/session.js +7 -2
  48. package/dist/graph/turn.d.ts +2 -3
  49. package/dist/graph/turn.js +28 -40
  50. package/dist/handoffProtocol.js +2 -2
  51. package/dist/handoffs.d.ts +3 -1
  52. package/dist/handoffs.js +90 -59
  53. package/dist/hostGraphApplicability.d.ts +46 -0
  54. package/dist/hostGraphApplicability.js +97 -0
  55. package/dist/hostQaAugmentation.d.ts +18 -0
  56. package/dist/hostQaAugmentation.js +81 -0
  57. package/dist/hostQaComparison.d.ts +21 -2
  58. package/dist/hostQaComparison.js +43 -28
  59. package/dist/hostSourcePreservation.d.ts +38 -0
  60. package/dist/hostSourcePreservation.js +72 -0
  61. package/dist/interviews.d.ts +7 -0
  62. package/dist/interviews.js +18 -1
  63. package/dist/legacyCommunicationDisplay.d.ts +2 -0
  64. package/dist/legacyCommunicationDisplay.js +2 -0
  65. package/dist/managerPacket.js +3 -3
  66. package/dist/markers.d.ts +4 -3
  67. package/dist/markers.js +4 -20
  68. package/dist/probeIpc.d.ts +14 -0
  69. package/dist/probeIpc.js +29 -0
  70. package/dist/qaBuildGate.d.ts +21 -0
  71. package/dist/qaBuildGate.js +48 -12
  72. package/dist/qaBuilderCoverage.js +22 -13
  73. package/dist/qaBuilderEvidence.d.ts +16 -1
  74. package/dist/qaBuilderEvidence.js +48 -8
  75. package/dist/qaContractCoverage.d.ts +2 -2
  76. package/dist/qaContractCoverage.js +11 -7
  77. package/dist/qaContractDelivery.js +34 -20
  78. package/dist/qaCoverageAssembly.d.ts +22 -0
  79. package/dist/qaCoverageAssembly.js +16 -0
  80. package/dist/qaDeliveryJournal.d.ts +2 -0
  81. package/dist/qaDeliveryJournal.js +19 -0
  82. package/dist/qaEffectiveConfig.d.ts +1 -0
  83. package/dist/qaEffectiveConfig.js +6 -1
  84. package/dist/qaFailureDelivery.js +97 -85
  85. package/dist/qaHostTransformation.d.ts +26 -0
  86. package/dist/qaHostTransformation.js +100 -0
  87. package/dist/qaPreparation.d.ts +8 -20
  88. package/dist/qaPreparation.js +133 -229
  89. package/dist/qaPreparationInputs.js +19 -0
  90. package/dist/qaPreparationStore.js +34 -6
  91. package/dist/qaPrerequisites.d.ts +2 -1
  92. package/dist/qaPrerequisites.js +21 -1
  93. package/dist/qaProtocolV2.d.ts +15 -0
  94. package/dist/qaRecovery.js +21 -4
  95. package/dist/qaReview.js +232 -394
  96. package/dist/qaReviewEvidence.d.ts +7 -3
  97. package/dist/qaReviewEvidence.js +15 -4
  98. package/dist/qaRuntime.js +1 -1
  99. package/dist/qaSnapshot.js +71 -21
  100. package/dist/qaStructuredResponse.d.ts +10 -0
  101. package/dist/qaStructuredResponse.js +57 -0
  102. package/dist/qaTimeline.js +5 -3
  103. package/dist/qaToolRecovery.d.ts +12 -0
  104. package/dist/qaToolRecovery.js +44 -0
  105. package/dist/qaVerificationContract.d.ts +3 -2
  106. package/dist/qaVerificationContract.js +58 -34
  107. package/dist/readinessGate.js +5 -4
  108. package/dist/runtimeReadiness.js +23 -15
  109. package/dist/statusReporter.d.ts +2 -0
  110. package/dist/statusReporter.js +2 -0
  111. package/dist/structuredCutover.d.ts +31 -0
  112. package/dist/structuredCutover.js +510 -0
  113. package/dist/structuredCutoverFiles.d.ts +10 -0
  114. package/dist/structuredCutoverFiles.js +181 -0
  115. package/dist/structuredDispatchAuthority.d.ts +22 -0
  116. package/dist/structuredDispatchAuthority.js +110 -0
  117. package/dist/structuredDispatchRecord.d.ts +29 -0
  118. package/dist/structuredDispatchRecord.js +36 -0
  119. package/dist/structuredLiveBudget.d.ts +2 -0
  120. package/dist/structuredLiveBudget.js +23 -0
  121. package/dist/structuredOperation.d.ts +18 -0
  122. package/dist/structuredOperation.js +257 -0
  123. package/dist/supervisedStart.d.ts +1 -1
  124. package/dist/supervisedStart.js +78 -31
  125. package/dist/supervisor.d.ts +18 -2
  126. package/dist/supervisor.js +74 -17
  127. package/dist/ticketPlanning.d.ts +1 -373
  128. package/dist/ticketPlanning.js +2 -26
  129. package/dist/ticketPopulation.d.ts +0 -3
  130. package/dist/ticketPopulation.js +4 -21
  131. package/dist/ticketPopulationPageAuthority.d.ts +13 -0
  132. package/dist/ticketPopulationPageAuthority.js +30 -0
  133. package/dist/ticketPopulationPaging.d.ts +27 -0
  134. package/dist/ticketPopulationPaging.js +105 -0
  135. package/dist/tickets/renderMarkdown.d.ts +14 -0
  136. package/dist/tickets/renderMarkdown.js +39 -5
  137. package/dist/tickets/ticketLoader.js +10 -0
  138. package/dist/tickets/ticketSchema.d.ts +107 -0
  139. package/dist/tickets/ticketSchema.js +4 -1
  140. package/dist/workflowDb.d.ts +100 -2
  141. package/dist/workflowDb.js +781 -35
  142. package/dist/workflowReader.d.ts +1 -0
  143. package/dist/workflowReader.js +9 -0
  144. package/package.json +15 -7
@@ -45,6 +45,25 @@ export function admittedPlanInput(projectDir, ticket) {
45
45
  requirements.push({ id: `req-plan-${fingerprint.slice(0, 24)}-${occurrence}`, inputRef: input.id, locator: `${location}${field}[${index}]`, statement, digest: contractDigest("statement", statement), obligation: "mandatory", origin: "explicit", authority: "approved" });
46
46
  });
47
47
  }
48
+ if (slice.verification_version === 2) {
49
+ for (const requirement of slice.verification_requirements ?? []) {
50
+ const statement = requirement.summary;
51
+ requirements.push({
52
+ id: `req-plan-v2-${slice.slice_ref}-${requirement.id}`,
53
+ inputRef: input.id,
54
+ locator: `slices.${slice.slice_ref}.verification_requirements.${requirement.id}`,
55
+ statement,
56
+ // A RequirementRef's historical digest domain is its statement.
57
+ // Keep that invariant and bind the executable V2 intent separately.
58
+ digest: contractDigest("statement", statement),
59
+ verificationIntentDigest: contractDigest("verification-intent-v2", requirement),
60
+ obligation: "mandatory",
61
+ origin: "explicit",
62
+ authority: "approved",
63
+ verificationIntent: structuredClone(requirement),
64
+ });
65
+ }
66
+ }
48
67
  return { input, plan, requirements };
49
68
  }
50
69
  throw new Error("Exact admitted approved-plan revision unavailable; restore its immutable artifact before preparation");
@@ -9,7 +9,7 @@ export function migrateQaPreparation(db) {
9
9
  db.exec(`
10
10
  CREATE TABLE IF NOT EXISTS qa_preparation_policy(run_id TEXT PRIMARY KEY REFERENCES workflow_runs(run_id),record_json TEXT NOT NULL);
11
11
  CREATE TABLE IF NOT EXISTS qa_preparation_budgets(id TEXT PRIMARY KEY,run_id TEXT NOT NULL REFERENCES workflow_runs(run_id),work_id TEXT NOT NULL,admission_digest TEXT NOT NULL,record_json TEXT NOT NULL,UNIQUE(run_id,work_id,admission_digest));
12
- CREATE TABLE IF NOT EXISTS qa_depth_decisions(decision_id TEXT PRIMARY KEY,run_id TEXT NOT NULL REFERENCES workflow_runs(run_id),work_id TEXT NOT NULL,record_json TEXT NOT NULL);
12
+ CREATE TABLE IF NOT EXISTS qa_depth_decisions(decision_id TEXT NOT NULL,run_id TEXT NOT NULL REFERENCES workflow_runs(run_id),work_id TEXT NOT NULL,admission_digest TEXT NOT NULL,record_json TEXT NOT NULL,PRIMARY KEY(decision_id,run_id,work_id,admission_digest));
13
13
  CREATE TABLE IF NOT EXISTS qa_preparation_attempts(operation_id TEXT PRIMARY KEY,budget_id TEXT NOT NULL REFERENCES qa_preparation_budgets(id),state TEXT NOT NULL,intent_json TEXT NOT NULL,result_json TEXT);
14
14
  CREATE TABLE IF NOT EXISTS qa_preparation_progress(budget_id TEXT NOT NULL REFERENCES qa_preparation_budgets(id),stage TEXT NOT NULL,digest TEXT NOT NULL,PRIMARY KEY(budget_id,stage));
15
15
  CREATE TABLE IF NOT EXISTS qa_verification_contracts(digest TEXT PRIMARY KEY,run_id TEXT NOT NULL REFERENCES workflow_runs(run_id),work_id TEXT NOT NULL,admission_digest TEXT NOT NULL,revision INTEGER NOT NULL,record_json TEXT NOT NULL,UNIQUE(run_id,work_id,admission_digest,revision));
@@ -19,6 +19,7 @@ export function migrateQaPreparation(db) {
19
19
  CREATE TABLE IF NOT EXISTS qa_contract_artifacts(digest TEXT PRIMARY KEY,kind TEXT NOT NULL,record_json TEXT NOT NULL);
20
20
  CREATE TABLE IF NOT EXISTS qa_preparation_events(event_id TEXT PRIMARY KEY,run_id TEXT NOT NULL REFERENCES workflow_runs(run_id),work_id TEXT NOT NULL,kind TEXT NOT NULL,record_json TEXT NOT NULL);
21
21
  `);
22
+ migrateQaDepthDecisions(db);
22
23
  for (const table of ["qa_preparation_policy", "qa_depth_decisions", "qa_verification_contracts", "qa_contract_check_history", "qa_contract_receipts", "qa_contract_artifacts", "qa_preparation_events"]) {
23
24
  for (const action of ["UPDATE", "DELETE"])
24
25
  db.exec(`CREATE TRIGGER IF NOT EXISTS preparation_immutable_${table}_${action} BEFORE ${action} ON ${table} BEGIN SELECT RAISE(ABORT,'Preparation authority and evidence are immutable'); END`);
@@ -33,6 +34,35 @@ export function migrateQaPreparation(db) {
33
34
  db.exec(`CREATE TRIGGER IF NOT EXISTS preparation_owner_${table}_${action} BEFORE ${action} ON ${table} BEGIN SELECT CASE WHEN rafi_qa_preparation_owner(${expression})<>1 THEN RAISE(ABORT,'Original preparation owner required') END; END`);
34
35
  }
35
36
  }
37
+ /** Provider decision ids are scoped to the admitted preparation, not global. */
38
+ function migrateQaDepthDecisions(db) {
39
+ const columns = db.prepare("PRAGMA table_info(qa_depth_decisions)").all();
40
+ if (!columns.length || columns.some(column => column.name === "admission_digest"))
41
+ return;
42
+ db.transaction(() => {
43
+ for (const action of ["INSERT", "UPDATE", "DELETE"]) {
44
+ db.exec(`DROP TRIGGER IF EXISTS preparation_immutable_qa_depth_decisions_${action}`);
45
+ db.exec(`DROP TRIGGER IF EXISTS preparation_owner_qa_depth_decisions_${action}`);
46
+ db.exec(`DROP TRIGGER IF EXISTS preparation_lease_qa_depth_decisions_${action}`);
47
+ }
48
+ db.exec("CREATE TABLE qa_depth_decisions_scoped(decision_id TEXT NOT NULL,run_id TEXT NOT NULL REFERENCES workflow_runs(run_id),work_id TEXT NOT NULL,admission_digest TEXT NOT NULL,record_json TEXT NOT NULL,PRIMARY KEY(decision_id,run_id,work_id,admission_digest))");
49
+ db.exec("INSERT INTO qa_depth_decisions_scoped(decision_id,run_id,work_id,admission_digest,record_json) SELECT decision_id,run_id,work_id,COALESCE(NULLIF(json_extract(record_json,'$.admissionDigest'),''),'legacy:' || run_id || ':' || work_id),record_json FROM qa_depth_decisions");
50
+ db.exec("DROP TABLE qa_depth_decisions");
51
+ db.exec("ALTER TABLE qa_depth_decisions_scoped RENAME TO qa_depth_decisions");
52
+ for (const action of ["INSERT", "UPDATE", "DELETE"]) {
53
+ const row = action === "DELETE" ? "OLD" : "NEW";
54
+ db.exec(`CREATE TRIGGER preparation_immutable_qa_depth_decisions_${action} BEFORE ${action} ON qa_depth_decisions BEGIN SELECT RAISE(ABORT,'Preparation authority and evidence are immutable'); END`);
55
+ const ownerRow = action === "DELETE" ? "OLD" : "NEW";
56
+ db.exec(`CREATE TRIGGER preparation_owner_qa_depth_decisions_${action} BEFORE ${action} ON qa_depth_decisions BEGIN SELECT CASE WHEN rafi_qa_preparation_owner(${ownerRow}.run_id)<>1 THEN RAISE(ABORT,'Original preparation owner required') END; END`);
57
+ const leaseRun = ownerRow;
58
+ db.exec(`CREATE TRIGGER preparation_lease_qa_depth_decisions_${action} BEFORE ${action} ON qa_depth_decisions
59
+ WHEN (EXISTS(SELECT 1 FROM build_admission) OR rafi_build_writer_run()<>'' OR EXISTS(SELECT 1 FROM build_runtime_runs WHERE run_id=${leaseRun}.run_id))
60
+ AND NOT EXISTS(SELECT 1 FROM build_admission WHERE json_extract(record_json,'$.token')=rafi_build_writer_token() AND (json_extract(record_json,'$.runId')=${leaseRun}.run_id OR EXISTS(SELECT 1 FROM build_child_runs WHERE child=${leaseRun}.run_id AND parent=json_extract(record_json,'$.runId'))))
61
+ AND NOT (NOT EXISTS(SELECT 1 FROM build_admission) AND EXISTS(SELECT 1 FROM project_lease WHERE owner=rafi_build_lease_owner() AND generation=rafi_build_lease_generation() AND run_id=${leaseRun}.run_id))
62
+ BEGIN SELECT RAISE(ABORT,'Preparation requires the original durable lease generation'); END`);
63
+ }
64
+ })();
65
+ }
36
66
  /** Tagged enforcing authority also fences older callers that omit new fields. */
37
67
  export function migrateQaPreparationGuards(db) {
38
68
  const scopes = {
@@ -242,13 +272,11 @@ export class QaPreparationStore {
242
272
  current.level = decision.level;
243
273
  current.deadlineMs = Math.max(current.deadlineMs, current.startMs + current.policy.wallTimeMs[decision.level - 1]);
244
274
  this.saveBudget(current);
245
- const prior = this.read("qa_depth_decisions", "decision_id", decision.decisionId);
246
- const priorScope = this.db.prepare("SELECT run_id,work_id FROM qa_depth_decisions WHERE decision_id=?").get(decision.decisionId);
247
- if (priorScope && (priorScope.run_id !== budget.runId || priorScope.work_id !== budget.workId))
248
- throw new Error("Planner depth identity belongs to another admitted work");
275
+ const priorRow = this.db.prepare("SELECT record_json FROM qa_depth_decisions WHERE decision_id=? AND run_id=? AND work_id=? AND admission_digest=?").get(decision.decisionId, budget.runId, budget.workId, budget.admissionDigest);
276
+ const prior = priorRow ? this.decode(priorRow.record_json) : undefined;
249
277
  if (prior && this.protectedJson(prior) !== this.protectedJson(decision))
250
278
  throw new Error("Conflicting planner depth identity");
251
- this.db.prepare("INSERT INTO qa_depth_decisions VALUES(?,?,?,?) ON CONFLICT(decision_id) DO NOTHING").run(decision.decisionId, budget.runId, budget.workId, this.protectedJson(decision));
279
+ this.db.prepare("INSERT INTO qa_depth_decisions VALUES(?,?,?,?,?) ON CONFLICT(decision_id,run_id,work_id,admission_digest) DO NOTHING").run(decision.decisionId, budget.runId, budget.workId, budget.admissionDigest, this.protectedJson(decision));
252
280
  });
253
281
  }
254
282
  retainProgress(budget, stage, value) {
@@ -6,10 +6,11 @@ export interface QaPrerequisiteEvidence {
6
6
  checkedAt: string;
7
7
  /** Host absence is actionable; host availability never proves provider access. */
8
8
  authority: "host-preflight";
9
+ /** not_applicable: a typed applicability condition is false, so the requirement does not apply (never a missing prerequisite). */
9
10
  checks: Array<{
10
11
  requirement: string;
11
12
  capability: string;
12
- outcome: "available" | "not_run";
13
+ outcome: "available" | "not_run" | "not_applicable";
13
14
  evidence: string;
14
15
  }>;
15
16
  sourceDefects: string[];
@@ -17,12 +17,32 @@ export async function checkQaPrerequisites(input) {
17
17
  catch { /* malformed manifests remain source findings */ }
18
18
  const evidence = {
19
19
  version: 3, snapshotPath: realpathSync(input.snapshotPath), checkedAt: new Date().toISOString(), authority: "host-preflight",
20
- scopeDigest: qaDigest("qa-prerequisites-v3", { sourceDigest: input.sourceDigest, snapshotPath: realpathSync(input.snapshotPath), manifests, node: process.version, path: env.PATH ?? "", runtimeContext: input.runtimeContext ?? null, requirements: input.ticket.required_tests }),
20
+ scopeDigest: qaDigest("qa-prerequisites-v4", { sourceDigest: input.sourceDigest, snapshotPath: realpathSync(input.snapshotPath), manifests, node: process.version, path: env.PATH ?? "", runtimeContext: input.runtimeContext ?? null, requirements: input.ticket.verification_version === 2 ? input.ticket.verification_requirements : input.ticket.required_tests }),
21
21
  checks: [], sourceDefects: [],
22
22
  };
23
23
  const probe = input.probe ?? ((executable, args) => new Promise(resolve => {
24
24
  execFile(executable, args, { cwd: input.snapshotPath, env, timeout: timeoutMs, maxBuffer: 4096, windowsHide: true }, error => resolve(!error));
25
25
  }));
26
+ // V2 never extracts or executes a command from provider prose. Its closed
27
+ // probe vocabulary is resolved by the host and records availability only.
28
+ if (input.ticket.verification_version === 2) {
29
+ for (const requirement of input.ticket.verification_requirements ?? []) {
30
+ if (requirement.applicability.kind !== "host-executable-available")
31
+ continue;
32
+ const executable = resolveExecutablePath(requirement.applicability.executable, env);
33
+ const available = Boolean(executable);
34
+ const expected = requirement.applicability.expected;
35
+ evidence.checks.push({
36
+ requirement: requirement.id,
37
+ capability: `host-probe:executable-available:${requirement.applicability.executable}`,
38
+ outcome: available === expected ? "available" : "not_applicable",
39
+ evidence: `Host registry resolved executable availability=${available}; expected=${expected}; ${available === expected ? "requirement applies" : "requirement does not apply"}; snapshot=${evidence.snapshotPath}`,
40
+ });
41
+ }
42
+ return evidence;
43
+ }
44
+ // V1 compatibility only: historical prose is read conservatively and never
45
+ // promoted to a typed applicability condition.
26
46
  for (const requirement of input.ticket.required_tests) {
27
47
  const command = /^\s*`?(pnpm|npm|yarn|bun|node|docker|python3|pytest|cargo|go)\b/.exec(requirement)?.[1];
28
48
  if (command) {
@@ -73,6 +73,13 @@ export interface QaSessionHandleV2 {
73
73
  acceptedHandoffReceiptDigest?: string;
74
74
  }
75
75
  export interface QaTurnIntentV2 {
76
+ sealedDispatch?: import("./structuredDispatchRecord.js").SealedDispatchRecordV2;
77
+ outputContract?: {
78
+ protocolVersion: 2;
79
+ operation: string;
80
+ schemaVersion: number;
81
+ schemaDigest: string;
82
+ };
76
83
  version: 2;
77
84
  operationId: string;
78
85
  runId: string;
@@ -87,6 +94,14 @@ export interface QaTurnIntentV2 {
87
94
  intendedAt: string;
88
95
  }
89
96
  export interface QaTurnReceiptV2 {
97
+ retainedEvidence?: {
98
+ rawOriginalBytes?: number;
99
+ rawRetainedBytes?: number;
100
+ cleanedOriginalBytes: number;
101
+ cleanedRetainedBytes: number;
102
+ };
103
+ sealedDispatch?: import("./structuredDispatchRecord.js").SealedDispatchRecordV2;
104
+ structuredResponseDigest?: string;
90
105
  version: 2;
91
106
  operationId: string;
92
107
  dispatch: "not-dispatched" | "completed" | "uncertain";
@@ -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 { parseProviderContract, parseQaFailureReport, qaRecoveryAcknowledgementV1Schema } from "rafi-spec";
5
+ import { parseProviderContract, parseStrictJson, validateQaFailureReport, QA_FAILURE_REPORT_MAX_BYTES, 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";
@@ -81,6 +81,8 @@ export function canonicalizeQaRecoveryRequirements(packet) {
81
81
  return revise(loaded, { resources });
82
82
  }
83
83
  export function loadQaRecoveryPacket(directory) {
84
+ if (existsSync(join(directory, ".structured-communication-v2-cutover")))
85
+ throw new Error("structured-communication-v2-cutover: this recovery packet cannot resume; start a new native dispatch");
84
86
  const root = resolve(directory);
85
87
  const manifestPath = join(root, "manifest.json");
86
88
  if (!existsSync(manifestPath) || lstatSync(manifestPath).isSymbolicLink())
@@ -455,7 +457,22 @@ export function readManualQaReport(packetDirectory) { return JSON.parse(readFile
455
457
  export function validateManualQaReport(packet) {
456
458
  const path = join(packet.directory, "report.json");
457
459
  const raw = existsSync(path) ? readFileSync(path) : Buffer.alloc(0);
458
- const parsed = parseQaFailureReport(raw.toString());
460
+ let value;
461
+ let errors = [];
462
+ if (raw.length > QA_FAILURE_REPORT_MAX_BYTES)
463
+ errors = [`serialized report exceeds ${QA_FAILURE_REPORT_MAX_BYTES} bytes`];
464
+ else {
465
+ try {
466
+ value = parseStrictJson(raw.toString());
467
+ }
468
+ catch (error) {
469
+ errors = [`invalid JSON: ${String(error)}`];
470
+ }
471
+ if (!errors.length)
472
+ errors = validateQaFailureReport(value).errors;
473
+ }
474
+ const parsed = { report: errors.length ? undefined : value,
475
+ validation: { valid: errors.length === 0, errors } };
459
476
  let next = appendQaRecoveryResource(packet, `validation/manual-repair-r${packet.manifest.revision + 1}.json`, parsed.validation, { purpose: "Validation result for the latest operator-edited report.json" });
460
477
  const editable = writeEditableReport(packet.directory, raw);
461
478
  next = revise(next, { resources: next.manifest.resources.filter((resource) => resource.path !== "report.json").concat(editable).sort(resourceOrder) });
@@ -499,8 +516,8 @@ export function renderQaRecoveryAcknowledgementInstruction(packet, relativePath)
499
516
  `Packet digest: ${packet.manifest.packetDigest}`,
500
517
  `Reviewed-state digest: ${packet.manifest.reviewedStateDigest}`,
501
518
  "Required resources:", required,
502
- "Reply with exactly the following acknowledgement line, followed by the required RAFI_CONTINUITY_DELTA record:",
503
- `RAFI_QA_RECOVERY_ACK packet="${packet.manifest.packetDigest}" reviewed_state="${packet.manifest.reviewedStateDigest}" required_resources_read="all"`,
519
+ "Return the following payload in the native qa-recovery-acceptance response, with bounded continuity in the same object:",
520
+ JSON.stringify({ packet: packet.manifest.packetDigest, reviewed_state: packet.manifest.reviewedStateDigest, required_resources_read: "all" }),
504
521
  ].join("\n");
505
522
  }
506
523
  export function validateQaRecoveryAcknowledgement(text, packet, options = {}) {