# Conflicts: # apps/server/src/core/ai-chat/ai-chat.service.spec.ts # apps/server/src/core/ai-chat/ai-chat.service.ts
98 lines
4.4 KiB
JavaScript
98 lines
4.4 KiB
JavaScript
import { readExisting, computePullActions, applyPullActions } from "./pull.js";
|
|
import { runPush } from "./push.js";
|
|
/**
|
|
* Run ONE full reconcile cycle for a space: PULL (Docmost -> vault) then PUSH
|
|
* (vault -> Docmost), under the engine's required branch choreography. This is
|
|
* the single entry point the app drives — it owns the staging order so it can
|
|
* never drift from the engine it ships with.
|
|
*
|
|
* Staging (the ⭐ data-loss-critical order, SPEC §6/§9):
|
|
* 1. assertGitAvailable + ensureRepo (the git state store must exist).
|
|
* 2. refuse on an unresolved merge (a prior conflicting pull); next checkout
|
|
* would fail otherwise.
|
|
* 3. ensureBranch('docmost','main') + checkout('docmost'). Pull writes MUST
|
|
* land on `docmost`, not `main`: applyPullActions commits on `docmost`,
|
|
* then checks out `main` and merges docmost -> main. Writing Docmost
|
|
* content straight onto `main` would clobber local file edits before push
|
|
* can diff them.
|
|
* 4. PULL: readExisting -> listSpaceTree -> computePullActions -> apply.
|
|
* 5. PUSH: optional dry-run to feed the delete-cap hook, then the real apply.
|
|
*
|
|
* Lock + cap POLICY live in the caller; this owns only the mechanics.
|
|
*/
|
|
export async function runCycle(deps) {
|
|
const { spaceId, client, vault, settings, fs, log, resolveApplyClient } = deps;
|
|
const vaultRoot = settings.vaultPath;
|
|
const abs = (relPath) => `${vaultRoot}/${relPath}`;
|
|
// 1. The engine state store is git: make sure the repo + branches exist
|
|
// before any tracked-file listing or diff.
|
|
await vault.assertGitAvailable();
|
|
await vault.ensureRepo();
|
|
// 2. Refuse to run on top of an unresolved merge (SPEC §9): a prior
|
|
// conflicting pull leaves the vault mid-merge; the next checkout would fail.
|
|
if (await vault.isMergeInProgress()) {
|
|
log(`vault has an unresolved merge — resolve it (or 'git merge --abort') ` +
|
|
`and re-run (SPEC §9); skipping cycle.`);
|
|
return { ran: false, skipped: "merge-in-progress" };
|
|
}
|
|
// 3. Pull writes happen on `docmost`; be on it BEFORE applying (see docstring).
|
|
await vault.ensureBranch("docmost", "main");
|
|
await vault.checkout("docmost");
|
|
// 4. PULL --------------------------------------------------------------------
|
|
const existing = await readExisting({
|
|
listTracked: () => vault.listTrackedFiles("*.md"),
|
|
readFile: (relPath) => fs.readFile(abs(relPath)),
|
|
});
|
|
const tree = await client.listSpaceTree(spaceId);
|
|
const pullActions = computePullActions({
|
|
pages: tree.pages,
|
|
treeComplete: tree.complete,
|
|
existing,
|
|
});
|
|
const pullResult = await applyPullActions({
|
|
client,
|
|
git: vault,
|
|
writeFile: (absPath, text) => fs.writeFile(absPath, text),
|
|
mkdir: (absDir) => fs.mkdir(absDir),
|
|
rm: (absPath) => fs.rm(absPath),
|
|
}, pullActions, vaultRoot);
|
|
// 5. PUSH --------------------------------------------------------------------
|
|
const pushDeps = {
|
|
settings,
|
|
git: vault,
|
|
makeClient: () => client,
|
|
readFile: (relPath) => fs.readFile(abs(relPath)),
|
|
writeFile: (relPath, text) => fs.writeFile(abs(relPath), text),
|
|
log,
|
|
};
|
|
let applyClient = client;
|
|
if (resolveApplyClient) {
|
|
// Plan the push as a DRY-RUN first to read the delete count, then let the
|
|
// caller decide the apply client (e.g. neutralize deletes over a cap). A
|
|
// failed dry-run yields Infinity so the hook can fail safe.
|
|
let plannedDeletes;
|
|
try {
|
|
const dry = await runPush(pushDeps, { dryRun: true });
|
|
plannedDeletes = dry.planned?.deletes ?? 0;
|
|
}
|
|
catch (err) {
|
|
log(`push dry-run planning failed (${err instanceof Error ? err.message : String(err)}); deferring deletion policy to the cap hook (fail-safe).`);
|
|
plannedDeletes = Number.POSITIVE_INFINITY;
|
|
}
|
|
applyClient = resolveApplyClient(plannedDeletes, client);
|
|
}
|
|
const pushResult = await runPush({ ...pushDeps, makeClient: () => applyClient }, { dryRun: false });
|
|
return {
|
|
ran: true,
|
|
pull: {
|
|
written: pullResult.written,
|
|
deleted: pullResult.deleted,
|
|
conflict: pullResult.merge.conflict,
|
|
},
|
|
push: {
|
|
mode: pushResult.mode,
|
|
failures: pushResult.failures?.length ?? 0,
|
|
},
|
|
};
|
|
}
|