singularity-forge/src/resources/extensions/sf/parallel-intent.js
2026-05-08 05:51:06 +02:00

152 lines
4.3 KiB
JavaScript

/**
* Parallel Worker Intent/Claim Registry
*
* Purpose: before editing files, parallel workers declare intent so the
* coordinator can detect conflicts without waiting for git merge failures.
* This is SF's implementation of Wit-style symbol-level coordination for
* parallel milestone workers.
*
* Consumer: parallel-orchestrator.js before dispatching overlapping milestones.
*/
import { mkdirSync } from "node:fs";
import { join } from "node:path";
import { getDatabase, getDbPath, openDatabase } from "./sf-db.js";
import { UokCoordinationStore } from "./uok/coordination-store.js";
import { logWarning } from "./workflow-logger.js";
const INTENT_KEY_PREFIX = "parallel:intent:";
const INTENT_STREAM = "parallel:intents";
function projectDbPath(basePath) {
const sfDir = join(basePath, ".sf");
mkdirSync(sfDir, { recursive: true });
return join(sfDir, "sf.db");
}
function getStore(basePath) {
const dbPath = projectDbPath(basePath);
if (!getDatabase() || getDbPath() !== dbPath) {
openDatabase(dbPath);
}
const db = getDatabase();
if (!db) throw new Error("SF database is not open");
return new UokCoordinationStore(db);
}
function intentKey(milestoneId) {
return `${INTENT_KEY_PREFIX}${milestoneId}`;
}
function normalizeFiles(files) {
return files.map((f) => f.replace(/^\/+/, ""));
}
/**
* Declare editing intent before making changes.
*
* Purpose: let other workers know which files this worker plans to modify,
* so conflicts can be detected proactively.
*
* @param {string} basePath — project root
* @param {string} milestoneId — worker's milestone
* @param {string[]} files — relative file paths worker intends to edit
* @param {object} opts — optional: symbolRanges (for fine-grained claims)
*/
export function declareIntent(basePath, milestoneId, files, opts = {}) {
try {
const store = getStore(basePath);
const record = {
milestoneId,
files: normalizeFiles(files),
symbolRanges: opts.symbolRanges ?? [],
declaredAt: new Date().toISOString(),
status: "claimed",
};
store.set(intentKey(milestoneId), record);
store.xadd(INTENT_STREAM, "intent-claimed", record);
return { ok: true };
} catch (err) {
logWarning(
"parallel-intent",
`declareIntent failed for ${milestoneId}: ${err.message}`,
);
return { ok: false, error: err.message };
}
}
/**
* Release intent claim when editing is complete or abandoned.
*/
export function releaseIntent(basePath, milestoneId) {
try {
const store = getStore(basePath);
const record = store.get(intentKey(milestoneId));
if (record) {
record.status = "released";
record.releasedAt = new Date().toISOString();
store.set(intentKey(milestoneId), record);
store.xadd(INTENT_STREAM, "intent-released", record);
}
return { ok: true };
} catch (err) {
logWarning(
"parallel-intent",
`releaseIntent failed for ${milestoneId}: ${err.message}`,
);
return { ok: false, error: err.message };
}
}
/**
* Check if any active worker has claimed overlapping files.
*
* @returns {Array<{milestoneId: string, files: string[]}>} conflicts
*/
export function checkIntentConflicts(basePath, milestoneId, files) {
const conflicts = [];
const normalized = normalizeFiles(files);
try {
for (const record of getActiveIntents(basePath)) {
if (record.milestoneId === milestoneId) continue;
const overlap = normalized.filter((f) => record.files.includes(f));
if (overlap.length > 0) {
conflicts.push({ milestoneId: record.milestoneId, files: overlap });
}
}
} catch (err) {
logWarning(
"parallel-intent",
`checkIntentConflicts failed: ${err.message}`,
);
}
return conflicts;
}
/**
* Get all active intent claims.
*/
export function getActiveIntents(basePath) {
try {
return getStore(basePath)
.entries(INTENT_KEY_PREFIX)
.map((entry) => entry.value)
.filter((record) => record?.status === "claimed");
} catch (err) {
logWarning("parallel-intent", `getActiveIntents failed: ${err.message}`);
return [];
}
}
/**
* Clear all intent records (used on orchestrator shutdown).
*/
export function clearAllIntents(basePath) {
try {
const store = getStore(basePath);
for (const entry of store.entries(INTENT_KEY_PREFIX)) {
store.delete(entry.key);
}
} catch (err) {
logWarning("parallel-intent", `clearAllIntents failed: ${err.message}`);
}
}