forgeos 0.1.0-alpha.71 → 0.1.0-alpha.72
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +1 -1
- package/CHANGELOG.md +6 -0
- package/docs/agent-fabric-programs.md +34 -2
- package/docs/architecture/agent-fabric/WORKFLOW_PROGRAM_COMPETITIVE_EVIDENCE.md +75 -0
- package/docs/architecture/agent-fabric/WORKFLOW_PROGRAM_COMPETITIVE_PLAN.md +137 -0
- package/docs/architecture/agent-fabric/WORKFLOW_PROGRAM_COMPETITIVE_REVIEW.md +56 -0
- package/examples/agent-fabric-v2/templates/README.md +46 -0
- package/examples/agent-fabric-v2/templates/bugfix.json +12 -0
- package/examples/agent-fabric-v2/templates/migration.json +13 -0
- package/examples/agent-fabric-v2/templates/review.json +12 -0
- package/package.json +5 -1
- package/scripts/summarize-fabric-benchmark.mjs +6 -0
- package/src/forge/_generated/releaseManifest.json +1 -1
- package/src/forge/_generated/releaseManifest.ts +3 -3
- package/src/forge/agent-fabric/claude-program-worker.ts +95 -0
- package/src/forge/agent-fabric/codex-sdk-worker.ts +10 -4
- package/src/forge/agent-fabric/index.ts +5 -0
- package/src/forge/agent-fabric/local-task-server.ts +2 -1
- package/src/forge/agent-fabric/managed-environment.ts +14 -8
- package/src/forge/agent-fabric/managed-workspace.ts +4 -0
- package/src/forge/agent-fabric/program-api.ts +1 -1
- package/src/forge/agent-fabric/program-authoring.ts +48 -0
- package/src/forge/agent-fabric/program-benchmark.ts +76 -0
- package/src/forge/agent-fabric/program-capabilities.ts +14 -0
- package/src/forge/agent-fabric/program-contract.ts +19 -3
- package/src/forge/agent-fabric/program-observation.ts +63 -0
- package/src/forge/agent-fabric/program-runtime-options.ts +23 -0
- package/src/forge/agent-fabric/program-scheduler.ts +25 -6
- package/src/forge/agent-fabric/program-service.ts +172 -35
- package/src/forge/agent-fabric/program-store.ts +9 -1
- package/src/forge/agent-fabric/program-templates.ts +96 -0
- package/src/forge/agent-fabric/program-view.ts +15 -1
- package/src/forge/agent-fabric/program-worker.ts +73 -20
- package/src/forge/agent-memory/mcp.ts +8 -2
- package/src/forge/cli/fabric.ts +4 -2
- package/src/forge/cli/main.ts +2 -0
- package/src/forge/cli/parse.ts +3 -1
- package/src/forge/version.ts +1 -1
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { proposeWorkflowTemplate, generateRegistryAuthorTypes } from "./program-authoring.ts";
|
|
2
|
+
import type { ProgramTemplateKind, ProgramTemplateOptions } from "./program-templates.ts";
|
|
3
|
+
import { recordProgramObservation, summarizeProgramUsage, programResourceLiability, type ProgramWorkerObservation } from "./program-observation.ts";
|
|
4
|
+
import { programVisualization, programDiagnostics } from "./program-view.ts";
|
|
2
5
|
import { programBlock, programGraph, canonicalItemKey, compareProgramKeys, programSegment, type ProgramBlock } from "./program-structure.ts";
|
|
3
6
|
import { ProgramActivityScheduler, type ActivityScope } from "./program-scheduler.ts";
|
|
4
7
|
import { ProgramOwnerLease } from "./program-owner.ts";
|
|
@@ -15,8 +18,9 @@ import { programAssert, programDigest, programValueBytes, programWithin, program
|
|
|
15
18
|
type WorkflowProgramV2, type ProgramRegistry, type ProgramOperation, type ProgramExpr, type ProgramRef,
|
|
16
19
|
type ProgramValue, type ProgramRunV2, type ProgramCandidate, type ProgramAssessment, type ProgramAssessmentContext, type ProgramRepairResult } from "./program-contract.ts";
|
|
17
20
|
|
|
18
|
-
export const PROGRAM_RUN_ACTIONS = ["program-validate", "program-start", "program-status", "program-wait", "program-pause", "program-resume", "program-signal", "program-replan", "program-cancel", "program-reconcile", "program-apply", "program-history", "program-artifact-get", "program-explain", "program-diff"] as const;
|
|
21
|
+
export const PROGRAM_RUN_ACTIONS = ["program-author", "program-author-types", "program-validate", "program-start", "program-status", "program-wait", "program-pause", "program-resume", "program-signal", "program-replan", "program-cancel", "program-reconcile", "program-apply", "program-history", "program-artifact-get", "program-explain", "program-diff"] as const;
|
|
19
22
|
export type ProgramRunAction = typeof PROGRAM_RUN_ACTIONS[number];
|
|
23
|
+
export interface ProgramRuntimeOptions { ownerCapacity?: number; ownerMaxTokens?: number }
|
|
20
24
|
interface Context { prefix: string; steps: ProgramOperation[]; program: WorkflowProgramV2; inputRef?: string; parent?: Context; item?: unknown; itemKey?: string; state?: unknown; depth: number; chain: Set<string>; dependencies?: Set<string>; activityScopes?: ActivityScope[]; signal?: AbortSignal }
|
|
21
25
|
interface ActivityOutput { data: unknown; candidate: ProgramCandidate; attemptId: string; evidenceRefs?: string[] }
|
|
22
26
|
const asRecord = (value: unknown): Record<string, unknown> => { programAssert(value && typeof value === "object" && !Array.isArray(value), "Object required"); return value as Record<string, unknown>; };
|
|
@@ -30,13 +34,16 @@ export class ProgramRunService {
|
|
|
30
34
|
private controllers = new Map<string, AbortController>();
|
|
31
35
|
private attemptControllers = new Map<string, AbortController>();
|
|
32
36
|
private jobs = new Map<string, Promise<unknown>>();
|
|
33
|
-
private scheduler
|
|
37
|
+
private scheduler: ProgramActivityScheduler;
|
|
38
|
+
private admissionTail: Promise<unknown> = Promise.resolve();
|
|
34
39
|
private reservations = new Map<string, () => void>();
|
|
35
40
|
private timers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
36
41
|
private wakeRequests = new Set<string>();
|
|
37
42
|
private closed = false;
|
|
38
|
-
private constructor(readonly store: ProgramRunStore, private registry: ProgramRegistry, private adapter: ProgramWorkerAdapter, private owner: ProgramOwnerLease, private registryFile: boolean) {}
|
|
39
|
-
static async open(root: string, registry?: ProgramRegistry, adapter: ProgramWorkerAdapter = prepareProgramWorker): Promise<ProgramRunService> {
|
|
43
|
+
private constructor(readonly store: ProgramRunStore, private registry: ProgramRegistry, private adapter: ProgramWorkerAdapter, private owner: ProgramOwnerLease, private registryFile: boolean, private runtime: ProgramRuntimeOptions) { this.scheduler = new ProgramActivityScheduler(runtime.ownerCapacity ?? 4); }
|
|
44
|
+
static async open(root: string, registry?: ProgramRegistry, adapter: ProgramWorkerAdapter = prepareProgramWorker, runtime: ProgramRuntimeOptions = {}): Promise<ProgramRunService> {
|
|
45
|
+
programAssert(runtime.ownerCapacity === undefined || Number.isSafeInteger(runtime.ownerCapacity) && runtime.ownerCapacity > 0 && runtime.ownerCapacity <= 32, "Invalid owner capacity");
|
|
46
|
+
programAssert(runtime.ownerMaxTokens === undefined || Number.isSafeInteger(runtime.ownerMaxTokens) && runtime.ownerMaxTokens > 0, "Invalid owner token budget");
|
|
40
47
|
const store = await ProgramRunStore.open(root);
|
|
41
48
|
const registryFile = !registry;
|
|
42
49
|
if (!registry) {
|
|
@@ -44,7 +51,7 @@ export class ProgramRunService {
|
|
|
44
51
|
catch (error) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; registry = { schemas: {}, executors: {}, policies: {}, acceptance: {}, populations: {} }; }
|
|
45
52
|
}
|
|
46
53
|
const owner = await ProgramOwnerLease.acquire(store.directory);
|
|
47
|
-
const service = new ProgramRunService(store, registry, adapter, owner, registryFile);
|
|
54
|
+
const service = new ProgramRunService(store, registry, adapter, owner, registryFile, runtime);
|
|
48
55
|
try {
|
|
49
56
|
for (const id of await store.list()) {
|
|
50
57
|
const run = (await store.read(id))!;
|
|
@@ -65,6 +72,29 @@ export class ProgramRunService {
|
|
|
65
72
|
} catch (error) { await owner.close(); throw error; }
|
|
66
73
|
}
|
|
67
74
|
async close(): Promise<void> { if (this.closed) return; this.closed = true; for (const timer of this.timers.values()) clearTimeout(timer); for (const controller of this.controllers.values()) controller.abort(); await Promise.allSettled([...this.active.values(), ...this.applications.values()]); await this.owner.close(); }
|
|
75
|
+
/** Trusted host API: resizing changes future admission, never cancels admitted work. */
|
|
76
|
+
async updateRuntimeOptions(options: ProgramRuntimeOptions): Promise<void> {
|
|
77
|
+
await this.owner.assert(); programAssert(!this.closed, "Owner is closed");
|
|
78
|
+
const next = { ...this.runtime, ...options };
|
|
79
|
+
programAssert(next.ownerCapacity === undefined || Number.isSafeInteger(next.ownerCapacity) && next.ownerCapacity > 0 && next.ownerCapacity <= 32, "Invalid owner capacity");
|
|
80
|
+
programAssert(next.ownerMaxTokens === undefined || Number.isSafeInteger(next.ownerMaxTokens) && next.ownerMaxTokens > 0, "Invalid owner token budget");
|
|
81
|
+
await this.admit(async () => { this.runtime = next; this.scheduler.setOwnerCapacity(next.ownerCapacity ?? 4); });
|
|
82
|
+
}
|
|
83
|
+
private async resourceSummary(): Promise<{ ownerMaxTokens: number | null; liabilityTokens: number; finalReservedTokens: number; runs: { runId: string; tokens: number; finalReservedTokens: number; unknownAttempts: number }[] }> {
|
|
84
|
+
const runs = [];
|
|
85
|
+
for (const runId of await this.store.list()) {
|
|
86
|
+
const state = await this.state(runId), attempts = Object.values(state.attempts);
|
|
87
|
+
const finalFloor = registryEntry(state.registry.policies, state.program.policy).resources?.finalReserveTokens ?? 0;
|
|
88
|
+
const finalSpent = attempts.filter(attempt => attempt.resourceReservation?.phase === "final").reduce((sum, attempt) => sum + programResourceLiability(attempt), 0);
|
|
89
|
+
runs.push({ runId, tokens: attempts.reduce((sum, attempt) => sum + programResourceLiability(attempt), 0), finalReservedTokens: ["completed", "acceptance-ready", "applied", "canceled"].includes(state.status) ? 0 : Math.max(0, finalFloor - finalSpent), unknownAttempts: attempts.filter(attempt => attempt.resourceReservation?.status !== "held" && summarizeProgramUsage(attempt).certainty === "unknown").length });
|
|
90
|
+
}
|
|
91
|
+
return { ownerMaxTokens: this.runtime.ownerMaxTokens ?? null, liabilityTokens: runs.reduce((sum, run) => sum + run.tokens, 0), finalReservedTokens: runs.reduce((sum, run) => sum + run.finalReservedTokens, 0), runs };
|
|
92
|
+
}
|
|
93
|
+
private async admit<T>(action: () => Promise<T>): Promise<T> {
|
|
94
|
+
const task = this.admissionTail.then(action);
|
|
95
|
+
this.admissionTail = task.catch(() => {});
|
|
96
|
+
return task;
|
|
97
|
+
}
|
|
68
98
|
private async authorize(run: ProgramRunV2, executorRef?: ProgramRef): Promise<void> {
|
|
69
99
|
await this.owner.assert(); programAssert(!this.closed, "Owner is closed");
|
|
70
100
|
const live = this.registryFile ? JSON.parse(await readFile(join(this.store.root, ".forge/fabric-programs.json"), "utf8")) as ProgramRegistry : this.registry;
|
|
@@ -124,9 +154,17 @@ export class ProgramRunService {
|
|
|
124
154
|
} finally { context.signal?.removeEventListener("abort", abort); }
|
|
125
155
|
}
|
|
126
156
|
async execute(action: ProgramRunAction, request: Record<string, unknown>): Promise<unknown> {
|
|
157
|
+
programAssert(!Object.hasOwn(request, "registry"), "Registry is owner-controlled project configuration, never request data");
|
|
158
|
+
if (action === "program-author" || action === "program-author-types") {
|
|
159
|
+
await this.owner.assert(); programAssert(!this.closed, "Owner is closed");
|
|
160
|
+
const registry = this.registryFile ? JSON.parse(await readFile(join(this.store.root, ".forge/fabric-programs.json"), "utf8")) as ProgramRegistry : this.registry;
|
|
161
|
+
if (action === "program-author-types") return { source: generateRegistryAuthorTypes(registry), registryDigest: programDigest(registry) };
|
|
162
|
+
programAssert(["review", "bugfix", "migration"].includes(String(request.kind)), "Unknown workflow template");
|
|
163
|
+
programValueBytes(request.options, 40000, 128);
|
|
164
|
+
return proposeWorkflowTemplate(request.kind as ProgramTemplateKind, asRecord(request.options) as unknown as ProgramTemplateOptions, registry);
|
|
165
|
+
}
|
|
127
166
|
await this.owner.assert();
|
|
128
167
|
programAssert((PROGRAM_RUN_ACTIONS as readonly string[]).includes(action), "Unsupported program action");
|
|
129
|
-
programAssert(!Object.hasOwn(request, "registry"), "Registry is owner-controlled project configuration, never request data");
|
|
130
168
|
if (action === "program-validate" || action === "program-start") {
|
|
131
169
|
const program = request.source ? lowerWorkflowSource(String(request.source)) : request.program as WorkflowProgramV2;
|
|
132
170
|
validateWorkflowProgram(program, this.registry);
|
|
@@ -156,16 +194,25 @@ export class ProgramRunService {
|
|
|
156
194
|
}
|
|
157
195
|
const inputRef = await this.store.put(request.input, policy.maxOutputBytes), baseRef = base ? await this.store.put(base) : undefined;
|
|
158
196
|
const now = new Date();
|
|
159
|
-
const run = await this.
|
|
197
|
+
const run = await this.admit(async () => {
|
|
198
|
+
const duplicate = await this.store.read(runId);
|
|
199
|
+
if (duplicate) { programAssert(duplicate.requestDigest === fingerprint, "Start requestId changed", "AF_PROGRAM_CONFLICT"); return duplicate; }
|
|
200
|
+
if (this.runtime.ownerMaxTokens !== undefined) {
|
|
201
|
+
programAssert(policy.resources, "Owner budget requires per-run reservation policy", "AF_PROGRAM_BUDGET");
|
|
202
|
+
const resources = await this.resourceSummary();
|
|
203
|
+
programAssert(resources.liabilityTokens + resources.finalReservedTokens + (policy.resources.finalReserveTokens ?? 0) <= this.runtime.ownerMaxTokens, "Owner cannot reserve final evaluation budget", "AF_PROGRAM_BUDGET");
|
|
204
|
+
}
|
|
205
|
+
return this.store.transact(runId, "start", () => ({ schemaVersion: 2, runId, requestId, requestDigest: fingerprint,
|
|
160
206
|
program, registry, programDigest: programDigest(program), registryDigest: programDigest(registry),
|
|
161
207
|
inputRef, baselineDigest, ...(baseRef ? { baseRef } : {}), createdAt: now.toISOString(), deadlineAt: new Date(now.getTime() + policy.deadlineMs).toISOString(),
|
|
162
208
|
version: 0, semanticVersion: 1, revision: 1, planRevision: 1, status: "executing", operations: {}, attempts: {}, seals: {}, signals: [], candidates: {}, assessments: {}, coverageReceipts: {}, deltas: {}, waits: {}, repairs: {}, collections: {}, totalAttempts: 0 }), { requestId, fingerprint, expectedVersion: 0 });
|
|
209
|
+
});
|
|
163
210
|
this.schedule(runId); return run;
|
|
164
211
|
}
|
|
165
212
|
const runId = idText(request.runId);
|
|
166
213
|
if (action === "program-status") return this.state(runId);
|
|
167
214
|
if (action === "program-history") return this.store.history(runId);
|
|
168
|
-
if (action === "program-explain") { const state = await this.state(runId); return { runId, graph: programVisualization(state.program, state.operations), status: state.status, reason: state.reason ?? null, operations: state.operations, scopes: state.scopeResults ?? {}, queue: state.queue ?? [], waits: state.waits, assessments: state.assessments, scheduler: this.scheduler.snapshot(), storage: this.store.metrics, guarantees: { attemptsUnit: "owner-visible-dispatch", providerCalls: "adapter-dependent", crossRunCache: false } }; }
|
|
215
|
+
if (action === "program-explain") { const state = await this.state(runId); return { runId, graph: programVisualization(state.program, state.operations), diagnostics: programDiagnostics(state), status: state.status, reason: state.reason ?? null, operations: state.operations, scopes: state.scopeResults ?? {}, queue: state.queue ?? [], waits: state.waits, assessments: state.assessments, scheduler: this.scheduler.snapshot(), storage: this.store.metrics, resources: await this.resourceSummary(), guarantees: { attemptsUnit: "owner-visible-dispatch", providerCalls: "adapter-dependent", crossRunCache: false, resourceEnforcement: "admission-only", ownerTokenLimit: this.runtime.ownerMaxTokens ?? null } }; }
|
|
169
216
|
if (action === "program-artifact-get") {
|
|
170
217
|
const state = await this.state(runId), ref = String(request.ref); programAssert(/^sha256:[a-f0-9]{64}$/.test(ref) && JSON.stringify(state).includes(ref), "Artifact reference is not reachable from this run");
|
|
171
218
|
if (request.binary === true) { programAssert(Object.values(state.artifactRecords ?? {}).some(record => record.artifactRef === ref), "Binary artifact not registered for this run"); return { ref, base64: (await this.store.getBinary(ref, Math.min(8 * 1024 * 1024, Number(request.maxBytes ?? 8 * 1024 * 1024)))).toString("base64") }; } return this.store.get(ref);
|
|
@@ -188,7 +235,7 @@ export class ProgramRunService {
|
|
|
188
235
|
if (action === "program-reconcile" && (request.publication === true || request.publication === "retry")) return this.reconcileApply(runId, request);
|
|
189
236
|
const state = await this.store.transact(runId, action, current => {
|
|
190
237
|
programAssert(current, "Run not found");
|
|
191
|
-
programAssert(current.status !== "applied" && (current.status !== "canceled" || action === "program-reconcile"), "Terminal runs require an explicit successor");
|
|
238
|
+
programAssert((current.status !== "applied" || action === "program-reconcile" && request.resolution === "usage") && (current.status !== "canceled" || action === "program-reconcile"), "Terminal runs require an explicit successor");
|
|
192
239
|
if (current.status === "applying" || current.status === "apply-uncertain") {
|
|
193
240
|
programAssert(action === "program-signal" || action === "program-cancel", "Apply intent frozen; reconcile before changing execution");
|
|
194
241
|
}
|
|
@@ -236,13 +283,26 @@ export class ProgramRunService {
|
|
|
236
283
|
current.program = program; current.programDigest = programDigest(program); current.planRevision++; current.semanticVersion++;
|
|
237
284
|
delete current.gateRef; delete current.acceptedCandidate; delete current.resultRef;
|
|
238
285
|
} else if (action === "program-reconcile") {
|
|
239
|
-
programAssert(request.resolution
|
|
286
|
+
programAssert(["failed", "usage"].includes(String(request.resolution)) && typeof request.reason === "string" && request.reason.length > 0, "Reconciliation requires observed failure/effect evidence");
|
|
287
|
+
if (request.resolution === "usage") {
|
|
288
|
+
const attempt = current.attempts[idText(request.attemptId)];
|
|
289
|
+
programAssert(attempt && attempt.outcome !== "running" && summarizeProgramUsage(attempt).certainty === "unknown" && typeof request.authorization === "string" && request.authorization.length > 0, "Usage reconciliation requires unknown consumption, stopped attempt and authorized evidence");
|
|
290
|
+
const usage = asRecord(request.usage) as unknown as NonNullable<ProgramWorkerObservation["usage"]>;
|
|
291
|
+
programAssert(Number.isSafeInteger(usage.input_tokens) && Number.isSafeInteger(usage.output_tokens), "Observed usage counters required");
|
|
292
|
+
recordProgramObservation(attempt, { id: `reconcile:${idText(request.requestId)}`, type: "usage.reconciled", source: "owner-evidence", at: new Date().toISOString(), usage, metadata: { reason: String(request.reason), authorization: request.authorization } });
|
|
293
|
+
return current;
|
|
294
|
+
}
|
|
240
295
|
if (request.operationId) {
|
|
241
296
|
const operation = current.operations[idText(request.operationId)]; programAssert(operation?.status === "uncertain" && !Object.values(current.attempts).some(attempt => (attempt.operationId === operation.id || attempt.operationId.startsWith(`${operation.id}/`)) && (attempt.outcome === "running" || attempt.outcome === "uncertain")), "Observe/reconcile worker attempts first");
|
|
242
297
|
operation.status = operation.retired ? "skipped" : "needs-attention"; operation.reason = String(request.reason); return current;
|
|
243
298
|
}
|
|
244
299
|
const attempt = current.attempts[idText(request.attemptId)]; programAssert(attempt?.outcome === "uncertain", "Only uncertain attempts can be reconciled");
|
|
245
|
-
|
|
300
|
+
if (request.resumeThread === true) {
|
|
301
|
+
const observation = asRecord(request.observation);
|
|
302
|
+
programAssert(observation.terminated === true && typeof observation.workspaceDigest === "string" && /^sha256:[a-f0-9]{64}$/.test(observation.workspaceDigest) && attempt.threadId && attempt.recovery && current.operations[attempt.operationId]?.generation === attempt.generation && !current.operations[attempt.operationId]?.retired, "Resume requires observed termination, matching generation and workspace identity");
|
|
303
|
+
attempt.recovery = { ...attempt.recovery, threadId: attempt.threadId, workspaceDigest: observation.workspaceDigest, terminationObserved: true, generationCompatible: true };
|
|
304
|
+
}
|
|
305
|
+
attempt.outcome = "infrastructure_failed"; attempt.reason = String(request.reason); if (attempt.resourceReservation) attempt.resourceReservation.status = "released"; if (attempt.reservation) attempt.reservation.status = "released";
|
|
246
306
|
const operation = current.operations[attempt.operationId]; if (operation) { operation.status = operation.retired ? "skipped" : "needs-attention"; operation.reason = String(request.reason); }
|
|
247
307
|
}
|
|
248
308
|
return current;
|
|
@@ -251,7 +311,7 @@ export class ProgramRunService {
|
|
|
251
311
|
if (action === "program-replan" && request.mode === "fenced") for (const attempt of Object.values(state.attempts)) if (attempt.outcome === "uncertain") this.attemptControllers.get(attempt.attemptId)?.abort();
|
|
252
312
|
if (action === "program-reconcile") for (const attempt of Object.values(state.attempts)) if (attempt.reservation?.status === "released") { this.reservations.get(attempt.attemptId)?.(); this.reservations.delete(attempt.attemptId); }
|
|
253
313
|
if (action === "program-resume") this.schedule(runId);
|
|
254
|
-
if (action === "program-signal" && ["waiting", "executing"].includes(state.status)) { await this.store.transact(runId, "signal-wakeup", current => { if (current!.status === "waiting") current!.status = "executing"; return current!; }); this.schedule(runId); }
|
|
314
|
+
if (action === "program-signal" && ["waiting", "executing"].includes(state.status)) { await this.store.transact(runId, "signal-wakeup", current => { if (current!.status === "waiting") { current!.status = "executing"; delete current!.reason; } return current!; }); this.schedule(runId); }
|
|
255
315
|
return state;
|
|
256
316
|
}
|
|
257
317
|
private async run(runId: string): Promise<void> {
|
|
@@ -280,7 +340,7 @@ export class ProgramRunService {
|
|
|
280
340
|
if (data?.status === "accepted") programAssert(final.gateRef && final.acceptedCandidate, "Accepted result requires current owner gate");
|
|
281
341
|
if (final.gateRef && (data?.candidate || data?.acceptedCandidate)) programAssert(this.resolveCandidate(final, data.candidate ?? data.acceptedCandidate).digest === final.acceptedCandidate?.digest, "Result candidate differs from the owner acceptance gate");
|
|
282
342
|
const resultRef = await this.store.put(result, registryEntry(state.registry.policies, state.program.policy).maxOutputBytes);
|
|
283
|
-
await this.store.transact(runId, "program-result", current => { programAssert(current?.status === "executing" && current.programDigest === state.programDigest && current.semanticVersion === state.semanticVersion, "Execution paused, canceled or revised"); current.resultRef = resultRef; current.status = current.gateRef ? "acceptance-ready" : "completed"; return current; });
|
|
343
|
+
await this.store.transact(runId, "program-result", current => { programAssert(current?.status === "executing" && current.programDigest === state.programDigest && current.semanticVersion === state.semanticVersion, "Execution paused, canceled or revised"); current.resultRef = resultRef; current.status = current.gateRef ? "acceptance-ready" : "completed"; delete current.reason; return current; });
|
|
284
344
|
} catch (error) {
|
|
285
345
|
await this.store.transact(runId, "execution-stopped", current => {
|
|
286
346
|
const state = current!;
|
|
@@ -418,7 +478,7 @@ export class ProgramRunService {
|
|
|
418
478
|
const run = await this.state(runId); this.resolveCandidate(run, candidate);
|
|
419
479
|
const artifacts: ManagedArtifact[] = []; for (const ref of candidate.deltas) { const delta = run.deltas[ref]; programAssert(delta, "Delta does not belong to this run"); artifacts.push(await this.store.get<ManagedArtifact>(delta.artifactRef)); } return artifacts;
|
|
420
480
|
}
|
|
421
|
-
private async activity(runId: string, id: string, ref: ProgramRef, data: unknown, candidate: ProgramCandidate, requestedScope: string[], context?: Context): Promise<ActivityOutput> {
|
|
481
|
+
private async activity(runId: string, id: string, ref: ProgramRef, data: unknown, candidate: ProgramCandidate, requestedScope: string[], context?: Context, finalAssessment = false): Promise<ActivityOutput> {
|
|
422
482
|
const run = await this.checkRunning(runId), executor = registryEntry(run.registry.executors, ref), policy = registryEntry(run.registry.policies, run.program.policy), acceptance = registryEntry(run.registry.acceptance, run.program.acceptance);
|
|
423
483
|
if (executor.inputSchema) validateProgramData(data, registryEntry(run.registry.schemas, executor.inputSchema));
|
|
424
484
|
candidate = this.resolveCandidate(run, candidate);
|
|
@@ -445,6 +505,13 @@ export class ProgramRunService {
|
|
|
445
505
|
finally { clearTimeout(admissionDeadline); parentSignal.removeEventListener("abort", abortAdmission); }
|
|
446
506
|
this.reservations.set(attemptId, release);
|
|
447
507
|
let completionObserved = false;
|
|
508
|
+
const pendingObservations: ProgramWorkerObservation[] = [];
|
|
509
|
+
let dispatched = false;
|
|
510
|
+
const onObservation = async (observation: ProgramWorkerObservation) => {
|
|
511
|
+
if (!dispatched) { programAssert(pendingObservations.length < 1024, "Preparation observation budget exceeded"); pendingObservations.push(observation); return; }
|
|
512
|
+
await this.owner.assert();
|
|
513
|
+
await this.store.transact(runId, "worker-observation", current => { const attempt = current!.attempts[attemptId]; programAssert(attempt?.outcome === "running" && attempt.reservation?.ownerEpoch === this.owner.epoch && attempt.generation === current!.operations[id].generation, "Fenced worker observation"); recordProgramObservation(attempt, observation); return current!; });
|
|
514
|
+
};
|
|
448
515
|
let timeout: ReturnType<typeof setTimeout> | undefined, removeAbort: (() => void) | undefined;
|
|
449
516
|
try {
|
|
450
517
|
await this.checkRunning(runId);
|
|
@@ -455,8 +522,9 @@ export class ProgramRunService {
|
|
|
455
522
|
const base = run.baseRef ? await this.store.get<ManagedBase>(run.baseRef) : undefined;
|
|
456
523
|
const evidence = [];
|
|
457
524
|
for (const ref of (data as { assessmentContext?: ProgramAssessmentContext })?.assessmentContext?.evidenceRefs ?? []) { const capture = run.artifactRecords?.[ref]; programAssert(capture && capture.candidateDigest === candidate.digest, "Evidence ref outside current candidate"); evidence.push({ receiptId: ref, bytes: await this.store.getBinary(capture.artifactRef), mime: capture.mime, itemKey: capture.itemKey }); }
|
|
458
|
-
const
|
|
459
|
-
|
|
525
|
+
const recovery = (() => { const prior = [...(completed?.attempts ?? [])].reverse().map(attempt => run.attempts[attempt]).find(attempt => attempt?.recovery?.terminationObserved && attempt.recovery.generationCompatible && attempt.inputDigest === inputDigest && attempt.executorDigest === programDigest(executor) && attempt.generation === completed?.generation); return prior?.recovery as Parameters<ProgramWorkerAdapter>[0]["recovery"]; })();
|
|
526
|
+
const preparation = await this.cancellable(this.adapter({ executor, data, candidate, artifacts: await this.artifacts(runId, candidate), base, attemptId, scope, signal: controller.signal, registry: run.registry, evidence, onObservation, recovery,
|
|
527
|
+
onThread: async threadId => { await this.owner.assert(); await this.store.transact(runId, "worker-thread", current => { const attempt = current!.attempts[attemptId]; programAssert(attempt?.outcome === "running" && attempt.reservation?.ownerEpoch === this.owner.epoch && attempt.generation === current!.operations[id].generation, "Fenced worker callback"); attempt.threadId = threadId; return current!; }); } }), controller.signal, () => { throw new ProgramError("AF_PROGRAM_STOPPED", "Preparation canceled before dispatch"); });
|
|
460
528
|
|
|
461
529
|
await this.store.transact(runId, "activity-materialize", current => {
|
|
462
530
|
programAssert(current?.status === "executing" && !controller.signal.aborted, "Preparation stopped before dispatch");
|
|
@@ -465,14 +533,31 @@ export class ProgramRunService {
|
|
|
465
533
|
current!.operations[id] = { id, kind: old?.kind ?? "activity", semanticDigest: programDigest(executor), inputDigest, generation: Math.max(old?.generation ?? 0, parentGeneration), status: "running", attempts: old?.attempts ?? [], ...(old?.dependencies ? { dependencies: old.dependencies } : {}) }; return current!;
|
|
466
534
|
});
|
|
467
535
|
await this.authorize(run, ref);
|
|
536
|
+
await this.admit(async () => {
|
|
537
|
+
const currentState = await this.state(runId), resourcePolicy = policy.resources;
|
|
538
|
+
programAssert(this.runtime.ownerMaxTokens === undefined || resourcePolicy, "Owner budget requires per-run reservation policy", "AF_PROGRAM_BUDGET");
|
|
539
|
+
const reservationTokens = executor.tokenAccounting === "none" ? 0 : resourcePolicy?.reserveTokensPerAttempt ?? 0;
|
|
540
|
+
const ownAttempts = Object.values(currentState.attempts);
|
|
541
|
+
const spent = ownAttempts.reduce((sum, attempt) => sum + programResourceLiability(attempt), 0);
|
|
542
|
+
if (resourcePolicy) {
|
|
543
|
+
programAssert(resourcePolicy.unknownUsage === "allow" || !ownAttempts.some(attempt => attempt.resourceReservation?.status !== "held" && summarizeProgramUsage(attempt).certainty === "unknown"), "Unknown usage requires explicit policy allowance", "AF_PROGRAM_BUDGET");
|
|
544
|
+
const finalSpent = ownAttempts.filter(attempt => attempt.resourceReservation?.phase === "final").reduce((sum, attempt) => sum + programResourceLiability(attempt), 0);
|
|
545
|
+
const finalReserve = Math.max(0, (resourcePolicy.finalReserveTokens ?? 0) - finalSpent - (finalAssessment ? reservationTokens : 0));
|
|
546
|
+
programAssert(spent + reservationTokens + finalReserve <= resourcePolicy.maxTokens, "Run admission token budget exhausted", "AF_PROGRAM_BUDGET");
|
|
547
|
+
}
|
|
548
|
+
if (this.runtime.ownerMaxTokens !== undefined) { const ownerResources = await this.resourceSummary(); programAssert(resourcePolicy?.unknownUsage === "allow" || !ownerResources.runs.some(run => run.unknownAttempts > 0), "Owner consumption includes unknown attempts", "AF_PROGRAM_BUDGET"); programAssert(ownerResources.liabilityTokens + ownerResources.finalReservedTokens + reservationTokens - (finalAssessment ? Math.min(reservationTokens, ownerResources.runs.find(run => run.runId === runId)?.finalReservedTokens ?? 0) : 0) <= this.runtime.ownerMaxTokens, "Owner admission token budget exhausted", "AF_PROGRAM_BUDGET"); }
|
|
468
549
|
await this.store.transact(runId, "attempt-dispatch-intent", current => {
|
|
469
550
|
programAssert(current?.status === "executing" && current.totalAttempts < policy.maxAttempts, "Attempt budget exhausted or run stopped");
|
|
470
|
-
current.totalAttempts++; current.attempts[attemptId] = { attemptId, operationId: id, generation: current.operations[id].generation, inputDigest, executorDigest: programDigest(executor), outcome: "running", startedAt: new Date().toISOString(), reservation: { scopes, status: "held", ownerEpoch: this.owner.epoch } };
|
|
551
|
+
current.totalAttempts++; current.attempts[attemptId] = { attemptId, operationId: id, generation: current.operations[id].generation, inputDigest, executorDigest: programDigest(executor), outcome: "running", startedAt: new Date().toISOString(), ...(recovery?.threadId ? { threadId: recovery.threadId } : {}), usageApplicability: executor.tokenAccounting === "none" ? "not-applicable" : "provider", reservation: { scopes, status: "held", ownerEpoch: this.owner.epoch }, ...(policy.resources ? { resourceReservation: { tokens: reservationTokens, status: "held" as const, phase: finalAssessment ? "final" as const : "work" as const } } : {}), ...(preparation.recovery ? { recovery: preparation.recovery } : {}) };
|
|
471
552
|
current.operations[id].attempts.push(attemptId); current.queue = current.queue?.filter(entry => entry.id !== id); return current;
|
|
472
553
|
});
|
|
554
|
+
});
|
|
555
|
+
dispatched = true;
|
|
556
|
+
for (const observation of pendingObservations) await onObservation(observation);
|
|
473
557
|
const result = await this.cancellable(preparation.execute(), controller.signal, () => ({ outcome: "uncertain" as const, reason: "Worker did not confirm cancellation; reservation retained" }), 1000); await this.owner.assert(); completionObserved = result.outcome === "completed"; clearTimeout(timeout); parentSignal.removeEventListener("abort", abort);
|
|
558
|
+
if (result.usage !== undefined) await this.store.transact(runId, "worker-final-usage", current => { const attempt = current!.attempts[attemptId]; programAssert(attempt?.outcome === "running" && attempt.generation === current!.operations[id].generation, "Fenced usage result"); attempt.usage = result.usage; const usage = result.usage as { input_tokens?: number; cached_input_tokens?: number; output_tokens?: number }; if (summarizeProgramUsage(attempt).certainty === "unknown" && Number.isSafeInteger(usage.input_tokens) && Number.isSafeInteger(usage.output_tokens)) recordProgramObservation(attempt, { id: `${attemptId}:final-usage`, type: "usage", source: "adapter-result", at: new Date().toISOString(), usage: usage as NonNullable<ProgramWorkerObservation["usage"]> }); attempt.usageSummary = summarizeProgramUsage(attempt); return current!; });
|
|
474
559
|
if (result.outcome !== "completed") {
|
|
475
|
-
await this.store.transact(runId, "attempt-failed", current => { const attempt = current!.attempts[attemptId]; if (attempt.outcome === "running" && attempt.generation === current!.operations[id].generation) { attempt.outcome = result.outcome; attempt.completedAt = new Date().toISOString(); attempt.reason = result.reason ?? result.outcome; if (
|
|
560
|
+
await this.store.transact(runId, "attempt-failed", current => { const attempt = current!.attempts[attemptId]; if (attempt.outcome === "running" && attempt.generation === current!.operations[id].generation) { attempt.outcome = result.outcome; attempt.completedAt = new Date().toISOString(); attempt.reason = result.reason ?? result.outcome; if (result.outcome !== "uncertain") { if (attempt.reservation) attempt.reservation.status = "released"; if (attempt.resourceReservation) attempt.resourceReservation.status = "released"; } } return current!; });
|
|
476
561
|
throw new ProgramError("AF_PROGRAM_ACTIVITY", result.reason ?? result.outcome);
|
|
477
562
|
}
|
|
478
563
|
validateProgramData(result.data, registryEntry(run.registry.schemas, executor.schema));
|
|
@@ -502,7 +587,7 @@ export class ProgramRunService {
|
|
|
502
587
|
}
|
|
503
588
|
const output: ActivityOutput = { data: result.data, candidate: next, attemptId, ...(evidenceRefs.length ? { evidenceRefs } : {}) }, outputRef = await this.store.put(output, policy.maxOutputBytes);
|
|
504
589
|
await this.store.transact(runId, "attempt-result", current => {
|
|
505
|
-
const attempt = current!.attempts[attemptId]; programAssert(attempt.outcome === "running" && attempt.generation === current!.operations[id].generation, "Fenced attempt result"); attempt.outcome = "completed"; attempt.completedAt = new Date().toISOString(); attempt.outputRef = outputRef; if (attempt.reservation) attempt.reservation.status = "released";
|
|
590
|
+
const attempt = current!.attempts[attemptId]; programAssert(attempt.outcome === "running" && attempt.generation === current!.operations[id].generation, "Fenced attempt result"); attempt.outcome = "completed"; attempt.completedAt = new Date().toISOString(); attempt.outputRef = outputRef; if (attempt.reservation) attempt.reservation.status = "released"; if (attempt.resourceReservation) attempt.resourceReservation.status = "released";
|
|
506
591
|
Object.assign(current!.artifactRecords ??= {}, captureRecords);
|
|
507
592
|
if (deltaId && deltaRecord) current!.deltas[deltaId] = deltaRecord;
|
|
508
593
|
if (result.usage !== undefined) attempt.usage = result.usage;
|
|
@@ -510,7 +595,7 @@ export class ProgramRunService {
|
|
|
510
595
|
const operation = current!.operations[id]; operation.status = "completed"; operation.outputRef = outputRef; return current!;
|
|
511
596
|
}); return output;
|
|
512
597
|
} catch (error) {
|
|
513
|
-
await this.store.transact(runId, "dispatch-uncertainty", current => { const attempt = current!.attempts[attemptId]; if (attempt?.outcome === "running") { attempt.outcome = completionObserved ? "invalid_output" : "uncertain"; attempt.reason = (error as Error).message.slice(0, 1000); current!.operations[id].status = completionObserved ? "needs-attention" : "uncertain"; if (completionObserved
|
|
598
|
+
await this.store.transact(runId, "dispatch-uncertainty", current => { const attempt = current!.attempts[attemptId]; if (attempt?.outcome === "running") { attempt.outcome = completionObserved ? "invalid_output" : "uncertain"; attempt.reason = (error as Error).message.slice(0, 1000); current!.operations[id].status = completionObserved ? "needs-attention" : "uncertain"; if (completionObserved) { if (attempt.reservation) attempt.reservation.status = "released"; if (attempt.resourceReservation) attempt.resourceReservation.status = "released"; } } return current!; }); throw error;
|
|
514
599
|
} finally { if (timeout) clearTimeout(timeout); removeAbort?.(); this.attemptControllers.delete(attemptId); const observed = (await this.state(runId)).attempts[attemptId];
|
|
515
600
|
if (!observed || observed.outcome !== "uncertain") { release(); this.reservations.delete(attemptId); } }
|
|
516
601
|
}
|
|
@@ -591,12 +676,48 @@ export class ProgramRunService {
|
|
|
591
676
|
programAssert(candidate.baselineDigest === run.baselineDigest && run.candidates[candidate.candidateId]?.digest === candidate.digest, "Candidate ancestry/baseline mismatch");
|
|
592
677
|
for (const ref of candidate.deltas) if (!seen.has(ref)) { seen.add(ref); refs.push(ref); }
|
|
593
678
|
}
|
|
594
|
-
const artifacts = await Promise.all(refs.map(ref => { programAssert(run.deltas[ref], "Composition delta missing"); return this.store.get<ManagedArtifact>(run.deltas[ref].artifactRef); })), writers = new Map<string, string>();
|
|
679
|
+
const artifacts = await Promise.all(refs.map(ref => { programAssert(run.deltas[ref], "Composition delta missing"); return this.store.get<ManagedArtifact>(run.deltas[ref].artifactRef); })), writers = new Map<string, string>(), conflicts = new Set<string>();
|
|
680
|
+
interface WriterNode { records: { path: string; prior: string }[]; children: Map<string, WriterNode> }
|
|
681
|
+
const root: WriterNode = { records: [], children: new Map() };
|
|
595
682
|
for (let index = 0; index < artifacts.length; index++) for (const file of artifacts[index].files) {
|
|
596
|
-
const
|
|
597
|
-
|
|
683
|
+
const compare = ({ path, prior }: { path: string; prior: string }) => {
|
|
684
|
+
if (prior === refs[index] || candidates.some(candidate => candidate.deltas.includes(prior) && candidate.deltas.includes(refs[index]) && candidate.deltas.indexOf(prior) < candidate.deltas.indexOf(refs[index]))) return;
|
|
685
|
+
conflicts.add(path); conflicts.add(file.path);
|
|
686
|
+
};
|
|
687
|
+
let node = root;
|
|
688
|
+
for (const segment of file.path.toLowerCase().split("/")) {
|
|
689
|
+
for (const record of node.records) compare(record);
|
|
690
|
+
if (!node.children.has(segment)) node.children.set(segment, { records: [], children: new Map() });
|
|
691
|
+
node = node.children.get(segment)!;
|
|
692
|
+
}
|
|
693
|
+
const descendants = [node];
|
|
694
|
+
while (descendants.length) { const next = descendants.pop()!; for (const record of next.records) compare(record); descendants.push(...next.children.values()); }
|
|
695
|
+
node.records.push({ path: file.path, prior: refs[index] });
|
|
598
696
|
writers.set(file.path, refs[index]);
|
|
599
697
|
}
|
|
698
|
+
if (conflicts.size) {
|
|
699
|
+
const acceptance = registryEntry(run.registry.acceptance, run.program.acceptance);
|
|
700
|
+
programAssert(input.resolver, `Independent writers conflict at ${[...conflicts].join(", ")}`);
|
|
701
|
+
const resolver = registryEntry(run.registry.executors, input.resolver as ProgramRef);
|
|
702
|
+
programAssert(resolver.effect === "isolated-write" && resolver.role === "implementer", "Resolver must produce an isolated candidate");
|
|
703
|
+
const scope = (input.resolverWriteScope ?? [...writers.keys()]) as string[];
|
|
704
|
+
programAssert([...writers.keys()].every(path => programWithin(path, scope)), "Resolution scope must cover all proposed files");
|
|
705
|
+
programValueBytes({ artifacts, candidates }, policy.maxOutputBytes, policy.maxItems);
|
|
706
|
+
const baseline = await this.initialCandidate(runId);
|
|
707
|
+
const resolution = await this.activity(runId, `${id}/resolution`, input.resolver as ProgramRef, { input: input.resolverInput ?? {}, conflicts: [...conflicts], candidates, artifacts, instruction: "Resolve all proposed changes against the baseline; this output requires fresh final assessment." }, baseline, scope, context);
|
|
708
|
+
programAssert(resolution.candidate.digest !== baseline.digest || acceptance.allowNoWork, "Conflict resolution produced no changes outside allowNoWork contract");
|
|
709
|
+
if (resolution.candidate.digest === baseline.digest) {
|
|
710
|
+
resolution.candidate = { ...baseline, candidateId: `candidate-resolution-${resolution.attemptId}`, parents: [baseline.candidateId], producerAttempts: [resolution.attemptId] };
|
|
711
|
+
await this.store.transact(runId, "composition-no-work-candidate", current => { current!.candidates[resolution.candidate.candidateId] = resolution.candidate; return current!; });
|
|
712
|
+
}
|
|
713
|
+
await this.store.transact(runId, "composition-resolution", current => {
|
|
714
|
+
const operation = current!.operations[id], receiptId = `resolution-${resolution.attemptId}`;
|
|
715
|
+
programAssert(operation && current!.attempts[resolution.attemptId]?.outcome === "completed", "Unobserved conflict resolution");
|
|
716
|
+
(current!.compositionResolutions ??= {})[receiptId] = { receiptId, candidateId: resolution.candidate.candidateId, candidateDigest: resolution.candidate.digest, originalCandidateIds: candidates.map(candidate => candidate.candidateId), attemptId: resolution.attemptId, operationId: id, generation: operation.generation, semanticDigest: operation.semanticDigest, inputDigest: operation.inputDigest, populationDigest: run.program.population ? programDigest(registryEntry(run.registry.populations, run.program.population)) : programDigest(null), acceptanceDigest: programDigest(acceptance) };
|
|
717
|
+
return current!;
|
|
718
|
+
});
|
|
719
|
+
return { candidate: resolution.candidate, resolution: { attemptId: resolution.attemptId, conflictingPaths: [...conflicts], requiresFinalAssessment: true } };
|
|
720
|
+
}
|
|
600
721
|
const base = run.baseRef ? await this.store.get<ManagedBase>(run.baseRef) : undefined, digest = base ? (await previewManagedArtifacts(base, artifacts)).digest : programDigest({ candidates: candidates.map(candidate => candidate.digest), refs });
|
|
601
722
|
const parents = candidates.map(candidate => candidate.candidateId), producerAttempts = [...new Set(candidates.flatMap(candidate => candidate.producerAttempts))];
|
|
602
723
|
const candidate: ProgramCandidate = { candidateId: `candidate-${programDigest({ digest, id, parents, refs, producerAttempts }).slice(7, 39)}`, digest, baselineDigest: run.baselineDigest, parents, deltas: refs, producerAttempts };
|
|
@@ -660,28 +781,32 @@ export class ProgramRunService {
|
|
|
660
781
|
const environmentRef = programDigest({ baseline: run.baselineDigest, inputRef: run.inputRef, policy: run.program.policy });
|
|
661
782
|
const assess = async (): Promise<ProgramAssessment> => {
|
|
662
783
|
const candidate = checkpoint.candidate;
|
|
663
|
-
const
|
|
784
|
+
const candidateResolutions = phase === "final" ? this.compositionResolutions(await this.state(runId), candidate) : [];
|
|
785
|
+
const resolutionObligations = candidateResolutions.flatMap(receipt => expectedMembers.map(key => `${receipt.receiptId}/${programSegment(key)}`));
|
|
786
|
+
const assessedObligations = [...new Set([...obligationIds, ...resolutionObligations])];
|
|
787
|
+
const assessmentContext: ProgramAssessmentContext = { runId, invocationId: id, assessmentId: `${id}/assessment-${checkpoint.assessments}`, phase, candidateRef: candidate, obligationIds: assessedObligations, ...(itemKey ? { itemKey } : {}), environmentRef, evidenceRefs: [], input: input.input ?? null, ...(population?.visualCases ? { visualCases: population.visualCases.filter(entry => expectedMembers.includes(canonicalItemKey(entry.itemKey)) && (phase === "final" || canonicalItemKey(entry.itemKey) === itemKey)) } : {}) };
|
|
664
788
|
const evidenceAttempts: string[] = [];
|
|
665
789
|
for (const [index, executor] of ((input.evidence ?? []) as ProgramRef[]).entries()) {
|
|
666
|
-
const evidence = await this.activity(runId, `${id}/assessment-${checkpoint.assessments}/evidence-${index}`, executor, { ...activityInputs, assessmentContext, candidateDigest: candidate.digest }, candidate, [], context);
|
|
790
|
+
const evidence = await this.activity(runId, `${id}/assessment-${checkpoint.assessments}/evidence-${index}`, executor, { ...activityInputs, assessmentContext, candidateDigest: candidate.digest }, candidate, [], context, phase === "final");
|
|
667
791
|
assessmentContext.evidenceRefs.push(...(evidence.evidenceRefs ?? [])); evidenceAttempts.push(evidence.attemptId);
|
|
668
792
|
}
|
|
669
793
|
const observed = await this.state(runId);
|
|
670
794
|
const requiredCaptureKeys = captureKeys(acceptance, phase, expectedMembers, itemKey);
|
|
671
795
|
for (const key of requiredCaptureKeys) programAssert(assessmentContext.evidenceRefs.some(ref => { const capture = observed.artifactRecords?.[ref]; return capture?.itemKey === key && capture.candidateDigest === candidate.digest && capture.environmentRef === environmentRef && observed.attempts[capture.producerAttemptId]?.outcome === "completed"; }), `Missing current capture for ${key}`);
|
|
672
|
-
const review = await this.activity(runId, `${id}/assessment-${checkpoint.assessments}/review`, input.review as ProgramRef, { ...activityInputs, assessmentContext, candidateDigest: candidate.digest, feedback: checkpoint.feedback }, candidate, [], context), report = asRecord(review.data);
|
|
796
|
+
const review = await this.activity(runId, `${id}/assessment-${checkpoint.assessments}/review`, input.review as ProgramRef, { ...activityInputs, assessmentContext, candidateDigest: candidate.digest, feedback: checkpoint.feedback }, candidate, [], context, phase === "final"), report = asRecord(review.data);
|
|
673
797
|
programAssert(["approved", "changes_requested", "inconclusive"].includes(String(report.verdict)) && Array.isArray(report.findings), "Review verdict/findings required");
|
|
674
798
|
const outcomes: { id: string; passed: boolean }[] = [], attemptIds = [...evidenceAttempts, review.attemptId];
|
|
675
|
-
|
|
799
|
+
const assessmentChecks = [...new Set([...checks, ...(candidateResolutions.length ? acceptance.requiredChecksByScope?.item ?? acceptance.requiredChecks : [])])];
|
|
800
|
+
for (const check of assessmentChecks) {
|
|
676
801
|
programAssert(check.includes("@"), "Check must be versioned executor reference"); const split = check.lastIndexOf("@"), reference = { id: check.slice(0, split), version: check.slice(split + 1) }, checker = registryEntry(run.registry.executors, reference);
|
|
677
802
|
programAssert(checker.kind === "command" && checker.effect === "read" && programDigest(checker) !== programDigest(reviewer), "Checks require distinct observed command executors");
|
|
678
|
-
const output = await this.activity(runId, `${id}/assessment-${checkpoint.assessments}/check-${encodeURIComponent(check)}`, reference, { ...activityInputs, assessmentContext, candidateDigest: candidate.digest }, candidate, [], context);
|
|
803
|
+
const output = await this.activity(runId, `${id}/assessment-${checkpoint.assessments}/check-${encodeURIComponent(check)}`, reference, { ...activityInputs, assessmentContext, candidateDigest: candidate.digest }, candidate, [], context, phase === "final");
|
|
679
804
|
outcomes.push({ id: check, passed: asRecord(output.data).passed === true }); attemptIds.push(output.attemptId);
|
|
680
805
|
}
|
|
681
806
|
const covered = Array.isArray(report.coveredObligationIds) ? report.coveredObligationIds as string[] : [];
|
|
682
807
|
if (obligationIds.some(obligation => !covered.includes(obligation))) { report.verdict = "inconclusive"; report.findings = []; }
|
|
683
|
-
const satisfied = report.verdict === "approved" && !(report.findings as unknown[]).length && outcomes.every(outcome => outcome.passed) ?
|
|
684
|
-
const assessment: ProgramAssessment = { phase, obligationIds, satisfiedObligationIds: satisfied, unsatisfiedObligationIds:
|
|
808
|
+
const satisfied = report.verdict === "approved" && !(report.findings as unknown[]).length && outcomes.every(outcome => outcome.passed) ? assessedObligations.filter(obligation => covered.includes(obligation)) : [];
|
|
809
|
+
const assessment: ProgramAssessment = { phase, resolutionReceiptIds: candidateResolutions.map(receipt => receipt.receiptId), obligationIds: assessedObligations, satisfiedObligationIds: satisfied, unsatisfiedObligationIds: assessedObligations.filter(obligation => !satisfied.includes(obligation)), environmentRef, evidenceRefs: assessmentContext.evidenceRefs, contractDigest: programDigest(acceptance), receiptId: `assessment-${randomUUID()}`, operationId: id, generation: origin.generation, semanticDigest: origin.semanticDigest, inputDigest: origin.inputDigest, candidateDigest: candidate.digest, verdict: report.verdict as ProgramAssessment["verdict"], findings: report.findings as unknown[], checks: outcomes, attemptIds };
|
|
685
810
|
await this.store.put(assessment);
|
|
686
811
|
checkpoint.assessment = assessment; checkpoint.phase = "decide";
|
|
687
812
|
await this.store.transact(runId, "assessment-receipt-and-recipe", current => { programAssert(current!.operations[id].generation === origin.generation && current!.operations[id].semanticDigest === origin.semanticDigest && attemptIds.every(attemptId => current!.attempts[attemptId]?.outcome === "completed") && !candidate.producerAttempts.includes(review.attemptId), "Assessment lacks current distinct observed attempts"); current!.assessments[assessment.receiptId!] = assessment; current!.repairs[id] = structuredClone(checkpoint); return current!; });
|
|
@@ -721,25 +846,37 @@ export class ProgramRunService {
|
|
|
721
846
|
}
|
|
722
847
|
}
|
|
723
848
|
}
|
|
849
|
+
private compositionResolutions(run: ProgramRunV2, candidate: ProgramCandidate) {
|
|
850
|
+
return Object.values(run.compositionResolutions ?? {}).filter(receipt => {
|
|
851
|
+
const resolved = run.candidates[receipt.candidateId];
|
|
852
|
+
return resolved && resolved.deltas.every(delta => candidate.deltas.includes(delta)) && resolved.producerAttempts.every(attempt => candidate.producerAttempts.includes(attempt));
|
|
853
|
+
});
|
|
854
|
+
}
|
|
724
855
|
private async gate(runId: string, id: string, input: Record<string, unknown>): Promise<unknown> {
|
|
725
856
|
const run = await this.state(runId), candidate = this.resolveCandidate(run, input.candidate), acceptance = registryEntry(run.registry.acceptance, run.program.acceptance), policy = registryEntry(run.registry.policies, run.program.policy);
|
|
726
857
|
programAssert(run.candidates[candidate.candidateId]?.digest === candidate.digest, "Unregistered candidate");
|
|
727
858
|
programAssert(input.obligations === undefined || programDigest(input.obligations) === programDigest(acceptance), "Gate cannot replace owner acceptance obligations");
|
|
728
859
|
programAssert(!Object.values(run.attempts).some(attempt => attempt.outcome === "uncertain"), "Uncertain effects block gate");
|
|
729
860
|
programAssert(run.program.mode !== "data", "Data profile cannot grant candidate publication");
|
|
730
|
-
const
|
|
861
|
+
const resolutions = this.compositionResolutions(run, candidate);
|
|
862
|
+
for (const receipt of resolutions) {
|
|
863
|
+
const origin = run.operations[receipt.operationId], attempt = run.attempts[receipt.attemptId];
|
|
864
|
+
programAssert(origin?.status === "completed" && !origin.retired && origin.generation === receipt.generation && origin.semanticDigest === receipt.semanticDigest && origin.inputDigest === receipt.inputDigest && attempt?.outcome === "completed" && run.candidates[receipt.candidateId]?.digest === receipt.candidateDigest && receipt.acceptanceDigest === programDigest(acceptance) && receipt.populationDigest === (run.program.population ? programDigest(registryEntry(run.registry.populations, run.program.population)) : programDigest(null)), "Composition resolution receipt is stale");
|
|
865
|
+
}
|
|
866
|
+
const requiredChecks = [...new Set([...(acceptance.requiredChecksByScope?.final ?? acceptance.requiredChecks), ...(resolutions.length ? acceptance.requiredChecksByScope?.item ?? acceptance.requiredChecks : [])])];
|
|
731
867
|
const expectedMembers = run.program.population ? registryEntry(run.registry.populations, run.program.population).members.filter(key => !registryEntry(run.registry.populations, run.program.population!).exclusions.some(exclusion => exclusion.id === key)).map(canonicalItemKey) : [];
|
|
732
868
|
const requiredObligations = (acceptance.obligations ?? []).flatMap(obligation => obligation.scope === "final" ? [obligation.id] : expectedMembers.map(key => `${obligation.id}/${programSegment(key)}`));
|
|
869
|
+
requiredObligations.push(...resolutions.flatMap(receipt => expectedMembers.map(key => `${receipt.receiptId}/${programSegment(key)}`)));
|
|
733
870
|
const assessment = Object.values(run.assessments).find(assessment => {
|
|
734
871
|
const origin = assessment.operationId ? run.operations[assessment.operationId] : undefined;
|
|
735
|
-
return origin && origin.status === "completed" && origin.generation === assessment.generation && origin.semanticDigest === assessment.semanticDigest && origin.inputDigest === assessment.inputDigest && assessment.candidateDigest === candidate.digest && assessment.verdict === "approved" && !assessment.findings.length && assessment.attemptIds.every(attemptId => run.attempts[attemptId]?.outcome === "completed") && (!acceptance.requiredChecksByScope || assessment.phase === "final") && requiredObligations.every(obligation => assessment.satisfiedObligationIds?.includes(obligation)) && requiredChecks.every(check => assessment.checks.some(outcome => outcome.id === check && outcome.passed));
|
|
872
|
+
return origin && origin.status === "completed" && origin.generation === assessment.generation && origin.semanticDigest === assessment.semanticDigest && origin.inputDigest === assessment.inputDigest && assessment.candidateDigest === candidate.digest && resolutions.every(receipt => assessment.resolutionReceiptIds?.includes(receipt.receiptId)) && assessment.verdict === "approved" && !assessment.findings.length && assessment.attemptIds.every(attemptId => run.attempts[attemptId]?.outcome === "completed") && (!acceptance.requiredChecksByScope && !resolutions.length || assessment.phase === "final") && requiredObligations.every(obligation => assessment.satisfiedObligationIds?.includes(obligation)) && requiredChecks.every(check => assessment.checks.some(outcome => outcome.id === check && outcome.passed));
|
|
736
873
|
});
|
|
737
|
-
programAssert(!acceptance.requireReview || assessment, "Applicable distinct review/checks missing");
|
|
874
|
+
programAssert(!acceptance.requireReview && !resolutions.length || assessment, "Applicable distinct review/checks missing");
|
|
738
875
|
programAssert(assessment || requiredChecks.length === 0, "Required checks missing");
|
|
739
876
|
if (run.program.population) {
|
|
740
877
|
const coverage = asRecord(input.coverage), registered = run.coverageReceipts[String(coverage.receiptId)];
|
|
741
878
|
programAssert(registered && programDigest(Object.fromEntries(Object.entries(coverage).filter(([key]) => key !== "receiptId"))) === programDigest(registered) && registered.populationDigest === programDigest(registryEntry(run.registry.populations, run.program.population)), "Owner coverage receipt missing/mismatched");
|
|
742
|
-
programAssert(Object.entries(run.collections).some(([operationId, collection]) => collection.coverageReceiptId === coverage.receiptId && collection.obligationsSatisfied && collection.seal === run.seals[operationId] && run.operations[operationId]?.status === "completed" && run.operations[operationId]?.generation === collection.generation && programDigest(collection.ids) === programDigest(registered.ids) && collection.acceptedCandidateIds.every(candidateId => { const accepted = run.candidates[candidateId]; return accepted && accepted.deltas.every(delta => candidate.deltas.includes(delta)) && accepted.producerAttempts.every(attempt => candidate.producerAttempts.includes(attempt)); }) && collection.acceptedAssessmentIds.every(receiptId => {
|
|
879
|
+
programAssert(Object.entries(run.collections).some(([operationId, collection]) => collection.coverageReceiptId === coverage.receiptId && collection.obligationsSatisfied && collection.seal === run.seals[operationId] && run.operations[operationId]?.status === "completed" && run.operations[operationId]?.generation === collection.generation && programDigest(collection.ids) === programDigest(registered.ids) && collection.acceptedCandidateIds.every(candidateId => { const accepted = run.candidates[candidateId]; return accepted && (accepted.deltas.every(delta => candidate.deltas.includes(delta)) && accepted.producerAttempts.every(attempt => candidate.producerAttempts.includes(attempt)) || !!assessment && resolutions.some(receipt => receipt.originalCandidateIds.some(originalId => { const original = run.candidates[originalId]; return original && accepted.deltas.every(delta => original.deltas.includes(delta)) && accepted.producerAttempts.every(attempt => original.producerAttempts.includes(attempt)); }))); }) && collection.acceptedAssessmentIds.every(receiptId => {
|
|
743
880
|
const receipt = run.assessments[receiptId], origin = receipt?.operationId ? run.operations[receipt.operationId] : undefined;
|
|
744
881
|
return origin?.status === "completed" && origin.generation === receipt.generation && receipt.verdict === "approved" && !receipt.findings.length && receipt.attemptIds.every(attemptId => run.attempts[attemptId]?.outcome === "completed") && (acceptance.requiredChecksByScope?.item ?? acceptance.requiredChecks).every(check => receipt.checks.some(outcome => outcome.id === check && outcome.passed));
|
|
745
882
|
})), "Population gate requires closed item acceptance, not discovery coverage alone");
|
|
@@ -17,7 +17,7 @@ async function retrySharing<T>(action: () => Promise<T>): Promise<T> {
|
|
|
17
17
|
}
|
|
18
18
|
/** One authoritative atomic record per run; artifacts commit before referring transitions. */
|
|
19
19
|
export class ProgramRunStore {
|
|
20
|
-
readonly metrics = { transactions: 0, transactionMs: 0, bytesWritten: 0, snapshotBytes: 0, journalBytes: 0, envelopeBytes: 0, artifactBytes: 0, artifactReads: 0 };
|
|
20
|
+
readonly metrics = { transactions: 0, transactionMs: 0, bytesWritten: 0, snapshotBytes: 0, journalBytes: 0, envelopeBytes: 0, artifactBytes: 0, artifactReads: 0, deduplicatedWrites: 0, deduplicatedBytes: 0 };
|
|
21
21
|
private tails = new Map<string, Promise<unknown>>();
|
|
22
22
|
private definitions = new Set<string>();
|
|
23
23
|
private validate(state: ProgramRunV2): void {
|
|
@@ -34,6 +34,10 @@ export class ProgramRunStore {
|
|
|
34
34
|
async put(value: unknown, maxBytes = 4 * 1024 * 1024, category: "snapshotBytes" | "journalBytes" | "artifactBytes" = "artifactBytes"): Promise<string> {
|
|
35
35
|
const bytes = stableStringify(value), digest = programDigest(value); programAssert(Buffer.byteLength(bytes) <= maxBytes, "Artifact too large");
|
|
36
36
|
const path = join(this.directory, "artifacts", `${digest.slice(7)}.json`); await assertAttachedSafePath(this.root, path);
|
|
37
|
+
try {
|
|
38
|
+
programAssert(await readFile(path, "utf8") === bytes, "Immutable artifact corrupted");
|
|
39
|
+
this.metrics.deduplicatedWrites++; this.metrics.deduplicatedBytes += Buffer.byteLength(bytes); return digest;
|
|
40
|
+
} catch (error) { if (!absent(error)) throw error; }
|
|
37
41
|
const temp = `${path}.${randomUUID()}.tmp`, handle = await open(temp, "wx", 0o600);
|
|
38
42
|
try { await handle.writeFile(bytes); await handle.sync(); this.metrics.bytesWritten += Buffer.byteLength(bytes); this.metrics[category] += Buffer.byteLength(bytes); } finally { await handle.close(); }
|
|
39
43
|
try {
|
|
@@ -53,6 +57,10 @@ export class ProgramRunStore {
|
|
|
53
57
|
programAssert(bytes.byteLength <= maxBytes, "Binary artifact exceeds byte budget");
|
|
54
58
|
const digest = `sha256:${createHash("sha256").update(bytes).digest("hex")}`, path = join(this.directory, "artifacts", `${digest.slice(7)}.blob`);
|
|
55
59
|
await assertAttachedSafePath(this.root, path);
|
|
60
|
+
try {
|
|
61
|
+
programAssert((await readFile(path)).equals(Buffer.from(bytes)), "Immutable binary corrupted");
|
|
62
|
+
this.metrics.deduplicatedWrites++; this.metrics.deduplicatedBytes += bytes.byteLength; return digest;
|
|
63
|
+
} catch (error) { if (!absent(error)) throw error; }
|
|
56
64
|
const temp = `${path}.${randomUUID()}.tmp`, handle = await open(temp, "wx", 0o600);
|
|
57
65
|
try { await handle.writeFile(bytes); await handle.sync(); this.metrics.bytesWritten += bytes.byteLength; this.metrics.artifactBytes += bytes.byteLength; } finally { await handle.close(); }
|
|
58
66
|
try { try { programAssert((await readFile(path)).equals(Buffer.from(bytes)), "Immutable binary corrupted"); } catch (error) { if (!absent(error)) throw error; await retrySharing(() => rename(temp, path)); } }
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { programAssert, programDigest, programWithin, refKey, registryEntry, validateProgramData, type ProgramRef, type ProgramRegistry, type WorkflowProgramV2 } from "./program-contract.ts";
|
|
2
|
+
import { validateProgramCapabilities } from "./program-worker.ts";
|
|
3
|
+
|
|
4
|
+
export type ProgramTemplateKind = "review" | "bugfix" | "migration";
|
|
5
|
+
export interface ProgramTemplateOptions {
|
|
6
|
+
id?: string;
|
|
7
|
+
inputSchema: ProgramRef; outputSchema: ProgramRef; policy: ProgramRef; acceptance: ProgramRef;
|
|
8
|
+
implement: ProgramRef; review: ProgramRef; population?: ProgramRef;
|
|
9
|
+
resolver?: ProgramRef; resolverWriteScope?: string[];
|
|
10
|
+
evidence?: ProgramRef[];
|
|
11
|
+
approvalSchema?: ProgramRef;
|
|
12
|
+
}
|
|
13
|
+
const expr = ($expr: string, ...args: unknown[]) => ({ $expr, args });
|
|
14
|
+
|
|
15
|
+
/** Owner references only. This constructs finite data; it never dispatches workers. */
|
|
16
|
+
export function buildWorkflowTemplate(kind: ProgramTemplateKind, options: ProgramTemplateOptions, registry: ProgramRegistry): WorkflowProgramV2 {
|
|
17
|
+
programAssert(["review", "bugfix", "migration"].includes(kind), "Unknown workflow template");
|
|
18
|
+
programAssert(options.id === undefined || typeof options.id === "string" && options.id.length > 0 && options.id.length <= 200, "Template identity exceeds supported event correlation bounds");
|
|
19
|
+
const acceptance = registryEntry(registry.acceptance, options.acceptance);
|
|
20
|
+
const policy = registryEntry(registry.policies, options.policy);
|
|
21
|
+
const reviewer = registryEntry(registry.executors, options.review);
|
|
22
|
+
const implementer = registryEntry(registry.executors, options.implement);
|
|
23
|
+
const writeScope = [...new Set([...acceptance.writeScope, ...policy.writeScope, ...implementer.writeScope])].filter(path => [acceptance.writeScope, policy.writeScope, implementer.writeScope].every(scope => programWithin(path, scope)));
|
|
24
|
+
programAssert(kind === "review" || implementer.effect === "isolated-write" && implementer.role === "implementer" && writeScope.length > 0, "Writing templates require an authorized implementer scope");
|
|
25
|
+
validateProgramCapabilities(reviewer, policy, acceptance, []);
|
|
26
|
+
if (kind !== "review") validateProgramCapabilities(implementer, policy, acceptance, writeScope);
|
|
27
|
+
const captureRequired = (acceptance.obligations ?? []).some(obligation => obligation.requiredEvidence?.includes("capture"));
|
|
28
|
+
programAssert((acceptance.obligations ?? []).every(obligation => (obligation.requiredEvidence ?? []).every(evidence => evidence === "capture")), "Template cannot attest unsupported evidence obligations");
|
|
29
|
+
programAssert(!captureRequired || kind === "migration" && options.population && options.evidence?.length, "Capture obligations require a population migration and evidence executors");
|
|
30
|
+
for (const ref of options.evidence ?? []) {
|
|
31
|
+
const executor = registryEntry(registry.executors, ref);
|
|
32
|
+
programAssert(executor.effect === "read", "Template evidence must be readonly");
|
|
33
|
+
validateProgramCapabilities(executor, policy, acceptance, []);
|
|
34
|
+
}
|
|
35
|
+
if (acceptance.requireHumanApproval) {
|
|
36
|
+
programAssert(options.approvalSchema, "Human approval requires an owner-registered approval schema");
|
|
37
|
+
const schema = registryEntry(registry.schemas, options.approvalSchema);
|
|
38
|
+
programAssert(!schema.type || (Array.isArray(schema.type) ? schema.type : [schema.type]).includes("object"), "Approval schema must allow object payloads");
|
|
39
|
+
if (schema.properties?.approved) validateProgramData(true, schema.properties.approved);
|
|
40
|
+
if (schema.const) programAssert(typeof schema.const === "object" && !Array.isArray(schema.const) && schema.const.approved === true, "Approval constant must authorize approval");
|
|
41
|
+
}
|
|
42
|
+
let resolverWriteScope: string[] | undefined;
|
|
43
|
+
if (options.resolver) {
|
|
44
|
+
const resolver = registryEntry(registry.executors, options.resolver);
|
|
45
|
+
programAssert(kind === "migration" && resolver.role === "implementer" && resolver.effect === "isolated-write" && policy.executors.includes(refKey(options.resolver)), "Migration resolver must be an owner-authorized implementer");
|
|
46
|
+
const intersection = [...new Set([...acceptance.writeScope, ...policy.writeScope, ...resolver.writeScope])].filter(path => [acceptance.writeScope, policy.writeScope, resolver.writeScope].every(scope => programWithin(path, scope)));
|
|
47
|
+
resolverWriteScope = validateProgramCapabilities(resolver, policy, acceptance, options.resolverWriteScope ?? intersection);
|
|
48
|
+
}
|
|
49
|
+
programAssert(!options.resolverWriteScope || options.resolver, "Resolver scope requires a resolver");
|
|
50
|
+
programAssert(reviewer.role === "reviewer" && reviewer.effect === "read", "Template requires a readonly reviewer");
|
|
51
|
+
programAssert(programDigest(reviewer) !== programDigest(implementer), "Template requires a distinct reviewer");
|
|
52
|
+
for (const ref of [options.implement, options.review]) programAssert(policy.executors.includes(refKey(ref)), "Template executor not authorized by owner policy");
|
|
53
|
+
for (const key of new Set([...acceptance.requiredChecks, ...(acceptance.requiredChecksByScope?.item ?? []), ...(acceptance.requiredChecksByScope?.final ?? [])])) {
|
|
54
|
+
const checker = registry.executors[key];
|
|
55
|
+
programAssert(checker?.kind === "command" && checker.effect === "read" && policy.executors.includes(key), `Required check not an authorized readonly command: ${key}`);
|
|
56
|
+
programAssert(programDigest(checker) !== programDigest(reviewer), "Required check must be distinct from reviewer");
|
|
57
|
+
validateProgramCapabilities(checker, policy, acceptance, []);
|
|
58
|
+
}
|
|
59
|
+
const scoped = acceptance.requiredChecksByScope;
|
|
60
|
+
const repair = (id: string, initialCandidate: unknown, phase: "item" | "final", entryMode: "assess-first" | "implement-first", scope: unknown) => ({
|
|
61
|
+
kind: "repair", id, options: {
|
|
62
|
+
recipe: { id: "repair", version: "v2" }, implement: options.implement, review: options.review,
|
|
63
|
+
checks: scoped?.[phase] ?? acceptance.requiredChecks,
|
|
64
|
+
...(options.evidence?.length ? { evidence: options.evidence } : {}),
|
|
65
|
+
...(scoped ? { assessmentScope: phase } : {}), entryMode, initialCandidate, writeScope: scope,
|
|
66
|
+
maxRepairRounds: kind === "review" ? 1 : 3, maxAssessmentAttempts: 6, maxInfrastructureAttempts: 2,
|
|
67
|
+
progressPolicy: { unchangedCandidateRounds: 1, repeatedFindingsRounds: 2 },
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
const program: WorkflowProgramV2 = {
|
|
71
|
+
schemaVersion: 2, operatorVersion: 2, mode: "candidate", id: options.id ?? `${kind}-workflow`, version: 1,
|
|
72
|
+
inputSchema: options.inputSchema, outputSchema: options.outputSchema, policy: options.policy, acceptance: options.acceptance,
|
|
73
|
+
steps: [], result: expr("output", "final"),
|
|
74
|
+
};
|
|
75
|
+
if (kind === "migration") {
|
|
76
|
+
programAssert(options.population, "Migration requires an owner population");
|
|
77
|
+
registryEntry(registry.populations, options.population);
|
|
78
|
+
program.population = options.population;
|
|
79
|
+
const items = expr("field", expr("workflowInput"), "items");
|
|
80
|
+
program.steps.push({ kind: "map", id: "migrate", options: {
|
|
81
|
+
items, key: expr("field", expr("item"), "id"),
|
|
82
|
+
coverage: expr("coverageFor", expr("population"), items), completion: "all-required", concurrency: policy.concurrency,
|
|
83
|
+
body: repair("component", expr("candidateFromBaseline", expr("item")), "item", "assess-first", expr("field", expr("item"), "allowedPaths")),
|
|
84
|
+
} }, { kind: "compose", id: "integrate", options: { candidates: expr("acceptedCandidates", "migrate"), onConflict: "needs-resolution", ...(options.resolver ? { resolver: options.resolver, resolverWriteScope } : {}) } });
|
|
85
|
+
}
|
|
86
|
+
program.steps.push(repair("assessment", kind === "migration" ? expr("outputCandidate", "integrate") : expr("candidateFromBaseline"), "final", "assess-first", kind === "review" ? [] : writeScope));
|
|
87
|
+
if (acceptance.requireHumanApproval) program.steps.push({ kind: "waitEvent", id: "human-approval", options: {
|
|
88
|
+
schema: options.approvalSchema!, type: "human-approval", correlation: `${program.id}:approval`, timeoutMs: policy.deadlineMs,
|
|
89
|
+
subject: expr("object", { candidate: expr("acceptedCandidate", "assessment"), contract: expr("acceptance") }),
|
|
90
|
+
} });
|
|
91
|
+
program.steps.push({ kind: "gate", id: "final", options: {
|
|
92
|
+
candidate: expr("acceptedCandidate", "assessment"), ...(kind === "migration" ? { coverage: expr("coverageReceipt", "migrate") } : {}),
|
|
93
|
+
...(acceptance.requireHumanApproval ? { authorization: expr("output", "human-approval") } : {}),
|
|
94
|
+
} });
|
|
95
|
+
return program;
|
|
96
|
+
}
|