@ricsam/r5d-worker 0.0.74 → 0.0.76

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.
Files changed (32) hide show
  1. package/README.md +6 -4
  2. package/dist/cjs/main.cjs +1031 -976
  3. package/dist/cjs/package.json +1 -1
  4. package/dist/cjs/project-workspace-state.cjs +777 -0
  5. package/dist/cjs/project-worktrees.cjs +559 -0
  6. package/dist/cjs/working-tree-mirror.cjs +225 -0
  7. package/dist/cjs/workspace-git-sync.cjs +565 -0
  8. package/dist/cjs/workspace-incident-state.cjs +2 -38
  9. package/dist/mjs/main.mjs +1048 -987
  10. package/dist/mjs/package.json +1 -1
  11. package/dist/mjs/project-workspace-state.mjs +745 -0
  12. package/dist/mjs/project-worktrees.mjs +513 -0
  13. package/dist/mjs/working-tree-mirror.mjs +190 -0
  14. package/dist/mjs/workspace-git-sync.mjs +524 -0
  15. package/dist/mjs/workspace-incident-state.mjs +1 -35
  16. package/dist/types/main.d.ts +35 -55
  17. package/dist/types/project-workspace-state.d.ts +144 -0
  18. package/dist/types/project-worktrees.d.ts +112 -0
  19. package/dist/types/working-tree-mirror.d.ts +24 -0
  20. package/dist/types/workspace-git-sync.d.ts +97 -0
  21. package/dist/types/workspace-incident-state.d.ts +0 -17
  22. package/dist/types/workspace-mutation-gate.d.ts +3 -3
  23. package/package.json +1 -1
  24. package/dist/cjs/workspace-convergence.cjs +0 -280
  25. package/dist/cjs/workspace-manifest-admission.cjs +0 -60
  26. package/dist/cjs/workspace-sync.cjs +0 -2469
  27. package/dist/mjs/workspace-convergence.mjs +0 -236
  28. package/dist/mjs/workspace-manifest-admission.mjs +0 -26
  29. package/dist/mjs/workspace-sync.mjs +0 -2417
  30. package/dist/types/workspace-convergence.d.ts +0 -93
  31. package/dist/types/workspace-manifest-admission.d.ts +0 -22
  32. package/dist/types/workspace-sync.d.ts +0 -167
@@ -1,2469 +0,0 @@
1
- "use strict";
2
- var __create = Object.create;
3
- var __defProp = Object.defineProperty;
4
- var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
- var __getOwnPropNames = Object.getOwnPropertyNames;
6
- var __getProtoOf = Object.getPrototypeOf;
7
- var __hasOwnProp = Object.prototype.hasOwnProperty;
8
- var __export = (target, all) => {
9
- for (var name in all)
10
- __defProp(target, name, { get: all[name], enumerable: true });
11
- };
12
- var __copyProps = (to, from, except, desc) => {
13
- if (from && typeof from === "object" || typeof from === "function") {
14
- for (let key of __getOwnPropNames(from))
15
- if (!__hasOwnProp.call(to, key) && key !== except)
16
- __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
- }
18
- return to;
19
- };
20
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
- // If the importer is in node compatibility mode or this is not an ESM
22
- // file that has been converted to a CommonJS file using a Babel-
23
- // compatible transform (i.e. "__esModule" has not been set), then set
24
- // "default" to the CommonJS "module.exports" for node compatibility.
25
- isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
- mod
27
- ));
28
- var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
- var workspace_sync_exports = {};
30
- __export(workspace_sync_exports, {
31
- MAX_WORKSPACE_SYNC_DIFF_BYTES: () => MAX_WORKSPACE_SYNC_DIFF_BYTES,
32
- WORKSPACE_BRANCH: () => WORKSPACE_BRANCH,
33
- WORKSPACE_INTENT_REF_PREFIX: () => WORKSPACE_INTENT_REF_PREFIX,
34
- WORKSPACE_NATIVE_PLANS_RELATIVE_PATH: () => WORKSPACE_NATIVE_PLANS_RELATIVE_PATH,
35
- WORKSPACE_PUBLICATION_REF_PREFIX: () => WORKSPACE_PUBLICATION_REF_PREFIX,
36
- WorkspaceSyncSingleFlight: () => WorkspaceSyncSingleFlight,
37
- assertCanonicalCheckoutIndexMatchesWorktree: () => assertCanonicalCheckoutIndexMatchesWorktree,
38
- assertWorkspaceQuarantineRef: () => assertWorkspaceQuarantineRef,
39
- calculateWorkspaceDiffFingerprint: () => calculateWorkspaceDiffFingerprint,
40
- calculateWorkspaceLocalDiffFingerprint: () => calculateWorkspaceLocalDiffFingerprint,
41
- convergeWorkspaceHead: () => convergeWorkspaceHead,
42
- encodeWorkspaceBranch: () => encodeWorkspaceBranch,
43
- mirrorShadowWorkspaceToVisible: () => mirrorShadowWorkspaceToVisible,
44
- mirrorVisibleWorkspaceToShadow: () => mirrorVisibleWorkspaceToShadow,
45
- synchronizeWorkspace: () => synchronizeWorkspace,
46
- visibleProjectBranchPath: () => visibleProjectBranchPath,
47
- workspacePlansRelativePath: () => workspacePlansRelativePath,
48
- workspaceProjectBranchRelativePath: () => workspaceProjectBranchRelativePath,
49
- workspaceProjectsForSync: () => workspaceProjectsForSync
50
- });
51
- module.exports = __toCommonJS(workspace_sync_exports);
52
- var import_node_crypto = require("node:crypto");
53
- var import_node_fs = __toESM(require("node:fs"), 1);
54
- var import_node_path = __toESM(require("node:path"), 1);
55
- var import_managed_paths = require("./managed-paths.cjs");
56
- var import_workspace_mutation_gate = require("./workspace-mutation-gate.cjs");
57
- const WORKSPACE_BRANCH = "main";
58
- const WORKSPACE_INTENT_REF_PREFIX = "refs/r5d/workspace-intents/";
59
- const WORKSPACE_PUBLICATION_REF_PREFIX = "refs/r5d/workspace-publications/";
60
- const MAX_WORKSPACE_SYNC_DIFF_BYTES = 5 * 1024 * 1024;
61
- const MAX_REPORTED_WORKSPACE_PATHS = 2e3;
62
- const MAX_REPORTED_WORKSPACE_STATUS_BYTES = 256 * 1024;
63
- const MAX_MIRROR_COMPARISON_CACHE_ENTRIES = 2e5;
64
- const MIRROR_COMPARE_BUFFER_BYTES = 64 * 1024;
65
- const DESTRUCTIVE_CHECKOUT_MIN_TRACKED_FILES = 20;
66
- const DESTRUCTIVE_CHECKOUT_REMOVAL_RATIO = 0.8;
67
- const ACTIVE_GIT_LOCK_PATHS = ["index.lock", "HEAD.lock", "packed-refs.lock", "shallow.lock"];
68
- const mirrorComparisonCache = /* @__PURE__ */ new Map();
69
- const gitObjectFormatCache = /* @__PURE__ */ new Map();
70
- const hydrationEntryCache = /* @__PURE__ */ new Map();
71
- function gitArgs(input, args) {
72
- const configArgs = ["-c", "submodule.recurse=false", "-c", "fetch.recurseSubmodules=false", "-c", "push.recurseSubmodules=false"];
73
- return input.authHeader ? ["git", ...configArgs, "-c", `http.extraHeader=${input.authHeader}`, ...args] : ["git", ...configArgs, ...args];
74
- }
75
- function withoutSubmoduleRecursion(args) {
76
- return args[0] === "fetch" || args[0] === "push" ? [args[0], "--no-recurse-submodules", ...args.slice(1)] : args[0] === "clone" ? ["clone", "--no-recurse-submodules", ...args.slice(1)] : args[0] === "checkout" || args[0] === "reset" || args[0] === "restore" ? [args[0], "--no-recurse-submodules", ...args.slice(1)] : args;
77
- }
78
- function runGitResult(input, cwd, args) {
79
- const result = Bun.spawnSync(gitArgs(input, withoutSubmoduleRecursion(args)), {
80
- cwd,
81
- stdout: "pipe",
82
- stderr: "pipe",
83
- env: {
84
- ...process.env,
85
- GIT_TERMINAL_PROMPT: "0"
86
- }
87
- });
88
- return {
89
- exitCode: result.exitCode,
90
- stdout: result.stdout.toString().trim(),
91
- stderr: result.stderr.toString().trim()
92
- };
93
- }
94
- function runGit(input, cwd, args, action) {
95
- const result = runGitResult(input, cwd, args);
96
- if (result.exitCode !== 0) {
97
- throw new Error(`${action}: ${result.stderr || result.stdout || `git exited ${result.exitCode}`}`);
98
- }
99
- return result.stdout;
100
- }
101
- function tryGit(input, cwd, args) {
102
- return runGitResult(input, cwd, args).exitCode === 0;
103
- }
104
- function encodeWorkspaceBranch(branchName) {
105
- return encodeURIComponent(branchName);
106
- }
107
- function workspaceProjectsForSync(projects, trigger) {
108
- if (trigger.canonicalCheckoutOnly) {
109
- if (!trigger.projectId || !trigger.branchName) {
110
- throw new Error("A canonical-checkout-only synchronization requires projectId and branchName");
111
- }
112
- const project = projects.find((candidate) => candidate.projectId === trigger.projectId);
113
- const checkout = project?.canonicalCheckouts.find((candidate) => candidate.branchName === trigger.branchName);
114
- if (!project || !project.branches.includes(trigger.branchName) || !checkout) {
115
- throw new Error(`Canonical resolver checkout ${trigger.projectId}/${trigger.branchName} is not present in this worker manifest`);
116
- }
117
- return [
118
- {
119
- ...project,
120
- branches: [trigger.branchName],
121
- canonicalCheckouts: [checkout]
122
- }
123
- ];
124
- }
125
- return projects.flatMap((project) => {
126
- const canonicalBranches = new Set(project.canonicalCheckouts.map((checkout) => checkout.branchName));
127
- const branches = project.branches.filter((branchName) => !canonicalBranches.has(branchName));
128
- return branches.length > 0 || project.canonicalCheckouts.length > 0 ? [
129
- {
130
- ...project,
131
- branches
132
- }
133
- ] : [];
134
- });
135
- }
136
- function workspaceProjectBranchRelativePath(projectId, branchName) {
137
- return import_node_path.default.posix.join("projects", projectId, "branches", encodeWorkspaceBranch(branchName));
138
- }
139
- function workspacePlansRelativePath(projectId, branchName) {
140
- return import_node_path.default.posix.join("plans", projectId, encodeWorkspaceBranch(branchName));
141
- }
142
- const WORKSPACE_NATIVE_PLANS_RELATIVE_PATH = "workspace-plans";
143
- function visibleProjectBranchPath(projectsRoot, manifest, branchName) {
144
- return (0, import_managed_paths.managedBranchPath)(projectsRoot, manifest.checkoutPathSegments, branchName);
145
- }
146
- function localPlansBranchPath(plansRoot, projectId, branchName) {
147
- return import_node_path.default.join(plansRoot, projectId, branchName);
148
- }
149
- function localWorkspacePlansPath(plansRoot) {
150
- return import_node_path.default.join(plansRoot, "workspace");
151
- }
152
- function assertInside(root, candidate, label) {
153
- const resolvedRoot = import_node_path.default.resolve(root);
154
- const resolvedCandidate = import_node_path.default.resolve(candidate);
155
- const relative = import_node_path.default.relative(resolvedRoot, resolvedCandidate);
156
- if (relative === ".." || relative.startsWith(`..${import_node_path.default.sep}`) || import_node_path.default.isAbsolute(relative)) {
157
- throw new Error(`${label} escapes its managed root: ${candidate}`);
158
- }
159
- }
160
- function listGitEligibleFiles(checkoutPath) {
161
- if (!import_node_fs.default.existsSync(import_node_path.default.join(checkoutPath, ".git"))) {
162
- return [];
163
- }
164
- const result = Bun.spawnSync(
165
- [
166
- "git",
167
- "-c",
168
- "submodule.recurse=false",
169
- "-c",
170
- "fetch.recurseSubmodules=false",
171
- "-c",
172
- "push.recurseSubmodules=false",
173
- "ls-files",
174
- "-z",
175
- "--cached",
176
- "--others",
177
- "--exclude-standard",
178
- "--",
179
- "."
180
- ],
181
- {
182
- cwd: checkoutPath,
183
- stdout: "pipe",
184
- stderr: "pipe",
185
- env: { ...process.env, GIT_TERMINAL_PROMPT: "0" }
186
- }
187
- );
188
- if (result.exitCode !== 0) {
189
- throw new Error(`inspect eligible project files: ${result.stderr.toString().trim() || `git exited ${result.exitCode}`}`);
190
- }
191
- return result.stdout.toString().split("\0").filter((entry) => entry.length > 0 && entry !== ".git" && !entry.startsWith(".git/")).filter((entry) => {
192
- try {
193
- const stat = import_node_fs.default.lstatSync(import_node_path.default.join(checkoutPath, ...entry.split("/")));
194
- return stat.isFile() || stat.isSymbolicLink();
195
- } catch {
196
- return false;
197
- }
198
- }).sort();
199
- }
200
- function runCheckoutGitRaw(checkoutPath, args, action) {
201
- const result = Bun.spawnSync(
202
- ["git", "-c", "submodule.recurse=false", "-c", "fetch.recurseSubmodules=false", "-c", "push.recurseSubmodules=false", ...args],
203
- {
204
- cwd: checkoutPath,
205
- stdout: "pipe",
206
- stderr: "pipe",
207
- env: { ...process.env, GIT_TERMINAL_PROMPT: "0" }
208
- }
209
- );
210
- if (result.exitCode !== 0) {
211
- throw new Error(`${action}: ${result.stderr.toString().trim() || `git exited ${result.exitCode}`}`);
212
- }
213
- return result.stdout.toString();
214
- }
215
- function stagedCheckoutPaths(checkoutPath) {
216
- return runCheckoutGitRaw(
217
- checkoutPath,
218
- ["diff", "--cached", "--name-only", "--no-renames", "-z", "HEAD"],
219
- "inspect staged canonical resolver paths"
220
- ).split("\0").filter(Boolean).sort();
221
- }
222
- function parseGitIndexEntries(output) {
223
- return output.split("\0").filter(Boolean).flatMap((entry) => {
224
- const separator = entry.indexOf(" ");
225
- if (separator === -1) return [];
226
- const [mode, objectId, stage] = entry.slice(0, separator).split(" ");
227
- const filePath = entry.slice(separator + 1);
228
- return mode && objectId && stage && filePath ? [{ mode, objectId, stage, filePath }] : [];
229
- });
230
- }
231
- function indexEntriesForPath(checkoutPath, filePath) {
232
- return parseGitIndexEntries(
233
- runCheckoutGitRaw(
234
- checkoutPath,
235
- ["--literal-pathspecs", "ls-files", "--stage", "-z", "--", filePath],
236
- `inspect canonical resolver index entry for ${JSON.stringify(filePath)}`
237
- )
238
- );
239
- }
240
- function checkoutIndexEntries(checkoutPath) {
241
- return parseGitIndexEntries(runCheckoutGitRaw(checkoutPath, ["ls-files", "--stage", "-z", "--", "."], "inspect project Git index"));
242
- }
243
- function checkoutGitlinks(checkoutPath) {
244
- const entries = checkoutIndexEntries(checkoutPath);
245
- const unmergedPaths = [...new Set(entries.filter((entry) => entry.stage !== "0").map((entry) => entry.filePath))].sort();
246
- if (unmergedPaths.length > 0) {
247
- throw new Error(`Project checkout has unmerged Git index stages: ${JSON.stringify(unmergedPaths)}`);
248
- }
249
- return entries.filter((entry) => entry.mode === "160000").map(({ filePath, objectId }) => ({ filePath, objectId })).sort((left, right) => left.filePath.localeCompare(right.filePath));
250
- }
251
- function revisionGitlinks(checkoutPath, revision) {
252
- const output = runCheckoutGitRaw(checkoutPath, ["ls-tree", "-r", "-z", revision, "--", "."], `inspect ${revision} project gitlinks`);
253
- return output.split("\0").filter(Boolean).flatMap((entry) => {
254
- const separator = entry.indexOf(" ");
255
- if (separator === -1) return [];
256
- const [mode, type, objectId] = entry.slice(0, separator).split(" ");
257
- const filePath = entry.slice(separator + 1);
258
- return mode === "160000" && type === "commit" && objectId && filePath ? [{ filePath, objectId }] : [];
259
- }).sort((left, right) => left.filePath.localeCompare(right.filePath));
260
- }
261
- function stagedDeletedGitlinkPaths(checkoutPath) {
262
- const currentEntries = checkoutIndexEntries(checkoutPath);
263
- return revisionGitlinks(checkoutPath, "HEAD").filter(
264
- (entry) => !currentEntries.some((candidate) => candidate.filePath === entry.filePath || candidate.filePath.startsWith(`${entry.filePath}/`))
265
- ).map((entry) => entry.filePath).sort();
266
- }
267
- function lstatOrNull(filePath) {
268
- try {
269
- return import_node_fs.default.lstatSync(filePath);
270
- } catch (error) {
271
- if (error.code === "ENOENT") return null;
272
- throw error;
273
- }
274
- }
275
- const UNSTABLE_VISIBLE_ENTRY = /* @__PURE__ */ Symbol("unstable-visible-entry");
276
- function hydrationEntriesEqual(left, right) {
277
- if (!left || !right || left.kind !== right.kind || left.objectId !== right.objectId) return left === right;
278
- return left.kind === "gitlink" || right.kind === "blob" && left.mode === right.mode;
279
- }
280
- function gitObjectFormat(input) {
281
- const cached = gitObjectFormatCache.get(input.shadowRoot);
282
- if (cached) return cached;
283
- const format = runGit(input, input.shadowRoot, ["rev-parse", "--show-object-format"], "inspect workspace object format");
284
- if (format !== "sha1" && format !== "sha256") throw new Error(`Unsupported Git object format ${JSON.stringify(format)}`);
285
- gitObjectFormatCache.set(input.shadowRoot, format);
286
- return format;
287
- }
288
- function hashGitBlobBuffer(input, content) {
289
- return (0, import_node_crypto.createHash)(gitObjectFormat(input)).update(`blob ${content.byteLength}\0`).update(content).digest("hex");
290
- }
291
- function hashGitBlobFile(input, filePath, size) {
292
- const hash = (0, import_node_crypto.createHash)(gitObjectFormat(input));
293
- hash.update(`blob ${size}\0`);
294
- const file = import_node_fs.default.openSync(filePath, "r");
295
- const buffer = Buffer.allocUnsafe(Math.min(MIRROR_COMPARE_BUFFER_BYTES, Math.max(1, size)));
296
- try {
297
- let offset = 0;
298
- while (offset < size) {
299
- const bytesRead = import_node_fs.default.readSync(file, buffer, 0, Math.min(buffer.length, size - offset), offset);
300
- if (bytesRead === 0) return null;
301
- hash.update(buffer.subarray(0, bytesRead));
302
- offset += bytesRead;
303
- }
304
- return hash.digest("hex");
305
- } finally {
306
- import_node_fs.default.closeSync(file);
307
- }
308
- }
309
- function filesystemHydrationEntry(input, filePath) {
310
- const before = lstatOrNull(filePath);
311
- if (!before) return void 0;
312
- if (!before.isFile() && !before.isSymbolicLink()) return UNSTABLE_VISIBLE_ENTRY;
313
- const objectFormat = gitObjectFormat(input);
314
- const signature = entrySignature(before);
315
- const cached = hydrationEntryCache.get(filePath);
316
- if (cached?.signature === signature && cached.objectFormat === objectFormat) return cached.entry;
317
- let objectId = null;
318
- if (before.isSymbolicLink()) {
319
- try {
320
- objectId = hashGitBlobBuffer(input, Buffer.from(import_node_fs.default.readlinkSync(filePath)));
321
- } catch {
322
- return UNSTABLE_VISIBLE_ENTRY;
323
- }
324
- } else {
325
- try {
326
- objectId = hashGitBlobFile(input, filePath, before.size);
327
- } catch {
328
- return UNSTABLE_VISIBLE_ENTRY;
329
- }
330
- }
331
- const after = lstatOrNull(filePath);
332
- if (!objectId || !after || entrySignature(before) !== entrySignature(after)) return UNSTABLE_VISIBLE_ENTRY;
333
- const entry = {
334
- kind: "blob",
335
- mode: before.isSymbolicLink() ? "120000" : (before.mode & 73) !== 0 ? "100755" : "100644",
336
- objectId
337
- };
338
- if (hydrationEntryCache.size >= MAX_MIRROR_COMPARISON_CACHE_ENTRIES) hydrationEntryCache.clear();
339
- hydrationEntryCache.set(filePath, { signature, objectFormat, entry });
340
- return entry;
341
- }
342
- function projectVisibleHydrationEntry(input, visibleRoot, filePath, gitlinks) {
343
- if (!hasSafeDirectoryAncestors(visibleRoot, filePath, input.projectsRoot)) return UNSTABLE_VISIBLE_ENTRY;
344
- const gitlink = gitlinks.get(filePath);
345
- if (gitlink) {
346
- const target = lstatOrNull(import_node_path.default.join(visibleRoot, ...filePath.split("/")));
347
- if (target && !target.isDirectory()) return UNSTABLE_VISIBLE_ENTRY;
348
- return { kind: "gitlink", objectId: gitlink.objectId };
349
- }
350
- return filesystemHydrationEntry(input, import_node_path.default.join(visibleRoot, ...filePath.split("/")));
351
- }
352
- function revisionHydrationPreimage(input, revision, relativeRoot) {
353
- const result = runGitResult(input, input.shadowRoot, ["--literal-pathspecs", "ls-tree", "-r", "-z", revision, "--", relativeRoot]);
354
- if (result.exitCode !== 0) {
355
- throw new Error(
356
- `Inspect workspace hydration baseline ${revision}:${relativeRoot}: ${result.stderr || `git exited ${result.exitCode}`}`
357
- );
358
- }
359
- if (!result.stdout) return /* @__PURE__ */ new Map();
360
- const prefix = `${relativeRoot}/`;
361
- const preimage = /* @__PURE__ */ new Map();
362
- for (const serialized of result.stdout.split("\0").filter(Boolean)) {
363
- const separator = serialized.indexOf(" ");
364
- if (separator === -1) continue;
365
- const [mode, type, objectId] = serialized.slice(0, separator).split(" ");
366
- const workspacePath = serialized.slice(separator + 1);
367
- if (!objectId || !workspacePath.startsWith(prefix)) continue;
368
- const filePath = workspacePath.slice(prefix.length);
369
- if (mode === "160000" && type === "commit") {
370
- preimage.set(filePath, { kind: "gitlink", objectId });
371
- } else if ((mode === "100644" || mode === "100755" || mode === "120000") && type === "blob") {
372
- preimage.set(filePath, { kind: "blob", mode, objectId });
373
- }
374
- }
375
- return preimage;
376
- }
377
- function indexHydrationPreimage(input, relativeRoot) {
378
- const prefix = `${relativeRoot}/`;
379
- const entries = parseGitIndexEntries(
380
- runGit(input, input.shadowRoot, ["--literal-pathspecs", "ls-files", "--stage", "-z", "--", relativeRoot], "inspect hydrated workspace")
381
- );
382
- const unmergedPaths = entries.filter((entry) => entry.stage !== "0").map((entry) => entry.filePath);
383
- if (unmergedPaths.length > 0) throw new Error(`Canonical workspace has unmerged Git index stages: ${JSON.stringify(unmergedPaths)}`);
384
- const preimage = /* @__PURE__ */ new Map();
385
- for (const entry of entries) {
386
- if (!entry.filePath.startsWith(prefix)) continue;
387
- const filePath = entry.filePath.slice(prefix.length);
388
- if (entry.mode === "160000") {
389
- preimage.set(filePath, { kind: "gitlink", objectId: entry.objectId });
390
- } else if (entry.mode === "100644" || entry.mode === "100755" || entry.mode === "120000") {
391
- preimage.set(filePath, { kind: "blob", mode: entry.mode, objectId: entry.objectId });
392
- }
393
- }
394
- return preimage;
395
- }
396
- function worktreeDiffersFromIndex(checkoutPath, filePath) {
397
- const result = Bun.spawnSync(
398
- ["git", "--literal-pathspecs", "-c", "core.fileMode=true", "diff", "--quiet", "--no-ext-diff", "--", filePath],
399
- {
400
- cwd: checkoutPath,
401
- stdout: "ignore",
402
- stderr: "pipe",
403
- env: { ...process.env, GIT_TERMINAL_PROMPT: "0" }
404
- }
405
- );
406
- if (result.exitCode === 0) return false;
407
- if (result.exitCode === 1) return true;
408
- throw new Error(
409
- `compare canonical resolver index and worktree for ${JSON.stringify(filePath)}: ${result.stderr.toString().trim() || `git exited ${result.exitCode}`}`
410
- );
411
- }
412
- function indexEntryTypeMatchesWorktree(entry, stat) {
413
- if (entry.mode === "120000") return stat.isSymbolicLink();
414
- if (entry.mode !== "100644" && entry.mode !== "100755") return false;
415
- if (!stat.isFile()) return false;
416
- return entry.mode === "100755" === ((stat.mode & 73) !== 0);
417
- }
418
- function assertCanonicalCheckoutIndexMatchesWorktree(checkoutPath) {
419
- checkoutGitlinks(checkoutPath);
420
- const mismatchedPaths = [];
421
- for (const filePath of stagedCheckoutPaths(checkoutPath)) {
422
- const entries = indexEntriesForPath(checkoutPath, filePath);
423
- const worktreeEntry = lstatOrNull(import_node_path.default.join(checkoutPath, ...filePath.split("/")));
424
- if (entries.length === 0) {
425
- if (worktreeEntry && !worktreeEntry.isDirectory()) mismatchedPaths.push(filePath);
426
- continue;
427
- }
428
- if (entries.length === 1 && entries[0]?.stage === "0" && entries[0].mode === "160000") {
429
- if (worktreeEntry && !worktreeEntry.isDirectory()) mismatchedPaths.push(filePath);
430
- continue;
431
- }
432
- if (entries.length !== 1 || entries[0]?.stage !== "0" || !worktreeEntry || !indexEntryTypeMatchesWorktree(entries[0], worktreeEntry) || worktreeDiffersFromIndex(checkoutPath, filePath)) {
433
- mismatchedPaths.push(filePath);
434
- }
435
- }
436
- if (mismatchedPaths.length > 0) {
437
- throw new Error(
438
- `Canonical resolver checkout has staged Git index state that is not represented by its worktree: ${JSON.stringify(
439
- mismatchedPaths
440
- )}. Make the worktree match the index or unstage these paths before synchronizing.`
441
- );
442
- }
443
- }
444
- function checkoutGitSnapshot(checkoutPath) {
445
- const gitPaths = Bun.spawnSync(
446
- [
447
- "git",
448
- "-c",
449
- "submodule.recurse=false",
450
- "-c",
451
- "fetch.recurseSubmodules=false",
452
- "-c",
453
- "push.recurseSubmodules=false",
454
- "rev-parse",
455
- "--git-path",
456
- "index",
457
- ...ACTIVE_GIT_LOCK_PATHS.flatMap((lock) => ["--git-path", lock])
458
- ],
459
- {
460
- cwd: checkoutPath,
461
- stdout: "pipe",
462
- stderr: "ignore",
463
- env: { ...process.env, GIT_TERMINAL_PROMPT: "0" }
464
- }
465
- );
466
- if (gitPaths.exitCode !== 0) return null;
467
- const [indexPath, ...lockPaths] = gitPaths.stdout.toString().trim().split(/\r?\n/).map((gitPath) => import_node_path.default.resolve(checkoutPath, gitPath));
468
- if (!indexPath || lockPaths.length !== ACTIVE_GIT_LOCK_PATHS.length || lockPaths.some((lockPath) => import_node_fs.default.existsSync(lockPath))) {
469
- return null;
470
- }
471
- const head = Bun.spawnSync(
472
- [
473
- "git",
474
- "-c",
475
- "submodule.recurse=false",
476
- "-c",
477
- "fetch.recurseSubmodules=false",
478
- "-c",
479
- "push.recurseSubmodules=false",
480
- "rev-parse",
481
- "--verify",
482
- "HEAD"
483
- ],
484
- {
485
- cwd: checkoutPath,
486
- stdout: "pipe",
487
- stderr: "ignore",
488
- env: { ...process.env, GIT_TERMINAL_PROMPT: "0" }
489
- }
490
- );
491
- let indexSignature = "missing";
492
- try {
493
- const stat = import_node_fs.default.statSync(indexPath);
494
- indexSignature = entrySignature(stat);
495
- } catch {
496
- }
497
- if (lockPaths.some((lockPath) => import_node_fs.default.existsSync(lockPath))) return null;
498
- return `${head.exitCode === 0 ? head.stdout.toString().trim() : "unborn"}\0${indexSignature}`;
499
- }
500
- function listShadowTrackedFiles(input, relativeRoot) {
501
- const normalizedRoot = relativeRoot.split(import_node_path.default.sep).join("/").replace(/^\/+|\/+$/g, "");
502
- const output = runGit(input, input.shadowRoot, ["ls-files", "-z", "--cached", "--", normalizedRoot], "list canonical workspace files");
503
- const prefix = `${normalizedRoot}/`;
504
- return output.split("\0").filter((entry) => entry.startsWith(prefix)).map((entry) => entry.slice(prefix.length)).filter(Boolean).sort();
505
- }
506
- function pathIsWithinOpaqueRoot(relativePath, opaqueRoots) {
507
- return opaqueRoots.some((root) => relativePath === root || relativePath.startsWith(`${root}/`));
508
- }
509
- function listFilesRecursively(root, filter, opaqueRoots = []) {
510
- if (!import_node_fs.default.existsSync(root)) return [];
511
- const files = [];
512
- const visit = (current, relativeDir) => {
513
- for (const entry of import_node_fs.default.readdirSync(current, { withFileTypes: true })) {
514
- if (entry.name === ".git") continue;
515
- const relativePath = relativeDir ? import_node_path.default.posix.join(relativeDir, entry.name) : entry.name;
516
- const absolutePath = import_node_path.default.join(current, entry.name);
517
- if (pathIsWithinOpaqueRoot(relativePath, opaqueRoots)) continue;
518
- if (entry.isDirectory()) {
519
- visit(absolutePath, relativePath);
520
- } else if (!filter || filter(relativePath)) {
521
- files.push(relativePath);
522
- }
523
- }
524
- };
525
- visit(root, "");
526
- return files.sort();
527
- }
528
- function removeEmptyDirectories(root, opaqueRoots = []) {
529
- if (!import_node_fs.default.existsSync(root)) return;
530
- const visit = (current, relativeDir) => {
531
- let empty = true;
532
- for (const entry of import_node_fs.default.readdirSync(current, { withFileTypes: true })) {
533
- const absolutePath = import_node_path.default.join(current, entry.name);
534
- const relativePath = relativeDir ? import_node_path.default.posix.join(relativeDir, entry.name) : entry.name;
535
- if (pathIsWithinOpaqueRoot(relativePath, opaqueRoots)) {
536
- empty = false;
537
- continue;
538
- }
539
- if (entry.isDirectory()) {
540
- if (!visit(absolutePath, relativePath)) empty = false;
541
- } else {
542
- empty = false;
543
- }
544
- }
545
- if (empty && current !== root) import_node_fs.default.rmdirSync(current);
546
- return empty;
547
- };
548
- visit(root, "");
549
- }
550
- function copyEntry(sourceRoot, targetRoot, relativePath, trustedRoots = {}) {
551
- const sourcePath = import_node_path.default.resolve(sourceRoot, ...relativePath.split("/"));
552
- const targetPath = import_node_path.default.resolve(targetRoot, ...relativePath.split("/"));
553
- assertInside(sourceRoot, sourcePath, "Workspace source path");
554
- assertInside(targetRoot, targetPath, "Workspace target path");
555
- if (!hasSafeDirectoryAncestors(sourceRoot, relativePath, trustedRoots.source ?? sourceRoot)) {
556
- throw new Error(`Workspace source path has a symlink or non-directory ancestor: ${relativePath}`);
557
- }
558
- if (!hasSafeDirectoryAncestors(targetRoot, relativePath, trustedRoots.target ?? targetRoot)) {
559
- throw new Error(`Workspace target path has a symlink or non-directory ancestor: ${relativePath}`);
560
- }
561
- const stat = import_node_fs.default.lstatSync(sourcePath);
562
- import_node_fs.default.mkdirSync(import_node_path.default.dirname(targetPath), { recursive: true });
563
- import_node_fs.default.rmSync(targetPath, { recursive: true, force: true });
564
- if (stat.isSymbolicLink()) {
565
- import_node_fs.default.symlinkSync(import_node_fs.default.readlinkSync(sourcePath), targetPath);
566
- return;
567
- }
568
- if (stat.isFile()) {
569
- import_node_fs.default.copyFileSync(sourcePath, targetPath);
570
- import_node_fs.default.chmodSync(targetPath, stat.mode);
571
- }
572
- }
573
- function hasSafeDirectoryAncestors(root, relativePath, trustedRoot = root) {
574
- const resolvedTrustedRoot = import_node_path.default.resolve(trustedRoot);
575
- const trustedRootStat = lstatOrNull(resolvedTrustedRoot);
576
- if (trustedRootStat && (!trustedRootStat.isDirectory() || trustedRootStat.isSymbolicLink())) return false;
577
- const resolvedRoot = import_node_path.default.resolve(root);
578
- const targetParent = import_node_path.default.dirname(import_node_path.default.resolve(resolvedRoot, ...relativePath.split("/")));
579
- const relativeParent = import_node_path.default.relative(resolvedTrustedRoot, targetParent);
580
- if (relativeParent === ".." || relativeParent.startsWith(`..${import_node_path.default.sep}`) || import_node_path.default.isAbsolute(relativeParent)) return false;
581
- let current = resolvedTrustedRoot;
582
- for (const segment of relativeParent.split(import_node_path.default.sep).filter(Boolean)) {
583
- current = import_node_path.default.join(current, segment);
584
- const stat = lstatOrNull(current);
585
- if (stat && (!stat.isDirectory() || stat.isSymbolicLink())) return false;
586
- }
587
- return true;
588
- }
589
- function entrySignature(stat) {
590
- return [stat.dev, stat.ino, stat.mode, stat.size, stat.mtimeMs, stat.ctimeMs].join(":");
591
- }
592
- function regularFilesEqual(sourcePath, targetPath, size) {
593
- const sourceFd = import_node_fs.default.openSync(sourcePath, "r");
594
- const targetFd = import_node_fs.default.openSync(targetPath, "r");
595
- const sourceBuffer = Buffer.allocUnsafe(Math.min(MIRROR_COMPARE_BUFFER_BYTES, Math.max(1, size)));
596
- const targetBuffer = Buffer.allocUnsafe(sourceBuffer.length);
597
- try {
598
- let offset = 0;
599
- while (offset < size) {
600
- const length = Math.min(sourceBuffer.length, size - offset);
601
- const sourceBytes = import_node_fs.default.readSync(sourceFd, sourceBuffer, 0, length, offset);
602
- const targetBytes = import_node_fs.default.readSync(targetFd, targetBuffer, 0, length, offset);
603
- if (sourceBytes === 0 || targetBytes === 0 || sourceBytes !== targetBytes || !sourceBuffer.subarray(0, sourceBytes).equals(targetBuffer.subarray(0, targetBytes))) {
604
- return false;
605
- }
606
- offset += sourceBytes;
607
- }
608
- return true;
609
- } finally {
610
- import_node_fs.default.closeSync(sourceFd);
611
- import_node_fs.default.closeSync(targetFd);
612
- }
613
- }
614
- function entriesEqual(sourcePath, targetPath) {
615
- let sourceStat;
616
- let targetStat;
617
- try {
618
- sourceStat = import_node_fs.default.lstatSync(sourcePath);
619
- targetStat = import_node_fs.default.lstatSync(targetPath);
620
- } catch {
621
- return false;
622
- }
623
- const cacheKey = `${sourcePath}\0${targetPath}`;
624
- const signature = `${entrySignature(sourceStat)}\0${entrySignature(targetStat)}`;
625
- if (mirrorComparisonCache.get(cacheKey) === signature) return true;
626
- let equal = false;
627
- if (sourceStat.isSymbolicLink() && targetStat.isSymbolicLink()) {
628
- equal = import_node_fs.default.readlinkSync(sourcePath) === import_node_fs.default.readlinkSync(targetPath);
629
- } else if (sourceStat.isFile() && targetStat.isFile()) {
630
- equal = sourceStat.size === targetStat.size && (sourceStat.mode & 73) === (targetStat.mode & 73) && regularFilesEqual(sourcePath, targetPath, sourceStat.size);
631
- }
632
- if (equal) {
633
- if (mirrorComparisonCache.size >= MAX_MIRROR_COMPARISON_CACHE_ENTRIES) mirrorComparisonCache.clear();
634
- mirrorComparisonCache.set(cacheKey, signature);
635
- } else {
636
- mirrorComparisonCache.delete(cacheKey);
637
- }
638
- return equal;
639
- }
640
- function mirrorFileSet(sourceRoot, targetRoot, sourceFiles, targetFiles, opaqueTargetRoots = [], opaqueSourceRoots = [], trustedRoots = {}) {
641
- if (!hasSafeDirectoryAncestors(targetRoot, ".r5d-mirror-root", trustedRoots.target ?? targetRoot)) {
642
- throw new Error(`Workspace target root has a symlink or non-directory ancestor: ${targetRoot}`);
643
- }
644
- import_node_fs.default.mkdirSync(targetRoot, { recursive: true });
645
- const sourceSet = new Set(sourceFiles);
646
- for (const relativePath of targetFiles) {
647
- if (pathIsWithinOpaqueRoot(relativePath, opaqueTargetRoots)) continue;
648
- if (sourceSet.has(relativePath)) continue;
649
- const targetPath = import_node_path.default.resolve(targetRoot, ...relativePath.split("/"));
650
- assertInside(targetRoot, targetPath, "Workspace deletion path");
651
- if (!hasSafeDirectoryAncestors(targetRoot, relativePath, trustedRoots.target ?? targetRoot)) {
652
- throw new Error(`Workspace deletion path has a symlink or non-directory ancestor: ${relativePath}`);
653
- }
654
- import_node_fs.default.rmSync(targetPath, { recursive: true, force: true });
655
- }
656
- for (const relativePath of sourceFiles) {
657
- if (pathIsWithinOpaqueRoot(relativePath, opaqueSourceRoots)) continue;
658
- if (pathIsWithinOpaqueRoot(relativePath, opaqueTargetRoots)) continue;
659
- if (!hasSafeDirectoryAncestors(sourceRoot, relativePath, trustedRoots.source ?? sourceRoot)) {
660
- throw new Error(`Workspace source path has a symlink or non-directory ancestor: ${relativePath}`);
661
- }
662
- if (!hasSafeDirectoryAncestors(targetRoot, relativePath, trustedRoots.target ?? targetRoot)) {
663
- throw new Error(`Workspace target path has a symlink or non-directory ancestor: ${relativePath}`);
664
- }
665
- const sourcePath = import_node_path.default.resolve(sourceRoot, ...relativePath.split("/"));
666
- const stat = lstatOrNull(sourcePath);
667
- if (!stat) continue;
668
- if (!stat.isFile() && !stat.isSymbolicLink()) continue;
669
- const targetPath = import_node_path.default.resolve(targetRoot, ...relativePath.split("/"));
670
- if (entriesEqual(sourcePath, targetPath)) continue;
671
- copyEntry(sourceRoot, targetRoot, relativePath, trustedRoots);
672
- }
673
- removeEmptyDirectories(targetRoot, opaqueTargetRoots);
674
- }
675
- function indexPathsUnderCheckoutPath(checkoutPath, filePath) {
676
- return runCheckoutGitRaw(
677
- checkoutPath,
678
- ["--literal-pathspecs", "ls-files", "-z", "--", filePath],
679
- `inspect index paths beneath ${JSON.stringify(filePath)}`
680
- ).split("\0").filter(Boolean);
681
- }
682
- function replaceCheckoutIndexPathWithGitlink(checkoutPath, gitlink) {
683
- const zeroObjectId = "0".repeat(gitlink.objectId.length);
684
- const records = [
685
- ...indexPathsUnderCheckoutPath(checkoutPath, gitlink.filePath).map((indexPath) => `0 ${zeroObjectId} ${indexPath}`),
686
- `160000 ${gitlink.objectId} ${gitlink.filePath}`
687
- ];
688
- const result = Bun.spawnSync(
689
- [
690
- "git",
691
- "-c",
692
- "submodule.recurse=false",
693
- "-c",
694
- "fetch.recurseSubmodules=false",
695
- "-c",
696
- "push.recurseSubmodules=false",
697
- "update-index",
698
- "-z",
699
- "--index-info"
700
- ],
701
- {
702
- cwd: checkoutPath,
703
- stdin: Buffer.from(`${records.join("\0")}\0`),
704
- stdout: "pipe",
705
- stderr: "pipe",
706
- env: { ...process.env, GIT_TERMINAL_PROMPT: "0" }
707
- }
708
- );
709
- if (result.exitCode !== 0) {
710
- throw new Error(
711
- `record gitlink ${JSON.stringify(gitlink.filePath)}: ${result.stderr.toString().trim() || `git exited ${result.exitCode}`}`
712
- );
713
- }
714
- }
715
- function removeCheckoutIndexPath(checkoutPath, filePath) {
716
- runCheckoutGitRaw(
717
- checkoutPath,
718
- ["--literal-pathspecs", "update-index", "--force-remove", "--", filePath],
719
- `remove gitlink ${JSON.stringify(filePath)}`
720
- );
721
- }
722
- function shadowProjectGitlinks(input, relativeRoot) {
723
- const prefix = `${relativeRoot}/`;
724
- const entries = parseGitIndexEntries(
725
- runGit(input, input.shadowRoot, ["--literal-pathspecs", "ls-files", "--stage", "-z", "--", relativeRoot], "inspect workspace gitlinks")
726
- );
727
- const unmergedPaths = [...new Set(entries.filter((entry) => entry.stage !== "0").map((entry) => entry.filePath))].sort();
728
- if (unmergedPaths.length > 0) {
729
- throw new Error(`Canonical workspace has unmerged Git index stages: ${JSON.stringify(unmergedPaths)}`);
730
- }
731
- return entries.filter((entry) => entry.mode === "160000" && entry.filePath.startsWith(prefix)).map((entry) => ({ filePath: entry.filePath.slice(prefix.length), objectId: entry.objectId })).sort((left, right) => left.filePath.localeCompare(right.filePath));
732
- }
733
- function replaceShadowIndexPathWithGitlink(input, relativeRoot, gitlink) {
734
- const workspacePath = import_node_path.default.posix.join(relativeRoot, gitlink.filePath);
735
- const existingPaths = runGit(
736
- input,
737
- input.shadowRoot,
738
- ["--literal-pathspecs", "ls-files", "-z", "--", workspacePath],
739
- `inspect workspace index beneath gitlink ${JSON.stringify(workspacePath)}`
740
- ).split("\0").filter(Boolean);
741
- for (const indexPath of existingPaths) {
742
- runGit(
743
- input,
744
- input.shadowRoot,
745
- ["--literal-pathspecs", "update-index", "--force-remove", "--", indexPath],
746
- `remove workspace index entry beneath gitlink ${JSON.stringify(workspacePath)}`
747
- );
748
- }
749
- const placeholderPath = import_node_path.default.join(input.shadowRoot, ...workspacePath.split("/"));
750
- import_node_fs.default.rmSync(placeholderPath, { recursive: true, force: true });
751
- import_node_fs.default.mkdirSync(placeholderPath, { recursive: true });
752
- runGit(
753
- input,
754
- input.shadowRoot,
755
- ["--literal-pathspecs", "update-index", "--add", "--cacheinfo", "160000", gitlink.objectId, workspacePath],
756
- `record workspace gitlink ${JSON.stringify(workspacePath)}`
757
- );
758
- }
759
- function mirrorVisibleGitlinksToShadow(input, relativeRoot, visibleGitlinks) {
760
- const desiredByPath = new Map(visibleGitlinks.map((entry) => [entry.filePath, entry]));
761
- for (const current of shadowProjectGitlinks(input, relativeRoot)) {
762
- if (desiredByPath.has(current.filePath)) continue;
763
- const workspacePath = import_node_path.default.posix.join(relativeRoot, current.filePath);
764
- runGit(
765
- input,
766
- input.shadowRoot,
767
- ["--literal-pathspecs", "update-index", "--force-remove", "--", workspacePath],
768
- `remove deleted workspace gitlink ${JSON.stringify(workspacePath)}`
769
- );
770
- import_node_fs.default.rmSync(import_node_path.default.join(input.shadowRoot, ...workspacePath.split("/")), { recursive: true, force: true });
771
- }
772
- for (const gitlink of visibleGitlinks) replaceShadowIndexPathWithGitlink(input, relativeRoot, gitlink);
773
- }
774
- function mirrorShadowGitlinksToVisible(desired, visibleRoot, trustedRoot) {
775
- const desiredByPath = new Map(desired.map((entry) => [entry.filePath, entry]));
776
- for (const current of checkoutGitlinks(visibleRoot)) {
777
- if (!desiredByPath.has(current.filePath)) removeCheckoutIndexPath(visibleRoot, current.filePath);
778
- }
779
- for (const gitlink of desired) {
780
- if (!hasSafeDirectoryAncestors(visibleRoot, gitlink.filePath, trustedRoot)) {
781
- throw new Error(`Visible gitlink path has a symlink or non-directory ancestor: ${gitlink.filePath}`);
782
- }
783
- const visiblePath = import_node_path.default.join(visibleRoot, ...gitlink.filePath.split("/"));
784
- const existing = lstatOrNull(visiblePath);
785
- if (existing && !existing.isDirectory()) import_node_fs.default.rmSync(visiblePath, { recursive: true, force: true });
786
- import_node_fs.default.mkdirSync(visiblePath, { recursive: true });
787
- replaceCheckoutIndexPathWithGitlink(visibleRoot, gitlink);
788
- }
789
- }
790
- function assertSafeVisibleGitlinkTransitions(visibleRoot, trustedRoot, currentGitlinks, desiredGitlinks, shadowFiles) {
791
- const desiredGitlinkPaths = new Set(desiredGitlinks.map((entry) => entry.filePath));
792
- for (const current of currentGitlinks) {
793
- if (desiredGitlinkPaths.has(current.filePath)) continue;
794
- const becomesOrdinaryPath = shadowFiles.some(
795
- (filePath) => filePath === current.filePath || filePath.startsWith(`${current.filePath}/`)
796
- );
797
- if (!becomesOrdinaryPath) continue;
798
- if (!hasSafeDirectoryAncestors(visibleRoot, current.filePath, trustedRoot)) {
799
- throw new Error(`Visible gitlink path has a symlink or non-directory ancestor: ${current.filePath}`);
800
- }
801
- const existing = lstatOrNull(import_node_path.default.join(visibleRoot, ...current.filePath.split("/")));
802
- if (!existing) continue;
803
- if (existing.isDirectory() && import_node_fs.default.readdirSync(import_node_path.default.join(visibleRoot, ...current.filePath.split("/"))).length === 0) {
804
- continue;
805
- }
806
- throw new Error(
807
- `Cannot replace initialized or nonempty gitlink ${JSON.stringify(
808
- current.filePath
809
- )} with ordinary workspace content without deleting submodule data`
810
- );
811
- }
812
- }
813
- function restoreShadowProjectSnapshot(input, relativeRoot) {
814
- const shadowRoot = import_node_path.default.join(input.shadowRoot, ...relativeRoot.split("/"));
815
- const headFiles = runGitResult(input, input.shadowRoot, ["ls-tree", "-r", "--name-only", "-z", "HEAD", "--", relativeRoot]);
816
- if (headFiles.exitCode === 0 && headFiles.stdout) {
817
- runGit(
818
- input,
819
- input.shadowRoot,
820
- ["restore", "--source=HEAD", "--staged", "--worktree", "--", relativeRoot],
821
- "restore deferred checkout scan"
822
- );
823
- runGit(input, input.shadowRoot, ["clean", "-fd", "--", relativeRoot], "clean deferred checkout scan");
824
- return;
825
- }
826
- runGitResult(input, input.shadowRoot, ["--literal-pathspecs", "rm", "-r", "-f", "--cached", "--ignore-unmatch", "--", relativeRoot]);
827
- import_node_fs.default.rmSync(shadowRoot, { recursive: true, force: true });
828
- }
829
- function copiedHydrationPreimage(input, shadowRoot, ordinaryFiles, gitlinks = []) {
830
- const preimage = new Map(
831
- gitlinks.map((entry) => [entry.filePath, { kind: "gitlink", objectId: entry.objectId }])
832
- );
833
- for (const filePath of ordinaryFiles) {
834
- const entry = filesystemHydrationEntry(input, import_node_path.default.join(shadowRoot, ...filePath.split("/")));
835
- if (!entry || entry === UNSTABLE_VISIBLE_ENTRY) return null;
836
- preimage.set(filePath, entry);
837
- }
838
- return preimage;
839
- }
840
- function visibleProjectMatchesCopiedPreimage(input, visibleRoot, ordinaryFiles, gitlinks, preimage, opaqueRoots) {
841
- const currentFiles = listGitEligibleFiles(visibleRoot).filter((filePath) => !pathIsWithinOpaqueRoot(filePath, opaqueRoots));
842
- if (currentFiles.length !== ordinaryFiles.length || currentFiles.some((filePath, index) => filePath !== ordinaryFiles[index]))
843
- return false;
844
- const currentGitlinks = checkoutGitlinks(visibleRoot);
845
- if (currentGitlinks.length !== gitlinks.length || currentGitlinks.some((entry, index) => entry.filePath !== gitlinks[index]?.filePath || entry.objectId !== gitlinks[index]?.objectId)) {
846
- return false;
847
- }
848
- const currentGitlinksByPath = new Map(currentGitlinks.map((entry) => [entry.filePath, entry]));
849
- return ordinaryFiles.every((filePath) => {
850
- const current = projectVisibleHydrationEntry(input, visibleRoot, filePath, currentGitlinksByPath);
851
- return current !== UNSTABLE_VISIBLE_ENTRY && hydrationEntriesEqual(current, preimage.get(filePath));
852
- });
853
- }
854
- function mirrorVisibleProjectToShadow(input, manifest, branchName) {
855
- const visibleRoot = visibleProjectBranchPath(input.projectsRoot, manifest, branchName);
856
- const unavailableProjection = () => ({
857
- entries: [],
858
- opaqueRoots: [],
859
- visiblePreimages: /* @__PURE__ */ new Map([[visibleRoot, null]])
860
- });
861
- if (!hasSafeDirectoryAncestors(visibleRoot, ".r5d-project-scan", input.projectsRoot)) return unavailableProjection();
862
- if (!import_node_fs.default.existsSync(import_node_path.default.join(visibleRoot, ".git"))) return unavailableProjection();
863
- const isCanonicalCheckout = manifest.canonicalCheckouts?.some((checkout) => checkout.branchName === branchName);
864
- if (isCanonicalCheckout) {
865
- assertCanonicalCheckoutIndexMatchesWorktree(visibleRoot);
866
- }
867
- const beforeSnapshot = checkoutGitSnapshot(visibleRoot);
868
- if (!beforeSnapshot) return unavailableProjection();
869
- const visibleGitlinks = checkoutGitlinks(visibleRoot);
870
- const visibleIndexEntries = checkoutIndexEntries(visibleRoot).filter((entry) => entry.stage === "0");
871
- const indexedGitlinkPaths = new Set(visibleGitlinks.map((entry) => entry.filePath));
872
- const stagedDeletedGitlinkRoots = stagedDeletedGitlinkPaths(visibleRoot);
873
- const relativeRoot = workspaceProjectBranchRelativePath(manifest.projectId, branchName);
874
- const shadowGitlinkRoots = shadowProjectGitlinks(input, relativeRoot).filter(
875
- (entry) => !indexedGitlinkPaths.has(entry.filePath) && !visibleIndexEntries.some(
876
- (candidate) => candidate.mode !== "160000" && (candidate.filePath === entry.filePath || candidate.filePath.startsWith(`${entry.filePath}/`))
877
- )
878
- ).map((entry) => entry.filePath);
879
- const visibleGitlinkRoots = [.../* @__PURE__ */ new Set([...indexedGitlinkPaths, ...stagedDeletedGitlinkRoots, ...shadowGitlinkRoots])].sort();
880
- const visibleFiles = listGitEligibleFiles(visibleRoot).filter((filePath) => !pathIsWithinOpaqueRoot(filePath, visibleGitlinkRoots));
881
- const shadowRoot = import_node_path.default.join(input.shadowRoot, ...relativeRoot.split("/"));
882
- mirrorVisibleGitlinksToShadow(input, relativeRoot, visibleGitlinks);
883
- mirrorFileSet(visibleRoot, shadowRoot, visibleFiles, listFilesRecursively(shadowRoot), visibleGitlinkRoots, visibleGitlinkRoots, {
884
- source: input.projectsRoot,
885
- target: input.shadowRoot
886
- });
887
- const hydrationPreimage = copiedHydrationPreimage(input, shadowRoot, visibleFiles, visibleGitlinks);
888
- if (!hydrationPreimage || checkoutGitSnapshot(visibleRoot) !== beforeSnapshot || !visibleProjectMatchesCopiedPreimage(input, visibleRoot, visibleFiles, visibleGitlinks, hydrationPreimage, visibleGitlinkRoots)) {
889
- restoreShadowProjectSnapshot(input, relativeRoot);
890
- return { entries: [], opaqueRoots: [], visiblePreimages: /* @__PURE__ */ new Map([[visibleRoot, null]]) };
891
- }
892
- return {
893
- entries: visibleGitlinks.map((entry) => ({
894
- filePath: import_node_path.default.posix.join(relativeRoot, entry.filePath),
895
- objectId: entry.objectId
896
- })),
897
- opaqueRoots: visibleGitlinkRoots.map((filePath) => import_node_path.default.posix.join(relativeRoot, filePath)),
898
- visiblePreimages: /* @__PURE__ */ new Map([[visibleRoot, hydrationPreimage]])
899
- };
900
- }
901
- function pathDepth(filePath) {
902
- return filePath.split("/").length;
903
- }
904
- function pathsOverlap(left, right) {
905
- return left === right || left.startsWith(`${right}/`) || right.startsWith(`${left}/`);
906
- }
907
- function removeVisibleBlob(visibleRoot, filePath, trustedRoot) {
908
- if (!hasSafeDirectoryAncestors(visibleRoot, filePath, trustedRoot)) return false;
909
- const absolutePath = import_node_path.default.join(visibleRoot, ...filePath.split("/"));
910
- const stat = lstatOrNull(absolutePath);
911
- if (!stat || !stat.isFile() && !stat.isSymbolicLink()) return false;
912
- import_node_fs.default.rmSync(absolutePath, { force: true });
913
- return true;
914
- }
915
- function removeEmptyVisibleAncestors(visibleRoot, filePath, trustedRoot, opaqueRoots = []) {
916
- if (!hasSafeDirectoryAncestors(visibleRoot, filePath, trustedRoot)) return;
917
- const resolvedRoot = import_node_path.default.resolve(visibleRoot);
918
- let current = import_node_path.default.dirname(import_node_path.default.join(resolvedRoot, ...filePath.split("/")));
919
- while (current !== resolvedRoot) {
920
- const relative = import_node_path.default.relative(resolvedRoot, current).split(import_node_path.default.sep).join("/");
921
- if (!relative || pathIsWithinOpaqueRoot(relative, opaqueRoots)) return;
922
- const stat = lstatOrNull(current);
923
- if (!stat) {
924
- current = import_node_path.default.dirname(current);
925
- continue;
926
- }
927
- if (!stat.isDirectory() || stat.isSymbolicLink() || import_node_fs.default.readdirSync(current).length > 0) return;
928
- try {
929
- import_node_fs.default.rmdirSync(current);
930
- } catch (error) {
931
- if (error.code === "ENOTEMPTY" || error.code === "ENOENT") return;
932
- throw error;
933
- }
934
- current = import_node_path.default.dirname(current);
935
- }
936
- }
937
- function copyShadowBlobToVisible(shadowRoot, visibleRoot, filePath, trustedRoot) {
938
- if (!hasSafeDirectoryAncestors(visibleRoot, filePath, trustedRoot)) return false;
939
- const targetPath = import_node_path.default.join(visibleRoot, ...filePath.split("/"));
940
- const existing = lstatOrNull(targetPath);
941
- if (existing?.isDirectory()) {
942
- if (import_node_fs.default.readdirSync(targetPath).length > 0) return false;
943
- import_node_fs.default.rmdirSync(targetPath);
944
- } else if (existing) {
945
- import_node_fs.default.rmSync(targetPath, { force: true });
946
- }
947
- try {
948
- copyEntry(shadowRoot, visibleRoot, filePath, { target: trustedRoot });
949
- return true;
950
- } catch (error) {
951
- if (error.code === "EEXIST" || error.code === "ENOTDIR") return false;
952
- throw error;
953
- }
954
- }
955
- function hydrateProjectFromShadowOptimistically(input, relativeRoot, visibleRoot, preimage, opaqueVisibleGitlinks) {
956
- const shadowRoot = import_node_path.default.join(input.shadowRoot, ...relativeRoot.split("/"));
957
- const desired = indexHydrationPreimage(input, relativeRoot);
958
- let expectedCheckoutSnapshot = checkoutGitSnapshot(visibleRoot);
959
- if (!expectedCheckoutSnapshot) return;
960
- const checkoutIndexIsStable = () => checkoutGitSnapshot(visibleRoot) === expectedCheckoutSnapshot;
961
- const refreshCheckoutSnapshot = () => {
962
- expectedCheckoutSnapshot = checkoutGitSnapshot(visibleRoot);
963
- return expectedCheckoutSnapshot !== null;
964
- };
965
- const currentGitlinks = new Map(checkoutGitlinks(visibleRoot).map((entry) => [entry.filePath, entry]));
966
- const changedPaths = [.../* @__PURE__ */ new Set([...preimage.keys(), ...desired.keys()])].filter(
967
- (filePath) => !hydrationEntriesEqual(preimage.get(filePath), desired.get(filePath))
968
- );
969
- for (const filePath of changedPaths.filter((candidate) => !desired.has(candidate)).sort((left, right) => pathDepth(right) - pathDepth(left) || left.localeCompare(right))) {
970
- const current = projectVisibleHydrationEntry(input, visibleRoot, filePath, currentGitlinks);
971
- if (current === UNSTABLE_VISIBLE_ENTRY || !hydrationEntriesEqual(current, preimage.get(filePath))) continue;
972
- const expected = preimage.get(filePath);
973
- if (expected?.kind === "gitlink") {
974
- if (!checkoutIndexIsStable()) return;
975
- removeCheckoutIndexPath(visibleRoot, filePath);
976
- if (!refreshCheckoutSnapshot()) return;
977
- currentGitlinks.delete(filePath);
978
- } else if (removeVisibleBlob(visibleRoot, filePath, input.projectsRoot)) {
979
- removeEmptyVisibleAncestors(visibleRoot, filePath, input.projectsRoot, opaqueVisibleGitlinks);
980
- }
981
- }
982
- for (const filePath of changedPaths.filter((candidate) => desired.has(candidate)).sort((left, right) => pathDepth(left) - pathDepth(right) || left.localeCompare(right))) {
983
- const wanted = desired.get(filePath);
984
- if (!wanted) continue;
985
- const current = projectVisibleHydrationEntry(input, visibleRoot, filePath, currentGitlinks);
986
- if (current === UNSTABLE_VISIBLE_ENTRY || hydrationEntriesEqual(current, wanted)) continue;
987
- if (!hydrationEntriesEqual(current, preimage.get(filePath))) continue;
988
- if (wanted.kind === "gitlink") {
989
- if (!hasSafeDirectoryAncestors(visibleRoot, filePath, input.projectsRoot) || !checkoutIndexIsStable()) return;
990
- const targetPath = import_node_path.default.join(visibleRoot, ...filePath.split("/"));
991
- const existing = lstatOrNull(targetPath);
992
- if (existing && !existing.isDirectory()) import_node_fs.default.rmSync(targetPath, { force: true });
993
- import_node_fs.default.mkdirSync(targetPath, { recursive: true });
994
- replaceCheckoutIndexPathWithGitlink(visibleRoot, { filePath, objectId: wanted.objectId });
995
- if (!refreshCheckoutSnapshot()) return;
996
- currentGitlinks.set(filePath, { filePath, objectId: wanted.objectId });
997
- continue;
998
- }
999
- const expected = preimage.get(filePath);
1000
- if (expected?.kind === "gitlink") {
1001
- if (!hasSafeDirectoryAncestors(visibleRoot, filePath, input.projectsRoot) || !checkoutIndexIsStable()) return;
1002
- const targetPath = import_node_path.default.join(visibleRoot, ...filePath.split("/"));
1003
- const existing = lstatOrNull(targetPath);
1004
- if (existing?.isDirectory() && import_node_fs.default.readdirSync(targetPath).length > 0) continue;
1005
- removeCheckoutIndexPath(visibleRoot, filePath);
1006
- if (!refreshCheckoutSnapshot()) return;
1007
- currentGitlinks.delete(filePath);
1008
- }
1009
- copyShadowBlobToVisible(shadowRoot, visibleRoot, filePath, input.projectsRoot);
1010
- }
1011
- }
1012
- function mirrorShadowProjectToVisible(input, manifest, branchName, additionalOpaqueRoots = [], hydrationPreimage) {
1013
- const visibleRoot = visibleProjectBranchPath(input.projectsRoot, manifest, branchName);
1014
- if (hydrationPreimage === null) return;
1015
- if (!hasSafeDirectoryAncestors(visibleRoot, ".r5d-project-hydration", input.projectsRoot)) {
1016
- if (hydrationPreimage) return;
1017
- throw new Error(`Visible project root has a symlink or non-directory ancestor: ${visibleRoot}`);
1018
- }
1019
- if (!import_node_fs.default.existsSync(import_node_path.default.join(visibleRoot, ".git"))) return;
1020
- checkoutGitlinks(visibleRoot);
1021
- const relativeRoot = workspaceProjectBranchRelativePath(manifest.projectId, branchName);
1022
- const shadowRoot = import_node_path.default.join(input.shadowRoot, ...relativeRoot.split("/"));
1023
- const shadowFiles = listShadowTrackedFiles(input, relativeRoot);
1024
- const desiredGitlinks = shadowProjectGitlinks(input, relativeRoot);
1025
- const desiredGitlinkPaths = new Set(desiredGitlinks.map((entry) => entry.filePath));
1026
- const visibleGitlinks = checkoutGitlinks(visibleRoot);
1027
- const protectedGitlinkRoots = [
1028
- .../* @__PURE__ */ new Set([...visibleGitlinks.map((entry) => entry.filePath), ...stagedDeletedGitlinkPaths(visibleRoot), ...additionalOpaqueRoots])
1029
- ].sort();
1030
- assertSafeVisibleGitlinkTransitions(
1031
- visibleRoot,
1032
- input.projectsRoot,
1033
- protectedGitlinkRoots.map((filePath) => ({ filePath, objectId: "" })),
1034
- desiredGitlinks,
1035
- shadowFiles
1036
- );
1037
- const opaqueVisibleGitlinks = protectedGitlinkRoots.filter(
1038
- (filePath) => desiredGitlinkPaths.has(filePath) || !shadowFiles.some((shadowFile) => shadowFile === filePath || shadowFile.startsWith(`${filePath}/`))
1039
- );
1040
- if (hydrationPreimage) {
1041
- hydrateProjectFromShadowOptimistically(input, relativeRoot, visibleRoot, hydrationPreimage, opaqueVisibleGitlinks);
1042
- return;
1043
- }
1044
- mirrorFileSet(shadowRoot, visibleRoot, shadowFiles, listGitEligibleFiles(visibleRoot), opaqueVisibleGitlinks, [], {
1045
- source: input.shadowRoot,
1046
- target: input.projectsRoot
1047
- });
1048
- mirrorShadowGitlinksToVisible(desiredGitlinks, visibleRoot, input.projectsRoot);
1049
- }
1050
- function visibleRegularRootMatchesCopiedPreimage(input, visibleRoot, files, preimage, filter) {
1051
- const currentFiles = listFilesRecursively(visibleRoot, filter);
1052
- if (currentFiles.length !== files.length || currentFiles.some((filePath, index) => filePath !== files[index])) return false;
1053
- return files.every((filePath) => {
1054
- const current = hasSafeDirectoryAncestors(visibleRoot, filePath, input.plansRoot) ? filesystemHydrationEntry(input, import_node_path.default.join(visibleRoot, ...filePath.split("/"))) : UNSTABLE_VISIBLE_ENTRY;
1055
- return current !== UNSTABLE_VISIBLE_ENTRY && hydrationEntriesEqual(current, preimage.get(filePath));
1056
- });
1057
- }
1058
- function mirrorRegularRootToShadowWithPreimage(input, sourceRoot, targetRoot, relativeRoot, filter) {
1059
- if (!hasSafeDirectoryAncestors(sourceRoot, ".r5d-plan-scan", input.plansRoot)) return null;
1060
- if (!import_node_fs.default.existsSync(sourceRoot)) return /* @__PURE__ */ new Map();
1061
- const sourceFiles = listFilesRecursively(sourceRoot, filter);
1062
- mirrorFileSet(sourceRoot, targetRoot, sourceFiles, listFilesRecursively(targetRoot, filter), [], [], {
1063
- source: input.plansRoot,
1064
- target: input.shadowRoot
1065
- });
1066
- const preimage = copiedHydrationPreimage(input, targetRoot, sourceFiles);
1067
- if (!preimage || !visibleRegularRootMatchesCopiedPreimage(input, sourceRoot, sourceFiles, preimage, filter)) {
1068
- restoreShadowProjectSnapshot(input, relativeRoot);
1069
- return null;
1070
- }
1071
- return preimage;
1072
- }
1073
- function hydrateRegularRootOptimistically(input, relativeRoot, targetRoot, preimage, filter) {
1074
- const sourceRoot = import_node_path.default.join(input.shadowRoot, ...relativeRoot.split("/"));
1075
- if (!hasSafeDirectoryAncestors(targetRoot, ".r5d-plan-hydration", input.plansRoot)) return;
1076
- const desired = indexHydrationPreimage(input, relativeRoot);
1077
- for (const [filePath, entry] of desired) {
1078
- if (entry.kind === "gitlink" || filter && !filter(filePath)) desired.delete(filePath);
1079
- }
1080
- const changedPaths = [.../* @__PURE__ */ new Set([...preimage.keys(), ...desired.keys()])].filter(
1081
- (filePath) => !hydrationEntriesEqual(preimage.get(filePath), desired.get(filePath))
1082
- );
1083
- for (const filePath of changedPaths.filter((candidate) => !desired.has(candidate)).sort((left, right) => pathDepth(right) - pathDepth(left) || left.localeCompare(right))) {
1084
- const current = hasSafeDirectoryAncestors(targetRoot, filePath, input.plansRoot) ? filesystemHydrationEntry(input, import_node_path.default.join(targetRoot, ...filePath.split("/"))) : UNSTABLE_VISIBLE_ENTRY;
1085
- if (current !== UNSTABLE_VISIBLE_ENTRY && hydrationEntriesEqual(current, preimage.get(filePath))) {
1086
- if (removeVisibleBlob(targetRoot, filePath, input.plansRoot)) {
1087
- removeEmptyVisibleAncestors(targetRoot, filePath, input.plansRoot);
1088
- }
1089
- }
1090
- }
1091
- for (const filePath of changedPaths.filter((candidate) => desired.has(candidate)).sort((left, right) => pathDepth(left) - pathDepth(right) || left.localeCompare(right))) {
1092
- const current = hasSafeDirectoryAncestors(targetRoot, filePath, input.plansRoot) ? filesystemHydrationEntry(input, import_node_path.default.join(targetRoot, ...filePath.split("/"))) : UNSTABLE_VISIBLE_ENTRY;
1093
- const wanted = desired.get(filePath);
1094
- if (!wanted || current === UNSTABLE_VISIBLE_ENTRY || hydrationEntriesEqual(current, wanted)) continue;
1095
- if (!hydrationEntriesEqual(current, preimage.get(filePath))) continue;
1096
- copyShadowBlobToVisible(sourceRoot, targetRoot, filePath, input.plansRoot);
1097
- }
1098
- }
1099
- function mirrorLocalPlansToShadow(input, manifest, branchName) {
1100
- const sourceRoot = localPlansBranchPath(input.plansRoot, manifest.projectId, branchName);
1101
- const relativeRoot = workspacePlansRelativePath(manifest.projectId, branchName);
1102
- const targetRoot = import_node_path.default.join(input.shadowRoot, ...relativeRoot.split("/"));
1103
- const planFilter = (relativePath) => relativePath.endsWith(".plan.md");
1104
- return mirrorRegularRootToShadowWithPreimage(input, sourceRoot, targetRoot, relativeRoot, planFilter);
1105
- }
1106
- function mirrorShadowPlansToLocal(input, manifest, branchName, hydrationPreimage) {
1107
- if (hydrationPreimage === null) return;
1108
- const relativeRoot = workspacePlansRelativePath(manifest.projectId, branchName);
1109
- const sourceRoot = import_node_path.default.join(input.shadowRoot, ...relativeRoot.split("/"));
1110
- const targetRoot = localPlansBranchPath(input.plansRoot, manifest.projectId, branchName);
1111
- const planFilter = (relativePath) => relativePath.endsWith(".plan.md");
1112
- if (hydrationPreimage) {
1113
- hydrateRegularRootOptimistically(input, relativeRoot, targetRoot, hydrationPreimage, planFilter);
1114
- return;
1115
- }
1116
- if (!hasSafeDirectoryAncestors(targetRoot, ".r5d-plan-hydration", input.plansRoot)) {
1117
- throw new Error(`Visible plan root has a symlink or non-directory ancestor: ${targetRoot}`);
1118
- }
1119
- mirrorFileSet(
1120
- sourceRoot,
1121
- targetRoot,
1122
- listShadowTrackedFiles(input, relativeRoot).filter(planFilter),
1123
- listFilesRecursively(targetRoot, planFilter),
1124
- [],
1125
- [],
1126
- { source: input.shadowRoot, target: input.plansRoot }
1127
- );
1128
- }
1129
- function mirrorLocalWorkspacePlansToShadow(input) {
1130
- if (input.trigger.canonicalCheckoutOnly) return /* @__PURE__ */ new Map();
1131
- const sourceRoot = localWorkspacePlansPath(input.plansRoot);
1132
- const relativeRoot = WORKSPACE_NATIVE_PLANS_RELATIVE_PATH;
1133
- const targetRoot = import_node_path.default.join(input.shadowRoot, relativeRoot);
1134
- const planFilter = (relativePath) => relativePath.endsWith(".plan.md");
1135
- return mirrorRegularRootToShadowWithPreimage(input, sourceRoot, targetRoot, relativeRoot, planFilter);
1136
- }
1137
- function mirrorShadowWorkspacePlansToLocal(input, hydrationPreimage) {
1138
- if (input.trigger.canonicalCheckoutOnly) return;
1139
- if (hydrationPreimage === null) return;
1140
- const sourceRoot = import_node_path.default.join(input.shadowRoot, WORKSPACE_NATIVE_PLANS_RELATIVE_PATH);
1141
- const targetRoot = localWorkspacePlansPath(input.plansRoot);
1142
- const planFilter = (relativePath) => relativePath.endsWith(".plan.md");
1143
- if (hydrationPreimage) {
1144
- hydrateRegularRootOptimistically(input, WORKSPACE_NATIVE_PLANS_RELATIVE_PATH, targetRoot, hydrationPreimage, planFilter);
1145
- return;
1146
- }
1147
- if (!hasSafeDirectoryAncestors(targetRoot, ".r5d-plan-hydration", input.plansRoot)) {
1148
- throw new Error(`Visible workspace-plan root has a symlink or non-directory ancestor: ${targetRoot}`);
1149
- }
1150
- mirrorFileSet(
1151
- sourceRoot,
1152
- targetRoot,
1153
- listShadowTrackedFiles(input, WORKSPACE_NATIVE_PLANS_RELATIVE_PATH).filter(planFilter),
1154
- listFilesRecursively(targetRoot, planFilter),
1155
- [],
1156
- [],
1157
- { source: input.shadowRoot, target: input.plansRoot }
1158
- );
1159
- }
1160
- function reconcileWorkspaceNativePlans(input, projection) {
1161
- if (input.trigger.canonicalCheckoutOnly) return;
1162
- const localRoot = localWorkspacePlansPath(input.plansRoot);
1163
- projection.visiblePreimages ??= /* @__PURE__ */ new Map();
1164
- projection.visiblePreimages.set(localRoot, /* @__PURE__ */ new Map());
1165
- if (import_node_fs.default.existsSync(localWorkspacePlansPath(input.plansRoot))) {
1166
- projection.visiblePreimages.set(localRoot, mirrorLocalWorkspacePlansToShadow(input));
1167
- return;
1168
- }
1169
- if (listShadowTrackedFiles(input, WORKSPACE_NATIVE_PLANS_RELATIVE_PATH).length > 0 || restoreShadowRootFromRemote(input, WORKSPACE_NATIVE_PLANS_RELATIVE_PATH)) {
1170
- mirrorShadowWorkspacePlansToLocal(input, /* @__PURE__ */ new Map());
1171
- projection.visiblePreimages.set(localRoot, indexHydrationPreimage(input, WORKSPACE_NATIVE_PLANS_RELATIVE_PATH));
1172
- }
1173
- }
1174
- function mergeGitlinkProjection(target, source) {
1175
- target.entries.push(...source.entries);
1176
- target.opaqueRoots.push(...source.opaqueRoots);
1177
- if (source.visiblePreimages) {
1178
- target.visiblePreimages ??= /* @__PURE__ */ new Map();
1179
- for (const [visibleRoot, preimage] of source.visiblePreimages) target.visiblePreimages.set(visibleRoot, preimage);
1180
- }
1181
- }
1182
- function mirrorVisibleWorkspaceToShadow(input, excludedCheckouts = /* @__PURE__ */ new Set()) {
1183
- const projection = { entries: [], opaqueRoots: [], visiblePreimages: /* @__PURE__ */ new Map() };
1184
- reconcileWorkspaceNativePlans(input, projection);
1185
- for (const manifest of [...input.projects].sort((left, right) => left.projectId.localeCompare(right.projectId))) {
1186
- for (const branchName of [...new Set(manifest.branches)].sort()) {
1187
- if (excludedCheckouts.has(`${manifest.projectId}\0${branchName}`)) continue;
1188
- mergeGitlinkProjection(projection, mirrorVisibleProjectToShadow(input, manifest, branchName));
1189
- if (!input.trigger.canonicalCheckoutOnly) {
1190
- const localPlansRoot = localPlansBranchPath(input.plansRoot, manifest.projectId, branchName);
1191
- projection.visiblePreimages?.set(localPlansRoot, mirrorLocalPlansToShadow(input, manifest, branchName));
1192
- }
1193
- }
1194
- }
1195
- projection.opaqueRoots = [...new Set(projection.opaqueRoots)].sort();
1196
- return projection;
1197
- }
1198
- function visibleWorkspaceMatchesProjection(input, projection) {
1199
- const preimages = projection.visiblePreimages;
1200
- if (!preimages) return false;
1201
- const workspacePlansRoot = localWorkspacePlansPath(input.plansRoot);
1202
- const workspacePlansPreimage = preimages.get(workspacePlansRoot);
1203
- if (!workspacePlansPreimage || !visibleRegularRootMatchesCopiedPreimage(
1204
- input,
1205
- workspacePlansRoot,
1206
- [...workspacePlansPreimage.keys()].sort(),
1207
- workspacePlansPreimage,
1208
- (filePath) => filePath.endsWith(".plan.md")
1209
- )) {
1210
- return false;
1211
- }
1212
- for (const manifest of input.projects) {
1213
- for (const branchName of manifest.branches) {
1214
- const relativeRoot = workspaceProjectBranchRelativePath(manifest.projectId, branchName);
1215
- const visibleRoot = visibleProjectBranchPath(input.projectsRoot, manifest, branchName);
1216
- const projectPreimage = preimages.get(visibleRoot);
1217
- if (!projectPreimage) return false;
1218
- const gitlinks = [...projectPreimage].filter((entry) => entry[1].kind === "gitlink").map(([filePath, entry]) => ({ filePath, objectId: entry.objectId })).sort((left, right) => left.filePath.localeCompare(right.filePath));
1219
- const ordinaryFiles = [...projectPreimage].filter(([, entry]) => entry.kind === "blob").map(([filePath]) => filePath).sort();
1220
- const rootPrefix = `${relativeRoot}/`;
1221
- const opaqueRoots = projection.opaqueRoots.filter((root) => root.startsWith(rootPrefix)).map((root) => root.slice(rootPrefix.length));
1222
- if (!visibleProjectMatchesCopiedPreimage(input, visibleRoot, ordinaryFiles, gitlinks, projectPreimage, opaqueRoots)) {
1223
- return false;
1224
- }
1225
- const plansRoot = localPlansBranchPath(input.plansRoot, manifest.projectId, branchName);
1226
- const plansPreimage = preimages.get(plansRoot);
1227
- if (!plansPreimage || !visibleRegularRootMatchesCopiedPreimage(
1228
- input,
1229
- plansRoot,
1230
- [...plansPreimage.keys()].sort(),
1231
- plansPreimage,
1232
- (filePath) => filePath.endsWith(".plan.md")
1233
- )) {
1234
- return false;
1235
- }
1236
- }
1237
- }
1238
- return true;
1239
- }
1240
- function reconcileNewVisibleCheckouts(input) {
1241
- const projection = { entries: [], opaqueRoots: [], visiblePreimages: /* @__PURE__ */ new Map() };
1242
- for (const target of input.newVisibleCheckouts ?? []) {
1243
- const manifest = input.projects.find((project) => project.projectId === target.projectId);
1244
- if (!manifest || !manifest.branches.includes(target.branchName)) continue;
1245
- const projectRoot = workspaceProjectBranchRelativePath(target.projectId, target.branchName);
1246
- if (input.trigger.canonicalCheckoutOnly) {
1247
- mergeGitlinkProjection(projection, mirrorVisibleProjectToShadow(input, manifest, target.branchName));
1248
- } else if (listShadowTrackedFiles(input, projectRoot).length > 0 || restoreShadowRootFromRemote(input, projectRoot)) {
1249
- mirrorShadowProjectToVisible(input, manifest, target.branchName);
1250
- projection.visiblePreimages?.set(
1251
- visibleProjectBranchPath(input.projectsRoot, manifest, target.branchName),
1252
- indexHydrationPreimage(input, projectRoot)
1253
- );
1254
- } else {
1255
- mergeGitlinkProjection(projection, mirrorVisibleProjectToShadow(input, manifest, target.branchName));
1256
- }
1257
- if (input.trigger.canonicalCheckoutOnly) continue;
1258
- const plansRoot = workspacePlansRelativePath(target.projectId, target.branchName);
1259
- if (listShadowTrackedFiles(input, plansRoot).length > 0 || restoreShadowRootFromRemote(input, plansRoot)) {
1260
- mirrorShadowPlansToLocal(input, manifest, target.branchName);
1261
- projection.visiblePreimages?.set(
1262
- localPlansBranchPath(input.plansRoot, manifest.projectId, target.branchName),
1263
- indexHydrationPreimage(input, plansRoot)
1264
- );
1265
- } else {
1266
- projection.visiblePreimages?.set(
1267
- localPlansBranchPath(input.plansRoot, manifest.projectId, target.branchName),
1268
- mirrorLocalPlansToShadow(input, manifest, target.branchName)
1269
- );
1270
- }
1271
- }
1272
- projection.opaqueRoots = [...new Set(projection.opaqueRoots)].sort();
1273
- return projection;
1274
- }
1275
- function mirrorShadowWorkspaceToVisible(input, opaqueWorkspaceRoots = [], hydrationPreimages) {
1276
- mirrorShadowWorkspacePlansToLocal(input, hydrationPreimages?.get(localWorkspacePlansPath(input.plansRoot)));
1277
- for (const manifest of [...input.projects].sort((left, right) => left.projectId.localeCompare(right.projectId))) {
1278
- for (const branchName of [...new Set(manifest.branches)].sort()) {
1279
- const relativeRoot = workspaceProjectBranchRelativePath(manifest.projectId, branchName);
1280
- const visibleRoot = visibleProjectBranchPath(input.projectsRoot, manifest, branchName);
1281
- const opaqueProjectRoots = opaqueWorkspaceRoots.filter((root) => root.startsWith(`${relativeRoot}/`)).map((root) => root.slice(relativeRoot.length + 1));
1282
- mirrorShadowProjectToVisible(input, manifest, branchName, opaqueProjectRoots, hydrationPreimages?.get(visibleRoot));
1283
- if (!input.trigger.canonicalCheckoutOnly) {
1284
- const localPlansRoot = localPlansBranchPath(input.plansRoot, manifest.projectId, branchName);
1285
- mirrorShadowPlansToLocal(input, manifest, branchName, hydrationPreimages?.get(localPlansRoot));
1286
- }
1287
- }
1288
- }
1289
- }
1290
- function hydrationPreimagesFromShadowRevision(input, revision) {
1291
- const preimages = /* @__PURE__ */ new Map();
1292
- const fromRevision = (relativeRoot) => revision ? revisionHydrationPreimage(input, revision, relativeRoot) : /* @__PURE__ */ new Map();
1293
- if (!input.trigger.canonicalCheckoutOnly) {
1294
- preimages.set(localWorkspacePlansPath(input.plansRoot), fromRevision(WORKSPACE_NATIVE_PLANS_RELATIVE_PATH));
1295
- }
1296
- for (const manifest of input.projects) {
1297
- for (const branchName of manifest.branches) {
1298
- preimages.set(
1299
- visibleProjectBranchPath(input.projectsRoot, manifest, branchName),
1300
- fromRevision(workspaceProjectBranchRelativePath(manifest.projectId, branchName))
1301
- );
1302
- if (!input.trigger.canonicalCheckoutOnly) {
1303
- preimages.set(
1304
- localPlansBranchPath(input.plansRoot, manifest.projectId, branchName),
1305
- fromRevision(workspacePlansRelativePath(manifest.projectId, branchName))
1306
- );
1307
- }
1308
- }
1309
- }
1310
- return preimages;
1311
- }
1312
- function visibleHydrationConflictPaths(input, preimages, desiredRevision) {
1313
- const conflicts = [];
1314
- const inspectRegularRoot = (visibleRoot, relativeRoot, filter) => {
1315
- const preimage = preimages.get(visibleRoot);
1316
- if (!preimage) return;
1317
- const desired = revisionHydrationPreimage(input, desiredRevision, relativeRoot);
1318
- for (const [filePath, entry] of desired) {
1319
- if (entry.kind === "gitlink" || filter && !filter(filePath)) desired.delete(filePath);
1320
- }
1321
- const changedPaths = [.../* @__PURE__ */ new Set([...preimage.keys(), ...desired.keys()])].filter(
1322
- (filePath) => !hydrationEntriesEqual(preimage.get(filePath), desired.get(filePath))
1323
- );
1324
- if (changedPaths.length === 0) return;
1325
- if (!hasSafeDirectoryAncestors(visibleRoot, ".r5d-hydration-scan", input.plansRoot)) {
1326
- conflicts.push(...changedPaths.map((filePath) => import_node_path.default.posix.join(relativeRoot, filePath)));
1327
- return;
1328
- }
1329
- const affectsChangedTree = (filePath) => changedPaths.some((changedPath) => pathsOverlap(filePath, changedPath));
1330
- const currentEntries = /* @__PURE__ */ new Map();
1331
- const unstablePaths = /* @__PURE__ */ new Set();
1332
- for (const filePath of listFilesRecursively(visibleRoot).filter(affectsChangedTree)) {
1333
- const current = hasSafeDirectoryAncestors(visibleRoot, filePath, input.plansRoot) ? filesystemHydrationEntry(input, import_node_path.default.join(visibleRoot, ...filePath.split("/"))) : UNSTABLE_VISIBLE_ENTRY;
1334
- if (current === UNSTABLE_VISIBLE_ENTRY || !current) unstablePaths.add(filePath);
1335
- else currentEntries.set(filePath, current);
1336
- }
1337
- for (const filePath of /* @__PURE__ */ new Set([...changedPaths, ...currentEntries.keys(), ...unstablePaths])) {
1338
- if (!affectsChangedTree(filePath)) continue;
1339
- const current = unstablePaths.has(filePath) ? UNSTABLE_VISIBLE_ENTRY : currentEntries.get(filePath);
1340
- if (current === UNSTABLE_VISIBLE_ENTRY || !hydrationEntriesEqual(current, preimage.get(filePath)) && !hydrationEntriesEqual(current, desired.get(filePath))) {
1341
- conflicts.push(import_node_path.default.posix.join(relativeRoot, filePath));
1342
- }
1343
- }
1344
- };
1345
- if (!input.trigger.canonicalCheckoutOnly) {
1346
- inspectRegularRoot(
1347
- localWorkspacePlansPath(input.plansRoot),
1348
- WORKSPACE_NATIVE_PLANS_RELATIVE_PATH,
1349
- (filePath) => filePath.endsWith(".plan.md")
1350
- );
1351
- }
1352
- for (const manifest of input.projects) {
1353
- for (const branchName of manifest.branches) {
1354
- const visibleRoot = visibleProjectBranchPath(input.projectsRoot, manifest, branchName);
1355
- const relativeRoot = workspaceProjectBranchRelativePath(manifest.projectId, branchName);
1356
- const preimage = preimages.get(visibleRoot);
1357
- if (preimage && import_node_fs.default.existsSync(import_node_path.default.join(visibleRoot, ".git"))) {
1358
- const desired = revisionHydrationPreimage(input, desiredRevision, relativeRoot);
1359
- const changedPaths = [.../* @__PURE__ */ new Set([...preimage.keys(), ...desired.keys()])].filter(
1360
- (filePath) => !hydrationEntriesEqual(preimage.get(filePath), desired.get(filePath))
1361
- );
1362
- if (!hasSafeDirectoryAncestors(visibleRoot, ".r5d-hydration-scan", input.projectsRoot)) {
1363
- conflicts.push(...changedPaths.map((filePath) => import_node_path.default.posix.join(relativeRoot, filePath)));
1364
- continue;
1365
- }
1366
- const affectsChangedTree = (filePath) => changedPaths.some((changedPath) => pathsOverlap(filePath, changedPath));
1367
- const currentGitlinks = new Map(checkoutGitlinks(visibleRoot).map((entry) => [entry.filePath, entry]));
1368
- const residualOpaqueGitlinks = [...preimage].filter(
1369
- ([filePath, entry]) => entry.kind === "gitlink" && !desired.has(filePath) && ![...desired.keys()].some((desiredPath) => desiredPath.startsWith(`${filePath}/`))
1370
- ).map(([filePath]) => filePath);
1371
- const opaqueRoots = [.../* @__PURE__ */ new Set([...currentGitlinks.keys(), ...residualOpaqueGitlinks])];
1372
- const currentEntries = /* @__PURE__ */ new Map();
1373
- const unstablePaths = /* @__PURE__ */ new Set();
1374
- for (const filePath of [...currentGitlinks.keys()].filter(affectsChangedTree)) {
1375
- const current = projectVisibleHydrationEntry(input, visibleRoot, filePath, currentGitlinks);
1376
- if (current === UNSTABLE_VISIBLE_ENTRY || !current) unstablePaths.add(filePath);
1377
- else currentEntries.set(filePath, current);
1378
- }
1379
- for (const filePath of listGitEligibleFiles(visibleRoot).filter(
1380
- (candidate) => affectsChangedTree(candidate) && !pathIsWithinOpaqueRoot(candidate, opaqueRoots)
1381
- )) {
1382
- const current = projectVisibleHydrationEntry(input, visibleRoot, filePath, currentGitlinks);
1383
- if (current === UNSTABLE_VISIBLE_ENTRY || !current) unstablePaths.add(filePath);
1384
- else currentEntries.set(filePath, current);
1385
- }
1386
- for (const filePath of /* @__PURE__ */ new Set([...changedPaths, ...currentEntries.keys(), ...unstablePaths])) {
1387
- if (!affectsChangedTree(filePath)) continue;
1388
- const current = unstablePaths.has(filePath) ? UNSTABLE_VISIBLE_ENTRY : currentEntries.get(filePath);
1389
- if (current === UNSTABLE_VISIBLE_ENTRY || !hydrationEntriesEqual(current, preimage.get(filePath)) && !hydrationEntriesEqual(current, desired.get(filePath))) {
1390
- conflicts.push(import_node_path.default.posix.join(relativeRoot, filePath));
1391
- }
1392
- }
1393
- }
1394
- if (!input.trigger.canonicalCheckoutOnly) {
1395
- inspectRegularRoot(
1396
- localPlansBranchPath(input.plansRoot, manifest.projectId, branchName),
1397
- workspacePlansRelativePath(manifest.projectId, branchName),
1398
- (filePath) => filePath.endsWith(".plan.md")
1399
- );
1400
- }
1401
- }
1402
- }
1403
- return [...new Set(conflicts)].sort();
1404
- }
1405
- function unavailableHydrationConflictPaths(input, preimages, baselineRevision, desiredRevision) {
1406
- const conflicts = [];
1407
- const inspectRoot = (visibleRoot, relativeRoot, filter) => {
1408
- if (preimages.get(visibleRoot) !== null) return;
1409
- const baseline = revisionHydrationPreimage(input, baselineRevision, relativeRoot);
1410
- const desired = revisionHydrationPreimage(input, desiredRevision, relativeRoot);
1411
- for (const filePath of /* @__PURE__ */ new Set([...baseline.keys(), ...desired.keys()])) {
1412
- if (filter && !filter(filePath)) continue;
1413
- if (!hydrationEntriesEqual(baseline.get(filePath), desired.get(filePath))) {
1414
- conflicts.push(import_node_path.default.posix.join(relativeRoot, filePath));
1415
- }
1416
- }
1417
- };
1418
- if (!input.trigger.canonicalCheckoutOnly) {
1419
- inspectRoot(
1420
- localWorkspacePlansPath(input.plansRoot),
1421
- WORKSPACE_NATIVE_PLANS_RELATIVE_PATH,
1422
- (filePath) => filePath.endsWith(".plan.md")
1423
- );
1424
- }
1425
- for (const manifest of input.projects) {
1426
- for (const branchName of manifest.branches) {
1427
- inspectRoot(
1428
- visibleProjectBranchPath(input.projectsRoot, manifest, branchName),
1429
- workspaceProjectBranchRelativePath(manifest.projectId, branchName)
1430
- );
1431
- if (!input.trigger.canonicalCheckoutOnly) {
1432
- inspectRoot(
1433
- localPlansBranchPath(input.plansRoot, manifest.projectId, branchName),
1434
- workspacePlansRelativePath(manifest.projectId, branchName),
1435
- (filePath) => filePath.endsWith(".plan.md")
1436
- );
1437
- }
1438
- }
1439
- }
1440
- return [...new Set(conflicts)].sort();
1441
- }
1442
- function hydrationConflictPaths(input, preimages, baselineRevision, desiredRevision) {
1443
- return [
1444
- .../* @__PURE__ */ new Set([
1445
- ...visibleHydrationConflictPaths(input, preimages, desiredRevision),
1446
- ...unavailableHydrationConflictPaths(input, preimages, baselineRevision, desiredRevision)
1447
- ])
1448
- ].sort();
1449
- }
1450
- function forceStageCanonicalCheckoutFiles(input, preservedGitlinks = [], opaqueRoots = []) {
1451
- for (const manifest of input.projects) {
1452
- for (const checkout of manifest.canonicalCheckouts ?? []) {
1453
- if (!manifest.branches.includes(checkout.branchName)) continue;
1454
- const relativeRoot = workspaceProjectBranchRelativePath(manifest.projectId, checkout.branchName);
1455
- const absoluteRoot = import_node_path.default.join(input.shadowRoot, ...relativeRoot.split("/"));
1456
- if (!import_node_fs.default.existsSync(absoluteRoot)) continue;
1457
- const scopedOpaqueRoots = opaqueRoots.filter((root) => root.startsWith(`${relativeRoot}/`)).map((root) => root.slice(relativeRoot.length + 1));
1458
- runGit(
1459
- input,
1460
- input.shadowRoot,
1461
- [
1462
- "add",
1463
- "-f",
1464
- "-A",
1465
- "--",
1466
- `:(literal)${relativeRoot}`,
1467
- ...scopedOpaqueRoots.map((root) => `:(exclude,literal)${import_node_path.default.posix.join(relativeRoot, root)}`)
1468
- ],
1469
- `force-stage canonical resolver checkout ${manifest.projectId}/${checkout.branchName}`
1470
- );
1471
- }
1472
- }
1473
- for (const gitlink of preservedGitlinks) {
1474
- replaceShadowIndexPathWithGitlink(input, "", gitlink);
1475
- }
1476
- const preservedPaths = new Set(preservedGitlinks.map((entry) => entry.filePath));
1477
- for (const opaqueRoot of opaqueRoots) {
1478
- if (preservedPaths.has(opaqueRoot)) continue;
1479
- runGit(
1480
- input,
1481
- input.shadowRoot,
1482
- ["--literal-pathspecs", "update-index", "--force-remove", "--", opaqueRoot],
1483
- `remove explicitly deleted workspace gitlink ${JSON.stringify(opaqueRoot)}`
1484
- );
1485
- }
1486
- }
1487
- function canonicalCheckoutRelativeRoots(input) {
1488
- const roots = /* @__PURE__ */ new Set();
1489
- for (const manifest of input.projects) {
1490
- for (const checkout of manifest.canonicalCheckouts) {
1491
- roots.add(workspaceProjectBranchRelativePath(manifest.projectId, checkout.branchName));
1492
- }
1493
- }
1494
- return [...roots].sort();
1495
- }
1496
- function restoreUnscopedCanonicalCheckoutSubtrees(input) {
1497
- if (input.trigger.canonicalCheckoutOnly) return;
1498
- const remoteHead = revParse(input, `origin/${WORKSPACE_BRANCH}`);
1499
- const emptyTree = emptyTreeHash(input);
1500
- for (const relativeRoot of canonicalCheckoutRelativeRoots(input)) {
1501
- const remoteTree = remoteHead ? workspaceSubtreeTreeHashAtRevision(input, remoteHead, relativeRoot) : emptyTree;
1502
- if (remoteTree === emptyTree) {
1503
- runGitResult(input, input.shadowRoot, ["--literal-pathspecs", "rm", "-r", "-f", "--cached", "--ignore-unmatch", "--", relativeRoot]);
1504
- import_node_fs.default.rmSync(import_node_path.default.join(input.shadowRoot, ...relativeRoot.split("/")), { recursive: true, force: true });
1505
- continue;
1506
- }
1507
- runGit(
1508
- input,
1509
- input.shadowRoot,
1510
- ["--literal-pathspecs", "restore", `--source=${remoteHead}`, "--staged", "--worktree", "--", relativeRoot],
1511
- `restore excluded canonical resolver subtree ${relativeRoot}`
1512
- );
1513
- }
1514
- }
1515
- function assertUnscopedCanonicalCheckoutSubtreesMatchRemote(input, revision) {
1516
- if (input.trigger.canonicalCheckoutOnly) return;
1517
- const remoteHead = revParse(input, `origin/${WORKSPACE_BRANCH}`);
1518
- const emptyTree = emptyTreeHash(input);
1519
- for (const relativeRoot of canonicalCheckoutRelativeRoots(input)) {
1520
- const candidateTree = workspaceSubtreeTreeHashAtRevision(input, revision, relativeRoot);
1521
- const remoteTree = remoteHead ? workspaceSubtreeTreeHashAtRevision(input, remoteHead, relativeRoot) : emptyTree;
1522
- if (candidateTree !== remoteTree) {
1523
- throw new Error(`Generic workspace synchronization cannot publish canonical resolver subtree ${relativeRoot}`);
1524
- }
1525
- }
1526
- }
1527
- function ensureShadowWorkspace(input) {
1528
- import_node_fs.default.mkdirSync(import_node_path.default.dirname(input.shadowRoot), { recursive: true });
1529
- let created = false;
1530
- if (!import_node_fs.default.existsSync(import_node_path.default.join(input.shadowRoot, ".git"))) {
1531
- created = true;
1532
- import_node_fs.default.rmSync(input.shadowRoot, { recursive: true, force: true });
1533
- const clone = runGitResult(input, import_node_path.default.dirname(input.shadowRoot), ["clone", "--origin", "origin", input.remoteUrl, input.shadowRoot]);
1534
- if (clone.exitCode !== 0) {
1535
- throw new Error(`clone canonical workspace: ${clone.stderr || clone.stdout || `git exited ${clone.exitCode}`}`);
1536
- }
1537
- }
1538
- if (tryGit(input, input.shadowRoot, ["remote", "get-url", "origin"])) {
1539
- runGit(input, input.shadowRoot, ["remote", "set-url", "origin", input.remoteUrl], "update canonical workspace remote");
1540
- } else {
1541
- runGit(input, input.shadowRoot, ["remote", "add", "origin", input.remoteUrl], "add canonical workspace remote");
1542
- }
1543
- runGit(input, input.shadowRoot, ["config", "user.name", "r5d.dev Worker"], "configure workspace Git name");
1544
- runGit(input, input.shadowRoot, ["config", "user.email", "worker@r5d.dev"], "configure workspace Git email");
1545
- runGit(input, input.shadowRoot, ["fetch", "origin", "--prune"], "fetch canonical workspace");
1546
- const hasHead = tryGit(input, input.shadowRoot, ["rev-parse", "--verify", "HEAD"]);
1547
- const hasRemoteMain = tryGit(input, input.shadowRoot, ["show-ref", "--verify", "--quiet", `refs/remotes/origin/${WORKSPACE_BRANCH}`]);
1548
- if (!hasHead && hasRemoteMain) {
1549
- runGit(input, input.shadowRoot, ["checkout", "-B", WORKSPACE_BRANCH, `origin/${WORKSPACE_BRANCH}`], "checkout canonical workspace");
1550
- } else if (!hasHead) {
1551
- runGit(input, input.shadowRoot, ["checkout", "--orphan", WORKSPACE_BRANCH], "create canonical workspace branch");
1552
- }
1553
- return created;
1554
- }
1555
- function revParse(input, revision) {
1556
- const result = runGitResult(input, input.shadowRoot, ["rev-parse", "--verify", revision]);
1557
- return result.exitCode === 0 ? result.stdout : null;
1558
- }
1559
- function emptyTreeHash(input) {
1560
- const result = Bun.spawnSync(gitArgs(input, ["mktree"]), {
1561
- cwd: input.shadowRoot,
1562
- stdin: Buffer.alloc(0),
1563
- stdout: "pipe",
1564
- stderr: "pipe",
1565
- env: { ...process.env, GIT_TERMINAL_PROMPT: "0" }
1566
- });
1567
- if (result.exitCode !== 0) {
1568
- throw new Error(`create empty workspace tree: ${result.stderr.toString().trim() || `git exited ${result.exitCode}`}`);
1569
- }
1570
- return result.stdout.toString().trim();
1571
- }
1572
- function workspaceSubtreeTreeHashAtRevision(input, revision, relativeRoot) {
1573
- const rootType = runGitResult(input, input.shadowRoot, ["cat-file", "-t", revision]);
1574
- if (rootType.exitCode !== 0 || rootType.stdout !== "commit" && rootType.stdout !== "tree") {
1575
- throw new Error(`Cannot inspect canonical resolver subtree at invalid workspace revision ${revision}`);
1576
- }
1577
- let treeHash = rootType.stdout === "commit" ? runGit(input, input.shadowRoot, ["rev-parse", "--verify", `${revision}^{tree}`], "resolve workspace root tree") : revision;
1578
- for (const segment of relativeRoot.split("/")) {
1579
- const entry = runGitResult(input, input.shadowRoot, ["--literal-pathspecs", "ls-tree", "-z", treeHash, "--", segment]);
1580
- if (entry.exitCode !== 0) {
1581
- throw new Error(`Inspect canonical resolver workspace path ${relativeRoot}: ${entry.stderr || entry.stdout}`);
1582
- }
1583
- if (!entry.stdout) return emptyTreeHash(input);
1584
- const separator = entry.stdout.indexOf(" ");
1585
- const [mode, type, objectId] = separator === -1 ? [] : entry.stdout.slice(0, separator).split(" ");
1586
- const entryName = separator === -1 ? "" : entry.stdout.slice(separator + 1).replace(/\0+$/, "");
1587
- if (!mode || type !== "tree" || !objectId || entryName !== segment) {
1588
- throw new Error(`Canonical resolver workspace path ${relativeRoot} contains a non-tree component ${segment}`);
1589
- }
1590
- treeHash = objectId;
1591
- }
1592
- return treeHash;
1593
- }
1594
- function canonicalCheckoutTreeHashAtRevision(input, revision) {
1595
- if (!input.trigger.canonicalCheckoutOnly || !input.trigger.projectId || !input.trigger.branchName) {
1596
- throw new Error("Canonical checkout tree hashing requires an exact canonical-checkout-only trigger");
1597
- }
1598
- return workspaceSubtreeTreeHashAtRevision(
1599
- input,
1600
- revision,
1601
- workspaceProjectBranchRelativePath(input.trigger.projectId, input.trigger.branchName)
1602
- );
1603
- }
1604
- function sampleCanonicalCheckoutTree(input) {
1605
- if (!input.trigger.canonicalCheckoutOnly) return void 0;
1606
- const stagedWorkspaceTree = runGit(input, input.shadowRoot, ["write-tree"], "write canonical resolver workspace tree");
1607
- return canonicalCheckoutTreeHashAtRevision(input, stagedWorkspaceTree);
1608
- }
1609
- function assertPublishedCanonicalCheckoutTree(input, revision, sampledTreeHash) {
1610
- if (!input.trigger.canonicalCheckoutOnly) return;
1611
- if (!sampledTreeHash) throw new Error("Canonical resolver synchronization did not capture a staged subtree tree hash");
1612
- const publishedTreeHash = canonicalCheckoutTreeHashAtRevision(input, revision);
1613
- if (publishedTreeHash !== sampledTreeHash) {
1614
- throw new Error(`Canonical resolver subtree changed while synchronizing: sampled ${sampledTreeHash}, published ${publishedTreeHash}`);
1615
- }
1616
- }
1617
- function gitStatus(input) {
1618
- return runGit(input, input.shadowRoot, ["status", "--porcelain=v1", "--untracked-files=all"], "read workspace status");
1619
- }
1620
- function resetUncommittedShadowSnapshot(input) {
1621
- if (!gitStatus(input)) return;
1622
- if (tryGit(input, input.shadowRoot, ["rev-parse", "--verify", "HEAD"])) {
1623
- runGit(input, input.shadowRoot, ["reset", "--hard", "HEAD"], "reset interrupted workspace snapshot");
1624
- } else {
1625
- runGit(input, input.shadowRoot, ["read-tree", "--empty"], "reset interrupted unborn workspace snapshot");
1626
- }
1627
- runGit(input, input.shadowRoot, ["clean", "-fd"], "clean interrupted workspace snapshot");
1628
- }
1629
- function restoreShadowToVisibleBaseline(input, canonicalHead) {
1630
- if (input.skipVisibleMirror || input.trigger.canonicalCheckoutOnly) return void 0;
1631
- if (input.visibleBaselineHead === void 0) {
1632
- if (revParse(input, "HEAD") !== canonicalHead) {
1633
- throw new Error("visible_workspace_baseline_missing: shadow HEAD differs from canonical main");
1634
- }
1635
- return void 0;
1636
- }
1637
- const baselineHead = input.visibleBaselineHead;
1638
- const currentHead = revParse(input, "HEAD");
1639
- if (!baselineHead) {
1640
- if (canonicalHead) {
1641
- throw new Error("Visible workspace has an unborn baseline after canonical main was created");
1642
- }
1643
- if (!currentHead) return null;
1644
- resetUncommittedShadowSnapshot(input);
1645
- const symbolicHead = runGit(input, input.shadowRoot, ["symbolic-ref", "-q", "HEAD"], "resolve unborn workspace branch");
1646
- runGit(input, input.shadowRoot, ["update-ref", "-d", symbolicHead], "restore unborn visible workspace baseline");
1647
- runGit(input, input.shadowRoot, ["read-tree", "--empty"], "clear unborn visible workspace index");
1648
- runGit(input, input.shadowRoot, ["clean", "-fdx"], "clean unborn visible workspace baseline");
1649
- return null;
1650
- }
1651
- if (currentHead === baselineHead) return baselineHead;
1652
- if (revParse(input, `${baselineHead}^{commit}`) !== baselineHead) {
1653
- throw new Error(`Visible workspace baseline ${baselineHead} is not available in the shadow repository`);
1654
- }
1655
- resetUncommittedShadowSnapshot(input);
1656
- runGit(input, input.shadowRoot, ["reset", "--hard", baselineHead], "restore visible workspace publication baseline");
1657
- runGit(input, input.shadowRoot, ["clean", "-fd"], "clean restored visible workspace publication baseline");
1658
- return baselineHead;
1659
- }
1660
- function restoreCleanShadowToExpectedIncidentCandidate(input) {
1661
- const expectedHead = input.expectedIncidentCandidateHead;
1662
- if (expectedHead === void 0) return;
1663
- if (input.trigger.type !== "remediation" || input.trigger.canonicalCheckoutOnly || input.trigger.processRunId !== void 0 || input.trigger.toolCallId !== void 0 || input.skipVisibleMirror !== true || input.confirmedLargeDiff === true || input.resetToCanonical === true) {
1664
- throw new Error("An expected incident candidate may only be used for stable remediation preparation");
1665
- }
1666
- const status = gitStatus(input);
1667
- if (status) {
1668
- throw new Error("incident_candidate_shadow_dirty: refusing to replace a dirty remediation shadow checkout");
1669
- }
1670
- if (revParse(input, `${expectedHead}^{commit}`) !== expectedHead) {
1671
- throw new Error(`incident_candidate_unavailable: elected workspace candidate ${expectedHead} is not available`);
1672
- }
1673
- if (revParse(input, "HEAD") !== expectedHead) {
1674
- runGit(input, input.shadowRoot, ["reset", "--hard", expectedHead], "restore elected workspace incident candidate");
1675
- }
1676
- }
1677
- function activeWorkspaceRebase(input) {
1678
- for (const name of ["rebase-merge", "rebase-apply"]) {
1679
- const resolved = runGitResult(input, input.shadowRoot, ["rev-parse", "--git-path", name]);
1680
- if (resolved.exitCode !== 0 || !resolved.stdout) continue;
1681
- const gitPath = import_node_path.default.isAbsolute(resolved.stdout) ? resolved.stdout : import_node_path.default.resolve(input.shadowRoot, resolved.stdout);
1682
- if (import_node_fs.default.existsSync(gitPath)) return name;
1683
- }
1684
- return null;
1685
- }
1686
- function remediationRebaseInProgressResult(input, result) {
1687
- if (input.trigger.type !== "remediation" && input.trigger.type !== "remediation_confirm") return null;
1688
- const rebaseKind = activeWorkspaceRebase(input);
1689
- if (!rebaseKind) return null;
1690
- const conflictPaths = runGitResult(input, input.shadowRoot, ["diff", "--name-only", "--diff-filter=U", "-z"]).stdout.split("\0").filter(Boolean).sort();
1691
- const status = reportedStatus(gitStatus(input));
1692
- return {
1693
- ...result,
1694
- outcome: "conflict_blocked",
1695
- expectedHead: revParse(input, `origin/${WORKSPACE_BRANCH}`),
1696
- candidateHead: revParse(input, "ORIG_HEAD") ?? revParse(input, "HEAD") ?? void 0,
1697
- rebaseCount: 1,
1698
- gitStatus: status,
1699
- affectedProjects: affectedProjects(conflictPaths),
1700
- affectedPaths: reportedPaths(conflictPaths),
1701
- error: `Workspace conflict remediation has an in-progress ${rebaseKind}; the index and worktree were preserved until the resolver completes or aborts it.`
1702
- };
1703
- }
1704
- function fastForwardCleanShadowForNewCheckouts(input) {
1705
- if (!input.newVisibleCheckouts?.length) return null;
1706
- runGit(input, input.shadowRoot, ["add", "-A"], "stage existing workspace changes before checkout hydration");
1707
- if (gitStatus(input)) return null;
1708
- const localHead = revParse(input, "HEAD");
1709
- const remoteHead = revParse(input, `origin/${WORKSPACE_BRANCH}`);
1710
- if (!localHead || !remoteHead || localHead === remoteHead) return null;
1711
- if (!tryGit(input, input.shadowRoot, ["merge-base", "--is-ancestor", localHead, remoteHead])) return null;
1712
- runGit(input, input.shadowRoot, ["reset", "--hard", remoteHead], "fast-forward before checkout hydration");
1713
- return localHead;
1714
- }
1715
- function restoreShadowRootFromRemote(input, relativeRoot) {
1716
- const remoteRevision = `origin/${WORKSPACE_BRANCH}`;
1717
- const remoteFiles = runGitResult(input, input.shadowRoot, ["ls-tree", "-r", "--name-only", "-z", remoteRevision, "--", relativeRoot]);
1718
- if (remoteFiles.exitCode !== 0 || !remoteFiles.stdout) return false;
1719
- runGit(
1720
- input,
1721
- input.shadowRoot,
1722
- ["restore", `--source=${remoteRevision}`, "--staged", "--worktree", "--", relativeRoot],
1723
- `hydrate ${relativeRoot} from canonical workspace`
1724
- );
1725
- return true;
1726
- }
1727
- function candidateBaseRevision(input) {
1728
- const localHead = revParse(input, "HEAD");
1729
- if (!localHead) return null;
1730
- const remoteHead = revParse(input, `origin/${WORKSPACE_BRANCH}`);
1731
- if (!remoteHead) {
1732
- const emptyTree = Bun.spawnSync(gitArgs(input, ["mktree"]), {
1733
- cwd: input.shadowRoot,
1734
- stdin: Buffer.alloc(0),
1735
- stdout: "pipe",
1736
- stderr: "pipe",
1737
- env: { ...process.env, GIT_TERMINAL_PROMPT: "0" }
1738
- });
1739
- if (emptyTree.exitCode !== 0) {
1740
- throw new Error(
1741
- `create empty workspace comparison tree: ${emptyTree.stderr.toString().trim() || `git exited ${emptyTree.exitCode}`}`
1742
- );
1743
- }
1744
- return emptyTree.stdout.toString().trim();
1745
- }
1746
- const mergeBase = runGitResult(input, input.shadowRoot, ["merge-base", localHead, remoteHead]);
1747
- return mergeBase.exitCode === 0 && mergeBase.stdout ? mergeBase.stdout : remoteHead;
1748
- }
1749
- function stagedPaths(input, baseRevision) {
1750
- return runGit(
1751
- input,
1752
- input.shadowRoot,
1753
- ["diff", "--cached", "--name-only", "-z", ...baseRevision ? [baseRevision] : []],
1754
- "list workspace changes"
1755
- ).split("\0").filter(Boolean).sort();
1756
- }
1757
- function stagedDeletedPaths(input, baseRevision) {
1758
- return runGit(
1759
- input,
1760
- input.shadowRoot,
1761
- ["diff", "--cached", "--diff-filter=D", "--name-only", "-z", ...baseRevision ? [baseRevision] : []],
1762
- "list workspace deletions"
1763
- ).split("\0").filter(Boolean).sort();
1764
- }
1765
- function managedCheckoutRoot(filePath) {
1766
- const projectMatch = /^projects\/([^/]+)\/branches\/([^/]+)\//.exec(filePath);
1767
- return projectMatch ? `projects/${projectMatch[1]}/branches/${projectMatch[2]}` : null;
1768
- }
1769
- function revisionTrackedFileCount(input, revision, relativeRoot) {
1770
- if (!revision) return 0;
1771
- const result = runGitResult(input, input.shadowRoot, ["ls-tree", "-r", "--name-only", "-z", revision, "--", relativeRoot]);
1772
- if (result.exitCode !== 0) return 0;
1773
- return result.stdout.split("\0").filter(Boolean).length;
1774
- }
1775
- function destructiveCheckoutReductions(input, baseRevision) {
1776
- const authoritativeRoot = input.trigger.canonicalCheckoutOnly && input.trigger.projectId && input.trigger.branchName ? workspaceProjectBranchRelativePath(input.trigger.projectId, input.trigger.branchName) : null;
1777
- const roots = new Set(
1778
- stagedDeletedPaths(input, baseRevision).map(managedCheckoutRoot).filter((root) => Boolean(root) && root !== authoritativeRoot)
1779
- );
1780
- const destructive = [];
1781
- for (const root of roots) {
1782
- const trackedBefore = revisionTrackedFileCount(input, baseRevision, root);
1783
- if (trackedBefore === 0) continue;
1784
- const trackedAfter = listShadowTrackedFiles(input, root).length;
1785
- const removedRatio = (trackedBefore - trackedAfter) / trackedBefore;
1786
- if (trackedAfter === 0 || trackedBefore >= DESTRUCTIVE_CHECKOUT_MIN_TRACKED_FILES && removedRatio >= DESTRUCTIVE_CHECKOUT_REMOVAL_RATIO) {
1787
- destructive.push({ root, trackedBefore, trackedAfter });
1788
- }
1789
- }
1790
- return destructive.sort((left, right) => left.root.localeCompare(right.root));
1791
- }
1792
- async function stagedDiffSizeBytes(input, baseRevision) {
1793
- const subprocess = Bun.spawn(gitArgs(input, ["diff", "--cached", "--binary", "--no-ext-diff", ...baseRevision ? [baseRevision] : []]), {
1794
- cwd: input.shadowRoot,
1795
- stdout: "pipe",
1796
- stderr: "pipe",
1797
- env: { ...process.env, GIT_TERMINAL_PROMPT: "0" }
1798
- });
1799
- const stderrPromise = new Response(subprocess.stderr).text();
1800
- const reader = subprocess.stdout.getReader();
1801
- let total = 0;
1802
- while (true) {
1803
- const chunk = await reader.read();
1804
- if (chunk.done) break;
1805
- total += chunk.value.byteLength;
1806
- if (total > MAX_WORKSPACE_SYNC_DIFF_BYTES) {
1807
- subprocess.kill();
1808
- await subprocess.exited;
1809
- await stderrPromise;
1810
- return MAX_WORKSPACE_SYNC_DIFF_BYTES + 1;
1811
- }
1812
- }
1813
- const exitCode = await subprocess.exited;
1814
- const stderr = (await stderrPromise).trim();
1815
- if (exitCode !== 0) throw new Error(`measure workspace diff: ${stderr || `git exited ${exitCode}`}`);
1816
- return total;
1817
- }
1818
- function reportedPaths(paths) {
1819
- if (paths.length <= MAX_REPORTED_WORKSPACE_PATHS) return paths;
1820
- return [
1821
- ...paths.slice(0, MAX_REPORTED_WORKSPACE_PATHS),
1822
- `[${paths.length - MAX_REPORTED_WORKSPACE_PATHS} additional paths omitted; inspect with git status]`
1823
- ];
1824
- }
1825
- function reportedStatus(status) {
1826
- const bytes = Buffer.from(status, "utf8");
1827
- if (bytes.length <= MAX_REPORTED_WORKSPACE_STATUS_BYTES) return status;
1828
- return `${new TextDecoder().decode(bytes.subarray(0, MAX_REPORTED_WORKSPACE_STATUS_BYTES))}
1829
- [status truncated; inspect on the origin worker]`;
1830
- }
1831
- function affectedProjects(paths) {
1832
- const ids = /* @__PURE__ */ new Set();
1833
- for (const filePath of paths) {
1834
- const match = /^(?:projects|plans)\/([^/]+)\//.exec(filePath);
1835
- if (match?.[1]) ids.add(match[1]);
1836
- }
1837
- return [...ids].sort();
1838
- }
1839
- function commitMessage(input) {
1840
- return JSON.stringify({
1841
- type: "workspace_sync",
1842
- attemptId: input.attemptId,
1843
- worker: input.workerLabel,
1844
- trigger: input.trigger,
1845
- ...input.confirmedLargeDiff ? { confirmedLargeDiff: true, confirmationReason: input.confirmationReason } : {}
1846
- });
1847
- }
1848
- function assertWorkspaceQuarantineRef(quarantineRef) {
1849
- const prefix = quarantineRef.startsWith(WORKSPACE_PUBLICATION_REF_PREFIX) ? WORKSPACE_PUBLICATION_REF_PREFIX : WORKSPACE_INTENT_REF_PREFIX;
1850
- const suffix = quarantineRef.slice(prefix.length);
1851
- 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)) {
1852
- throw new Error(`Workspace candidate ref must be below ${WORKSPACE_PUBLICATION_REF_PREFIX} or ${WORKSPACE_INTENT_REF_PREFIX}`);
1853
- }
1854
- const result = Bun.spawnSync(["git", "check-ref-format", quarantineRef], {
1855
- stdout: "pipe",
1856
- stderr: "pipe",
1857
- env: { ...process.env, GIT_TERMINAL_PROMPT: "0" }
1858
- });
1859
- if (result.exitCode !== 0) {
1860
- throw new Error(`Invalid workspace candidate ref ${JSON.stringify(quarantineRef)}`);
1861
- }
1862
- }
1863
- function uploadWorkspaceCandidate(input, candidateHead) {
1864
- const quarantineRef = input.quarantineRef;
1865
- if (!quarantineRef) {
1866
- throw new Error("A server-issued workspace candidate ref is required before uploading a canonical workspace candidate");
1867
- }
1868
- assertWorkspaceQuarantineRef(quarantineRef);
1869
- return runGitResult(input, input.shadowRoot, ["push", "origin", `${candidateHead}:${quarantineRef}`]);
1870
- }
1871
- function resetShadowToRemote(input, opaqueWorkspaceRoots = []) {
1872
- runGit(input, input.shadowRoot, ["fetch", "origin", "--prune"], "fetch canonical workspace before reset");
1873
- const remoteHead = revParse(input, `origin/${WORKSPACE_BRANCH}`);
1874
- if (!remoteHead) return null;
1875
- runGitResult(input, input.shadowRoot, ["rebase", "--abort"]);
1876
- runGit(input, input.shadowRoot, ["reset", "--hard", `origin/${WORKSPACE_BRANCH}`], "reset workspace to canonical state");
1877
- runGit(input, input.shadowRoot, ["clean", "-fd"], "clean reset workspace");
1878
- if (!input.trigger.canonicalCheckoutOnly && !input.skipVisibleMirror) {
1879
- mirrorShadowWorkspaceToVisible(input, opaqueWorkspaceRoots);
1880
- }
1881
- return remoteHead;
1882
- }
1883
- function baseResult(input, startingHead) {
1884
- return {
1885
- type: "workspace_sync",
1886
- attemptId: input.attemptId ?? crypto.randomUUID(),
1887
- workerLabel: input.workerLabel,
1888
- trigger: input.trigger,
1889
- startingHead,
1890
- expectedHead: startingHead,
1891
- rebaseCount: 0,
1892
- diffSizeBytes: 0,
1893
- gitStatus: "",
1894
- affectedProjects: [],
1895
- affectedPaths: [],
1896
- discardedPaths: [],
1897
- localChangesDiscarded: false
1898
- };
1899
- }
1900
- function convergeRedundantInboundCandidate(input, result) {
1901
- if (input.trigger.type !== "inbound_head" || !input.skipVisibleMirror || input.trigger.canonicalCheckoutOnly) return null;
1902
- const remoteHead = revParse(input, `origin/${WORKSPACE_BRANCH}`);
1903
- if (!remoteHead) return null;
1904
- const stagedTree = runGit(input, input.shadowRoot, ["write-tree"], "write inbound workspace candidate tree");
1905
- const remoteTree = runGit(
1906
- input,
1907
- input.shadowRoot,
1908
- ["rev-parse", "--verify", `${remoteHead}^{tree}`],
1909
- "resolve authoritative workspace tree"
1910
- );
1911
- if (stagedTree !== remoteTree) return null;
1912
- const localHead = revParse(input, "HEAD");
1913
- if (localHead === remoteHead) return null;
1914
- runGit(input, input.shadowRoot, ["reset", "--hard", remoteHead], "converge redundant inbound workspace candidate");
1915
- runGit(input, input.shadowRoot, ["clean", "-fd"], "clean redundant inbound workspace candidate");
1916
- return {
1917
- ...result,
1918
- outcome: "updated",
1919
- publishedHead: remoteHead,
1920
- diffSizeBytes: 0,
1921
- gitStatus: gitStatus(input),
1922
- affectedProjects: [],
1923
- affectedPaths: []
1924
- };
1925
- }
1926
- function preserveWorkspaceConflictCandidate(input, observed, options) {
1927
- try {
1928
- if (!options.baseRevision) throw new Error("the preserved workspace does not have an old-base commit");
1929
- runGit(input, input.shadowRoot, ["reset", "--hard", options.baseRevision], "restore old-base workspace conflict candidate");
1930
- runGit(input, input.shadowRoot, ["clean", "-fd"], "clean old-base workspace conflict candidate");
1931
- if (!options.preserveShadowCandidate) {
1932
- const projection = mirrorVisibleWorkspaceToShadow(input);
1933
- restoreUnscopedCanonicalCheckoutSubtrees(input);
1934
- const stagePathspecs = projection.opaqueRoots.flatMap((root) => [`:(exclude,literal)${root}`]);
1935
- runGit(input, input.shadowRoot, ["add", "-A", "--", ".", ...stagePathspecs], "stage preserved workspace conflict candidate");
1936
- forceStageCanonicalCheckoutFiles(input, projection.entries, projection.opaqueRoots);
1937
- if (stagedPaths(input).length > 0) {
1938
- runGit(input, input.shadowRoot, ["commit", "-m", commitMessage(input)], "commit preserved workspace conflict candidate");
1939
- }
1940
- }
1941
- const candidateHead = revParse(input, "HEAD");
1942
- if (!candidateHead) throw new Error("the preserved workspace conflict candidate does not have a commit");
1943
- assertUnscopedCanonicalCheckoutSubtreesMatchRemote(input, candidateHead);
1944
- const upload = uploadWorkspaceCandidate(input, candidateHead);
1945
- if (upload.exitCode !== 0) {
1946
- throw new Error(upload.stderr || upload.stdout || "workspace conflict candidate upload failed");
1947
- }
1948
- const conflictPaths = [...new Set(options.conflictPaths)].sort();
1949
- return {
1950
- ...observed,
1951
- outcome: "conflict_blocked",
1952
- expectedHead: options.expectedHead,
1953
- candidateHead,
1954
- quarantineRef: input.quarantineRef,
1955
- ...options.publishedHead ? { publishedHead: options.publishedHead } : {},
1956
- rebaseCount: options.rebaseCount ?? 0,
1957
- affectedProjects: affectedProjects(conflictPaths),
1958
- affectedPaths: reportedPaths(conflictPaths),
1959
- error: options.error,
1960
- gitStatus: gitStatus(input)
1961
- };
1962
- } catch (error) {
1963
- return {
1964
- ...observed,
1965
- outcome: "failed",
1966
- rebaseCount: options.rebaseCount ?? 0,
1967
- affectedProjects: affectedProjects(options.conflictPaths),
1968
- affectedPaths: reportedPaths(options.conflictPaths),
1969
- error: `Workspace synchronization found a conflict but could not preserve a durable candidate: ${error instanceof Error ? error.message : String(error)}`,
1970
- gitStatus: gitStatus(input)
1971
- };
1972
- }
1973
- }
1974
- function preserveInProgressWorkspaceRebaseConflict(input, observed, options) {
1975
- try {
1976
- assertUnscopedCanonicalCheckoutSubtreesMatchRemote(input, options.candidateHead);
1977
- const upload = uploadWorkspaceCandidate(input, options.candidateHead);
1978
- if (upload.exitCode !== 0) {
1979
- throw new Error(upload.stderr || upload.stdout || "workspace rebase conflict candidate upload failed");
1980
- }
1981
- const conflictPaths = [...new Set(options.conflictPaths)].sort();
1982
- return {
1983
- ...observed,
1984
- outcome: "conflict_blocked",
1985
- expectedHead: options.expectedHead,
1986
- candidateHead: options.candidateHead,
1987
- quarantineRef: input.quarantineRef,
1988
- rebaseCount: options.rebaseCount,
1989
- affectedProjects: affectedProjects(conflictPaths),
1990
- affectedPaths: reportedPaths(conflictPaths),
1991
- error: options.error,
1992
- gitStatus: gitStatus(input)
1993
- };
1994
- } catch (error) {
1995
- runGitResult(input, input.shadowRoot, ["rebase", "--abort"]);
1996
- return {
1997
- ...observed,
1998
- outcome: "failed",
1999
- rebaseCount: options.rebaseCount,
2000
- affectedProjects: affectedProjects(options.conflictPaths),
2001
- affectedPaths: reportedPaths(options.conflictPaths),
2002
- error: `Workspace synchronization found a rebase conflict but could not preserve its durable candidate: ${error instanceof Error ? error.message : String(error)}`,
2003
- gitStatus: gitStatus(input)
2004
- };
2005
- }
2006
- }
2007
- function preserveLargeDiffCandidate(input, observed, error) {
2008
- try {
2009
- if (stagedPaths(input).length > 0) {
2010
- runGit(input, input.shadowRoot, ["commit", "-m", commitMessage(input)], "commit blocked workspace candidate");
2011
- }
2012
- const candidateHead = revParse(input, "HEAD");
2013
- if (!candidateHead) throw new Error("the blocked workspace candidate does not have a commit");
2014
- assertUnscopedCanonicalCheckoutSubtreesMatchRemote(input, candidateHead);
2015
- const upload = uploadWorkspaceCandidate(input, candidateHead);
2016
- if (upload.exitCode !== 0) {
2017
- throw new Error(upload.stderr || upload.stdout || "blocked workspace candidate upload failed");
2018
- }
2019
- return {
2020
- ...observed,
2021
- outcome: "large_diff_blocked",
2022
- expectedHead: revParse(input, `origin/${WORKSPACE_BRANCH}`),
2023
- candidateHead,
2024
- quarantineRef: input.quarantineRef,
2025
- error,
2026
- gitStatus: gitStatus(input)
2027
- };
2028
- } catch (candidateError) {
2029
- return {
2030
- ...observed,
2031
- outcome: "failed",
2032
- error: `Workspace safety guard blocked publication but could not preserve a durable candidate: ${candidateError instanceof Error ? candidateError.message : String(candidateError)}`,
2033
- gitStatus: gitStatus(input)
2034
- };
2035
- }
2036
- }
2037
- async function synchronizeWorkspace(rawInput) {
2038
- let input = { ...rawInput, attemptId: rawInput.attemptId ?? crypto.randomUUID() };
2039
- try {
2040
- input = { ...input, projects: workspaceProjectsForSync(input.projects, input.trigger) };
2041
- const isCanonicalRemediationSync = input.skipVisibleMirror === true && (input.trigger.type === "remediation" || input.trigger.type === "remediation_confirm");
2042
- let mirroredGitlinkProjection = { entries: [], opaqueRoots: [], visiblePreimages: /* @__PURE__ */ new Map() };
2043
- let fastForwardedFromHead = null;
2044
- const shadowWasCreated = ensureShadowWorkspace(input);
2045
- let hydrationPreimages = input.skipVisibleMirror ? hydrationPreimagesFromShadowRevision(input, shadowWasCreated ? null : revParse(input, "HEAD")) : /* @__PURE__ */ new Map();
2046
- const remoteHeadAtStart = revParse(input, `origin/${WORKSPACE_BRANCH}`);
2047
- const result = baseResult(input, remoteHeadAtStart);
2048
- if (input.expectedCanonicalHead !== void 0 && remoteHeadAtStart !== input.expectedCanonicalHead) {
2049
- return {
2050
- ...result,
2051
- outcome: "failed",
2052
- expectedHead: input.expectedCanonicalHead,
2053
- publishedHead: remoteHeadAtStart ?? void 0,
2054
- error: `canonical_head_advanced: expected ${input.expectedCanonicalHead ?? "unborn"}, fetched ${remoteHeadAtStart ?? "unborn"}`
2055
- };
2056
- }
2057
- const inProgressRemediation = remediationRebaseInProgressResult(input, result);
2058
- if (inProgressRemediation) return inProgressRemediation;
2059
- restoreCleanShadowToExpectedIncidentCandidate(input);
2060
- const visibleBaselineHead = restoreShadowToVisibleBaseline(input, remoteHeadAtStart);
2061
- const inboundConflictsAtStart = input.skipVisibleMirror && !isCanonicalRemediationSync && remoteHeadAtStart ? visibleHydrationConflictPaths(input, hydrationPreimages, `origin/${WORKSPACE_BRANCH}`) : [];
2062
- if (input.resetToCanonical) {
2063
- if (input.trigger.canonicalCheckoutOnly) {
2064
- throw new Error("A canonical-checkout-only synchronization cannot reset the authoritative resolver checkout");
2065
- }
2066
- const publishedHead = resetShadowToRemote(input);
2067
- return {
2068
- ...result,
2069
- outcome: "reset",
2070
- publishedHead: publishedHead ?? void 0,
2071
- localChangesDiscarded: true,
2072
- gitStatus: gitStatus(input)
2073
- };
2074
- }
2075
- if (!input.skipVisibleMirror) resetUncommittedShadowSnapshot(input);
2076
- restoreUnscopedCanonicalCheckoutSubtrees(input);
2077
- if (!input.skipVisibleMirror) {
2078
- const newCheckoutKeys = new Set((input.newVisibleCheckouts ?? []).map((target) => `${target.projectId}\0${target.branchName}`));
2079
- mirroredGitlinkProjection = mirrorVisibleWorkspaceToShadow(input, newCheckoutKeys);
2080
- fastForwardedFromHead = fastForwardCleanShadowForNewCheckouts(input);
2081
- mergeGitlinkProjection(mirroredGitlinkProjection, reconcileNewVisibleCheckouts(input));
2082
- mirroredGitlinkProjection.opaqueRoots = [...new Set(mirroredGitlinkProjection.opaqueRoots)].sort();
2083
- hydrationPreimages = mirroredGitlinkProjection.visiblePreimages ?? hydrationPreimages;
2084
- if (input.trigger.type === "remediation" && !visibleWorkspaceMatchesProjection(input, mirroredGitlinkProjection)) {
2085
- resetUncommittedShadowSnapshot(input);
2086
- throw new Error("visible_workspace_unstable: visible files changed while preparing the remediation snapshot");
2087
- }
2088
- }
2089
- const stagePathspecs = mirroredGitlinkProjection.opaqueRoots.flatMap((root) => [`:(exclude,literal)${root}`]);
2090
- runGit(input, input.shadowRoot, ["add", "-A", "--", ".", ...stagePathspecs], "stage workspace changes");
2091
- forceStageCanonicalCheckoutFiles(input, mirroredGitlinkProjection.entries, mirroredGitlinkProjection.opaqueRoots);
2092
- const redundantInboundCandidate = convergeRedundantInboundCandidate(input, result);
2093
- if (redundantInboundCandidate) return redundantInboundCandidate;
2094
- const sampledCanonicalCheckoutTreeHash = sampleCanonicalCheckoutTree(input);
2095
- const stagedWorkingPaths = stagedPaths(input);
2096
- const preservesDivergentVisibleSnapshot = input.confirmedLargeDiff === true || input.trigger.type === "remediation" || input.trigger.type === "remediation_confirm";
2097
- const baseRevision = preservesDivergentVisibleSnapshot ? remoteHeadAtStart : visibleBaselineHead !== void 0 ? visibleBaselineHead : candidateBaseRevision(input);
2098
- const paths = stagedPaths(input, baseRevision);
2099
- const diffSizeBytes = await stagedDiffSizeBytes(input, baseRevision);
2100
- const statusBeforeCommit = reportedStatus(gitStatus(input));
2101
- const observed = {
2102
- ...result,
2103
- diffSizeBytes,
2104
- gitStatus: statusBeforeCommit,
2105
- affectedPaths: reportedPaths(paths),
2106
- affectedProjects: affectedProjects(paths),
2107
- ...sampledCanonicalCheckoutTreeHash ? { sampledCanonicalCheckoutTreeHash } : {}
2108
- };
2109
- const destructiveReductions = destructiveCheckoutReductions(input, baseRevision);
2110
- let blockedDiffError;
2111
- if (destructiveReductions.length > 0 && !input.confirmedLargeDiff) {
2112
- const summary = destructiveReductions.map(({ root, trackedBefore, trackedAfter }) => `${root} (${trackedBefore} -> ${trackedAfter} tracked files)`).join(", ");
2113
- blockedDiffError = `Workspace safety guard blocked a destructive checkout reduction: ${summary}`;
2114
- } else if (paths.length > 0 && diffSizeBytes > MAX_WORKSPACE_SYNC_DIFF_BYTES && !input.confirmedLargeDiff && !input.trigger.canonicalCheckoutOnly) {
2115
- blockedDiffError = `Workspace safety guard blocked a ${diffSizeBytes}-byte diff above the ${MAX_WORKSPACE_SYNC_DIFF_BYTES}-byte automatic publication limit.`;
2116
- }
2117
- if (stagedWorkingPaths.length > 0) {
2118
- runGit(input, input.shadowRoot, ["commit", "-m", commitMessage(input)], "commit workspace changes");
2119
- }
2120
- let candidateHead = revParse(input, "HEAD");
2121
- const candidateHeadBeforeRebase = candidateHead;
2122
- if (candidateHead) assertUnscopedCanonicalCheckoutSubtreesMatchRemote(input, candidateHead);
2123
- const remoteHeadBeforeRebase = revParse(input, `origin/${WORKSPACE_BRANCH}`);
2124
- const hasRemoteMain = Boolean(remoteHeadBeforeRebase);
2125
- const hasCapturedVisibleDelta = visibleBaselineHead === void 0 || paths.length > 0;
2126
- const mustDropVisibleBaselinePrefix = !preservesDivergentVisibleSnapshot && visibleBaselineHead !== void 0 && visibleBaselineHead !== remoteHeadBeforeRebase && stagedWorkingPaths.length > 0;
2127
- const hasUnpushedCommit = candidateHead && hasCapturedVisibleDelta ? mustDropVisibleBaselinePrefix || !hasRemoteMain || !tryGit(input, input.shadowRoot, ["merge-base", "--is-ancestor", candidateHead, `origin/${WORKSPACE_BRANCH}`]) : false;
2128
- if (hasUnpushedCommit && inboundConflictsAtStart.length > 0) {
2129
- const conflictPaths = [.../* @__PURE__ */ new Set([...paths, ...inboundConflictsAtStart])].sort();
2130
- return preserveWorkspaceConflictCandidate(input, observed, {
2131
- baseRevision: candidateHead,
2132
- expectedHead: revParse(input, `origin/${WORKSPACE_BRANCH}`),
2133
- conflictPaths,
2134
- error: "Inbound workspace update overlaps local visible changes; the old-base candidate was preserved for reconciliation."
2135
- });
2136
- }
2137
- if (!hasUnpushedCommit) {
2138
- const localHead = revParse(input, "HEAD");
2139
- const currentRemoteHead = revParse(input, `origin/${WORKSPACE_BRANCH}`);
2140
- if (currentRemoteHead && localHead !== currentRemoteHead) {
2141
- runGit(input, input.shadowRoot, ["reset", "--hard", `origin/${WORKSPACE_BRANCH}`], "update workspace from canonical state");
2142
- runGit(input, input.shadowRoot, ["clean", "-fd"], "clean updated workspace");
2143
- assertPublishedCanonicalCheckoutTree(input, currentRemoteHead, sampledCanonicalCheckoutTreeHash);
2144
- if (!isCanonicalRemediationSync) {
2145
- mirrorShadowWorkspaceToVisible(input, mirroredGitlinkProjection.opaqueRoots, hydrationPreimages);
2146
- }
2147
- const inboundConflicts = input.trigger.canonicalCheckoutOnly || isCanonicalRemediationSync ? [] : localHead ? hydrationConflictPaths(input, hydrationPreimages, localHead, currentRemoteHead) : visibleHydrationConflictPaths(input, hydrationPreimages, currentRemoteHead);
2148
- if (inboundConflicts.length > 0 && localHead) {
2149
- runGit(input, input.shadowRoot, ["reset", "--hard", localHead], "preserve old workspace base after inbound conflict");
2150
- runGit(input, input.shadowRoot, ["clean", "-fd"], "clean preserved old workspace base");
2151
- const conflictPaths = [.../* @__PURE__ */ new Set([...paths, ...inboundConflicts])].sort();
2152
- return preserveWorkspaceConflictCandidate(input, observed, {
2153
- baseRevision: localHead,
2154
- expectedHead: currentRemoteHead,
2155
- publishedHead: currentRemoteHead,
2156
- conflictPaths,
2157
- error: "Inbound workspace update overlaps local visible changes; disjoint updates were hydrated and the old base was preserved for reconciliation."
2158
- });
2159
- }
2160
- return {
2161
- ...observed,
2162
- outcome: "updated",
2163
- candidateHead: candidateHead ?? void 0,
2164
- publishedHead: currentRemoteHead,
2165
- gitStatus: gitStatus(input)
2166
- };
2167
- }
2168
- const authoritativeHead = currentRemoteHead ?? localHead;
2169
- if (authoritativeHead) {
2170
- assertPublishedCanonicalCheckoutTree(input, authoritativeHead, sampledCanonicalCheckoutTreeHash);
2171
- if (!isCanonicalRemediationSync) {
2172
- mirrorShadowWorkspaceToVisible(input, mirroredGitlinkProjection.opaqueRoots, hydrationPreimages);
2173
- }
2174
- const hydrationConflicts2 = input.trigger.canonicalCheckoutOnly || isCanonicalRemediationSync ? [] : fastForwardedFromHead ? hydrationConflictPaths(input, hydrationPreimages, fastForwardedFromHead, authoritativeHead) : visibleHydrationConflictPaths(input, hydrationPreimages, authoritativeHead);
2175
- if (hydrationConflicts2.length > 0 && fastForwardedFromHead) {
2176
- runGit(
2177
- input,
2178
- input.shadowRoot,
2179
- ["reset", "--hard", fastForwardedFromHead],
2180
- "preserve old workspace base after new-checkout fast-forward conflict"
2181
- );
2182
- runGit(input, input.shadowRoot, ["clean", "-fd"], "clean preserved new-checkout workspace base");
2183
- return preserveWorkspaceConflictCandidate(input, observed, {
2184
- baseRevision: fastForwardedFromHead,
2185
- expectedHead: currentRemoteHead,
2186
- publishedHead: currentRemoteHead ?? void 0,
2187
- conflictPaths: hydrationConflicts2,
2188
- error: "Workspace fast-forward for a new checkout overlaps local visible changes; compatible updates were hydrated and the old base was preserved for reconciliation."
2189
- });
2190
- }
2191
- }
2192
- return {
2193
- ...observed,
2194
- outcome: "no_change",
2195
- candidateHead: candidateHead ?? void 0,
2196
- publishedHead: currentRemoteHead ?? localHead ?? void 0,
2197
- gitStatus: gitStatus(input)
2198
- };
2199
- }
2200
- let rebaseCount = 0;
2201
- const expectedHead = remoteHeadBeforeRebase;
2202
- if (mustDropVisibleBaselinePrefix && !expectedHead) {
2203
- return {
2204
- ...observed,
2205
- outcome: "failed",
2206
- candidateHead: candidateHead ?? void 0,
2207
- error: "Cannot normalize a visible workspace tail onto an unborn canonical workspace",
2208
- gitStatus: gitStatus(input)
2209
- };
2210
- }
2211
- if (candidateHead && expectedHead && (mustDropVisibleBaselinePrefix || !tryGit(input, input.shadowRoot, ["merge-base", "--is-ancestor", expectedHead, candidateHead]))) {
2212
- const rebase = runGitResult(
2213
- input,
2214
- input.shadowRoot,
2215
- mustDropVisibleBaselinePrefix && visibleBaselineHead ? ["rebase", "--onto", `origin/${WORKSPACE_BRANCH}`, visibleBaselineHead] : ["rebase", `origin/${WORKSPACE_BRANCH}`]
2216
- );
2217
- rebaseCount += 1;
2218
- if (rebase.exitCode !== 0) {
2219
- const conflicted = runGitResult(input, input.shadowRoot, ["diff", "--name-only", "--diff-filter=U", "-z"]);
2220
- const conflictPaths = [.../* @__PURE__ */ new Set([...paths, ...conflicted.stdout.split("\0").filter(Boolean)])].sort();
2221
- if (mustDropVisibleBaselinePrefix) {
2222
- const originalCandidateHead = revParse(input, "ORIG_HEAD") ?? candidateHead;
2223
- return preserveInProgressWorkspaceRebaseConflict(input, observed, {
2224
- candidateHead: originalCandidateHead,
2225
- expectedHead,
2226
- rebaseCount,
2227
- conflictPaths,
2228
- error: `Workspace tail conflicts with canonical head ${expectedHead}; local changes were preserved for reconciliation in the in-progress rebase.`
2229
- });
2230
- }
2231
- runGit(input, input.shadowRoot, ["rebase", "--abort"], "preserve workspace candidate after rebase conflict");
2232
- return preserveWorkspaceConflictCandidate(input, observed, {
2233
- baseRevision: candidateHead,
2234
- expectedHead,
2235
- rebaseCount,
2236
- preserveShadowCandidate: isCanonicalRemediationSync,
2237
- conflictPaths,
2238
- error: `Workspace candidate conflicts with canonical head ${expectedHead}; local changes were preserved for reconciliation.`
2239
- });
2240
- }
2241
- candidateHead = revParse(input, "HEAD");
2242
- assertPublishedCanonicalCheckoutTree(input, "HEAD", sampledCanonicalCheckoutTreeHash);
2243
- assertUnscopedCanonicalCheckoutSubtreesMatchRemote(input, "HEAD");
2244
- }
2245
- if (!candidateHead) {
2246
- return {
2247
- ...observed,
2248
- outcome: "failed",
2249
- rebaseCount,
2250
- error: "Workspace candidate does not have a commit",
2251
- gitStatus: gitStatus(input)
2252
- };
2253
- }
2254
- assertPublishedCanonicalCheckoutTree(input, candidateHead, sampledCanonicalCheckoutTreeHash);
2255
- if (!isCanonicalRemediationSync) {
2256
- mirrorShadowWorkspaceToVisible(input, mirroredGitlinkProjection.opaqueRoots, hydrationPreimages);
2257
- }
2258
- const hydrationConflicts = input.trigger.canonicalCheckoutOnly || isCanonicalRemediationSync ? [] : candidateHeadBeforeRebase ? hydrationConflictPaths(input, hydrationPreimages, candidateHeadBeforeRebase, "HEAD") : visibleHydrationConflictPaths(input, hydrationPreimages, "HEAD");
2259
- if (hydrationConflicts.length > 0 && candidateHeadBeforeRebase) {
2260
- runGit(
2261
- input,
2262
- input.shadowRoot,
2263
- ["reset", "--hard", candidateHeadBeforeRebase],
2264
- "preserve old-base workspace candidate after hydration conflict"
2265
- );
2266
- runGit(input, input.shadowRoot, ["clean", "-fd"], "clean old-base workspace candidate");
2267
- const conflictPaths = [.../* @__PURE__ */ new Set([...paths, ...hydrationConflicts])].sort();
2268
- return preserveWorkspaceConflictCandidate(input, observed, {
2269
- baseRevision: candidateHeadBeforeRebase,
2270
- expectedHead,
2271
- rebaseCount,
2272
- conflictPaths,
2273
- error: "Workspace changed while its candidate was reconciling; visible changes and the old-base candidate were preserved."
2274
- });
2275
- }
2276
- if (blockedDiffError) {
2277
- return preserveLargeDiffCandidate(input, { ...observed, rebaseCount }, blockedDiffError);
2278
- }
2279
- const upload = uploadWorkspaceCandidate(input, candidateHead);
2280
- if (upload.exitCode !== 0) {
2281
- return {
2282
- ...observed,
2283
- outcome: "failed",
2284
- candidateHead,
2285
- quarantineRef: input.quarantineRef,
2286
- rebaseCount,
2287
- error: upload.stderr || upload.stdout || "Workspace candidate upload failed",
2288
- gitStatus: gitStatus(input)
2289
- };
2290
- }
2291
- return {
2292
- ...observed,
2293
- outcome: "candidate_ready",
2294
- expectedHead,
2295
- candidateHead,
2296
- quarantineRef: input.quarantineRef,
2297
- rebaseCount,
2298
- gitStatus: gitStatus(input)
2299
- };
2300
- } catch (error) {
2301
- const result = baseResult(input, null);
2302
- return {
2303
- ...result,
2304
- outcome: "failed",
2305
- error: error instanceof Error ? error.message : String(error)
2306
- };
2307
- }
2308
- }
2309
- async function convergeWorkspaceHead(rawInput, options) {
2310
- const input = {
2311
- ...rawInput,
2312
- projects: workspaceProjectsForSync(rawInput.projects, rawInput.trigger)
2313
- };
2314
- if (input.trigger.canonicalCheckoutOnly) {
2315
- throw new Error("Ordinary inbound convergence cannot target a canonical resolver checkout");
2316
- }
2317
- ensureShadowWorkspace(input);
2318
- const fetchedLocalHead = revParse(input, "HEAD");
2319
- const fetchedCanonicalHead = revParse(input, `origin/${WORKSPACE_BRANCH}`);
2320
- if (!options.hydrateVisible) {
2321
- return {
2322
- localHead: fetchedLocalHead,
2323
- canonicalHead: fetchedCanonicalHead,
2324
- localChanges: false,
2325
- hydrated: false,
2326
- hydrationConflicts: []
2327
- };
2328
- }
2329
- resetUncommittedShadowSnapshot(input);
2330
- restoreUnscopedCanonicalCheckoutSubtrees(input);
2331
- const localHead = fetchedLocalHead;
2332
- const canonicalHead = fetchedCanonicalHead;
2333
- const projection = mirrorVisibleWorkspaceToShadow(input);
2334
- runGit(
2335
- input,
2336
- input.shadowRoot,
2337
- ["add", "-A", "--", ".", ...projection.opaqueRoots.map((root) => `:(exclude,literal)${root}`)],
2338
- "stage inbound workspace observation"
2339
- );
2340
- forceStageCanonicalCheckoutFiles(input, projection.entries, projection.opaqueRoots);
2341
- const stagedTree = runGit(input, input.shadowRoot, ["write-tree"], "write inbound workspace observation tree");
2342
- const headTree = localHead ? revParse(input, `${localHead}^{tree}`) : null;
2343
- const localChanges = stagedTree !== headTree;
2344
- if (!options.hydrateVisible || localChanges || !canonicalHead || localHead === canonicalHead) {
2345
- resetUncommittedShadowSnapshot(input);
2346
- return { localHead, canonicalHead, localChanges, hydrated: false, hydrationConflicts: [] };
2347
- }
2348
- const preimages = projection.visiblePreimages ?? hydrationPreimagesFromShadowRevision(input, localHead);
2349
- const hydrationConflicts = localHead ? hydrationConflictPaths(input, preimages, localHead, canonicalHead) : [];
2350
- if (hydrationConflicts.length > 0) {
2351
- resetUncommittedShadowSnapshot(input);
2352
- return { localHead, canonicalHead, localChanges: true, hydrated: false, hydrationConflicts };
2353
- }
2354
- runGit(input, input.shadowRoot, ["reset", "--hard", canonicalHead], "fast-forward workspace to available canonical head");
2355
- runGit(input, input.shadowRoot, ["clean", "-fd"], "clean fast-forwarded workspace");
2356
- mirrorShadowWorkspaceToVisible(input, projection.opaqueRoots, preimages);
2357
- return { localHead: canonicalHead, canonicalHead, localChanges: false, hydrated: true, hydrationConflicts: [] };
2358
- }
2359
- function calculateWorkspaceLocalDiffFingerprint(rawInput) {
2360
- const input = {
2361
- ...rawInput,
2362
- projects: workspaceProjectsForSync(rawInput.projects, rawInput.trigger)
2363
- };
2364
- if (!import_node_fs.default.existsSync(import_node_path.default.join(input.shadowRoot, ".git"))) {
2365
- throw new Error("Cannot inspect local workspace changes before first bootstrap");
2366
- }
2367
- resetUncommittedShadowSnapshot(input);
2368
- restoreUnscopedCanonicalCheckoutSubtrees(input);
2369
- const projection = mirrorVisibleWorkspaceToShadow(input);
2370
- runGit(
2371
- input,
2372
- input.shadowRoot,
2373
- ["add", "-A", "--", ".", ...projection.opaqueRoots.map((root) => `:(exclude,literal)${root}`)],
2374
- "stage local workspace fingerprint"
2375
- );
2376
- forceStageCanonicalCheckoutFiles(input, projection.entries, projection.opaqueRoots);
2377
- const stagedTree = runGit(input, input.shadowRoot, ["write-tree"], "write local workspace fingerprint tree");
2378
- const headTree = revParse(input, "HEAD^{tree}");
2379
- resetUncommittedShadowSnapshot(input);
2380
- return stagedTree === headTree ? (0, import_node_crypto.createHash)("sha256").update("").digest("hex") : (0, import_node_crypto.createHash)("sha256").update(stagedTree).digest("hex");
2381
- }
2382
- function calculateWorkspaceDiffFingerprint(rawInput) {
2383
- const input = {
2384
- ...rawInput,
2385
- projects: workspaceProjectsForSync(rawInput.projects, rawInput.trigger)
2386
- };
2387
- ensureShadowWorkspace(input);
2388
- let gitlinkProjection = { entries: [], opaqueRoots: [] };
2389
- if (!input.skipVisibleMirror) {
2390
- resetUncommittedShadowSnapshot(input);
2391
- restoreUnscopedCanonicalCheckoutSubtrees(input);
2392
- gitlinkProjection = mirrorVisibleWorkspaceToShadow(input);
2393
- } else {
2394
- restoreUnscopedCanonicalCheckoutSubtrees(input);
2395
- }
2396
- runGit(
2397
- input,
2398
- input.shadowRoot,
2399
- ["add", "-A", "--", ".", ...gitlinkProjection.opaqueRoots.map((root) => `:(exclude,literal)${root}`)],
2400
- "stage workspace fingerprint"
2401
- );
2402
- forceStageCanonicalCheckoutFiles(input, gitlinkProjection.entries, gitlinkProjection.opaqueRoots);
2403
- const stagedTree = runGit(input, input.shadowRoot, ["write-tree"], "write workspace fingerprint tree");
2404
- const headTree = revParse(input, "HEAD^{tree}");
2405
- const localHead = revParse(input, "HEAD");
2406
- const remoteHead = revParse(input, `origin/${WORKSPACE_BRANCH}`);
2407
- if (stagedTree === headTree && localHead === remoteHead) {
2408
- return (0, import_node_crypto.createHash)("sha256").update("").digest("hex");
2409
- }
2410
- return (0, import_node_crypto.createHash)("sha256").update(JSON.stringify({ stagedTree, localHead, remoteHead })).digest("hex");
2411
- }
2412
- class WorkspaceSyncSingleFlight {
2413
- mutationGate = new import_workspace_mutation_gate.WorkspaceMutationGate();
2414
- queue = Promise.resolve();
2415
- enqueue(task) {
2416
- const queued = this.mutationGate.runSync(task);
2417
- this.queue = queued.then(
2418
- () => void 0,
2419
- () => void 0
2420
- );
2421
- return queued;
2422
- }
2423
- run(input) {
2424
- return this.enqueue(() => synchronizeWorkspace(input));
2425
- }
2426
- runPrepared(prepare) {
2427
- return this.enqueue(() => synchronizeWorkspace(prepare()));
2428
- }
2429
- fingerprint(input) {
2430
- return this.enqueue(() => calculateWorkspaceDiffFingerprint(input));
2431
- }
2432
- fingerprintPrepared(prepare) {
2433
- return this.enqueue(() => calculateWorkspaceDiffFingerprint(prepare()));
2434
- }
2435
- runExclusive(operation) {
2436
- return this.enqueue(operation);
2437
- }
2438
- runMutation(operation) {
2439
- return this.mutationGate.runMutation(operation);
2440
- }
2441
- acquireMutation() {
2442
- return this.mutationGate.acquireMutation();
2443
- }
2444
- afterCurrent() {
2445
- return this.queue;
2446
- }
2447
- }
2448
- // Annotate the CommonJS export names for ESM import in node:
2449
- 0 && (module.exports = {
2450
- MAX_WORKSPACE_SYNC_DIFF_BYTES,
2451
- WORKSPACE_BRANCH,
2452
- WORKSPACE_INTENT_REF_PREFIX,
2453
- WORKSPACE_NATIVE_PLANS_RELATIVE_PATH,
2454
- WORKSPACE_PUBLICATION_REF_PREFIX,
2455
- WorkspaceSyncSingleFlight,
2456
- assertCanonicalCheckoutIndexMatchesWorktree,
2457
- assertWorkspaceQuarantineRef,
2458
- calculateWorkspaceDiffFingerprint,
2459
- calculateWorkspaceLocalDiffFingerprint,
2460
- convergeWorkspaceHead,
2461
- encodeWorkspaceBranch,
2462
- mirrorShadowWorkspaceToVisible,
2463
- mirrorVisibleWorkspaceToShadow,
2464
- synchronizeWorkspace,
2465
- visibleProjectBranchPath,
2466
- workspacePlansRelativePath,
2467
- workspaceProjectBranchRelativePath,
2468
- workspaceProjectsForSync
2469
- });