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