@ricsam/r5d-worker 0.0.57 → 0.0.59
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/cjs/main.cjs +717 -140
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/workspace-convergence.cjs +320 -0
- package/dist/cjs/workspace-incident-state.cjs +32 -3
- package/dist/cjs/workspace-manifest-admission.cjs +70 -0
- package/dist/cjs/workspace-sync.cjs +259 -69
- package/dist/mjs/main.mjs +736 -143
- package/dist/mjs/package.json +1 -1
- package/dist/mjs/workspace-convergence.mjs +275 -0
- package/dist/mjs/workspace-incident-state.mjs +31 -3
- package/dist/mjs/workspace-manifest-admission.mjs +35 -0
- package/dist/mjs/workspace-sync.mjs +256 -68
- package/dist/types/workspace-convergence.d.ts +102 -0
- package/dist/types/workspace-incident-state.d.ts +15 -1
- package/dist/types/workspace-manifest-admission.d.ts +27 -0
- package/dist/types/workspace-sync.d.ts +31 -2
- package/package.json +1 -1
|
@@ -5,8 +5,8 @@ import { managedBranchPath } from "./managed-paths.mjs";
|
|
|
5
5
|
import { WorkspaceMutationGate } from "./workspace-mutation-gate.mjs";
|
|
6
6
|
const WORKSPACE_BRANCH = "main";
|
|
7
7
|
const WORKSPACE_INTENT_REF_PREFIX = "refs/r5d/workspace-intents/";
|
|
8
|
+
const WORKSPACE_PUBLICATION_REF_PREFIX = "refs/r5d/workspace-publications/";
|
|
8
9
|
const MAX_WORKSPACE_SYNC_DIFF_BYTES = 5 * 1024 * 1024;
|
|
9
|
-
const WORKSPACE_PERIODIC_SCAN_INTERVAL_MS = 5e3;
|
|
10
10
|
const MAX_REPORTED_WORKSPACE_PATHS = 2e3;
|
|
11
11
|
const MAX_REPORTED_WORKSPACE_STATUS_BYTES = 256 * 1024;
|
|
12
12
|
const MAX_MIRROR_COMPARISON_CACHE_ENTRIES = 2e5;
|
|
@@ -911,7 +911,7 @@ function hydrateProjectFromShadowOptimistically(input, relativeRoot, visibleRoot
|
|
|
911
911
|
expectedCheckoutSnapshot = checkoutGitSnapshot(visibleRoot);
|
|
912
912
|
return expectedCheckoutSnapshot !== null;
|
|
913
913
|
};
|
|
914
|
-
|
|
914
|
+
const currentGitlinks = new Map(checkoutGitlinks(visibleRoot).map((entry) => [entry.filePath, entry]));
|
|
915
915
|
const changedPaths = [.../* @__PURE__ */ new Set([...preimage.keys(), ...desired.keys()])].filter(
|
|
916
916
|
(filePath) => !hydrationEntriesEqual(preimage.get(filePath), desired.get(filePath))
|
|
917
917
|
);
|
|
@@ -1533,6 +1533,33 @@ function resetUncommittedShadowSnapshot(input) {
|
|
|
1533
1533
|
}
|
|
1534
1534
|
runGit(input, input.shadowRoot, ["clean", "-fd"], "clean interrupted workspace snapshot");
|
|
1535
1535
|
}
|
|
1536
|
+
function activeWorkspaceRebase(input) {
|
|
1537
|
+
for (const name of ["rebase-merge", "rebase-apply"]) {
|
|
1538
|
+
const resolved = runGitResult(input, input.shadowRoot, ["rev-parse", "--git-path", name]);
|
|
1539
|
+
if (resolved.exitCode !== 0 || !resolved.stdout) continue;
|
|
1540
|
+
const gitPath = path.isAbsolute(resolved.stdout) ? resolved.stdout : path.resolve(input.shadowRoot, resolved.stdout);
|
|
1541
|
+
if (fs.existsSync(gitPath)) return name;
|
|
1542
|
+
}
|
|
1543
|
+
return null;
|
|
1544
|
+
}
|
|
1545
|
+
function remediationRebaseInProgressResult(input, result) {
|
|
1546
|
+
if (input.trigger.type !== "remediation" && input.trigger.type !== "remediation_confirm") return null;
|
|
1547
|
+
const rebaseKind = activeWorkspaceRebase(input);
|
|
1548
|
+
if (!rebaseKind) return null;
|
|
1549
|
+
const conflictPaths = runGitResult(input, input.shadowRoot, ["diff", "--name-only", "--diff-filter=U", "-z"]).stdout.split("\0").filter(Boolean).sort();
|
|
1550
|
+
const status = reportedStatus(gitStatus(input));
|
|
1551
|
+
return {
|
|
1552
|
+
...result,
|
|
1553
|
+
outcome: "conflict_blocked",
|
|
1554
|
+
expectedHead: revParse(input, `origin/${WORKSPACE_BRANCH}`),
|
|
1555
|
+
candidateHead: revParse(input, "ORIG_HEAD") ?? revParse(input, "HEAD") ?? void 0,
|
|
1556
|
+
rebaseCount: 1,
|
|
1557
|
+
gitStatus: status,
|
|
1558
|
+
affectedProjects: affectedProjects(conflictPaths),
|
|
1559
|
+
affectedPaths: reportedPaths(conflictPaths),
|
|
1560
|
+
error: `Workspace conflict remediation has an in-progress ${rebaseKind}; the index and worktree were preserved until the resolver completes or aborts it.`
|
|
1561
|
+
};
|
|
1562
|
+
}
|
|
1536
1563
|
function fastForwardCleanShadowForNewCheckouts(input) {
|
|
1537
1564
|
if (!input.newVisibleCheckouts?.length) return null;
|
|
1538
1565
|
runGit(input, input.shadowRoot, ["add", "-A"], "stage existing workspace changes before checkout hydration");
|
|
@@ -1678,9 +1705,12 @@ function commitMessage(input) {
|
|
|
1678
1705
|
});
|
|
1679
1706
|
}
|
|
1680
1707
|
function assertWorkspaceQuarantineRef(quarantineRef) {
|
|
1681
|
-
const
|
|
1682
|
-
|
|
1683
|
-
|
|
1708
|
+
const prefix = quarantineRef.startsWith(WORKSPACE_PUBLICATION_REF_PREFIX) ? WORKSPACE_PUBLICATION_REF_PREFIX : WORKSPACE_INTENT_REF_PREFIX;
|
|
1709
|
+
const suffix = quarantineRef.slice(prefix.length);
|
|
1710
|
+
if (!quarantineRef.startsWith(prefix) || !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/.test(suffix)) {
|
|
1711
|
+
throw new Error(
|
|
1712
|
+
`Workspace candidate ref must be below ${WORKSPACE_PUBLICATION_REF_PREFIX} or ${WORKSPACE_INTENT_REF_PREFIX}`
|
|
1713
|
+
);
|
|
1684
1714
|
}
|
|
1685
1715
|
const result = Bun.spawnSync(["git", "check-ref-format", quarantineRef], {
|
|
1686
1716
|
stdout: "pipe",
|
|
@@ -1752,17 +1782,105 @@ function convergeRedundantInboundCandidate(input, result) {
|
|
|
1752
1782
|
affectedPaths: []
|
|
1753
1783
|
};
|
|
1754
1784
|
}
|
|
1785
|
+
function preserveWorkspaceConflictCandidate(input, observed, options) {
|
|
1786
|
+
try {
|
|
1787
|
+
if (!options.baseRevision) throw new Error("the preserved workspace does not have an old-base commit");
|
|
1788
|
+
runGit(input, input.shadowRoot, ["reset", "--hard", options.baseRevision], "restore old-base workspace conflict candidate");
|
|
1789
|
+
runGit(input, input.shadowRoot, ["clean", "-fd"], "clean old-base workspace conflict candidate");
|
|
1790
|
+
if (!options.preserveShadowCandidate) {
|
|
1791
|
+
const projection = mirrorVisibleWorkspaceToShadow(input);
|
|
1792
|
+
restoreUnscopedCanonicalCheckoutSubtrees(input);
|
|
1793
|
+
const stagePathspecs = projection.opaqueRoots.flatMap((root) => [`:(exclude,literal)${root}`]);
|
|
1794
|
+
runGit(input, input.shadowRoot, ["add", "-A", "--", ".", ...stagePathspecs], "stage preserved workspace conflict candidate");
|
|
1795
|
+
forceStageCanonicalCheckoutFiles(input, projection.entries, projection.opaqueRoots);
|
|
1796
|
+
if (stagedPaths(input).length > 0) {
|
|
1797
|
+
runGit(input, input.shadowRoot, ["commit", "-m", commitMessage(input)], "commit preserved workspace conflict candidate");
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
const candidateHead = revParse(input, "HEAD");
|
|
1801
|
+
if (!candidateHead) throw new Error("the preserved workspace conflict candidate does not have a commit");
|
|
1802
|
+
assertUnscopedCanonicalCheckoutSubtreesMatchRemote(input, candidateHead);
|
|
1803
|
+
const upload = uploadWorkspaceCandidate(input, candidateHead);
|
|
1804
|
+
if (upload.exitCode !== 0) {
|
|
1805
|
+
throw new Error(upload.stderr || upload.stdout || "workspace conflict candidate upload failed");
|
|
1806
|
+
}
|
|
1807
|
+
const conflictPaths = [...new Set(options.conflictPaths)].sort();
|
|
1808
|
+
return {
|
|
1809
|
+
...observed,
|
|
1810
|
+
outcome: "conflict_blocked",
|
|
1811
|
+
expectedHead: options.expectedHead,
|
|
1812
|
+
candidateHead,
|
|
1813
|
+
quarantineRef: input.quarantineRef,
|
|
1814
|
+
...options.publishedHead ? { publishedHead: options.publishedHead } : {},
|
|
1815
|
+
rebaseCount: options.rebaseCount ?? 0,
|
|
1816
|
+
affectedProjects: affectedProjects(conflictPaths),
|
|
1817
|
+
affectedPaths: reportedPaths(conflictPaths),
|
|
1818
|
+
error: options.error,
|
|
1819
|
+
gitStatus: gitStatus(input)
|
|
1820
|
+
};
|
|
1821
|
+
} catch (error) {
|
|
1822
|
+
return {
|
|
1823
|
+
...observed,
|
|
1824
|
+
outcome: "failed",
|
|
1825
|
+
rebaseCount: options.rebaseCount ?? 0,
|
|
1826
|
+
affectedProjects: affectedProjects(options.conflictPaths),
|
|
1827
|
+
affectedPaths: reportedPaths(options.conflictPaths),
|
|
1828
|
+
error: `Workspace synchronization found a conflict but could not preserve a durable candidate: ${error instanceof Error ? error.message : String(error)}`,
|
|
1829
|
+
gitStatus: gitStatus(input)
|
|
1830
|
+
};
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
function preserveLargeDiffCandidate(input, observed, error) {
|
|
1834
|
+
try {
|
|
1835
|
+
if (stagedPaths(input).length > 0) {
|
|
1836
|
+
runGit(input, input.shadowRoot, ["commit", "-m", commitMessage(input)], "commit blocked workspace candidate");
|
|
1837
|
+
}
|
|
1838
|
+
const candidateHead = revParse(input, "HEAD");
|
|
1839
|
+
if (!candidateHead) throw new Error("the blocked workspace candidate does not have a commit");
|
|
1840
|
+
assertUnscopedCanonicalCheckoutSubtreesMatchRemote(input, candidateHead);
|
|
1841
|
+
const upload = uploadWorkspaceCandidate(input, candidateHead);
|
|
1842
|
+
if (upload.exitCode !== 0) {
|
|
1843
|
+
throw new Error(upload.stderr || upload.stdout || "blocked workspace candidate upload failed");
|
|
1844
|
+
}
|
|
1845
|
+
return {
|
|
1846
|
+
...observed,
|
|
1847
|
+
outcome: "large_diff_blocked",
|
|
1848
|
+
expectedHead: revParse(input, `origin/${WORKSPACE_BRANCH}`),
|
|
1849
|
+
candidateHead,
|
|
1850
|
+
quarantineRef: input.quarantineRef,
|
|
1851
|
+
error,
|
|
1852
|
+
gitStatus: gitStatus(input)
|
|
1853
|
+
};
|
|
1854
|
+
} catch (candidateError) {
|
|
1855
|
+
return {
|
|
1856
|
+
...observed,
|
|
1857
|
+
outcome: "failed",
|
|
1858
|
+
error: `Workspace safety guard blocked publication but could not preserve a durable candidate: ${candidateError instanceof Error ? candidateError.message : String(candidateError)}`,
|
|
1859
|
+
gitStatus: gitStatus(input)
|
|
1860
|
+
};
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1755
1863
|
async function synchronizeWorkspace(rawInput) {
|
|
1756
1864
|
let input = { ...rawInput, attemptId: rawInput.attemptId ?? crypto.randomUUID() };
|
|
1757
1865
|
try {
|
|
1758
1866
|
input = { ...input, projects: workspaceProjectsForSync(input.projects, input.trigger) };
|
|
1867
|
+
const isCanonicalRemediationSync = input.skipVisibleMirror === true && (input.trigger.type === "remediation" || input.trigger.type === "remediation_confirm");
|
|
1759
1868
|
let mirroredGitlinkProjection = { entries: [], opaqueRoots: [], visiblePreimages: /* @__PURE__ */ new Map() };
|
|
1760
1869
|
let fastForwardedFromHead = null;
|
|
1761
1870
|
const shadowWasCreated = ensureShadowWorkspace(input);
|
|
1762
1871
|
let hydrationPreimages = input.skipVisibleMirror ? hydrationPreimagesFromShadowRevision(input, shadowWasCreated ? null : revParse(input, "HEAD")) : /* @__PURE__ */ new Map();
|
|
1763
1872
|
const remoteHeadAtStart = revParse(input, `origin/${WORKSPACE_BRANCH}`);
|
|
1764
1873
|
const result = baseResult(input, remoteHeadAtStart);
|
|
1765
|
-
|
|
1874
|
+
if (input.expectedCanonicalHead !== void 0 && remoteHeadAtStart !== input.expectedCanonicalHead) {
|
|
1875
|
+
return {
|
|
1876
|
+
...result,
|
|
1877
|
+
outcome: "failed",
|
|
1878
|
+
expectedHead: input.expectedCanonicalHead,
|
|
1879
|
+
publishedHead: remoteHeadAtStart ?? void 0,
|
|
1880
|
+
error: `canonical_head_advanced: expected ${input.expectedCanonicalHead ?? "unborn"}, fetched ${remoteHeadAtStart ?? "unborn"}`
|
|
1881
|
+
};
|
|
1882
|
+
}
|
|
1883
|
+
const inboundConflictsAtStart = input.skipVisibleMirror && !isCanonicalRemediationSync && remoteHeadAtStart ? visibleHydrationConflictPaths(input, hydrationPreimages, `origin/${WORKSPACE_BRANCH}`) : [];
|
|
1766
1884
|
if (input.resetToCanonical) {
|
|
1767
1885
|
if (input.trigger.canonicalCheckoutOnly) {
|
|
1768
1886
|
throw new Error("A canonical-checkout-only synchronization cannot reset the authoritative resolver checkout");
|
|
@@ -1776,6 +1894,8 @@ async function synchronizeWorkspace(rawInput) {
|
|
|
1776
1894
|
gitStatus: gitStatus(input)
|
|
1777
1895
|
};
|
|
1778
1896
|
}
|
|
1897
|
+
const inProgressRemediation = remediationRebaseInProgressResult(input, result);
|
|
1898
|
+
if (inProgressRemediation) return inProgressRemediation;
|
|
1779
1899
|
if (!input.skipVisibleMirror) resetUncommittedShadowSnapshot(input);
|
|
1780
1900
|
restoreUnscopedCanonicalCheckoutSubtrees(input);
|
|
1781
1901
|
if (!input.skipVisibleMirror) {
|
|
@@ -1808,18 +1928,18 @@ async function synchronizeWorkspace(rawInput) {
|
|
|
1808
1928
|
const destructiveReductions = destructiveCheckoutReductions(input, baseRevision);
|
|
1809
1929
|
if (destructiveReductions.length > 0 && !input.confirmedLargeDiff) {
|
|
1810
1930
|
const summary = destructiveReductions.map(({ root, trackedBefore, trackedAfter }) => `${root} (${trackedBefore} -> ${trackedAfter} tracked files)`).join(", ");
|
|
1811
|
-
return
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1931
|
+
return preserveLargeDiffCandidate(
|
|
1932
|
+
input,
|
|
1933
|
+
observed,
|
|
1934
|
+
`Workspace safety guard blocked a destructive checkout reduction: ${summary}`
|
|
1935
|
+
);
|
|
1816
1936
|
}
|
|
1817
1937
|
if (paths.length > 0 && diffSizeBytes > MAX_WORKSPACE_SYNC_DIFF_BYTES && !input.confirmedLargeDiff && !input.trigger.canonicalCheckoutOnly) {
|
|
1818
|
-
return
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1938
|
+
return preserveLargeDiffCandidate(
|
|
1939
|
+
input,
|
|
1940
|
+
observed,
|
|
1941
|
+
`Workspace safety guard blocked a ${diffSizeBytes}-byte diff above the ${MAX_WORKSPACE_SYNC_DIFF_BYTES}-byte automatic publication limit.`
|
|
1942
|
+
);
|
|
1823
1943
|
}
|
|
1824
1944
|
if (stagedWorkingPaths.length > 0) {
|
|
1825
1945
|
runGit(input, input.shadowRoot, ["commit", "-m", commitMessage(input)], "commit workspace changes");
|
|
@@ -1831,15 +1951,12 @@ async function synchronizeWorkspace(rawInput) {
|
|
|
1831
1951
|
const hasUnpushedCommit = candidateHead ? !hasRemoteMain || !tryGit(input, input.shadowRoot, ["merge-base", "--is-ancestor", candidateHead, `origin/${WORKSPACE_BRANCH}`]) : false;
|
|
1832
1952
|
if (hasUnpushedCommit && inboundConflictsAtStart.length > 0) {
|
|
1833
1953
|
const conflictPaths = [.../* @__PURE__ */ new Set([...paths, ...inboundConflictsAtStart])].sort();
|
|
1834
|
-
return {
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
error: "Inbound workspace update overlaps local visible changes; the old-base candidate was preserved for reconciliation.",
|
|
1841
|
-
gitStatus: gitStatus(input)
|
|
1842
|
-
};
|
|
1954
|
+
return preserveWorkspaceConflictCandidate(input, observed, {
|
|
1955
|
+
baseRevision: candidateHead,
|
|
1956
|
+
expectedHead: revParse(input, `origin/${WORKSPACE_BRANCH}`),
|
|
1957
|
+
conflictPaths,
|
|
1958
|
+
error: "Inbound workspace update overlaps local visible changes; the old-base candidate was preserved for reconciliation."
|
|
1959
|
+
});
|
|
1843
1960
|
}
|
|
1844
1961
|
if (!hasUnpushedCommit) {
|
|
1845
1962
|
const localHead = revParse(input, "HEAD");
|
|
@@ -1848,22 +1965,21 @@ async function synchronizeWorkspace(rawInput) {
|
|
|
1848
1965
|
runGit(input, input.shadowRoot, ["reset", "--hard", `origin/${WORKSPACE_BRANCH}`], "update workspace from canonical state");
|
|
1849
1966
|
runGit(input, input.shadowRoot, ["clean", "-fd"], "clean updated workspace");
|
|
1850
1967
|
assertPublishedCanonicalCheckoutTree(input, currentRemoteHead, sampledCanonicalCheckoutTreeHash);
|
|
1851
|
-
|
|
1852
|
-
|
|
1968
|
+
if (!isCanonicalRemediationSync) {
|
|
1969
|
+
mirrorShadowWorkspaceToVisible(input, mirroredGitlinkProjection.opaqueRoots, hydrationPreimages);
|
|
1970
|
+
}
|
|
1971
|
+
const inboundConflicts = input.trigger.canonicalCheckoutOnly || isCanonicalRemediationSync ? [] : localHead ? hydrationConflictPaths(input, hydrationPreimages, localHead, currentRemoteHead) : visibleHydrationConflictPaths(input, hydrationPreimages, currentRemoteHead);
|
|
1853
1972
|
if (inboundConflicts.length > 0 && localHead) {
|
|
1854
1973
|
runGit(input, input.shadowRoot, ["reset", "--hard", localHead], "preserve old workspace base after inbound conflict");
|
|
1855
1974
|
runGit(input, input.shadowRoot, ["clean", "-fd"], "clean preserved old workspace base");
|
|
1856
1975
|
const conflictPaths = [.../* @__PURE__ */ new Set([...paths, ...inboundConflicts])].sort();
|
|
1857
|
-
return {
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
candidateHead: candidateHead ?? void 0,
|
|
1976
|
+
return preserveWorkspaceConflictCandidate(input, observed, {
|
|
1977
|
+
baseRevision: localHead,
|
|
1978
|
+
expectedHead: currentRemoteHead,
|
|
1861
1979
|
publishedHead: currentRemoteHead,
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
gitStatus: gitStatus(input)
|
|
1866
|
-
};
|
|
1980
|
+
conflictPaths,
|
|
1981
|
+
error: "Inbound workspace update overlaps local visible changes; disjoint updates were hydrated and the old base was preserved for reconciliation."
|
|
1982
|
+
});
|
|
1867
1983
|
}
|
|
1868
1984
|
return {
|
|
1869
1985
|
...observed,
|
|
@@ -1876,8 +1992,10 @@ async function synchronizeWorkspace(rawInput) {
|
|
|
1876
1992
|
const authoritativeHead = currentRemoteHead ?? localHead;
|
|
1877
1993
|
if (authoritativeHead) {
|
|
1878
1994
|
assertPublishedCanonicalCheckoutTree(input, authoritativeHead, sampledCanonicalCheckoutTreeHash);
|
|
1879
|
-
|
|
1880
|
-
|
|
1995
|
+
if (!isCanonicalRemediationSync) {
|
|
1996
|
+
mirrorShadowWorkspaceToVisible(input, mirroredGitlinkProjection.opaqueRoots, hydrationPreimages);
|
|
1997
|
+
}
|
|
1998
|
+
const hydrationConflicts2 = input.trigger.canonicalCheckoutOnly || isCanonicalRemediationSync ? [] : fastForwardedFromHead ? hydrationConflictPaths(input, hydrationPreimages, fastForwardedFromHead, authoritativeHead) : visibleHydrationConflictPaths(input, hydrationPreimages, authoritativeHead);
|
|
1881
1999
|
if (hydrationConflicts2.length > 0 && fastForwardedFromHead) {
|
|
1882
2000
|
runGit(
|
|
1883
2001
|
input,
|
|
@@ -1886,16 +2004,13 @@ async function synchronizeWorkspace(rawInput) {
|
|
|
1886
2004
|
"preserve old workspace base after new-checkout fast-forward conflict"
|
|
1887
2005
|
);
|
|
1888
2006
|
runGit(input, input.shadowRoot, ["clean", "-fd"], "clean preserved new-checkout workspace base");
|
|
1889
|
-
return {
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
candidateHead: candidateHead ?? void 0,
|
|
2007
|
+
return preserveWorkspaceConflictCandidate(input, observed, {
|
|
2008
|
+
baseRevision: fastForwardedFromHead,
|
|
2009
|
+
expectedHead: currentRemoteHead,
|
|
1893
2010
|
publishedHead: currentRemoteHead ?? void 0,
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
gitStatus: gitStatus(input)
|
|
1898
|
-
};
|
|
2011
|
+
conflictPaths: hydrationConflicts2,
|
|
2012
|
+
error: "Workspace fast-forward for a new checkout overlaps local visible changes; compatible updates were hydrated and the old base was preserved for reconciliation."
|
|
2013
|
+
});
|
|
1899
2014
|
}
|
|
1900
2015
|
}
|
|
1901
2016
|
return {
|
|
@@ -1915,17 +2030,14 @@ async function synchronizeWorkspace(rawInput) {
|
|
|
1915
2030
|
const conflicted = runGitResult(input, input.shadowRoot, ["diff", "--name-only", "--diff-filter=U", "-z"]);
|
|
1916
2031
|
const conflictPaths = [.../* @__PURE__ */ new Set([...paths, ...conflicted.stdout.split("\0").filter(Boolean)])].sort();
|
|
1917
2032
|
runGit(input, input.shadowRoot, ["rebase", "--abort"], "preserve workspace candidate after rebase conflict");
|
|
1918
|
-
return {
|
|
1919
|
-
|
|
1920
|
-
outcome: "failed",
|
|
2033
|
+
return preserveWorkspaceConflictCandidate(input, observed, {
|
|
2034
|
+
baseRevision: candidateHead,
|
|
1921
2035
|
expectedHead,
|
|
1922
|
-
candidateHead: candidateHead ?? void 0,
|
|
1923
2036
|
rebaseCount,
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
error: `Workspace candidate conflicts with canonical head ${expectedHead}; local changes were preserved for reconciliation
|
|
1927
|
-
|
|
1928
|
-
};
|
|
2037
|
+
preserveShadowCandidate: isCanonicalRemediationSync,
|
|
2038
|
+
conflictPaths,
|
|
2039
|
+
error: `Workspace candidate conflicts with canonical head ${expectedHead}; local changes were preserved for reconciliation.`
|
|
2040
|
+
});
|
|
1929
2041
|
}
|
|
1930
2042
|
candidateHead = revParse(input, "HEAD");
|
|
1931
2043
|
assertPublishedCanonicalCheckoutTree(input, "HEAD", sampledCanonicalCheckoutTreeHash);
|
|
@@ -1941,8 +2053,10 @@ async function synchronizeWorkspace(rawInput) {
|
|
|
1941
2053
|
};
|
|
1942
2054
|
}
|
|
1943
2055
|
assertPublishedCanonicalCheckoutTree(input, candidateHead, sampledCanonicalCheckoutTreeHash);
|
|
1944
|
-
|
|
1945
|
-
|
|
2056
|
+
if (!isCanonicalRemediationSync) {
|
|
2057
|
+
mirrorShadowWorkspaceToVisible(input, mirroredGitlinkProjection.opaqueRoots, hydrationPreimages);
|
|
2058
|
+
}
|
|
2059
|
+
const hydrationConflicts = input.trigger.canonicalCheckoutOnly || isCanonicalRemediationSync ? [] : candidateHeadBeforeRebase ? hydrationConflictPaths(input, hydrationPreimages, candidateHeadBeforeRebase, "HEAD") : visibleHydrationConflictPaths(input, hydrationPreimages, "HEAD");
|
|
1946
2060
|
if (hydrationConflicts.length > 0 && candidateHeadBeforeRebase) {
|
|
1947
2061
|
runGit(
|
|
1948
2062
|
input,
|
|
@@ -1952,17 +2066,13 @@ async function synchronizeWorkspace(rawInput) {
|
|
|
1952
2066
|
);
|
|
1953
2067
|
runGit(input, input.shadowRoot, ["clean", "-fd"], "clean old-base workspace candidate");
|
|
1954
2068
|
const conflictPaths = [.../* @__PURE__ */ new Set([...paths, ...hydrationConflicts])].sort();
|
|
1955
|
-
return {
|
|
1956
|
-
|
|
1957
|
-
outcome: "failed",
|
|
2069
|
+
return preserveWorkspaceConflictCandidate(input, observed, {
|
|
2070
|
+
baseRevision: candidateHeadBeforeRebase,
|
|
1958
2071
|
expectedHead,
|
|
1959
|
-
candidateHead,
|
|
1960
2072
|
rebaseCount,
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
gitStatus: gitStatus(input)
|
|
1965
|
-
};
|
|
2073
|
+
conflictPaths,
|
|
2074
|
+
error: "Workspace changed while its candidate was reconciling; visible changes and the old-base candidate were preserved."
|
|
2075
|
+
});
|
|
1966
2076
|
}
|
|
1967
2077
|
const upload = uploadWorkspaceCandidate(input, candidateHead);
|
|
1968
2078
|
if (upload.exitCode !== 0) {
|
|
@@ -1994,6 +2104,79 @@ async function synchronizeWorkspace(rawInput) {
|
|
|
1994
2104
|
};
|
|
1995
2105
|
}
|
|
1996
2106
|
}
|
|
2107
|
+
async function convergeWorkspaceHead(rawInput, options) {
|
|
2108
|
+
const input = {
|
|
2109
|
+
...rawInput,
|
|
2110
|
+
projects: workspaceProjectsForSync(rawInput.projects, rawInput.trigger)
|
|
2111
|
+
};
|
|
2112
|
+
if (input.trigger.canonicalCheckoutOnly) {
|
|
2113
|
+
throw new Error("Ordinary inbound convergence cannot target a canonical resolver checkout");
|
|
2114
|
+
}
|
|
2115
|
+
ensureShadowWorkspace(input);
|
|
2116
|
+
const fetchedLocalHead = revParse(input, "HEAD");
|
|
2117
|
+
const fetchedCanonicalHead = revParse(input, `origin/${WORKSPACE_BRANCH}`);
|
|
2118
|
+
if (!options.hydrateVisible) {
|
|
2119
|
+
return {
|
|
2120
|
+
localHead: fetchedLocalHead,
|
|
2121
|
+
canonicalHead: fetchedCanonicalHead,
|
|
2122
|
+
localChanges: false,
|
|
2123
|
+
hydrated: false,
|
|
2124
|
+
hydrationConflicts: []
|
|
2125
|
+
};
|
|
2126
|
+
}
|
|
2127
|
+
resetUncommittedShadowSnapshot(input);
|
|
2128
|
+
restoreUnscopedCanonicalCheckoutSubtrees(input);
|
|
2129
|
+
const localHead = fetchedLocalHead;
|
|
2130
|
+
const canonicalHead = fetchedCanonicalHead;
|
|
2131
|
+
const projection = mirrorVisibleWorkspaceToShadow(input);
|
|
2132
|
+
runGit(
|
|
2133
|
+
input,
|
|
2134
|
+
input.shadowRoot,
|
|
2135
|
+
["add", "-A", "--", ".", ...projection.opaqueRoots.map((root) => `:(exclude,literal)${root}`)],
|
|
2136
|
+
"stage inbound workspace observation"
|
|
2137
|
+
);
|
|
2138
|
+
forceStageCanonicalCheckoutFiles(input, projection.entries, projection.opaqueRoots);
|
|
2139
|
+
const stagedTree = runGit(input, input.shadowRoot, ["write-tree"], "write inbound workspace observation tree");
|
|
2140
|
+
const headTree = localHead ? revParse(input, `${localHead}^{tree}`) : null;
|
|
2141
|
+
const localChanges = stagedTree !== headTree;
|
|
2142
|
+
if (!options.hydrateVisible || localChanges || !canonicalHead || localHead === canonicalHead) {
|
|
2143
|
+
resetUncommittedShadowSnapshot(input);
|
|
2144
|
+
return { localHead, canonicalHead, localChanges, hydrated: false, hydrationConflicts: [] };
|
|
2145
|
+
}
|
|
2146
|
+
const preimages = projection.visiblePreimages ?? hydrationPreimagesFromShadowRevision(input, localHead);
|
|
2147
|
+
const hydrationConflicts = localHead ? hydrationConflictPaths(input, preimages, localHead, canonicalHead) : [];
|
|
2148
|
+
if (hydrationConflicts.length > 0) {
|
|
2149
|
+
resetUncommittedShadowSnapshot(input);
|
|
2150
|
+
return { localHead, canonicalHead, localChanges: true, hydrated: false, hydrationConflicts };
|
|
2151
|
+
}
|
|
2152
|
+
runGit(input, input.shadowRoot, ["reset", "--hard", canonicalHead], "fast-forward workspace to available canonical head");
|
|
2153
|
+
runGit(input, input.shadowRoot, ["clean", "-fd"], "clean fast-forwarded workspace");
|
|
2154
|
+
mirrorShadowWorkspaceToVisible(input, projection.opaqueRoots, preimages);
|
|
2155
|
+
return { localHead: canonicalHead, canonicalHead, localChanges: false, hydrated: true, hydrationConflicts: [] };
|
|
2156
|
+
}
|
|
2157
|
+
function calculateWorkspaceLocalDiffFingerprint(rawInput) {
|
|
2158
|
+
const input = {
|
|
2159
|
+
...rawInput,
|
|
2160
|
+
projects: workspaceProjectsForSync(rawInput.projects, rawInput.trigger)
|
|
2161
|
+
};
|
|
2162
|
+
if (!fs.existsSync(path.join(input.shadowRoot, ".git"))) {
|
|
2163
|
+
throw new Error("Cannot inspect local workspace changes before first bootstrap");
|
|
2164
|
+
}
|
|
2165
|
+
resetUncommittedShadowSnapshot(input);
|
|
2166
|
+
restoreUnscopedCanonicalCheckoutSubtrees(input);
|
|
2167
|
+
const projection = mirrorVisibleWorkspaceToShadow(input);
|
|
2168
|
+
runGit(
|
|
2169
|
+
input,
|
|
2170
|
+
input.shadowRoot,
|
|
2171
|
+
["add", "-A", "--", ".", ...projection.opaqueRoots.map((root) => `:(exclude,literal)${root}`)],
|
|
2172
|
+
"stage local workspace fingerprint"
|
|
2173
|
+
);
|
|
2174
|
+
forceStageCanonicalCheckoutFiles(input, projection.entries, projection.opaqueRoots);
|
|
2175
|
+
const stagedTree = runGit(input, input.shadowRoot, ["write-tree"], "write local workspace fingerprint tree");
|
|
2176
|
+
const headTree = revParse(input, "HEAD^{tree}");
|
|
2177
|
+
resetUncommittedShadowSnapshot(input);
|
|
2178
|
+
return stagedTree === headTree ? createHash("sha256").update("").digest("hex") : createHash("sha256").update(stagedTree).digest("hex");
|
|
2179
|
+
}
|
|
1997
2180
|
function calculateWorkspaceDiffFingerprint(rawInput) {
|
|
1998
2181
|
const input = {
|
|
1999
2182
|
...rawInput,
|
|
@@ -2047,6 +2230,9 @@ class WorkspaceSyncSingleFlight {
|
|
|
2047
2230
|
fingerprintPrepared(prepare) {
|
|
2048
2231
|
return this.enqueue(() => calculateWorkspaceDiffFingerprint(prepare()));
|
|
2049
2232
|
}
|
|
2233
|
+
runExclusive(operation) {
|
|
2234
|
+
return this.enqueue(operation);
|
|
2235
|
+
}
|
|
2050
2236
|
runMutation(operation) {
|
|
2051
2237
|
return this.mutationGate.runMutation(operation);
|
|
2052
2238
|
}
|
|
@@ -2062,11 +2248,13 @@ export {
|
|
|
2062
2248
|
WORKSPACE_BRANCH,
|
|
2063
2249
|
WORKSPACE_INTENT_REF_PREFIX,
|
|
2064
2250
|
WORKSPACE_NATIVE_PLANS_RELATIVE_PATH,
|
|
2065
|
-
|
|
2251
|
+
WORKSPACE_PUBLICATION_REF_PREFIX,
|
|
2066
2252
|
WorkspaceSyncSingleFlight,
|
|
2067
2253
|
assertCanonicalCheckoutIndexMatchesWorktree,
|
|
2068
2254
|
assertWorkspaceQuarantineRef,
|
|
2069
2255
|
calculateWorkspaceDiffFingerprint,
|
|
2256
|
+
calculateWorkspaceLocalDiffFingerprint,
|
|
2257
|
+
convergeWorkspaceHead,
|
|
2070
2258
|
encodeWorkspaceBranch,
|
|
2071
2259
|
mirrorShadowWorkspaceToVisible,
|
|
2072
2260
|
mirrorVisibleWorkspaceToShadow,
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
export declare const WORKSPACE_CHECKPOINT_QUIET_MS = 5000;
|
|
2
|
+
export declare const WORKSPACE_IDLE_SAFETY_SCAN_MS = 60000;
|
|
3
|
+
export type WorkspaceCheckpointState = "idle" | "scheduled" | "preparing" | "submitting" | "blocked";
|
|
4
|
+
export type WorkspaceConvergenceSnapshot = {
|
|
5
|
+
localHead: string | null;
|
|
6
|
+
desiredCanonicalHead: string | null;
|
|
7
|
+
dirtyGeneration: number;
|
|
8
|
+
publishedGeneration: number;
|
|
9
|
+
checkpointState: WorkspaceCheckpointState;
|
|
10
|
+
};
|
|
11
|
+
export type ExplicitWorkspaceCheckpoint<TTrigger> = {
|
|
12
|
+
requestId: string;
|
|
13
|
+
trigger: TTrigger;
|
|
14
|
+
};
|
|
15
|
+
export type WorkspaceManifestRevisionToken = {
|
|
16
|
+
sequence: number;
|
|
17
|
+
revision: string;
|
|
18
|
+
};
|
|
19
|
+
export type WorkspaceWriterLease = () => void;
|
|
20
|
+
/**
|
|
21
|
+
* Makes checkpoint admission atomic with command startup. Once a checkpoint
|
|
22
|
+
* lease is admitted, later writers wait; once a writer lease is admitted, a
|
|
23
|
+
* checkpoint must defer. Writer acquisition increments synchronously before
|
|
24
|
+
* its promise resolves, covering asynchronous environment setup and spawn.
|
|
25
|
+
*/
|
|
26
|
+
export declare class WorkspaceFilesystemWriterGate {
|
|
27
|
+
private activeWriters;
|
|
28
|
+
private checkpointActive;
|
|
29
|
+
private readonly waitingWriters;
|
|
30
|
+
private readonly waitingCheckpoints;
|
|
31
|
+
acquireWriter(): Promise<WorkspaceWriterLease>;
|
|
32
|
+
tryAcquireCheckpoint(): WorkspaceWriterLease | null;
|
|
33
|
+
acquireCheckpoint(): Promise<WorkspaceWriterLease>;
|
|
34
|
+
private checkpointRelease;
|
|
35
|
+
private writerRelease;
|
|
36
|
+
private drain;
|
|
37
|
+
}
|
|
38
|
+
export declare function processWorkspaceFilesystemWriterGate(): WorkspaceFilesystemWriterGate;
|
|
39
|
+
export declare class WorkspaceManifestRevisionFence {
|
|
40
|
+
private sequence;
|
|
41
|
+
private requestedRevision;
|
|
42
|
+
private completedRevision;
|
|
43
|
+
begin(revision: string): WorkspaceManifestRevisionToken;
|
|
44
|
+
isCurrent(token: WorkspaceManifestRevisionToken): boolean;
|
|
45
|
+
complete(token: WorkspaceManifestRevisionToken): boolean;
|
|
46
|
+
readyRevision(): string | null;
|
|
47
|
+
}
|
|
48
|
+
export declare function workspaceCheckpointTelemetry(input: {
|
|
49
|
+
requestedAt: number;
|
|
50
|
+
prepareStartedAt: number;
|
|
51
|
+
synchronizeStartedAt: number;
|
|
52
|
+
finishedAt: number;
|
|
53
|
+
}): {
|
|
54
|
+
totalMs: number;
|
|
55
|
+
queueMs: number;
|
|
56
|
+
prepareMs: number;
|
|
57
|
+
synchronizeMs: number;
|
|
58
|
+
};
|
|
59
|
+
export declare function workspaceCheckpointIncidentAction(requestId: string | undefined): "defer_automatic" | "request_terminal_authority";
|
|
60
|
+
export declare class ExplicitWorkspaceCheckpointQueue<TTrigger> {
|
|
61
|
+
private scheduled;
|
|
62
|
+
private readonly queued;
|
|
63
|
+
schedule(checkpoint: ExplicitWorkspaceCheckpoint<TTrigger>, pendingRequestId?: string): boolean;
|
|
64
|
+
hasScheduled(): boolean;
|
|
65
|
+
consumeScheduled(requestId: string): void;
|
|
66
|
+
enqueue(checkpoint: ExplicitWorkspaceCheckpoint<TTrigger>, pendingRequestId?: string): void;
|
|
67
|
+
next(): ExplicitWorkspaceCheckpoint<TTrigger> | undefined;
|
|
68
|
+
}
|
|
69
|
+
type PersistedWorkspaceConvergenceState = Omit<WorkspaceConvergenceSnapshot, "checkpointState">;
|
|
70
|
+
export declare function readWorkspaceConvergenceState(statePath: string): PersistedWorkspaceConvergenceState;
|
|
71
|
+
export declare class WorkspaceConvergenceState {
|
|
72
|
+
private readonly statePath;
|
|
73
|
+
private state;
|
|
74
|
+
private checkpointState;
|
|
75
|
+
constructor(statePath: string);
|
|
76
|
+
snapshot(): WorkspaceConvergenceSnapshot;
|
|
77
|
+
setLocalHead(localHead: string | null): void;
|
|
78
|
+
observeCanonicalHead(canonicalHead: string | null): boolean;
|
|
79
|
+
markDirty(): number;
|
|
80
|
+
schedule(): void;
|
|
81
|
+
beginPreparing(): void;
|
|
82
|
+
beginSubmitting(): void;
|
|
83
|
+
defer(): void;
|
|
84
|
+
block(): void;
|
|
85
|
+
releaseBlock(): void;
|
|
86
|
+
observeIncidentState(blocked: boolean): void;
|
|
87
|
+
resetTransientCheckpointState(): void;
|
|
88
|
+
complete(input: {
|
|
89
|
+
generation: number;
|
|
90
|
+
canonicalHead: string | null;
|
|
91
|
+
published: boolean;
|
|
92
|
+
}): void;
|
|
93
|
+
private persist;
|
|
94
|
+
}
|
|
95
|
+
export declare function processWorkspaceConvergenceState(statePath: string): WorkspaceConvergenceState;
|
|
96
|
+
export declare function waitForWorkspaceCheckpointOnShutdown(input: {
|
|
97
|
+
snapshot: () => WorkspaceConvergenceSnapshot;
|
|
98
|
+
forceCheckpoint: () => void;
|
|
99
|
+
timeoutMs?: number;
|
|
100
|
+
pollMs?: number;
|
|
101
|
+
}): Promise<"settled" | "blocked" | "timed_out">;
|
|
102
|
+
export {};
|
|
@@ -2,7 +2,21 @@ export type WorkspaceIncidentUpdate = {
|
|
|
2
2
|
incidentId: string | null;
|
|
3
3
|
status: "remediating" | "waiting_for_worker" | "resolved" | "confirmed" | "reset" | null;
|
|
4
4
|
};
|
|
5
|
+
/**
|
|
6
|
+
* WebSocket writes are ordered, but independent server handlers can decide an
|
|
7
|
+
* incident state under the user lock and write their messages after releasing
|
|
8
|
+
* it. Remember terminal incident IDs so a delayed checkpoint response cannot
|
|
9
|
+
* resurrect a barrier that this connection has already observed as terminal.
|
|
10
|
+
*/
|
|
11
|
+
export declare class WorkspaceIncidentOrderingFence {
|
|
12
|
+
private readonly historyLimit;
|
|
13
|
+
private readonly terminalIncidentIds;
|
|
14
|
+
private readonly terminalIncidentOrder;
|
|
15
|
+
constructor(historyLimit?: number);
|
|
16
|
+
observe(update: WorkspaceIncidentUpdate): void;
|
|
17
|
+
permitsBlockedResponse(incidentId: string | null | undefined): boolean;
|
|
18
|
+
}
|
|
5
19
|
export declare function applyWorkspaceIncidentUpdate(currentIncidentId: string | null, update: WorkspaceIncidentUpdate): string | null;
|
|
6
20
|
export declare function releasePendingWorkspaceHead(previousIncidentId: string | null, currentIncidentId: string | null, terminalIncidentId?: string | null): {
|
|
7
|
-
|
|
21
|
+
fetchAuthoritativeHead: boolean;
|
|
8
22
|
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { WorkspaceFilesystemWriterGate, WorkspaceWriterLease } from "./workspace-convergence";
|
|
2
|
+
export type WorkspaceManifestCheckout = {
|
|
3
|
+
projectId: string;
|
|
4
|
+
projectPath: string;
|
|
5
|
+
checkoutPathSegments: [namespace: string, project: string];
|
|
6
|
+
branches: string[];
|
|
7
|
+
};
|
|
8
|
+
export type WorkspaceManifestAdmission = {
|
|
9
|
+
firstBootstrap: boolean;
|
|
10
|
+
missingCheckouts: Array<{
|
|
11
|
+
projectId: string;
|
|
12
|
+
branchName: string;
|
|
13
|
+
}>;
|
|
14
|
+
migratedProjectIds: string[];
|
|
15
|
+
requiresVisibleLease: boolean;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Inspects only the paths that make manifest acknowledgement mutate existing
|
|
19
|
+
* visible state. Missing checkouts are reported as target-specific pending
|
|
20
|
+
* work and hydrated opportunistically; they must not delay unrelated targets.
|
|
21
|
+
*/
|
|
22
|
+
export declare function inspectWorkspaceManifestAdmission(input: {
|
|
23
|
+
projectsRoot: string;
|
|
24
|
+
workspaceShadowRoot: string;
|
|
25
|
+
projects: WorkspaceManifestCheckout[];
|
|
26
|
+
}): WorkspaceManifestAdmission;
|
|
27
|
+
export declare function acquireWorkspaceManifestVisibleLease(gate: WorkspaceFilesystemWriterGate, admission: WorkspaceManifestAdmission): Promise<WorkspaceWriterLease | undefined>;
|