Setting the file. One moment. Orchestration State · Replatform · wix/skills · Skills Docsasync function writeJsonAtomic
— line 141
This file
- Number
- 21.17
- Position
- 17 of 20
- Type
- JavaScript
- Size
- 15 KB
- Lines
- 455
lib/orchestration-state.js
JavaScript·455 lines·15 KB
=
new
Set
([
'running'
,
'completed'
,
'partial'
,
'blocked'
,
'failed'
]);
10const ACTIVE_PHASE = new Set(['orchestration', 'discovery', 'mapping', 'setup', 'codegen', 'execution']);
11const ORCHESTRATION_STATES = new Set([
12 'initialized',
13 'awaiting_source',
14 'awaiting_import_scope',
15 'awaiting_credentials',
16 'awaiting_files',
17 'awaiting_destination_strategy',
18 'preflight_running',
19 'preflight_blocked',
20 'discovery_running',
21 'discovery_complete',
22 'mapping_running',
23 'awaiting_mapping_approval',
24 'mapping_approved',
25 'setup_discovery_running',
26 'setup_complete',
27 'codegen_running',
28 'awaiting_execution_approval',
29 'execution_approved',
30 'executing_setup',
31 'executing_import',
32 'completed',
33 'partial',
34 'blocked',
35 'failed',
36]);
37const PHASE_STATUS = new Set(['not_started', 'running', 'complete', 'partial', 'blocked', 'failed']);
38const EXECUTION_ITEM_STATUS = new Set(['not_started', 'running', 'complete', 'partial', 'blocked', 'failed', 'skipped']);
39const APPROVAL_STATUS = new Set(['pending', 'approved', 'rejected', 'provisional']);
40const APPROVAL_ACTOR = new Set(['user', 'system', null]);
41const DECISION_SOURCE = new Set(['user', 'deterministic_inference', 'prior_artifact']);
42
43function nowIso(date = new Date()) {
44 return date.toISOString();
45}
46
47function randomId(prefix) {
48 return `${prefix}_${crypto.randomUUID()}`;
49}
50
51function orchestrationDir(projectDir) {
52 return path.join(projectDir, 'orchestration');
53}
54
55function phaseStatusTemplate(timestamp) {
56 return {
57 status: 'not_started',
58 lastCompletedStep: null,
59 artifactRefs: [],
60 updatedAt: timestamp,
61 };
62}
63
64function executionStatusTemplate(timestamp) {
65 return {
66 ...phaseStatusTemplate(timestamp),
67 setupStatus: 'not_started',
68 importStatus: 'not_started',
69 lastCheckpointId: null,
70 };
71}
72
73function createRun(projectId, timestamp = nowIso()) {
74 return {
75 schemaVersion: SCHEMA_VERSION,
76 projectId,
77 status: 'running',
78 currentState: 'initialized',
79 activePhase: 'orchestration',
80 startedAt: timestamp,
81 updatedAt: timestamp,
82 sourcePlatform: null,
83 sourceMode: null,
84 resumeFrom: null,
85 needsUserInput: false,
86 needsLlm: false,
87 lastEventId: null,
88 };
89}
90
91function createCheckpoints(timestamp = nowIso()) {
92 return {
93 schemaVersion: SCHEMA_VERSION,
94 discovery: phaseStatusTemplate(timestamp),
95 mapping: phaseStatusTemplate(timestamp),
96 setup: phaseStatusTemplate(timestamp),
97 codegen: phaseStatusTemplate(timestamp),
98 execution: executionStatusTemplate(timestamp),
99 };
100}
101
102function createApprovals() {
103 return {
104 schemaVersion: SCHEMA_VERSION,
105 mapping: {
106 status: 'pending',
107 decidedAt: null,
108 artifactRefs: [],
109 notes: null,
110 decidedBy: null,
111 },
112 execution: {
113 status: 'pending',
114 decidedAt: null,
115 artifactRefs: [],
116 notes: null,
117 decidedBy: null,
118 },
119 };
120}
121
122function createDecisions() {
123 return {
124 schemaVersion: SCHEMA_VERSION,
125 };
126}
127
128function createPreflight(timestamp = nowIso()) {
129 return {
130 schemaVersion: SCHEMA_VERSION,
131 status: 'not_started',
132 updatedAt: timestamp,
133 checks: [],
134 };
135}
136
137async function mkdirp(dirPath) {
138 await fs.mkdir(dirPath, { recursive: true });
139}
140
141async function writeJsonAtomic(filePath, data) {
142 const dirPath = path.dirname(filePath);
143 await mkdirp(dirPath);
144 const tempPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
145 await fs.writeFile(tempPath, `${JSON.stringify(data, null, 2)}\n`, 'utf8');
146 await fs.rename(tempPath, filePath);
147}
148
149async function readJson(filePath) {
150 const raw = await fs.readFile(filePath, 'utf8');
151 return JSON.parse(raw);
152}
153
154async function maybeReadJson(filePath) {
155 try {
156 return await readJson(filePath);
157 } catch (error) {
158 if (error && error.code === 'ENOENT') {
159 return null;
160 }
161 throw error;
162 }
163}
164
165function reasonRef(code, message, artifactRefs = []) {
166 return { code, message, artifactRefs };
167}
168
169function pushError(errors, code, message) {
170 errors.push(reasonRef(code, message));
171}
172
173function requireSetMember(errors, code, value, allowed, label) {
174 if (!allowed.has(value)) {
175 pushError(errors, code, `${label} must be one of: ${Array.from(allowed).join(', ')} (got: ${value})`);
176 }
177}
178
179function isAwaitingState(state) {
180 return typeof state === 'string' && state.startsWith('awaiting_');
181}
182
183function validateRun(run, errors) {
184 if (!run || typeof run !== 'object') {
185 pushError(errors, 'run_missing', 'run.json is missing or not an object');
186 return;
187 }
188 if (run.schemaVersion !== SCHEMA_VERSION) {
189 pushError(errors, 'run_schema_version', `run.json schemaVersion must be ${SCHEMA_VERSION}`);
190 }
191 if (!run.projectId || typeof run.projectId !== 'string') {
192 pushError(errors, 'run_project_id', 'run.json projectId must be a non-empty string');
193 }
194 requireSetMember(errors, 'run_status', run.status, RUN_STATUS, 'run.json status');
195 requireSetMember(errors, 'run_current_state', run.currentState, ORCHESTRATION_STATES, 'run.json currentState');
196 requireSetMember(errors, 'run_active_phase', run.activePhase, ACTIVE_PHASE, 'run.json activePhase');
197 if (run.resumeFrom !== null && typeof run.resumeFrom !== 'object') {
198 pushError(errors, 'run_resume_from', 'run.json resumeFrom must be null or a reasonRef object');
199 }
200 if ((isAwaitingState(run.currentState) || run.currentState === 'blocked') && run.needsUserInput !== true) {
201 pushError(errors, 'run_needs_user_input', 'needsUserInput must be true for awaiting_* and blocked states');
202 }
203}
204
205function validateCheckpoints(checkpoints, errors) {
206 if (!checkpoints || typeof checkpoints !== 'object') {
207 pushError(errors, 'checkpoints_missing', 'checkpoints.json is missing or not an object');
208 return;
209 }
210 if (checkpoints.schemaVersion !== SCHEMA_VERSION) {
211 pushError(errors, 'checkpoints_schema_version', `checkpoints.json schemaVersion must be ${SCHEMA_VERSION}`);
212 }
213 for (const phase of ['discovery', 'mapping', 'setup', 'codegen']) {
214 const item = checkpoints[phase];
215 if (!item || typeof item !== 'object') {
216 pushError(errors, `checkpoint_${phase}_missing`, `${phase} checkpoint is missing`);
217 continue;
218 }
219 requireSetMember(errors, `checkpoint_${phase}_status`, item.status, PHASE_STATUS, `${phase} checkpoint status`);
220 if (!Array.isArray(item.artifactRefs)) {
221 pushError(errors, `checkpoint_${phase}_artifact_refs`, `${phase} checkpoint artifactRefs must be an array`);
222 }
223 }
224 const execution = checkpoints.execution;
225 if (!execution || typeof execution !== 'object') {
226 pushError(errors, 'checkpoint_execution_missing', 'execution checkpoint is missing');
227 return;
228 }
229 requireSetMember(errors, 'checkpoint_execution_status', execution.status, PHASE_STATUS, 'execution checkpoint status');
230 requireSetMember(errors, 'checkpoint_execution_setup_status', execution.setupStatus, EXECUTION_ITEM_STATUS, 'execution.setupStatus');
231 requireSetMember(errors, 'checkpoint_execution_import_status', execution.importStatus, EXECUTION_ITEM_STATUS, 'execution.importStatus');
232}
233
234function validateApprovals(approvals, errors) {
235 if (!approvals || typeof approvals !== 'object') {
236 pushError(errors, 'approvals_missing', 'approvals.json is missing or not an object');
237 return;
238 }
239 if (approvals.schemaVersion !== SCHEMA_VERSION) {
240 pushError(errors, 'approvals_schema_version', `approvals.json schemaVersion must be ${SCHEMA_VERSION}`);
241 }
242 for (const approvalType of ['mapping', 'execution']) {
243 const item = approvals[approvalType];
244 if (!item || typeof item !== 'object') {
245 pushError(errors, `approval_${approvalType}_missing`, `${approvalType} approval is missing`);
246 continue;
247 }
248 requireSetMember(errors, `approval_${approvalType}_status`, item.status, APPROVAL_STATUS, `${approvalType} approval status`);
249 requireSetMember(errors, `approval_${approvalType}_decided_by`, item.decidedBy, APPROVAL_ACTOR, `${approvalType} approval decidedBy`);
250 if (!Array.isArray(item.artifactRefs)) {
251 pushError(errors, `approval_${approvalType}_artifact_refs`, `${approvalType} approval artifactRefs must be an array`);
252 }
253 if (approvalType === 'execution' && item.status === 'provisional') {
254 pushError(errors, 'approval_execution_provisional', 'execution approval must not be provisional');
255 }
256 }
257}
258
259function validateDecisions(decisions, errors) {
260 if (!decisions || typeof decisions !== 'object') {
261 pushError(errors, 'decisions_missing', 'decisions.json is missing or not an object');
262 return;
263 }
264 if (decisions.schemaVersion !== SCHEMA_VERSION) {
265 pushError(errors, 'decisions_schema_version', `decisions.json schemaVersion must be ${SCHEMA_VERSION}`);
266 }
267 for (const [key, value] of Object.entries(decisions)) {
268 if (key === 'schemaVersion') {
269 continue;
270 }
271 if (!value || typeof value !== 'object') {
272 pushError(errors, 'decision_entry_invalid', `decision ${key} must be an object`);
273 continue;
274 }
275 requireSetMember(errors, `decision_${key}_source`, value.source, DECISION_SOURCE, `decision ${key} source`);
276 }
277}
278
279function validateCrossArtifactInvariants(artifacts, errors) {
280 const { run, checkpoints, approvals } = artifacts;
281 if (!run || !checkpoints || !approvals) {
282 return;
283 }
284 if (run.currentState === 'preflight_running' || run.currentState === 'preflight_blocked' || run.currentState === 'awaiting_destination_strategy') {
285 if (run.activePhase !== 'orchestration') {
286 pushError(errors, 'state_phase_preflight', `${run.currentState} requires activePhase=orchestration`);
287 }
288 }
289 if (run.currentState === 'mapping_running' && checkpoints.mapping.status === 'not_started') {
290 pushError(errors, 'state_mapping_checkpoint', 'mapping_running requires mapping checkpoint status to be started');
291 }
292 if (run.currentState === 'awaiting_mapping_approval' && approvals.mapping.status !== 'pending') {
293 pushError(errors, 'state_mapping_approval_pending', 'awaiting_mapping_approval requires approvals.mapping.status=pending');
294 }
295 if (run.currentState === 'mapping_approved' && !['approved', 'provisional'].includes(approvals.mapping.status)) {
296 pushError(errors, 'state_mapping_approved', 'mapping_approved requires approvals.mapping.status=approved|provisional');
297 }
298 if (run.currentState === 'awaiting_execution_approval' && approvals.execution.status !== 'pending') {
299 pushError(errors, 'state_execution_approval_pending', 'awaiting_execution_approval requires approvals.execution.status=pending');
300 }
301 if (['execution_approved', 'executing_setup', 'executing_import', 'completed'].includes(run.currentState) &&
302 approvals.execution.status !== 'approved') {
303 pushError(errors, 'state_execution_approved', `${run.currentState} requires approvals.execution.status=approved`);
304 }
305}
306
307function validateArtifacts(artifacts) {
308 const errors = [];
309 validateRun(artifacts.run, errors);
310 validateCheckpoints(artifacts.checkpoints, errors);
311 validateApprovals(artifacts.approvals, errors);
312 validateDecisions(artifacts.decisions, errors);
313 validateCrossArtifactInvariants(artifacts, errors);
314 return {
315 ok: errors.length === 0,
316 errors,
317 };
318}
319
320async function appendEvent(projectDir, event) {
321 const dirPath = orchestrationDir(projectDir);
322 await mkdirp(dirPath);
323 const filePath = path.join(dirPath, 'events.jsonl');
324 const line = `${JSON.stringify({ schemaVersion: SCHEMA_VERSION, ...event })}\n`;
325 await fs.appendFile(filePath, line, 'utf8');
326}
327
328async function initArtifacts(projectDir, { projectId = path.basename(projectDir), timestamp = nowIso() } = {}) {
329 const dirPath = orchestrationDir(projectDir);
330 await mkdirp(dirPath);
331 const run = createRun(projectId, timestamp);
332 const checkpoints = createCheckpoints(timestamp);
333 const decisions = createDecisions();
334 const approvals = createApprovals();
335 const preflight = createPreflight(timestamp);
336 await writeJsonAtomic(path.join(dirPath, 'run.json'), run);
337 await writeJsonAtomic(path.join(dirPath, 'checkpoints.json'), checkpoints);
338 await writeJsonAtomic(path.join(dirPath, 'decisions.json'), decisions);
339 await writeJsonAtomic(path.join(dirPath, 'approvals.json'), approvals);
340 await writeJsonAtomic(path.join(dirPath, 'preflight.json'), preflight);
341 const eventId = randomId('evt');
342 await appendEvent(projectDir, {
343 id: eventId,
344 timestamp,
345 phase: 'orchestration',
346 state: 'initialized',
347 type: 'orchestration.initialized',
348 payload: { projectId },
349 });
350 run.lastEventId = eventId;
351 await writeJsonAtomic(path.join(dirPath, 'run.json'), run);
352 return loadArtifacts(projectDir);
353}
354
355async function loadArtifacts(projectDir) {
356 const dirPath = orchestrationDir(projectDir);
357 return {
358 run: await maybeReadJson(path.join(dirPath, 'run.json')),
359 checkpoints: await maybeReadJson(path.join(dirPath, 'checkpoints.json')),
360 decisions: await maybeReadJson(path.join(dirPath, 'decisions.json')),
361 approvals: await maybeReadJson(path.join(dirPath, 'approvals.json')),
362 preflight: await maybeReadJson(path.join(dirPath, 'preflight.json')),
363 };
364}
365
366function updateDecisionObject(decisions, key, value, { source = 'user', rationale = null, timestamp = nowIso() } = {}) {
367 if (!DECISION_SOURCE.has(source)) {
368 throw new Error(`invalid decision source: ${source}`);
369 }
370 decisions[key] = {
371 value,
372 decidedAt: timestamp,
373 source,
374 rationale,
375 };
376 return decisions;
377}
378
379function updateApprovalObject(approvals, approvalType, status, { artifactRefs = [], notes = null, decidedBy = 'user', timestamp = nowIso() } = {}) {
380 if (!approvals[approvalType]) {
381 throw new Error(`unknown approval type: ${approvalType}`);
382 }
383 if (!APPROVAL_STATUS.has(status)) {
384 throw new Error(`invalid approval status: ${status}`);
385 }
386 if (approvalType === 'execution' && status === 'provisional') {
387 throw new Error('execution approval cannot be provisional');
388 }
389 approvals[approvalType] = {
390 status,
391 decidedAt: status === 'pending' ? null : timestamp,
392 artifactRefs,
393 notes,
394 decidedBy: status === 'pending' ? null : decidedBy,
395 };
396 return approvals;
397}
398
399function updateCheckpointObject(checkpoints, phase, patch, timestamp = nowIso()) {
400 if (!checkpoints[phase]) {
401 throw new Error(`unknown checkpoint phase: ${phase}`);
402 }
403 checkpoints[phase] = {
404 ...checkpoints[phase],
405 ...patch,
406 updatedAt: timestamp,
407 };
408 return checkpoints;
409}
410
411function transitionRun(run, { state, activePhase, status = run.status, needsUserInput = run.needsUserInput, needsLlm = run.needsLlm, resumeFrom = run.resumeFrom, timestamp = nowIso() }) {
412 if (!ORCHESTRATION_STATES.has(state)) {
413 throw new Error(`invalid state: ${state}`);
414 }
415 if (!ACTIVE_PHASE.has(activePhase)) {
416 throw new Error(`invalid active phase: ${activePhase}`);
417 }
418 if (!RUN_STATUS.has(status)) {
419 throw new Error(`invalid run status: ${status}`);
420 }
421 return {
422 ...run,
423 currentState: state,
424 activePhase,
425 status,
426 needsUserInput,
427 needsLlm,
428 resumeFrom,
429 updatedAt: timestamp,
430 };
431}
432
433module.exports = {
434 SCHEMA_VERSION,
435 orchestrationDir,
436 nowIso,
437 randomId,
438 reasonRef,
439 createRun,
440 createCheckpoints,
441 createApprovals,
442 createDecisions,
443 createPreflight,
444 writeJsonAtomic,
445 readJson,
446 maybeReadJson,
447 loadArtifacts,
448 initArtifacts,
449 validateArtifacts,
450 appendEvent,
451 updateDecisionObject,
452 updateApprovalObject,
453 updateCheckpointObject,
454 transitionRun,
455};