human-to-code 0.1.13 → 0.1.14

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 (117) hide show
  1. package/Readme.md +4 -39
  2. package/dist/agents/direct/application.d.ts +7 -0
  3. package/dist/agents/direct/application.d.ts.map +1 -0
  4. package/dist/agents/direct/application.js +30 -0
  5. package/dist/agents/direct/application.js.map +1 -0
  6. package/dist/agents/direct/discovery.d.ts +4 -0
  7. package/dist/agents/direct/discovery.d.ts.map +1 -0
  8. package/dist/agents/direct/discovery.js +101 -0
  9. package/dist/agents/direct/discovery.js.map +1 -0
  10. package/dist/agents/direct/file-memory.d.ts +20 -0
  11. package/dist/agents/direct/file-memory.d.ts.map +1 -0
  12. package/dist/agents/direct/file-memory.js +174 -0
  13. package/dist/agents/direct/file-memory.js.map +1 -0
  14. package/dist/agents/direct/generation-client.d.ts +4 -0
  15. package/dist/agents/direct/generation-client.d.ts.map +1 -0
  16. package/dist/agents/direct/generation-client.js +56 -0
  17. package/dist/agents/direct/generation-client.js.map +1 -0
  18. package/dist/agents/direct/index.d.ts +10 -0
  19. package/dist/agents/direct/index.d.ts.map +1 -0
  20. package/dist/agents/direct/index.js +10 -0
  21. package/dist/agents/direct/index.js.map +1 -0
  22. package/dist/agents/direct/languages.d.ts +5 -0
  23. package/dist/agents/direct/languages.d.ts.map +1 -0
  24. package/dist/agents/direct/languages.js +17 -0
  25. package/dist/agents/direct/languages.js.map +1 -0
  26. package/dist/agents/direct/marker-parser.d.ts +8 -0
  27. package/dist/agents/direct/marker-parser.d.ts.map +1 -0
  28. package/dist/agents/direct/marker-parser.js +68 -0
  29. package/dist/agents/direct/marker-parser.js.map +1 -0
  30. package/dist/agents/direct/presentation.d.ts +4 -0
  31. package/dist/agents/direct/presentation.d.ts.map +1 -0
  32. package/dist/agents/direct/presentation.js +28 -0
  33. package/dist/agents/direct/presentation.js.map +1 -0
  34. package/dist/agents/direct/types.d.ts +74 -0
  35. package/dist/agents/direct/types.d.ts.map +1 -0
  36. package/dist/agents/direct/types.js +2 -0
  37. package/dist/agents/direct/types.js.map +1 -0
  38. package/dist/agents/guided/api-grounding.d.ts +6 -0
  39. package/dist/agents/guided/api-grounding.d.ts.map +1 -0
  40. package/dist/agents/guided/api-grounding.js +158 -0
  41. package/dist/agents/guided/api-grounding.js.map +1 -0
  42. package/dist/agents/guided/index.d.ts +7 -0
  43. package/dist/agents/guided/index.d.ts.map +1 -0
  44. package/dist/agents/guided/index.js +7 -0
  45. package/dist/agents/guided/index.js.map +1 -0
  46. package/dist/agents/guided/patch-diff.d.ts +4 -0
  47. package/dist/agents/guided/patch-diff.d.ts.map +1 -0
  48. package/dist/agents/guided/patch-diff.js +31 -0
  49. package/dist/agents/guided/patch-diff.js.map +1 -0
  50. package/dist/agents/guided/types.d.ts +55 -0
  51. package/dist/agents/guided/types.d.ts.map +1 -0
  52. package/dist/agents/guided/types.js +2 -0
  53. package/dist/agents/guided/types.js.map +1 -0
  54. package/dist/agents/guided/workflow.d.ts +17 -0
  55. package/dist/agents/guided/workflow.d.ts.map +1 -0
  56. package/dist/agents/guided/workflow.js +1360 -0
  57. package/dist/agents/guided/workflow.js.map +1 -0
  58. package/dist/agents/guided/workspace-policy.d.ts +9 -0
  59. package/dist/agents/guided/workspace-policy.d.ts.map +1 -0
  60. package/dist/agents/guided/workspace-policy.js +83 -0
  61. package/dist/agents/guided/workspace-policy.js.map +1 -0
  62. package/dist/cli.d.ts.map +1 -1
  63. package/dist/cli.js +4 -55
  64. package/dist/cli.js.map +1 -1
  65. package/dist/index.d.ts +3 -1
  66. package/dist/index.d.ts.map +1 -1
  67. package/dist/index.js +4 -1
  68. package/dist/index.js.map +1 -1
  69. package/dist/pipeline/simple.d.ts +3 -160
  70. package/dist/pipeline/simple.d.ts.map +1 -1
  71. package/dist/pipeline/simple.js +3 -502
  72. package/dist/pipeline/simple.js.map +1 -1
  73. package/dist/pipeline/workflow.d.ts +5 -66
  74. package/dist/pipeline/workflow.d.ts.map +1 -1
  75. package/dist/pipeline/workflow.js +5 -1642
  76. package/dist/pipeline/workflow.js.map +1 -1
  77. package/dist/prompts/direct-conversion.d.ts +13 -0
  78. package/dist/prompts/direct-conversion.d.ts.map +1 -0
  79. package/dist/prompts/direct-conversion.js +35 -0
  80. package/dist/prompts/direct-conversion.js.map +1 -0
  81. package/dist/prompts/guided-patch.d.ts +14 -0
  82. package/dist/prompts/guided-patch.d.ts.map +1 -0
  83. package/dist/prompts/guided-patch.js +41 -0
  84. package/dist/prompts/guided-patch.js.map +1 -0
  85. package/dist/prompts/guided-repair.d.ts +12 -0
  86. package/dist/prompts/guided-repair.d.ts.map +1 -0
  87. package/dist/prompts/guided-repair.js +30 -0
  88. package/dist/prompts/guided-repair.js.map +1 -0
  89. package/dist/prompts/index.d.ts +6 -0
  90. package/dist/prompts/index.d.ts.map +1 -0
  91. package/dist/prompts/index.js +6 -0
  92. package/dist/prompts/index.js.map +1 -0
  93. package/dist/prompts/provider-output.d.ts +4 -0
  94. package/dist/prompts/provider-output.d.ts.map +1 -0
  95. package/dist/prompts/provider-output.js +12 -0
  96. package/dist/prompts/provider-output.js.map +1 -0
  97. package/dist/providers/providers.d.ts.map +1 -1
  98. package/dist/providers/providers.js +2 -10
  99. package/dist/providers/providers.js.map +1 -1
  100. package/docs/ARCHITECTURE.md +30 -29
  101. package/docs/MODULES.md +46 -10
  102. package/docs/SCALABILITY.md +10 -9
  103. package/docs/roadmap/README.md +1 -1
  104. package/docs/roadmap/ruby.md +1 -1
  105. package/package.json +1 -6
  106. package/dist/pipeline/agent-tools.d.ts +0 -348
  107. package/dist/pipeline/agent-tools.d.ts.map +0 -1
  108. package/dist/pipeline/agent-tools.js +0 -542
  109. package/dist/pipeline/agent-tools.js.map +0 -1
  110. package/dist/pipeline/agent-tui.d.ts +0 -15
  111. package/dist/pipeline/agent-tui.d.ts.map +0 -1
  112. package/dist/pipeline/agent-tui.js +0 -133
  113. package/dist/pipeline/agent-tui.js.map +0 -1
  114. package/dist/pipeline/deep-agent.d.ts +0 -61
  115. package/dist/pipeline/deep-agent.d.ts.map +0 -1
  116. package/dist/pipeline/deep-agent.js +0 -395
  117. package/dist/pipeline/deep-agent.js.map +0 -1
@@ -0,0 +1,1360 @@
1
+ /** Auditable generation, isolated validation, and explicit application workflow. */
2
+ import { spawn } from "node:child_process";
3
+ import { randomUUID } from "node:crypto";
4
+ import { chmod, lstat } from "node:fs/promises";
5
+ import { relative, resolve, sep } from "node:path";
6
+ import { analyzeProject, SUPPORT_MATRIX_VERSION } from "../../analysis/analyzer.js";
7
+ import { evaluateProviderCertification, providerProfileId } from "../../providers/certification.js";
8
+ import { CompilerToolExecutor } from "../../context/compiler-tools.js";
9
+ import { validateConfig } from "../../config/config.js";
10
+ import { canonicalJson, hashCanonical, sha256Text, validateChangeContractV1, validatePatchSetV1, validateValidationPlanV1, validateValidationReportV1, } from "../../core/contracts.js";
11
+ import { hashContextManifest, selectContext, validateContextManifestV1, } from "../../context/context.js";
12
+ import { applyPatchAtomic, normalizePatchPath, PatchSafetyError, preparePatch } from "../../pipeline/patch.js";
13
+ import { DocumentationError, OfficialDocumentationClient } from "../../context/documentation.js";
14
+ import { COMPILER_CONTEXT_TOOLS, ProviderBudgetTracker, ProviderError, generateValidated, withProviderRetries, } from "../../providers/provider.js";
15
+ import { RunStore } from "../../pipeline/run-store.js";
16
+ import { PATCH_SET_SCHEMA_V1 } from "../../providers/schemas.js";
17
+ import { ProjectSecretScanError, scanProjectForSecrets } from "../../security/secret-scan.js";
18
+ import { buildGuidedPatchPrompt } from "../../prompts/guided-patch.js";
19
+ import { buildGuidedRepairPrompt } from "../../prompts/guided-repair.js";
20
+ import { cloneWorkspaceSnapshot, createWorkspaceSnapshot, disposeWorkspaceSnapshot } from "../../pipeline/snapshot.js";
21
+ import { strongSandboxAvailable, validateBaselineAndCandidate } from "../../pipeline/validation.js";
22
+ import { createValidationPlan, resolveWorkspaceConfig, targetWorkspaces, unique, } from "./workspace-policy.js";
23
+ import { renderPatchDiff } from "./patch-diff.js";
24
+ import { assertExternalApisGrounded } from "./api-grounding.js";
25
+ export { createValidationPlan, resolveWorkspaceConfig } from "./workspace-policy.js";
26
+ export { renderPatchDiff } from "./patch-diff.js";
27
+ function now() {
28
+ return new Date().toISOString();
29
+ }
30
+ function initialContextCandidates(contract, workspaces, config) {
31
+ const candidates = [{
32
+ origin: "project",
33
+ path: contract.source.path,
34
+ reason: "Reviewed natural-language change source bound by the contract hash.",
35
+ required: true,
36
+ priority: 1000,
37
+ }];
38
+ for (const workspace of workspaces) {
39
+ for (const path of workspace.manifests) {
40
+ candidates.push({ origin: "project", path, reason: `Manifest for target workspace ${workspace.id}.`, required: true, priority: 900 });
41
+ }
42
+ for (const path of [...workspace.entryPoints, ...workspace.routes]) {
43
+ candidates.push({ origin: "project", path, reason: `Target entry point or route in ${workspace.id}.`, priority: 700 });
44
+ }
45
+ for (const evidence of workspace.evidence) {
46
+ if (evidence.kind === "source" || evidence.kind === "config" || evidence.kind === "lockfile") {
47
+ candidates.push({
48
+ origin: evidence.kind === "lockfile" ? "dependency" : "project",
49
+ path: evidence.path,
50
+ reason: `${workspace.id} analysis evidence: ${evidence.detail}`,
51
+ priority: evidence.kind === "source" ? 600 : 500,
52
+ ...(evidence.kind === "lockfile" && workspace.lockfiles.includes(evidence.path) ? { version: workspace.framework.resolvedVersion } : {}),
53
+ });
54
+ }
55
+ }
56
+ }
57
+ for (const path of config.documentation.privatePaths) {
58
+ candidates.push({ origin: "private_documentation", path, reason: "Operator-configured private project documentation.", priority: 400 });
59
+ }
60
+ return candidates.filter((candidate) => {
61
+ if (candidate.origin === "official_documentation")
62
+ return true;
63
+ return !config.privacy.excludedPaths.some((excluded) => candidate.path === excluded || candidate.path.startsWith(`${excluded}/`));
64
+ });
65
+ }
66
+ function officialDocumentationHosts(config) {
67
+ return unique([
68
+ "react.dev", "nextjs.org", "vite.dev", "docs.nestjs.com", "fastapi.tiangolo.com",
69
+ "docs.pydantic.dev", "docs.python.org", "doc.rust-lang.org", "docs.rs",
70
+ ...config.documentation.officialDomains,
71
+ ]);
72
+ }
73
+ function dependencyIdentity(value) {
74
+ return value.toLowerCase().replaceAll("_", "-");
75
+ }
76
+ function officialDocumentationUrl(config, workspace, dependency, version) {
77
+ const configured = config.documentation.officialSources.find((source) => source.ecosystem === workspace.ecosystem
78
+ && dependencyIdentity(source.dependency) === dependencyIdentity(dependency)
79
+ && source.version === version);
80
+ if (configured)
81
+ return configured.url;
82
+ if (workspace.ecosystem !== "rust")
83
+ return undefined;
84
+ const crate = dependency.replaceAll("_", "-");
85
+ return `https://docs.rs/${encodeURIComponent(crate)}/${encodeURIComponent(version)}/${encodeURIComponent(crate.replaceAll("-", "_"))}/`;
86
+ }
87
+ function contextBudget(config) {
88
+ const maxBytes = Math.max(1024, Math.min(8 * 1024 * 1024, config.privacy.maxContextTokens * 4));
89
+ return {
90
+ maxBytes,
91
+ maxBytesPerItem: Math.min(config.privacy.maxFileBytes, maxBytes),
92
+ maxEstimatedTokens: config.privacy.maxContextTokens,
93
+ };
94
+ }
95
+ function permittedContextCandidate(candidate, config) {
96
+ if (candidate.origin === "official_documentation")
97
+ return true;
98
+ return !config.privacy.excludedPaths.some((excluded) => candidate.path === excluded || candidate.path.startsWith(`${excluded}/`));
99
+ }
100
+ function manifestCandidates(manifest) {
101
+ return manifest.evidence.map((item) => item.origin === "official_documentation"
102
+ ? {
103
+ origin: "official_documentation",
104
+ url: item.url,
105
+ version: item.version,
106
+ content: item.content,
107
+ contentSha256: item.sha256,
108
+ reason: item.reason,
109
+ cached: item.cached,
110
+ range: { startLine: item.startLine, endLine: item.endLine },
111
+ required: true,
112
+ }
113
+ : {
114
+ origin: item.origin,
115
+ path: item.path,
116
+ reason: item.reason,
117
+ range: { startLine: item.startLine, endLine: item.endLine },
118
+ required: true,
119
+ ...(item.version === undefined ? {} : { version: item.version }),
120
+ });
121
+ }
122
+ async function precomputeContractContext(root, profile, contract, config, offline, initial) {
123
+ const documentation = new OfficialDocumentationClient({
124
+ allowedHosts: officialDocumentationHosts(config),
125
+ maxBytes: Math.min(2 * 1024 * 1024, config.privacy.maxFileBytes * 4),
126
+ });
127
+ const executor = new CompilerToolExecutor(root, profile, {
128
+ maximumRequests: 8,
129
+ officialDocumentation: async ({ workspace, dependency, version, reason }) => {
130
+ const url = officialDocumentationUrl(config, workspace, dependency, version);
131
+ if (!url)
132
+ return undefined;
133
+ return documentation.retrieve({
134
+ url,
135
+ version,
136
+ reason: `Version-matched official API evidence for ${dependency} ${version}: ${reason}`,
137
+ offline,
138
+ });
139
+ },
140
+ });
141
+ const candidates = [...initial];
142
+ let requestNumber = 0;
143
+ const workspaces = targetWorkspaces(profile, contract);
144
+ const symbols = unique(contract.targetSymbols)
145
+ .filter((symbol) => symbol.length >= 2 && symbol.length <= 256 && !/[\r\n\0]/u.test(symbol))
146
+ .slice(0, 4);
147
+ for (const workspace of workspaces) {
148
+ for (const symbol of symbols) {
149
+ if (requestNumber >= 4)
150
+ break;
151
+ requestNumber += 1;
152
+ const found = await executor.execute({
153
+ schemaVersion: 1,
154
+ requestId: `host-symbol-${requestNumber}`,
155
+ kind: "symbol",
156
+ workspace: workspace.id,
157
+ query: symbol,
158
+ reason: "Deterministic pre-provider lookup for a reviewed target symbol.",
159
+ maxItems: 3,
160
+ path: null,
161
+ }).catch(() => []);
162
+ candidates.push(...found.map((item) => ({ ...item, priority: 800 })));
163
+ }
164
+ }
165
+ const firstManifest = await selectContext({
166
+ root,
167
+ projectFingerprint: profile.fingerprint,
168
+ candidates: candidates.filter((candidate) => permittedContextCandidate(candidate, config)),
169
+ offline,
170
+ secretPolicy: "block",
171
+ budget: contextBudget(config),
172
+ officialDocumentationHosts: officialDocumentationHosts(config),
173
+ });
174
+ const manifestPaths = new Set(workspaces.flatMap((workspace) => [...workspace.manifests, ...workspace.lockfiles]));
175
+ const corpus = `${canonicalJson(contract)}\n${firstManifest.evidence
176
+ .filter((item) => (item.origin === "project" || item.origin === "private_documentation")
177
+ && !("path" in item && manifestPaths.has(item.path)))
178
+ .map((item) => item.content)
179
+ .join("\n")}`.toLowerCase();
180
+ const coreNames = new Set(workspaces.flatMap((workspace) => {
181
+ if (workspace.ecosystem === "react")
182
+ return ["react", "react-dom", "next", "vite"];
183
+ if (workspace.ecosystem === "nestjs")
184
+ return ["@nestjs/core", "@nestjs/common", "@nestjs/platform-express", "@nestjs/platform-fastify"];
185
+ if (workspace.ecosystem === "fastapi")
186
+ return ["fastapi", "pydantic"];
187
+ return [];
188
+ }));
189
+ const mentioned = workspaces.flatMap((workspace) => workspace.framework.dependencies
190
+ .map((dependency) => {
191
+ const names = unique([
192
+ dependency.name.toLowerCase(),
193
+ dependency.name.toLowerCase().replaceAll("-", "_"),
194
+ dependency.name.toLowerCase().replace(/^@[^/]+\//u, ""),
195
+ ]);
196
+ const explicit = names.some((name) => name.length >= 2 && corpus.includes(name));
197
+ const core = coreNames.has(dependency.name.toLowerCase());
198
+ return { workspace, dependency, score: explicit ? 2 : core ? 1 : 0 };
199
+ })
200
+ .filter((item) => item.score > 0))
201
+ .sort((left, right) => right.score - left.score
202
+ || left.dependency.name.localeCompare(right.dependency.name)
203
+ || left.workspace.id.localeCompare(right.workspace.id));
204
+ for (const { workspace, dependency } of mentioned) {
205
+ if (requestNumber >= 8)
206
+ break;
207
+ requestNumber += 1;
208
+ const found = await executor.execute({
209
+ schemaVersion: 1,
210
+ requestId: `host-dependency-${requestNumber}`,
211
+ kind: "dependency-doc",
212
+ workspace: workspace.id,
213
+ query: dependency.name,
214
+ reason: "Installed API evidence referenced by the reviewed contract or selected project code.",
215
+ maxItems: 4,
216
+ path: null,
217
+ });
218
+ candidates.push(...found.map((item) => ({ ...item, priority: 650 })));
219
+ }
220
+ return candidates.filter((candidate) => permittedContextCandidate(candidate, config));
221
+ }
222
+ export async function buildContextPreview(rootInput, profile, contract, config, offline = config.documentation.mode === "offline") {
223
+ const root = resolve(rootInput);
224
+ await scanProjectForSecrets(root);
225
+ config = resolveWorkspaceConfig(config, profile, contract);
226
+ const workspaces = targetWorkspaces(profile, contract);
227
+ const candidates = await precomputeContractContext(root, profile, contract, config, offline, initialContextCandidates(contract, workspaces, config));
228
+ return selectContext({
229
+ root,
230
+ projectFingerprint: profile.fingerprint,
231
+ candidates,
232
+ offline,
233
+ secretPolicy: "block",
234
+ budget: contextBudget(config),
235
+ officialDocumentationHosts: officialDocumentationHosts(config),
236
+ });
237
+ }
238
+ function toolRequestFromOutput(output) {
239
+ if (typeof output !== "object" || output === null || Array.isArray(output))
240
+ throw new ProviderError("schema", "Provider tool output was not an object.");
241
+ const calls = output.toolCalls;
242
+ if (!Array.isArray(calls) || calls.length !== 1)
243
+ throw new ProviderError("schema", "Exactly one context tool call is allowed per model request.");
244
+ const call = calls[0];
245
+ if (typeof call !== "object" || call === null || Array.isArray(call))
246
+ throw new ProviderError("schema", "Context tool call was invalid.");
247
+ const record = call;
248
+ if (record.name !== "request_context")
249
+ throw new ProviderError("safety", "Provider requested an unauthorized tool.");
250
+ return record.arguments;
251
+ }
252
+ function providerBudget(config, maxElapsedMs = config.budgets.timeoutMs) {
253
+ return new ProviderBudgetTracker({
254
+ maxInputTokens: config.budgets.maxInputTokens,
255
+ maxOutputTokens: config.budgets.maxOutputTokens,
256
+ maxRequests: config.budgets.maxRequests,
257
+ maxRepairs: config.budgets.maxRepairs,
258
+ maxCostUsd: config.budgets.maxCostUsd,
259
+ maxElapsedMs,
260
+ });
261
+ }
262
+ function usageSummary(budget) {
263
+ const usage = budget.usage;
264
+ return {
265
+ inputTokens: usage.inputTokens,
266
+ outputTokens: usage.outputTokens,
267
+ totalTokens: usage.inputTokens + usage.outputTokens,
268
+ requests: usage.requests,
269
+ repairs: usage.repairs,
270
+ costUsd: usage.costUsd,
271
+ };
272
+ }
273
+ function certificationFor(profile, contract, profileId) {
274
+ // Certification is derived only from re-scored, host-owned benchmark evidence
275
+ // for this exact provider/model profile. The shipped registry is empty, so a
276
+ // normal preview run resolves to certified: false and cannot reach VERIFIED.
277
+ const targets = targetWorkspaces(profile, contract);
278
+ const certifiedKeys = new Set(evaluateProviderCertification(profileId).certifiedMatrixKeys);
279
+ const providerCertified = certifiedKeys.size > 0;
280
+ const uncertified = targets.filter((workspace) => !certifiedKeys.has(workspace.support.matrixKey));
281
+ const profileCertified = targets.length > 0 && uncertified.length === 0;
282
+ const provider = {
283
+ matrixKey: profileId,
284
+ certified: providerCertified,
285
+ reason: providerCertified
286
+ ? `Provider profile ${profileId} has passing certification benchmark evidence.`
287
+ : `No certified benchmark evidence exists for provider profile ${profileId}.`,
288
+ };
289
+ const reasons = [
290
+ ...(profileCertified ? [] : [
291
+ uncertified.length === targets.length
292
+ ? "No target ecosystem has passed the certification benchmark for this provider/model."
293
+ : `These target ecosystems lack certification evidence for this provider/model: ${uncertified.map((workspace) => workspace.support.matrixKey).join(", ")}.`,
294
+ ]),
295
+ ...(provider.certified ? [] : [provider.reason]),
296
+ ];
297
+ return {
298
+ schemaVersion: 1,
299
+ supportMatrixVersion: SUPPORT_MATRIX_VERSION,
300
+ provider,
301
+ profileCertified,
302
+ certified: profileCertified && provider.certified,
303
+ reasons,
304
+ };
305
+ }
306
+ function runRecord(root, runId) {
307
+ const timestamp = now();
308
+ return {
309
+ runId,
310
+ schemaVersion: 1,
311
+ createdAt: timestamp,
312
+ updatedAt: timestamp,
313
+ root,
314
+ status: "INCONCLUSIVE",
315
+ diagnostics: ["Run created; no success status is implied until isolated validation completes."],
316
+ };
317
+ }
318
+ async function persistOutcome(store, runId, status, diagnostics, fields = {}) {
319
+ return store.update(runId, (current) => ({
320
+ ...current,
321
+ ...fields,
322
+ runId: current.runId,
323
+ schemaVersion: 1,
324
+ updatedAt: now(),
325
+ status,
326
+ diagnostics,
327
+ }));
328
+ }
329
+ export async function generateRun(input) {
330
+ const options = { ...input, config: resolveWorkspaceConfig(input.config, input.profile, input.contract) };
331
+ const root = resolve(options.root);
332
+ const store = options.store ?? new RunStore();
333
+ const runId = randomUUID();
334
+ await store.create(runRecord(root, runId));
335
+ const diagnostics = [];
336
+ let baseline;
337
+ try {
338
+ const contract = validateChangeContractV1(options.contract);
339
+ if (options.profile.status !== "SUPPORTED")
340
+ throw new Error(`Project profile status is ${options.profile.status}.`);
341
+ if (contract.projectFingerprint !== options.profile.fingerprint)
342
+ throw new Error("Contract profile fingerprint is stale.");
343
+ const workspaces = targetWorkspaces(options.profile, contract);
344
+ // The strict pipeline refuses to generate what it cannot validate. The
345
+ // ungrounded `general` fallback is the sole, explicitly-degraded exception:
346
+ // it has no toolchain to validate against, produces a reviewable patch, and
347
+ // is pinned to INCONCLUSIVE for the life of the run (never VERIFIED/applied).
348
+ const generalRun = workspaces.length > 0 && workspaces.every((workspace) => workspace.ecosystem === "general");
349
+ const validationPlan = generalRun ? undefined : createValidationPlan(options.profile, contract);
350
+ if (!generalRun && (validationPlan === undefined || validationPlan.commands.length === 0)) {
351
+ throw new Error("No required project validation command could be selected before generation.");
352
+ }
353
+ baseline = await createWorkspaceSnapshot(root, { excludeNames: [".venv", "venv", "target", ".next", "coverage"] });
354
+ let manifest = await buildContextPreview(root, options.profile, contract, options.config, options.offline ?? options.config.documentation.mode === "offline");
355
+ // Keep every item in the reviewed preview when local-only context tools add
356
+ // further evidence. Re-selection must never silently drop the preview set.
357
+ const candidates = manifestCandidates(manifest);
358
+ await store.writeArtifact(runId, "profile.json", options.profile);
359
+ await store.writeArtifact(runId, "run-config.json", options.config);
360
+ await store.writeArtifact(runId, "contract.json", contract);
361
+ if (validationPlan)
362
+ await store.writeArtifact(runId, "validation-plan.json", validationPlan);
363
+ await store.writeArtifact(runId, "context.json", manifest);
364
+ if (options.provider.capabilities.remote && !options.config.privacy.remoteProviderConsent) {
365
+ diagnostics.push("Outbound context preview was created, but remote provider consent is false. Review context.json and explicitly enable consent.");
366
+ await persistOutcome(store, runId, "NEEDS_INPUT", diagnostics, {
367
+ contractHash: hashCanonical(contract),
368
+ contextManifestHash: hashContextManifest(manifest),
369
+ });
370
+ return { runId, status: "NEEDS_INPUT", diagnostics };
371
+ }
372
+ const budget = providerBudget(options.config);
373
+ const documentation = new OfficialDocumentationClient({
374
+ allowedHosts: officialDocumentationHosts(options.config),
375
+ maxBytes: Math.min(2 * 1024 * 1024, options.config.privacy.maxFileBytes * 4),
376
+ });
377
+ const executor = new CompilerToolExecutor(root, options.profile, {
378
+ maximumRequests: 8,
379
+ officialDocumentation: async ({ workspace, dependency, version, reason }) => {
380
+ const url = officialDocumentationUrl(options.config, workspace, dependency, version);
381
+ if (!url)
382
+ return undefined;
383
+ return documentation.retrieve({
384
+ url,
385
+ version,
386
+ reason: `Version-matched official API evidence for ${dependency} ${version}: ${reason}`,
387
+ offline: options.offline ?? options.config.documentation.mode === "offline",
388
+ });
389
+ },
390
+ });
391
+ const requestIds = [];
392
+ let result;
393
+ let patch;
394
+ for (;;) {
395
+ const request = {
396
+ operation: "patch",
397
+ model: options.config.provider.model,
398
+ messages: buildGuidedPatchPrompt({
399
+ profile: options.profile,
400
+ contract,
401
+ manifest,
402
+ snapshotHash: baseline.snapshotHash,
403
+ workspaces,
404
+ }),
405
+ responseSchema: PATCH_SET_SCHEMA_V1,
406
+ ...(options.provider.capabilities.toolCalling && !options.provider.capabilities.remote ? { tools: [...COMPILER_CONTEXT_TOOLS] } : {}),
407
+ timeoutMs: Math.min(options.config.budgets.timeoutMs, 60 * 60_000),
408
+ maxOutputTokens: Math.min(options.config.budgets.maxOutputTokens, 100_000),
409
+ temperature: 0,
410
+ signal: options.signal,
411
+ };
412
+ const generated = await withProviderRetries(() => generateValidated(options.provider, request, (value) => value, { budget }), { maxRetries: 2, maxElapsedMs: options.config.budgets.timeoutMs, signal: options.signal });
413
+ result = generated.result;
414
+ requestIds.push(result.requestId);
415
+ if (result.finishReason !== "tool_call") {
416
+ patch = validatePatchSetV1(generated.value, contract);
417
+ break;
418
+ }
419
+ const more = await executor.execute(toolRequestFromOutput(generated.value));
420
+ if (more.length === 0)
421
+ throw new ProviderError("schema", "Requested context could not be grounded by project evidence.");
422
+ candidates.push(...more);
423
+ manifest = await selectContext({
424
+ root,
425
+ projectFingerprint: options.profile.fingerprint,
426
+ candidates,
427
+ offline: options.offline ?? options.config.documentation.mode === "offline",
428
+ secretPolicy: "block",
429
+ budget: contextBudget(options.config),
430
+ officialDocumentationHosts: officialDocumentationHosts(options.config),
431
+ });
432
+ await store.writeArtifact(runId, "context.json", manifest);
433
+ }
434
+ if (!patch || !result)
435
+ throw new Error("Provider generation completed without a patch artifact.");
436
+ if (patch.snapshotHash !== baseline.snapshotHash)
437
+ throw new ProviderError("schema", "Patch snapshot hash does not match the immutable generation snapshot.");
438
+ assertExternalApisGrounded(patch, workspaces, manifest);
439
+ const policy = {
440
+ allowedPaths: contract.scope.allowedPaths,
441
+ protectedPaths: contract.scope.prohibitedPaths,
442
+ expectedSnapshotHash: baseline.snapshotHash,
443
+ allowDeletes: contract.scope.allowedOperations.includes("delete"),
444
+ allowRenames: contract.scope.allowedOperations.includes("rename"),
445
+ };
446
+ await preparePatch(baseline.root, patch, policy);
447
+ const diff = renderPatchDiff(patch);
448
+ const providerIdentity = {
449
+ name: options.provider.name,
450
+ requestedModel: options.config.provider.model,
451
+ resolvedModel: result.resolvedModelId,
452
+ requestIds,
453
+ };
454
+ // Certification is resolved exclusively from shipped, immutable evidence
455
+ // bound to this exact provider/model profile. Programmatic callers cannot
456
+ // self-attest a provider/model as certified.
457
+ const certification = certificationFor(options.profile, contract, providerProfileId(providerIdentity.name, providerIdentity.resolvedModel));
458
+ await store.writeArtifact(runId, "context.json", manifest);
459
+ await store.writeArtifact(runId, "patch.json", patch);
460
+ await store.writeArtifact(runId, "diff.json", { diff });
461
+ await store.writeArtifact(runId, "certification.json", certification);
462
+ diagnostics.push("A reviewable patch was generated and safety-checked; isolated validation is still required.");
463
+ if (!certification.certified)
464
+ diagnostics.push(...certification.reasons);
465
+ await persistOutcome(store, runId, "INCONCLUSIVE", diagnostics, {
466
+ contractHash: hashCanonical(contract),
467
+ contextManifestHash: hashContextManifest(manifest),
468
+ patchHash: hashCanonical(patch),
469
+ provider: providerIdentity,
470
+ usage: usageSummary(budget),
471
+ });
472
+ return { runId, status: "INCONCLUSIVE", diagnostics, diff, usage: usageSummary(budget) };
473
+ }
474
+ catch (error) {
475
+ const security = error instanceof ProjectSecretScanError && error.code === "SECRET_DETECTED"
476
+ || error instanceof ProviderError && error.code === "safety"
477
+ || error instanceof Error && /secret|credential|path escape|symlink|hardlink/iu.test(error.message);
478
+ const contextInsufficient = error instanceof DocumentationError
479
+ || error instanceof ProviderError && error.code === "schema" && /context|ground/iu.test(error.message);
480
+ const status = security ? "SECURITY_BLOCKED"
481
+ : contextInsufficient ? "INCONCLUSIVE"
482
+ : error instanceof ProviderError && ["configuration", "authentication"].includes(error.code) ? "NEEDS_INPUT" : "FAILED";
483
+ diagnostics.push(error instanceof Error ? error.message : String(error));
484
+ await persistOutcome(store, runId, status, diagnostics).catch(() => undefined);
485
+ const exitCode = security ? 4
486
+ : error instanceof ProjectSecretScanError && error.code === "PARTIAL_SCAN" ? 6
487
+ : error instanceof DocumentationError || error instanceof ProviderError ? 5
488
+ : status === "INCONCLUSIVE" || status === "NEEDS_INPUT" ? 3 : 2;
489
+ return { runId, status, diagnostics, exitCode };
490
+ }
491
+ finally {
492
+ if (baseline)
493
+ await disposeWorkspaceSnapshot(baseline).catch(() => undefined);
494
+ }
495
+ }
496
+ function sandboxImage(profile, override) {
497
+ if (override)
498
+ return override;
499
+ const ecosystems = new Set(profile.workspaces.map((workspace) => workspace.ecosystem));
500
+ if ([...ecosystems].some((name) => name === "rust"))
501
+ return ecosystems.size === 1 ? "rust:1.85-bookworm" : undefined;
502
+ if ([...ecosystems].some((name) => name === "fastapi"))
503
+ return ecosystems.size === 1 ? "python:3.13-bookworm" : undefined;
504
+ return "node:24-bookworm";
505
+ }
506
+ async function sandboxBinary(config, override) {
507
+ if (override)
508
+ return override;
509
+ if (config.sandbox.engine === "docker")
510
+ return "docker";
511
+ if (config.sandbox.engine === "podman")
512
+ return "podman";
513
+ if (await strongSandboxAvailable("docker"))
514
+ return "docker";
515
+ if (await strongSandboxAvailable("podman"))
516
+ return "podman";
517
+ return "docker";
518
+ }
519
+ const REPAIR_FROZEN_PATH = /(?:^|\/)(?:package\.json|Cargo\.toml|Cargo\.lock|pyproject\.toml|setup\.(?:py|cfg)|environment\.ya?ml|requirements(?:-[^/]*)?\.(?:txt|in)|poetry\.lock|uv\.lock|Pipfile(?:\.lock)?|package-lock\.json|pnpm-lock\.yaml|yarn\.lock|bun\.lockb?|nest-cli\.json|nx\.json|turbo\.json|angular\.json|tsconfig(?:\.[^/]*)?\.json|[^/]+\.config\.[^/]+|\.github\/workflows|\.cargo\/config(?:\.toml)?|__tests__|tests?|specs?|e2e|integration|fixtures?|snapshots?|migrations?|generated)(?:\/|$)|\.(?:test|spec)\.[^/]+$|\.snap$/iu;
520
+ const INLINE_TEST_CONTENT = /#\s*\[\s*cfg\s*\(\s*test\s*\)\s*\]|\b(?:describe|it|test)\s*\(|@pytest\b|\bunittest\.|\bTestCase\b|\bassert(?:_eq|_ne)?!\s*\(/iu;
521
+ function patchOperationIdentity(operation) {
522
+ return operation.kind === "rename"
523
+ ? `rename:${operation.from}->${operation.path}`
524
+ : `${operation.kind}:${operation.path}`;
525
+ }
526
+ function repairFrozenOperation(operation) {
527
+ const content = operation.kind === "create"
528
+ ? operation.content
529
+ : operation.kind === "edit"
530
+ ? `${operation.oldText}\n${operation.newText}`
531
+ : "";
532
+ return REPAIR_FROZEN_PATH.test(operation.path)
533
+ || operation.kind === "rename" && REPAIR_FROZEN_PATH.test(operation.from)
534
+ || INLINE_TEST_CONTENT.test(content);
535
+ }
536
+ /** A repair may correct implementation text, but may not redesign the patch. */
537
+ function assertRepairPatchConstraints(original, repaired) {
538
+ if (repaired.contractHash !== original.contractHash || repaired.snapshotHash !== original.snapshotHash) {
539
+ throw new ProviderError("safety", "Repair attempted to change immutable contract or snapshot provenance.");
540
+ }
541
+ const originalIdentities = original.operations.map(patchOperationIdentity).sort();
542
+ const repairedIdentities = repaired.operations.map(patchOperationIdentity).sort();
543
+ if (canonicalJson(originalIdentities) !== canonicalJson(repairedIdentities)) {
544
+ throw new ProviderError("safety", "Repair attempted to add, remove, rename, or change the kind of a patch path.");
545
+ }
546
+ if (canonicalJson([...original.requirementIds].sort()) !== canonicalJson([...repaired.requirementIds].sort())) {
547
+ throw new ProviderError("safety", "Repair attempted to change requirement coverage.");
548
+ }
549
+ const repairedTests = new Set(repaired.proposedTests);
550
+ if (original.proposedTests.some((test) => !repairedTests.has(test))) {
551
+ throw new ProviderError("safety", "Repair attempted to remove a proposed validation or test obligation.");
552
+ }
553
+ const originalByIdentity = new Map(original.operations.map((operation) => [patchOperationIdentity(operation), operation]));
554
+ for (const operation of repaired.operations) {
555
+ const previous = originalByIdentity.get(patchOperationIdentity(operation));
556
+ if (!repairFrozenOperation(operation) && (previous === undefined || !repairFrozenOperation(previous)))
557
+ continue;
558
+ if (previous === undefined || canonicalJson(previous) !== canonicalJson(operation)) {
559
+ throw new ProviderError("safety", "Repair attempted to modify a dependency, lockfile, test, or validation-configuration operation.");
560
+ }
561
+ }
562
+ }
563
+ function cleanValidationResult(result) {
564
+ return result.status === "passed"
565
+ && result.exitCode === 0
566
+ && !result.timedOut
567
+ && !result.flaky
568
+ && !result.outputTruncated
569
+ && !result.stderr.startsWith("SECURITY_BLOCKED:");
570
+ }
571
+ /** Repairs are allowed only for deterministic candidate regressions. */
572
+ function repairableValidationFailure(report, plan) {
573
+ if (report.status !== "failed" || report.sandbox !== "strong")
574
+ return false;
575
+ if (report.diagnostics.some((diagnostic) => diagnostic.startsWith("SECURITY_BLOCKED:")))
576
+ return false;
577
+ if (report.baseline.length !== plan.commands.length || report.candidate.length !== plan.commands.length)
578
+ return false;
579
+ if (!report.baseline.every(cleanValidationResult))
580
+ return false;
581
+ const required = new Set(plan.commands.filter((command) => command.required).map((command) => command.id));
582
+ const requiredFailures = report.candidate.filter((result) => required.has(result.id) && result.status !== "passed");
583
+ if (requiredFailures.length === 0)
584
+ return false;
585
+ if (report.candidate.some((result) => result.status === "error" || result.status === "skipped"
586
+ || result.timedOut || result.flaky || result.outputTruncated
587
+ || result.signal !== null || [126, 127, 137, 139, 143].includes(result.exitCode ?? -1)
588
+ || /command not found|executable file not found|ENOSPC|out of memory|(?:^|\s)killed(?:\s|$)|toolchain .*not installed/iu.test(`${result.stdout}\n${result.stderr}`)
589
+ || result.stderr.startsWith("SECURITY_BLOCKED:")))
590
+ return false;
591
+ return requiredFailures.every((result) => result.status === "failed" && result.exitCode !== null);
592
+ }
593
+ function repairDiagnosticPayload(report, plan) {
594
+ const required = new Set(plan.commands.filter((command) => command.required).map((command) => command.id));
595
+ const failures = report.candidate
596
+ .filter((result) => required.has(result.id) && result.status === "failed")
597
+ .map((result) => ({
598
+ id: result.id,
599
+ exitCode: result.exitCode,
600
+ signal: result.signal,
601
+ stdout: result.stdout,
602
+ stderr: result.stderr,
603
+ }));
604
+ const payload = canonicalJson({
605
+ status: report.status,
606
+ diagnostics: report.diagnostics,
607
+ failures,
608
+ });
609
+ if (Buffer.byteLength(payload, "utf8") > 1024 * 1024) {
610
+ throw new ProviderError("budget", "Validation diagnostics exceed the bounded repair context; no diagnostic was silently truncated.");
611
+ }
612
+ return payload;
613
+ }
614
+ function validateRepairCheckpoint(value, expected) {
615
+ const record = exactRecord(value, "repairCheckpoint", [
616
+ "schemaVersion", "contractHash", "contextManifestHash", "snapshotHash", "validationPlanHash",
617
+ "attempts", "provider", "usage", "updatedAt",
618
+ ]);
619
+ if (record.schemaVersion !== 1
620
+ || record.contractHash !== expected.contractHash
621
+ || record.contextManifestHash !== expected.contextManifestHash
622
+ || record.snapshotHash !== expected.snapshotHash
623
+ || record.validationPlanHash !== expected.validationPlanHash
624
+ || typeof record.updatedAt !== "string" || !Number.isFinite(Date.parse(record.updatedAt))) {
625
+ throw new ProviderError("safety", "Repair checkpoint provenance is invalid.");
626
+ }
627
+ const provider = exactRecord(record.provider, "repairCheckpoint.provider", ["name", "requestedModel", "resolvedModel", "requestIds"]);
628
+ if (typeof provider.name !== "string" || typeof provider.requestedModel !== "string" || typeof provider.resolvedModel !== "string"
629
+ || !Array.isArray(provider.requestIds) || provider.requestIds.length === 0
630
+ || provider.requestIds.some((id) => typeof id !== "string" || id.length === 0)
631
+ || new Set(provider.requestIds).size !== provider.requestIds.length
632
+ || !expected.record.provider
633
+ || provider.name !== expected.record.provider.name
634
+ || provider.requestedModel !== expected.record.provider.requestedModel
635
+ || provider.resolvedModel !== expected.record.provider.resolvedModel
636
+ || expected.record.provider.requestIds.some((id) => !provider.requestIds.includes(id))) {
637
+ throw new ProviderError("safety", "Repair checkpoint provider identity is invalid.");
638
+ }
639
+ const usage = exactRecord(record.usage, "repairCheckpoint.usage", [
640
+ "inputTokens", "outputTokens", "totalTokens", "requests", "repairs", "costUsd",
641
+ ]);
642
+ for (const key of ["inputTokens", "outputTokens", "totalTokens", "requests", "repairs"]) {
643
+ if (!Number.isSafeInteger(usage[key]) || usage[key] < 0)
644
+ throw new ProviderError("safety", "Repair checkpoint usage is invalid.");
645
+ }
646
+ if (usage.repairs > 2 || usage.totalTokens !== usage.inputTokens + usage.outputTokens
647
+ || typeof usage.costUsd !== "number" || !Number.isFinite(usage.costUsd) || usage.costUsd < 0) {
648
+ throw new ProviderError("safety", "Repair checkpoint usage totals are invalid.");
649
+ }
650
+ const checkedAttempts = validateValidationReportV1({
651
+ schemaVersion: 1,
652
+ status: "unvalidated",
653
+ sandbox: "none",
654
+ baseline: [],
655
+ candidate: [],
656
+ repairs: record.attempts,
657
+ manualChecks: [],
658
+ diagnostics: [],
659
+ startedAt: record.updatedAt,
660
+ finishedAt: record.updatedAt,
661
+ }).repairs;
662
+ if (checkedAttempts.length !== usage.repairs) {
663
+ throw new ProviderError("safety", "Repair checkpoint attempts do not match its cumulative budget usage.");
664
+ }
665
+ const prior = expected.record.usage;
666
+ if (prior && (usage.inputTokens < prior.inputTokens
667
+ || usage.outputTokens < prior.outputTokens
668
+ || usage.requests < prior.requests
669
+ || usage.repairs < (prior.repairs ?? 0)
670
+ || usage.costUsd < (prior.costUsd ?? 0))) {
671
+ throw new ProviderError("safety", "Repair checkpoint usage regresses persisted cumulative usage.");
672
+ }
673
+ return {
674
+ schemaVersion: 1,
675
+ contractHash: record.contractHash,
676
+ contextManifestHash: record.contextManifestHash,
677
+ snapshotHash: record.snapshotHash,
678
+ validationPlanHash: record.validationPlanHash,
679
+ attempts: checkedAttempts.map((attempt) => ({ ...attempt, diagnostics: [...attempt.diagnostics] })),
680
+ provider: {
681
+ name: provider.name,
682
+ requestedModel: provider.requestedModel,
683
+ resolvedModel: provider.resolvedModel,
684
+ requestIds: [...provider.requestIds],
685
+ },
686
+ usage: {
687
+ inputTokens: usage.inputTokens,
688
+ outputTokens: usage.outputTokens,
689
+ totalTokens: usage.totalTokens,
690
+ requests: usage.requests,
691
+ repairs: usage.repairs,
692
+ costUsd: usage.costUsd,
693
+ },
694
+ updatedAt: record.updatedAt,
695
+ };
696
+ }
697
+ async function writeRepairCheckpoint(store, runId, provenance, attempts, provider, budget) {
698
+ const usage = usageSummary(budget);
699
+ await store.writeArtifact(runId, "repair-checkpoint.json", {
700
+ schemaVersion: 1,
701
+ ...provenance,
702
+ attempts: attempts.map((attempt) => ({ ...attempt, diagnostics: [...attempt.diagnostics] })),
703
+ provider: { ...provider, requestIds: [...provider.requestIds] },
704
+ usage: { ...usage, repairs: budget.usage.repairs },
705
+ updatedAt: now(),
706
+ });
707
+ }
708
+ function hydratedRepairBudget(config, record, persisted = record.usage) {
709
+ const elapsed = Math.max(0, Date.now() - Date.parse(record.createdAt));
710
+ const remainingElapsed = config.budgets.timeoutMs - elapsed;
711
+ if (!Number.isFinite(remainingElapsed) || remainingElapsed < 1) {
712
+ throw new ProviderError("budget", "Run elapsed-time budget expired before repair.");
713
+ }
714
+ const budget = providerBudget(config, Math.floor(remainingElapsed));
715
+ if (!persisted)
716
+ return budget;
717
+ if (!Number.isSafeInteger(persisted.requests) || persisted.requests < 0
718
+ || persisted.requests === 0 && (persisted.inputTokens !== 0 || persisted.outputTokens !== 0)) {
719
+ throw new ProviderError("schema", "Persisted provider usage cannot initialize a repair budget.");
720
+ }
721
+ for (let index = 0; index < persisted.requests; index += 1) {
722
+ const final = index === persisted.requests - 1;
723
+ budget.recordUsage({
724
+ inputTokens: final ? persisted.inputTokens : 0,
725
+ outputTokens: final ? persisted.outputTokens : 0,
726
+ ...(final && persisted.costUsd !== undefined ? { costUsd: persisted.costUsd } : {}),
727
+ });
728
+ }
729
+ for (let index = 0; index < (persisted.repairs ?? 0); index += 1)
730
+ budget.recordRepair();
731
+ return budget;
732
+ }
733
+ function repairProviderIdentity(provider, config, record) {
734
+ const identity = record.provider;
735
+ if (!identity)
736
+ throw new ProviderError("configuration", "Run has no persisted provider identity for repair.");
737
+ if (provider.name !== identity.name || config.provider.model !== identity.requestedModel) {
738
+ throw new ProviderError("configuration", "Repair provider or requested model differs from the generation run.");
739
+ }
740
+ return identity;
741
+ }
742
+ async function assertStoredRepairInputsUnchanged(store, runId, contractHash, contextManifestHash, snapshotHash, planHash) {
743
+ const [contractRaw, contextRaw, patchRaw, planRaw] = await Promise.all([
744
+ store.readArtifact(runId, "contract.json"),
745
+ store.readArtifact(runId, "context.json"),
746
+ store.readArtifact(runId, "patch.json"),
747
+ store.readArtifact(runId, "validation-plan.json"),
748
+ ]);
749
+ const context = validateContextManifestV1(contextRaw);
750
+ if (hashCanonical(contractRaw) !== contractHash
751
+ || hashContextManifest(context) !== contextManifestHash
752
+ || patchRaw.snapshotHash !== snapshotHash
753
+ || hashCanonical(planRaw) !== planHash) {
754
+ throw new ProviderError("safety", "Persisted contract, context, snapshot, or validation plan changed during repair.");
755
+ }
756
+ return context;
757
+ }
758
+ async function validateStoredRunLocked(options) {
759
+ const store = options.store;
760
+ const record = await store.read(options.runId);
761
+ const diagnostics = [];
762
+ const root = resolve(record.root);
763
+ let baseline;
764
+ let validationBaseline;
765
+ let candidate;
766
+ try {
767
+ const [storedProfile, storedConfigRaw, contractRaw, contextRaw, patchRaw, planRaw, certificationRaw] = await Promise.all([
768
+ store.readArtifact(options.runId, "profile.json"),
769
+ store.readArtifact(options.runId, "run-config.json"),
770
+ store.readArtifact(options.runId, "contract.json"),
771
+ store.readArtifact(options.runId, "context.json"),
772
+ store.readArtifact(options.runId, "patch.json"),
773
+ store.readArtifact(options.runId, "validation-plan.json"),
774
+ store.readArtifact(options.runId, "certification.json"),
775
+ ]);
776
+ const storedConfig = validateConfig(storedConfigRaw);
777
+ const contract = validateChangeContractV1(contractRaw);
778
+ const context = validateContextManifestV1(contextRaw);
779
+ const originalPatch = validatePatchSetV1(patchRaw, contract);
780
+ let patch = originalPatch;
781
+ const plan = validateValidationPlanV1(planRaw);
782
+ const certification = validateCertificationArtifact(certificationRaw);
783
+ const contractHash = hashCanonical(contract);
784
+ const contextManifestHash = record.contextManifestHash;
785
+ const validationPlanHash = hashCanonical(plan);
786
+ if (record.contractHash !== contractHash || record.patchHash !== hashCanonical(originalPatch)
787
+ || contextManifestHash === undefined || contextManifestHash !== hashContextManifest(context)) {
788
+ throw new ProviderError("safety", "Stored run provenance does not match its contract, context, or patch artifacts.");
789
+ }
790
+ const currentProfile = await analyzeProject(root);
791
+ if (currentProfile.fingerprint !== storedProfile.fingerprint || currentProfile.fingerprint !== plan.profileFingerprint) {
792
+ throw new Error("Project analysis changed after generation; regenerate the patch against the current profile.");
793
+ }
794
+ baseline = await createWorkspaceSnapshot(root, { excludeNames: [".venv", "venv", "target", ".next", "coverage"] });
795
+ if (baseline.snapshotHash !== patch.snapshotHash)
796
+ throw new Error("Workspace snapshot changed after generation; validation is stale.");
797
+ const policy = {
798
+ allowedPaths: contract.scope.allowedPaths,
799
+ protectedPaths: contract.scope.prohibitedPaths,
800
+ expectedSnapshotHash: baseline.snapshotHash,
801
+ allowDeletes: contract.scope.allowedOperations.includes("delete"),
802
+ allowRenames: contract.scope.allowedOperations.includes("rename"),
803
+ };
804
+ const image = sandboxImage(currentProfile, options.sandboxImage);
805
+ const validationRunner = options.validationRunner ?? validateBaselineAndCandidate;
806
+ const executionPlan = options.manualChecksPassed ? { ...plan, manualChecks: [] } : plan;
807
+ const dockerBinary = image
808
+ ? await sandboxBinary(storedConfig, options.dockerBinary)
809
+ : undefined;
810
+ const repairAttempts = [];
811
+ const checkpointRaw = await store.readArtifactOptional(options.runId, "repair-checkpoint.json");
812
+ const checkpoint = checkpointRaw === undefined ? undefined : validateRepairCheckpoint(checkpointRaw, {
813
+ contractHash,
814
+ contextManifestHash,
815
+ snapshotHash: baseline.snapshotHash,
816
+ validationPlanHash,
817
+ record,
818
+ });
819
+ const persistedRepairCount = checkpoint?.usage.repairs ?? record.usage?.repairs ?? 0;
820
+ if (!Number.isInteger(persistedRepairCount) || persistedRepairCount < 0 || persistedRepairCount > 2) {
821
+ throw new ProviderError("schema", "Persisted repair usage is invalid.");
822
+ }
823
+ if (checkpoint) {
824
+ repairAttempts.push(...checkpoint.attempts.map((attempt) => ({
825
+ ...attempt,
826
+ diagnostics: [...attempt.diagnostics],
827
+ })));
828
+ }
829
+ else if (persistedRepairCount > 0) {
830
+ const previousReport = validateValidationReportV1(await store.readArtifact(options.runId, "validation-report.json"));
831
+ if (record.validationReportHash !== hashCanonical(previousReport)
832
+ || previousReport.repairs.length !== persistedRepairCount) {
833
+ throw new ProviderError("safety", "Persisted repair count does not match the prior validation provenance.");
834
+ }
835
+ repairAttempts.push(...previousReport.repairs.map((attempt) => ({
836
+ ...attempt,
837
+ diagnostics: [...attempt.diagnostics],
838
+ })));
839
+ }
840
+ const repairResults = [];
841
+ const validateFreshCandidate = async () => {
842
+ if (candidate) {
843
+ await disposeWorkspaceSnapshot(candidate);
844
+ candidate = undefined;
845
+ }
846
+ if (validationBaseline) {
847
+ await disposeWorkspaceSnapshot(validationBaseline);
848
+ validationBaseline = undefined;
849
+ }
850
+ await preparePatch(baseline.root, patch, policy);
851
+ // Validators execute arbitrary project code against disposable copies.
852
+ // The content-addressed master snapshot is never mounted or mutated.
853
+ validationBaseline = await cloneWorkspaceSnapshot(baseline);
854
+ candidate = await cloneWorkspaceSnapshot(baseline);
855
+ await applyPatchAtomic(candidate.root, patch, policy);
856
+ let next;
857
+ if (!image) {
858
+ const timestamp = now();
859
+ next = {
860
+ schemaVersion: 1,
861
+ status: "unvalidated",
862
+ sandbox: "none",
863
+ baseline: [],
864
+ candidate: [],
865
+ repairs: [],
866
+ manualChecks: plan.manualChecks.map((description) => ({ description, status: "pending" })),
867
+ diagnostics: ["Mixed-ecosystem validation requires an explicitly provisioned, trusted multi-toolchain image."],
868
+ startedAt: timestamp,
869
+ finishedAt: timestamp,
870
+ };
871
+ }
872
+ else {
873
+ next = await validationRunner(executionPlan, {
874
+ baselineRoot: validationBaseline.root,
875
+ candidateRoot: candidate.root,
876
+ image,
877
+ dockerBinary,
878
+ retryFailedOnce: true,
879
+ });
880
+ if (options.manualChecksPassed) {
881
+ next.manualChecks = plan.manualChecks.map((description) => ({
882
+ description,
883
+ status: "passed",
884
+ evidence: "Explicit operator attestation supplied to validate.",
885
+ }));
886
+ }
887
+ }
888
+ return validateValidationReportV1({
889
+ ...next,
890
+ repairs: repairAttempts.map((attempt) => ({ ...attempt, diagnostics: [...attempt.diagnostics] })),
891
+ });
892
+ };
893
+ let report = await validateFreshCandidate();
894
+ await store.writeArtifact(options.runId, "validation-report-attempt-0.json", report);
895
+ let budget;
896
+ let providerIdentity;
897
+ let repairFailureStatus;
898
+ if (repairableValidationFailure(report, plan)
899
+ && (options.provider !== undefined || options.config !== undefined)) {
900
+ if (options.provider === undefined || options.config === undefined) {
901
+ throw new ProviderError("configuration", "Guided repair requires both the original provider and immutable run configuration.");
902
+ }
903
+ if (canonicalJson(options.config) !== canonicalJson(storedConfig)) {
904
+ throw new ProviderError("configuration", "Repair configuration differs from the configuration persisted during generation.");
905
+ }
906
+ const generatedIdentity = repairProviderIdentity(options.provider, options.config, record);
907
+ providerIdentity = checkpoint
908
+ ? { ...checkpoint.provider, requestIds: [...checkpoint.provider.requestIds] }
909
+ : { ...generatedIdentity, requestIds: [...generatedIdentity.requestIds] };
910
+ budget = hydratedRepairBudget(storedConfig, record, checkpoint?.usage);
911
+ }
912
+ const maximumRepairs = Math.min(2, storedConfig.budgets.maxRepairs);
913
+ const checkpointProvenance = {
914
+ contractHash,
915
+ contextManifestHash,
916
+ snapshotHash: baseline.snapshotHash,
917
+ validationPlanHash,
918
+ };
919
+ while (options.provider && budget && providerIdentity
920
+ && repairAttempts.length < maximumRepairs
921
+ && repairableValidationFailure(report, plan)) {
922
+ const attempt = repairAttempts.length + 1;
923
+ const promptingDiagnostics = report.diagnostics.length > 0
924
+ ? [...report.diagnostics]
925
+ : ["Candidate validation failed a required command while the baseline remained healthy."];
926
+ let repairRequestId;
927
+ try {
928
+ const diagnosticPayload = repairDiagnosticPayload(report, plan);
929
+ budget.recordRepair();
930
+ const provenance = { attempt, diagnostics: promptingDiagnostics };
931
+ repairAttempts.push(provenance);
932
+ await writeRepairCheckpoint(store, options.runId, checkpointProvenance, repairAttempts, providerIdentity, budget);
933
+ const context = await assertStoredRepairInputsUnchanged(store, options.runId, contractHash, contextManifestHash, baseline.snapshotHash, validationPlanHash);
934
+ const generated = await withProviderRetries(() => generateValidated(options.provider, {
935
+ operation: "repair",
936
+ model: providerIdentity.requestedModel,
937
+ messages: buildGuidedRepairPrompt({
938
+ contract,
939
+ patch,
940
+ validationPlan: plan,
941
+ diagnosticPayload,
942
+ attempt,
943
+ }),
944
+ responseSchema: PATCH_SET_SCHEMA_V1,
945
+ timeoutMs: Math.min(storedConfig.budgets.timeoutMs, 60 * 60_000),
946
+ maxOutputTokens: Math.min(storedConfig.budgets.maxOutputTokens, 100_000),
947
+ temperature: 0,
948
+ signal: options.signal,
949
+ }, (value) => value, {
950
+ budget,
951
+ onBeforeRequest: () => writeRepairCheckpoint(store, options.runId, checkpointProvenance, repairAttempts, providerIdentity, budget),
952
+ }), {
953
+ maxRetries: 2,
954
+ maxElapsedMs: Math.max(1, budget.remainingElapsedMs),
955
+ signal: options.signal,
956
+ onRetry: (_retry, error) => {
957
+ const requestId = error.options.requestId;
958
+ if (requestId && !providerIdentity.requestIds.includes(requestId))
959
+ providerIdentity.requestIds.push(requestId);
960
+ },
961
+ });
962
+ repairRequestId = generated.result.requestId;
963
+ const reusedRequestId = providerIdentity.requestIds.includes(generated.result.requestId);
964
+ if (!reusedRequestId)
965
+ providerIdentity.requestIds.push(generated.result.requestId);
966
+ // Persist cumulative request/token usage and request identity before
967
+ // interpreting output, so a crash cannot reset the repair allowance.
968
+ await writeRepairCheckpoint(store, options.runId, checkpointProvenance, repairAttempts, providerIdentity, budget);
969
+ if (generated.result.resolvedModelId !== providerIdentity.resolvedModel) {
970
+ throw new ProviderError("schema", "Repair resolved to a different model identity than generation.", {
971
+ requestId: generated.result.requestId,
972
+ });
973
+ }
974
+ if (reusedRequestId) {
975
+ throw new ProviderError("schema", "Repair provider reused a request id; provenance would be ambiguous.");
976
+ }
977
+ const repaired = validatePatchSetV1(generated.value, contract);
978
+ assertRepairPatchConstraints(originalPatch, repaired);
979
+ if (repaired.snapshotHash !== baseline.snapshotHash) {
980
+ throw new ProviderError("safety", "Repair patch changed the immutable workspace snapshot hash.");
981
+ }
982
+ assertExternalApisGrounded(repaired, targetWorkspaces(currentProfile, contract), context);
983
+ await preparePatch(baseline.root, repaired, policy);
984
+ provenance.patchHash = hashCanonical(repaired);
985
+ await store.writeArtifact(options.runId, `patch-repair-${attempt}.json`, repaired);
986
+ await store.writeArtifact(options.runId, `diff-repair-${attempt}.json`, { diff: renderPatchDiff(repaired) });
987
+ const previousPatch = patch;
988
+ patch = repaired;
989
+ try {
990
+ report = await validateFreshCandidate();
991
+ }
992
+ catch (error) {
993
+ // Canonical patch.json is not replaced until a complete report exists.
994
+ // A crash or unavailable validator therefore remains resumable from
995
+ // the last provenance-bound patch rather than leaving mismatched state.
996
+ patch = previousPatch;
997
+ throw error;
998
+ }
999
+ await store.writeArtifact(options.runId, `validation-report-attempt-${attempt}.json`, report);
1000
+ repairResults.push({
1001
+ attempt,
1002
+ status: report.status === "validated" ? "validated"
1003
+ : report.status === "failed" ? "failed" : "inconclusive",
1004
+ patchHash: provenance.patchHash,
1005
+ requestId: generated.result.requestId,
1006
+ diagnostic: `Fresh candidate validation completed with status ${report.status}.`,
1007
+ });
1008
+ await writeRepairCheckpoint(store, options.runId, checkpointProvenance, repairAttempts, providerIdentity, budget);
1009
+ }
1010
+ catch (error) {
1011
+ const requestId = error instanceof ProviderError ? error.options.requestId : undefined;
1012
+ if (requestId && !providerIdentity.requestIds.includes(requestId))
1013
+ providerIdentity.requestIds.push(requestId);
1014
+ const rejection = `Repair rejected: ${error instanceof Error ? error.message : String(error)}`;
1015
+ const existingAttempt = repairAttempts.find((item) => item.attempt === attempt);
1016
+ if (existingAttempt)
1017
+ existingAttempt.diagnostics.push(rejection);
1018
+ else
1019
+ repairAttempts.push({ attempt, diagnostics: [...promptingDiagnostics, rejection] });
1020
+ repairResults.push({
1021
+ attempt,
1022
+ status: "rejected",
1023
+ ...(existingAttempt?.patchHash ? { patchHash: existingAttempt.patchHash } : {}),
1024
+ ...(repairRequestId ? { requestId: repairRequestId } : {}),
1025
+ diagnostic: rejection,
1026
+ });
1027
+ report = { ...report, repairs: repairAttempts.map((item) => ({ ...item, diagnostics: [...item.diagnostics] })) };
1028
+ diagnostics.push(error instanceof Error ? error.message : String(error));
1029
+ await writeRepairCheckpoint(store, options.runId, checkpointProvenance, repairAttempts, providerIdentity, budget);
1030
+ repairFailureStatus = error instanceof PatchSafetyError
1031
+ || error instanceof ProviderError && error.code === "safety"
1032
+ || error instanceof Error && /out.of.scope|path escape|weaken|dependency|lockfile|validation.configuration/iu.test(error.message)
1033
+ ? "SECURITY_BLOCKED"
1034
+ : "FAILED";
1035
+ break;
1036
+ }
1037
+ }
1038
+ if (repairableValidationFailure(report, plan) && repairAttempts.length >= maximumRepairs) {
1039
+ diagnostics.push(`Candidate validation still fails after the bounded ${maximumRepairs}-repair allowance.`);
1040
+ }
1041
+ report = validateValidationReportV1({
1042
+ ...report,
1043
+ repairs: repairAttempts.map((attempt) => ({ ...attempt, diagnostics: [...attempt.diagnostics] })),
1044
+ });
1045
+ await store.writeArtifact(options.runId, "validation-report.json", report);
1046
+ await store.writeArtifact(options.runId, "patch.json", patch);
1047
+ await store.writeArtifact(options.runId, "diff.json", { diff: renderPatchDiff(patch) });
1048
+ const existingRepairProvenance = await store.readArtifactOptional(options.runId, "repair-provenance.json");
1049
+ if (repairResults.length > 0 || existingRepairProvenance === undefined) {
1050
+ await store.writeArtifact(options.runId, "repair-provenance.json", {
1051
+ schemaVersion: 1,
1052
+ contractHash,
1053
+ snapshotHash: baseline.snapshotHash,
1054
+ validationPlanHash,
1055
+ attempts: repairAttempts,
1056
+ results: repairResults,
1057
+ provider: providerIdentity ?? record.provider ?? null,
1058
+ usage: budget?.usage ?? record.usage ?? null,
1059
+ });
1060
+ }
1061
+ const validationSecurityBlock = report.diagnostics.some((diagnostic) => diagnostic.startsWith("SECURITY_BLOCKED:"));
1062
+ const status = repairFailureStatus === "SECURITY_BLOCKED" || validationSecurityBlock ? "SECURITY_BLOCKED"
1063
+ : report.status === "validated" && report.sandbox === "strong" && certification.certified
1064
+ ? "VERIFIED"
1065
+ : repairFailureStatus === "FAILED" || report.status === "failed" ? "FAILED" : "INCONCLUSIVE";
1066
+ diagnostics.push(...report.diagnostics);
1067
+ if (!certification.certified)
1068
+ diagnostics.push(...certification.reasons);
1069
+ if (status === "VERIFIED")
1070
+ diagnostics.push("All immutable validation checks passed in a strong sandbox for a certified profile/model combination.");
1071
+ await persistOutcome(store, options.runId, status, diagnostics, {
1072
+ patchHash: hashCanonical(patch),
1073
+ validationReportHash: hashCanonical(report),
1074
+ ...(providerIdentity ? { provider: providerIdentity } : {}),
1075
+ ...(budget ? { usage: usageSummary(budget) } : {}),
1076
+ });
1077
+ return {
1078
+ runId: options.runId,
1079
+ status,
1080
+ diagnostics,
1081
+ report,
1082
+ diff: renderPatchDiff(patch),
1083
+ ...(budget ? { usage: usageSummary(budget) } : {}),
1084
+ };
1085
+ }
1086
+ catch (error) {
1087
+ diagnostics.push(error instanceof Error ? error.message : String(error));
1088
+ const security = error instanceof PatchSafetyError
1089
+ || error instanceof ProviderError && error.code === "safety";
1090
+ const status = security ? "SECURITY_BLOCKED" : "INCONCLUSIVE";
1091
+ await persistOutcome(store, options.runId, status, diagnostics).catch(() => undefined);
1092
+ return { runId: options.runId, status, diagnostics };
1093
+ }
1094
+ finally {
1095
+ if (candidate)
1096
+ await disposeWorkspaceSnapshot(candidate).catch(() => undefined);
1097
+ if (validationBaseline)
1098
+ await disposeWorkspaceSnapshot(validationBaseline).catch(() => undefined);
1099
+ if (baseline)
1100
+ await disposeWorkspaceSnapshot(baseline).catch(() => undefined);
1101
+ }
1102
+ }
1103
+ export async function validateStoredRun(options) {
1104
+ const store = options.store ?? new RunStore();
1105
+ return store.exclusive(options.runId, () => validateStoredRunLocked({ ...options, store }));
1106
+ }
1107
+ async function isGitProject(root) {
1108
+ const marker = await lstat(resolve(root, ".git")).catch(() => undefined);
1109
+ if (!marker)
1110
+ return false;
1111
+ return new Promise((complete) => {
1112
+ const child = spawn("git", ["rev-parse", "--is-inside-work-tree"], {
1113
+ cwd: root,
1114
+ stdio: ["ignore", "pipe", "ignore"],
1115
+ env: { PATH: process.env.PATH ?? "" },
1116
+ });
1117
+ let output = "";
1118
+ child.stdout.on("data", (chunk) => { if (output.length < 1024)
1119
+ output += chunk.toString("utf8"); });
1120
+ child.once("error", () => complete(false));
1121
+ child.once("close", (code) => complete(code === 0 && output.trim() === "true"));
1122
+ });
1123
+ }
1124
+ function exactRecord(value, label, required, optional = []) {
1125
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
1126
+ throw new Error(`${label} must be an object.`);
1127
+ }
1128
+ const record = value;
1129
+ const allowed = new Set([...required, ...optional]);
1130
+ for (const key of required) {
1131
+ if (!Object.hasOwn(record, key))
1132
+ throw new Error(`${label}.${key} is required.`);
1133
+ }
1134
+ for (const key of Object.keys(record)) {
1135
+ if (!allowed.has(key))
1136
+ throw new Error(`${label}.${key} is not allowed.`);
1137
+ }
1138
+ return record;
1139
+ }
1140
+ function boundedArtifactText(value, label) {
1141
+ if (typeof value !== "string" || Buffer.byteLength(value, "utf8") > 10 * 1024 * 1024) {
1142
+ throw new Error(`${label} must be a bounded string.`);
1143
+ }
1144
+ return value;
1145
+ }
1146
+ function artifactPath(value, label) {
1147
+ if (typeof value !== "string" || normalizePatchPath(value) !== value) {
1148
+ throw new Error(`${label} must be a canonical confined path.`);
1149
+ }
1150
+ return value;
1151
+ }
1152
+ function artifactHash(value, label, nullable = false) {
1153
+ if (nullable && value === null)
1154
+ return null;
1155
+ if (typeof value !== "string" || !/^[a-f0-9]{64}$/u.test(value)) {
1156
+ throw new Error(`${label} must be a SHA-256 digest${nullable ? " or null" : ""}.`);
1157
+ }
1158
+ return value;
1159
+ }
1160
+ function validateRollbackArtifact(value, patch) {
1161
+ const record = exactRecord(value, "rollback", ["schemaVersion", "patchHash", "createdAt", "entries"]);
1162
+ if (record.schemaVersion !== 1 || record.patchHash !== hashCanonical(patch)) {
1163
+ throw new Error("Rollback artifact provenance is invalid.");
1164
+ }
1165
+ if (typeof record.createdAt !== "string" || !Number.isFinite(Date.parse(record.createdAt))) {
1166
+ throw new Error("Rollback artifact timestamp is invalid.");
1167
+ }
1168
+ if (!Array.isArray(record.entries) || record.entries.length !== patch.operations.length || record.entries.length === 0 || record.entries.length > 200) {
1169
+ throw new Error("Rollback entries do not exactly correspond to the stored patch.");
1170
+ }
1171
+ const entries = [];
1172
+ for (let index = 0; index < patch.operations.length; index++) {
1173
+ const operation = patch.operations[index];
1174
+ const raw = exactRecord(record.entries[index], `rollback.entries[${index}]`, ["kind", "path", "before", "afterHash", "mode"], ["from", "after"]);
1175
+ const path = artifactPath(raw.path, `rollback.entries[${index}].path`);
1176
+ const before = boundedArtifactText(raw.before, `rollback.entries[${index}].before`);
1177
+ if (!Number.isInteger(raw.mode) || raw.mode < 0 || raw.mode > 0o777) {
1178
+ throw new Error(`rollback.entries[${index}].mode is invalid.`);
1179
+ }
1180
+ const mode = raw.mode;
1181
+ if (operation.kind === "create") {
1182
+ const afterHash = artifactHash(raw.afterHash, `rollback.entries[${index}].afterHash`);
1183
+ if (raw.kind !== "created" || path !== operation.path || before !== "" || raw.from !== undefined || raw.after !== undefined
1184
+ || afterHash !== sha256Text(operation.content)) {
1185
+ throw new Error(`Rollback create entry ${index} does not match the stored patch.`);
1186
+ }
1187
+ entries.push({ kind: "created", path, before, afterHash, mode });
1188
+ continue;
1189
+ }
1190
+ if (operation.kind === "edit") {
1191
+ const after = boundedArtifactText(raw.after, `rollback.entries[${index}].after`);
1192
+ const afterHash = artifactHash(raw.afterHash, `rollback.entries[${index}].afterHash`);
1193
+ const first = before.indexOf(operation.oldText);
1194
+ const unique = first >= 0 && before.indexOf(operation.oldText, first + 1) < 0;
1195
+ const expectedAfter = unique
1196
+ ? before.slice(0, first) + operation.newText + before.slice(first + operation.oldText.length)
1197
+ : undefined;
1198
+ if (raw.kind !== "edited" || path !== operation.path || raw.from !== undefined
1199
+ || sha256Text(before) !== operation.baseHash || after !== expectedAfter || afterHash !== sha256Text(after)) {
1200
+ throw new Error(`Rollback edit entry ${index} does not match the stored patch.`);
1201
+ }
1202
+ entries.push({ kind: "edited", path, before, after, afterHash, mode });
1203
+ continue;
1204
+ }
1205
+ if (operation.kind === "delete") {
1206
+ const afterHash = artifactHash(raw.afterHash, `rollback.entries[${index}].afterHash`, true);
1207
+ if (raw.kind !== "deleted" || path !== operation.path || raw.from !== undefined || raw.after !== undefined
1208
+ || sha256Text(before) !== operation.baseHash || afterHash !== null) {
1209
+ throw new Error(`Rollback delete entry ${index} does not match the stored patch.`);
1210
+ }
1211
+ entries.push({ kind: "deleted", path, before, afterHash: null, mode });
1212
+ continue;
1213
+ }
1214
+ const from = artifactPath(raw.from, `rollback.entries[${index}].from`);
1215
+ const afterHash = artifactHash(raw.afterHash, `rollback.entries[${index}].afterHash`);
1216
+ if (raw.kind !== "renamed" || path !== operation.path || from !== operation.from || raw.after !== undefined
1217
+ || sha256Text(before) !== operation.baseHash || afterHash !== operation.baseHash) {
1218
+ throw new Error(`Rollback rename entry ${index} does not match the stored patch.`);
1219
+ }
1220
+ entries.push({ kind: "renamed", path, from, before, afterHash, mode });
1221
+ }
1222
+ return { schemaVersion: 1, patchHash: record.patchHash, createdAt: record.createdAt, entries };
1223
+ }
1224
+ function validateCertificationArtifact(value) {
1225
+ const record = exactRecord(value, "certification", ["schemaVersion", "supportMatrixVersion", "provider", "profileCertified", "certified", "reasons"]);
1226
+ const provider = exactRecord(record.provider, "certification.provider", ["matrixKey", "certified", "reason"]);
1227
+ if (record.schemaVersion !== 1 || record.supportMatrixVersion !== SUPPORT_MATRIX_VERSION
1228
+ || typeof record.profileCertified !== "boolean" || typeof record.certified !== "boolean"
1229
+ || typeof provider.matrixKey !== "string" || typeof provider.certified !== "boolean" || typeof provider.reason !== "string"
1230
+ || !Array.isArray(record.reasons) || record.reasons.some((reason) => typeof reason !== "string" || reason.length === 0)
1231
+ || record.certified !== (record.profileCertified && provider.certified)) {
1232
+ throw new Error("Certification artifact is invalid.");
1233
+ }
1234
+ return record;
1235
+ }
1236
+ function validateApplyArtifact(value, patchHash) {
1237
+ const record = exactRecord(value, "apply", ["appliedAt", "paths", "patchHash"]);
1238
+ if (record.patchHash !== patchHash || typeof record.appliedAt !== "string" || !Number.isFinite(Date.parse(record.appliedAt))
1239
+ || !Array.isArray(record.paths) || record.paths.some((path) => typeof path !== "string" || path.length === 0)) {
1240
+ throw new Error("Apply artifact provenance is invalid.");
1241
+ }
1242
+ }
1243
+ export async function applyVerifiedRun(runId, store = new RunStore()) {
1244
+ const initialRecord = await store.read(runId);
1245
+ if (initialRecord.status !== "VERIFIED")
1246
+ return { runId, status: initialRecord.status, diagnostics: ["Automatic apply requires a VERIFIED run."] };
1247
+ if (!(await isGitProject(initialRecord.root)))
1248
+ return { runId, status: "INCONCLUSIVE", diagnostics: ["Automatic apply is disabled for non-Git projects because no verified rollback backend is available."] };
1249
+ try {
1250
+ const result = await store.exclusive(runId, async () => {
1251
+ const record = await store.read(runId);
1252
+ if (record.status !== "VERIFIED")
1253
+ throw new Error("Run status changed before apply; automatic application was blocked.");
1254
+ const contract = validateChangeContractV1(await store.readArtifact(runId, "contract.json"));
1255
+ const patch = validatePatchSetV1(await store.readArtifact(runId, "patch.json"), contract);
1256
+ const context = validateContextManifestV1(await store.readArtifact(runId, "context.json"));
1257
+ const validationReport = validateValidationReportV1(await store.readArtifact(runId, "validation-report.json"));
1258
+ const certification = validateCertificationArtifact(await store.readArtifact(runId, "certification.json"));
1259
+ if (record.contractHash !== hashCanonical(contract) || record.patchHash !== hashCanonical(patch))
1260
+ throw new Error("Stored contract or patch provenance changed after validation.");
1261
+ if (record.contextManifestHash !== hashContextManifest(context))
1262
+ throw new Error("Stored context provenance changed after validation.");
1263
+ if (record.validationReportHash !== hashCanonical(validationReport))
1264
+ throw new Error("Stored validation report provenance changed after validation.");
1265
+ if (validationReport.status !== "validated" || validationReport.sandbox !== "strong" || !certification.certified) {
1266
+ throw new Error("Apply requires a validated strong-sandbox report and shipped certification evidence.");
1267
+ }
1268
+ const policy = {
1269
+ allowedPaths: contract.scope.allowedPaths,
1270
+ protectedPaths: contract.scope.prohibitedPaths,
1271
+ allowDeletes: contract.scope.allowedOperations.includes("delete"),
1272
+ allowRenames: contract.scope.allowedOperations.includes("rename"),
1273
+ };
1274
+ const prepared = await preparePatch(record.root, patch, policy);
1275
+ const rollback = {
1276
+ schemaVersion: 1,
1277
+ patchHash: hashCanonical(patch),
1278
+ createdAt: now(),
1279
+ entries: prepared.operations.map((operation) => {
1280
+ if (operation.kind === "create")
1281
+ return { kind: "created", path: operation.path, before: "", afterHash: sha256Text(operation.content), mode: operation.mode };
1282
+ if (operation.kind === "edit")
1283
+ return { kind: "edited", path: operation.path, before: operation.before, after: operation.after, afterHash: sha256Text(operation.after), mode: operation.mode };
1284
+ if (operation.kind === "delete")
1285
+ return { kind: "deleted", path: operation.path, before: operation.before, afterHash: null, mode: operation.mode };
1286
+ return { kind: "renamed", path: operation.path, from: operation.from, before: operation.before, afterHash: sha256Text(operation.before), mode: operation.mode };
1287
+ }),
1288
+ };
1289
+ await store.writeArtifact(runId, "rollback.json", rollback);
1290
+ const applied = await applyPatchAtomic(record.root, patch, policy);
1291
+ await store.writeArtifact(runId, "apply.json", { appliedAt: now(), paths: applied.applied, patchHash: applied.patchHash });
1292
+ return applied;
1293
+ });
1294
+ const diagnostics = [`Applied ${result.applied.length} structured operation(s) atomically after exact base-hash checks.`];
1295
+ return { runId, status: "VERIFIED", diagnostics };
1296
+ }
1297
+ catch (error) {
1298
+ return { runId, status: "INCONCLUSIVE", diagnostics: [error instanceof Error ? error.message : String(error)] };
1299
+ }
1300
+ }
1301
+ export async function rollbackAppliedRun(runId, store = new RunStore()) {
1302
+ try {
1303
+ const restored = await store.exclusive(runId, async () => {
1304
+ const record = await store.read(runId);
1305
+ const contract = validateChangeContractV1(await store.readArtifact(runId, "contract.json"));
1306
+ const patch = validatePatchSetV1(await store.readArtifact(runId, "patch.json"), contract);
1307
+ const patchHash = hashCanonical(patch);
1308
+ if (record.patchHash !== patchHash)
1309
+ throw new Error("Stored patch provenance changed before rollback.");
1310
+ validateApplyArtifact(await store.readArtifact(runId, "apply.json"), patchHash);
1311
+ const artifact = validateRollbackArtifact(await store.readArtifact(runId, "rollback.json"), patch);
1312
+ const operations = [];
1313
+ for (const entry of [...artifact.entries].reverse()) {
1314
+ if (entry.kind === "created") {
1315
+ operations.push({ kind: "delete", path: entry.path, baseHash: entry.afterHash });
1316
+ }
1317
+ else if (entry.kind === "edited") {
1318
+ operations.push({ kind: "edit", path: entry.path, baseHash: entry.afterHash, oldText: entry.after, newText: entry.before });
1319
+ }
1320
+ else if (entry.kind === "deleted") {
1321
+ operations.push({ kind: "create", path: entry.path, content: entry.before });
1322
+ }
1323
+ else {
1324
+ if (!entry.from)
1325
+ throw new Error("Rollback rename source is missing.");
1326
+ operations.push({ kind: "rename", from: entry.path, path: entry.from, baseHash: entry.afterHash });
1327
+ }
1328
+ }
1329
+ const inverse = {
1330
+ schemaVersion: 1,
1331
+ contractHash: "0".repeat(64),
1332
+ snapshotHash: "0".repeat(64),
1333
+ operations,
1334
+ requirementIds: ["ROLLBACK"],
1335
+ proposedTests: [],
1336
+ };
1337
+ const paths = unique(artifact.entries.flatMap((entry) => [entry.path, ...(entry.from ? [entry.from] : [])]));
1338
+ const result = await applyPatchAtomic(record.root, inverse, { allowedPaths: paths, allowDeletes: true, allowRenames: true });
1339
+ for (const entry of artifact.entries) {
1340
+ if (entry.kind === "created")
1341
+ continue;
1342
+ const restoredPath = entry.kind === "renamed" ? entry.from : entry.path;
1343
+ await chmod(resolve(record.root, restoredPath), entry.mode);
1344
+ }
1345
+ await store.writeArtifact(runId, "rollback-result.json", { rolledBackAt: now(), paths: result.applied, patchHash: result.patchHash });
1346
+ return result;
1347
+ });
1348
+ return { runId, status: "VERIFIED", diagnostics: [`Rollback restored ${restored.applied.length} operation(s) after exact post-apply hash checks.`] };
1349
+ }
1350
+ catch (error) {
1351
+ return { runId, status: "INCONCLUSIVE", diagnostics: [error instanceof Error ? error.message : String(error)] };
1352
+ }
1353
+ }
1354
+ export function rootRelative(root, path) {
1355
+ const value = relative(resolve(root), resolve(path));
1356
+ if (value === ".." || value.startsWith(`..${sep}`))
1357
+ throw new Error("Path escapes root.");
1358
+ return value.split(sep).join("/") || ".";
1359
+ }
1360
+ //# sourceMappingURL=workflow.js.map