singularity-forge/src/resources/extensions/sf/uok/loop-adapter.js

239 lines
6.7 KiB
JavaScript

import { buildAuditEnvelope, emitUokAuditEvent } from "./audit.js";
import { ChaosMonkey } from "./chaos-monkey.js";
import {
writeTurnCloseoutGitRecord,
writeTurnGitTransaction,
} from "./gitops.js";
import {
acquireWriterToken,
nextWriteRecord,
releaseWriterToken,
} from "./writer.js";
const GITOPS_TIMEOUT_MS = 10_000;
function writeGitTransactionWithTimeout(args) {
return Promise.race([
writeTurnGitTransaction(args),
new Promise((_, reject) =>
setTimeout(
() => reject(new Error("Git transaction timed out")),
GITOPS_TIMEOUT_MS,
),
),
]);
}
export function createTurnObserver(options) {
let current = null;
let writerToken = null;
const phaseResults = [];
const chaosMonkey = options.enableChaosMonkey ? new ChaosMonkey() : null;
/**
* Enrich metadata with write sequence info when a writer token is active.
*
* Purpose: Provide audit/traceability by attaching sequence numbers to
* gitops and audit metadata. When no token is active (e.g., early in
* turn setup), returns metadata unchanged.
*
* @param {string} category — e.g., "gitops", "audit"
* @param {string} operation — e.g., "insert", "update"
* @param {object} [metadata] — caller-provided metadata
* @returns {object} metadata with optional writeSequence and writerTokenId
*/
function nextSequenceMetadata(category, operation, metadata) {
if (!writerToken) return metadata ?? {};
const record = nextWriteRecord({
basePath: options.basePath,
token: writerToken,
category,
operation,
metadata,
});
return {
...(metadata ?? {}),
writeSequence: record.sequence.sequence,
writerTokenId: record.writerToken.tokenId,
};
}
return {
onTurnStart(contract) {
if (chaosMonkey) chaosMonkey.strike("turn-start");
current = {
...contract,
runControl: options.runControl,
permissionProfile: options.permissionProfile,
};
phaseResults.length = 0;
writerToken = acquireWriterToken({
basePath: options.basePath,
traceId: current.traceId,
turnId: current.turnId,
});
if (options.enableGitops) {
writeGitTransactionWithTimeout({
basePath: options.basePath,
traceId: current.traceId,
turnId: current.turnId,
unitType: current.unitType,
unitId: current.unitId,
stage: "turn-start",
action: options.gitAction,
push: options.gitPush,
status: "ok",
metadata: nextSequenceMetadata("gitops", "insert", {
iteration: current.iteration,
sidecarKind: current.sidecarKind,
runControl: current.runControl,
permissionProfile: current.permissionProfile,
}),
}).catch((err) => {
console.error(`[loop-adapter] Git transaction failed: ${err.message}`);
});
}
if (options.enableAudit) {
emitUokAuditEvent(
options.basePath,
buildAuditEnvelope({
traceId: current.traceId,
turnId: current.turnId,
category: "orchestration",
type: "turn-start",
payload: nextSequenceMetadata("audit", "append", {
iteration: current.iteration,
unitType: current.unitType,
unitId: current.unitId,
sidecarKind: current.sidecarKind,
runControl: current.runControl,
permissionProfile: current.permissionProfile,
}),
}),
);
}
},
onPhaseResult(phase, action, data) {
if (chaosMonkey) chaosMonkey.strike(`after-${phase}`);
phaseResults.push({
phase,
action,
ts: new Date().toISOString(),
data,
});
if (!current || !options.enableGitops) return;
if (phase === "dispatch") {
writeGitTransactionWithTimeout({
basePath: options.basePath,
traceId: current.traceId,
turnId: current.turnId,
unitType: data?.unitType,
unitId: data?.unitId,
stage: "stage",
action: options.gitAction,
push: options.gitPush,
status: "ok",
metadata: nextSequenceMetadata("gitops", "update", { action }),
}).catch((err) => {
console.error(`[loop-adapter] Git transaction failed: ${err.message}`);
});
}
if (phase === "unit") {
writeGitTransactionWithTimeout({
basePath: options.basePath,
traceId: current.traceId,
turnId: current.turnId,
unitType: data?.unitType,
unitId: data?.unitId,
stage: "checkpoint",
action: options.gitAction,
push: options.gitPush,
status: "ok",
metadata: nextSequenceMetadata("gitops", "update", { action }),
}).catch((err) => {
console.error(`[loop-adapter] Git transaction failed: ${err.message}`);
});
}
if (phase === "finalize") {
writeGitTransactionWithTimeout({
basePath: options.basePath,
traceId: current.traceId,
turnId: current.turnId,
unitType: data?.unitType,
unitId: data?.unitId,
stage: "publish",
action: options.gitAction,
push: options.gitPush,
status: "ok",
metadata: nextSequenceMetadata("gitops", "update", { action }),
}).catch((err) => {
console.error(`[loop-adapter] Git transaction failed: ${err.message}`);
});
}
},
onTurnResult(result) {
const merged = {
runControl: options.runControl,
permissionProfile: options.permissionProfile,
...result,
phaseResults:
result.phaseResults.length > 0
? result.phaseResults
: [...phaseResults],
};
if (options.enableAudit) {
emitUokAuditEvent(
options.basePath,
buildAuditEnvelope({
traceId: merged.traceId,
turnId: merged.turnId,
category: "orchestration",
type: "turn-result",
payload: nextSequenceMetadata("audit", "append", {
unitType: merged.unitType,
unitId: merged.unitId,
status: merged.status,
failureClass: merged.failureClass,
error: merged.error,
phaseCount: merged.phaseResults.length,
runControl: merged.runControl,
permissionProfile: merged.permissionProfile,
}),
}),
);
}
if (options.enableGitops) {
const closeout = merged.closeout ?? {
traceId: merged.traceId,
turnId: merged.turnId,
unitType: merged.unitType,
unitId: merged.unitId,
status: merged.status,
failureClass: merged.failureClass,
gitAction: options.gitAction,
gitPushed: options.gitPush,
finishedAt: merged.finishedAt,
};
Promise.race([
writeTurnCloseoutGitRecord(
options.basePath,
closeout,
nextSequenceMetadata("gitops", "update", { action: "record" }),
),
new Promise((_, reject) =>
setTimeout(
() => reject(new Error("Git closeout timed out")),
GITOPS_TIMEOUT_MS,
),
),
]).catch((err) => {
console.error(`[loop-adapter] Git closeout failed: ${err.message}`);
});
}
if (writerToken) {
releaseWriterToken(options.basePath, writerToken);
}
writerToken = null;
current = null;
phaseResults.length = 0;
},
};
}