@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
@@ -0,0 +1,777 @@
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 project_workspace_state_exports = {};
30
+ __export(project_workspace_state_exports, {
31
+ PROJECT_WORKSPACE_STATE_FILE: () => PROJECT_WORKSPACE_STATE_FILE,
32
+ ProjectWorkspaceStateStore: () => ProjectWorkspaceStateStore,
33
+ discoverStaleOuterProjectWorkspaceEntries: () => discoverStaleOuterProjectWorkspaceEntries,
34
+ pruneAuthoritativelyDesiredBranchDeletions: () => pruneAuthoritativelyDesiredBranchDeletions
35
+ });
36
+ module.exports = __toCommonJS(project_workspace_state_exports);
37
+ var import_node_crypto = require("node:crypto");
38
+ var import_node_fs = __toESM(require("node:fs"), 1);
39
+ var import_node_path = __toESM(require("node:path"), 1);
40
+ var import_managed_paths = require("./managed-paths.cjs");
41
+ const PROJECT_WORKSPACE_STATE_FILE = "project-workspace-state.json";
42
+ const PROJECT_WORKSPACE_STATE_VERSION = 1;
43
+ const PROJECT_ID_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
44
+ const GIT_OBJECT_ID_PATTERN = /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/;
45
+ const EMPTY_STATE = {
46
+ version: PROJECT_WORKSPACE_STATE_VERSION,
47
+ desiredProjects: [],
48
+ locallyPendingCreatedBranches: [],
49
+ tombstones: []
50
+ };
51
+ function isRecord(value) {
52
+ return typeof value === "object" && value !== null && !Array.isArray(value);
53
+ }
54
+ function validateProjectId(projectId) {
55
+ if (typeof projectId !== "string" || projectId !== "local" && projectId !== "local-code" && !PROJECT_ID_PATTERN.test(projectId)) {
56
+ throw new Error(`Invalid project workspace state project id: ${String(projectId)}`);
57
+ }
58
+ return projectId;
59
+ }
60
+ function comparePathSegments(left, right) {
61
+ return left[0].localeCompare(right[0]) || left[1].localeCompare(right[1]);
62
+ }
63
+ function samePathSegments(left, right) {
64
+ return left[0] === right[0] && left[1] === right[1];
65
+ }
66
+ function pathsOverlap(left, right) {
67
+ const relative = import_node_path.default.relative(import_node_path.default.resolve(left), import_node_path.default.resolve(right));
68
+ const reverse = import_node_path.default.relative(import_node_path.default.resolve(right), import_node_path.default.resolve(left));
69
+ return relative === "" || relative !== ".." && !relative.startsWith(`..${import_node_path.default.sep}`) && !import_node_path.default.isAbsolute(relative) || reverse !== ".." && !reverse.startsWith(`..${import_node_path.default.sep}`) && !import_node_path.default.isAbsolute(reverse);
70
+ }
71
+ function normalizeBranchNames(branches, label) {
72
+ const branchNames = branches.map(({ branchName }) => {
73
+ (0, import_managed_paths.validateManagedBranchName)(branchName);
74
+ return branchName;
75
+ });
76
+ if (new Set(branchNames).size !== branchNames.length) throw new Error(`${label} contains duplicate branches`);
77
+ const sorted = [...branchNames].sort();
78
+ for (const [index, branchName] of sorted.entries()) {
79
+ const overlapping = sorted.slice(index + 1).find((candidate) => candidate.startsWith(`${branchName}/`));
80
+ if (overlapping) throw new Error(`${label} contains overlapping branch paths ${branchName} and ${overlapping}`);
81
+ }
82
+ return sorted;
83
+ }
84
+ function normalizeDesiredProjects(projects) {
85
+ const normalized = projects.map((project) => {
86
+ const projectId = validateProjectId(project.projectId);
87
+ const checkoutPathSegments = (0, import_managed_paths.validateCheckoutPathSegments)(project.checkoutPathSegments);
88
+ (0, import_managed_paths.validateManagedBranchName)(project.defaultBranch);
89
+ const branchNames = normalizeBranchNames(project.branches, `Project ${projectId}`);
90
+ return {
91
+ projectId,
92
+ checkoutPathSegments,
93
+ defaultBranch: project.defaultBranch,
94
+ branches: branchNames.map((branchName) => ({ branchName }))
95
+ };
96
+ });
97
+ if (new Set(normalized.map(({ projectId }) => projectId)).size !== normalized.length) {
98
+ throw new Error("Project workspace state contains duplicate project ids");
99
+ }
100
+ const checkoutPaths = /* @__PURE__ */ new Map();
101
+ for (const project of normalized) {
102
+ const checkoutPath = project.checkoutPathSegments.join("/");
103
+ const existing = checkoutPaths.get(checkoutPath);
104
+ if (existing) throw new Error(`Projects ${existing} and ${project.projectId} share managed checkout path ${checkoutPath}`);
105
+ checkoutPaths.set(checkoutPath, project.projectId);
106
+ }
107
+ return normalized.sort(
108
+ (left, right) => left.projectId.localeCompare(right.projectId) || comparePathSegments(left.checkoutPathSegments, right.checkoutPathSegments)
109
+ );
110
+ }
111
+ function normalizePendingCreatedBranches(value) {
112
+ if (!Array.isArray(value)) throw new Error("Invalid durable locally pending project branches");
113
+ const pending = value.map((entry) => {
114
+ if (!isRecord(entry) || typeof entry.branchName !== "string") {
115
+ throw new Error("Invalid durable locally pending project branch");
116
+ }
117
+ const projectId = validateProjectId(entry.projectId);
118
+ (0, import_managed_paths.validateManagedBranchName)(entry.branchName);
119
+ return { projectId, branchName: entry.branchName };
120
+ });
121
+ const keys = pending.map(({ projectId, branchName }) => `${projectId}\0${branchName}`);
122
+ if (new Set(keys).size !== keys.length) throw new Error("Duplicate durable locally pending project branch");
123
+ return pending.sort((left, right) => left.projectId.localeCompare(right.projectId) || left.branchName.localeCompare(right.branchName));
124
+ }
125
+ function tombstoneBranchNames(tombstone) {
126
+ return tombstone.kind === "branch" ? [tombstone.branchName] : tombstone.branchNames;
127
+ }
128
+ function deletionId(input) {
129
+ const digest = (0, import_node_crypto.createHash)("sha256").update(
130
+ JSON.stringify([input.kind, input.projectId, input.checkoutPathSegments[0], input.checkoutPathSegments[1], input.branchName ?? null])
131
+ ).digest("hex");
132
+ return `${input.kind}:${digest}`;
133
+ }
134
+ function normalizeDeletedMirrorRefs(value, branchNames, label) {
135
+ if (!Array.isArray(value) || value.some((branchName) => typeof branchName !== "string")) {
136
+ throw new Error(`${label} has invalid deleted mirror refs`);
137
+ }
138
+ const normalized = [...new Set(value)].sort();
139
+ if (normalized.some((branchName) => !branchNames.includes(branchName))) {
140
+ throw new Error(`${label} records a mirror ref outside its branch set`);
141
+ }
142
+ return normalized;
143
+ }
144
+ function normalizeTreePublishedHead(value, label) {
145
+ if (value === null) return null;
146
+ if (typeof value !== "string" || !GIT_OBJECT_ID_PATTERN.test(value)) {
147
+ throw new Error(`${label} has an invalid published workspace head`);
148
+ }
149
+ return value;
150
+ }
151
+ function normalizeTombstone(value) {
152
+ if (!isRecord(value)) throw new Error("Invalid project workspace deletion tombstone");
153
+ const projectId = validateProjectId(value.projectId);
154
+ const checkoutPathSegments = (0, import_managed_paths.validateCheckoutPathSegments)(value.checkoutPathSegments);
155
+ if (value.kind === "branch") {
156
+ if (typeof value.branchName !== "string") throw new Error("Invalid project workspace branch deletion tombstone");
157
+ (0, import_managed_paths.validateManagedBranchName)(value.branchName);
158
+ const expectedId2 = deletionId({ kind: "branch", projectId, checkoutPathSegments, branchName: value.branchName });
159
+ if (value.id !== expectedId2) throw new Error("Project workspace branch deletion tombstone id does not match its target");
160
+ return {
161
+ id: expectedId2,
162
+ kind: "branch",
163
+ projectId,
164
+ checkoutPathSegments,
165
+ branchName: value.branchName,
166
+ treePublishedHead: normalizeTreePublishedHead(value.treePublishedHead, `Deletion ${expectedId2}`),
167
+ mirrorRefsDeleted: normalizeDeletedMirrorRefs(value.mirrorRefsDeleted, [value.branchName], `Deletion ${expectedId2}`)
168
+ };
169
+ }
170
+ if (value.kind !== "project" || !Array.isArray(value.branchNames)) {
171
+ throw new Error("Invalid project workspace project deletion tombstone");
172
+ }
173
+ const branchNames = normalizeBranchNames(
174
+ value.branchNames.map((branchName) => ({ branchName: String(branchName) })),
175
+ `Deletion ${String(value.id)}`
176
+ );
177
+ const expectedId = deletionId({ kind: "project", projectId, checkoutPathSegments });
178
+ if (value.id !== expectedId) throw new Error("Project workspace project deletion tombstone id does not match its target");
179
+ return {
180
+ id: expectedId,
181
+ kind: "project",
182
+ projectId,
183
+ checkoutPathSegments,
184
+ branchNames,
185
+ treePublishedHead: normalizeTreePublishedHead(value.treePublishedHead, `Deletion ${expectedId}`),
186
+ mirrorRefsDeleted: normalizeDeletedMirrorRefs(value.mirrorRefsDeleted, branchNames, `Deletion ${expectedId}`)
187
+ };
188
+ }
189
+ function normalizeState(value) {
190
+ if (!isRecord(value) || value.version !== PROJECT_WORKSPACE_STATE_VERSION) {
191
+ throw new Error("Unsupported or invalid project workspace state file");
192
+ }
193
+ if (!Array.isArray(value.desiredProjects) || !Array.isArray(value.locallyPendingCreatedBranches) || !Array.isArray(value.tombstones)) {
194
+ throw new Error("Invalid project workspace state file");
195
+ }
196
+ const desiredProjects = normalizeDesiredProjects(value.desiredProjects);
197
+ const locallyPendingCreatedBranches = normalizePendingCreatedBranches(value.locallyPendingCreatedBranches);
198
+ const desiredById = new Map(desiredProjects.map((project) => [project.projectId, project]));
199
+ for (const pending of locallyPendingCreatedBranches) {
200
+ const project = desiredById.get(pending.projectId);
201
+ if (!project || !project.branches.some(({ branchName }) => branchName === pending.branchName)) {
202
+ throw new Error(`Durable pending branch ${pending.projectId}/${pending.branchName} is missing from desired project state`);
203
+ }
204
+ }
205
+ const tombstones = value.tombstones.map(normalizeTombstone);
206
+ if (new Set(tombstones.map(({ id }) => id)).size !== tombstones.length) {
207
+ throw new Error("Project workspace state contains duplicate deletion tombstones");
208
+ }
209
+ return {
210
+ version: PROJECT_WORKSPACE_STATE_VERSION,
211
+ desiredProjects,
212
+ locallyPendingCreatedBranches,
213
+ tombstones: tombstones.sort((left, right) => left.id.localeCompare(right.id))
214
+ };
215
+ }
216
+ function cloneDesiredProjects(projects) {
217
+ return projects.map((project) => ({
218
+ ...project,
219
+ checkoutPathSegments: [...project.checkoutPathSegments],
220
+ branches: project.branches.map((branch) => ({ ...branch }))
221
+ }));
222
+ }
223
+ function cloneTombstones(tombstones) {
224
+ return tombstones.map((tombstone) => ({
225
+ ...tombstone,
226
+ checkoutPathSegments: [...tombstone.checkoutPathSegments],
227
+ mirrorRefsDeleted: [...tombstone.mirrorRefsDeleted],
228
+ ...tombstone.kind === "project" ? { branchNames: [...tombstone.branchNames] } : {}
229
+ }));
230
+ }
231
+ function readState(statePath) {
232
+ try {
233
+ return normalizeState(JSON.parse(import_node_fs.default.readFileSync(statePath, "utf8")));
234
+ } catch (error) {
235
+ if (error.code === "ENOENT") return normalizeState(EMPTY_STATE);
236
+ throw new Error(
237
+ `Could not read durable project workspace state at ${statePath}: ${error instanceof Error ? error.message : String(error)}`,
238
+ { cause: error }
239
+ );
240
+ }
241
+ }
242
+ function fsyncDirectory(directory) {
243
+ let descriptor;
244
+ try {
245
+ descriptor = import_node_fs.default.openSync(directory, "r");
246
+ import_node_fs.default.fsyncSync(descriptor);
247
+ } finally {
248
+ if (descriptor !== void 0) import_node_fs.default.closeSync(descriptor);
249
+ }
250
+ }
251
+ function writeStateAtomically(stateRoot, statePath, state) {
252
+ import_node_fs.default.mkdirSync(stateRoot, { recursive: true, mode: 448 });
253
+ const temporaryPath = import_node_path.default.join(stateRoot, `.${PROJECT_WORKSPACE_STATE_FILE}.${process.pid}.${(0, import_node_crypto.randomUUID)()}.tmp`);
254
+ let descriptor;
255
+ try {
256
+ descriptor = import_node_fs.default.openSync(temporaryPath, "wx", 384);
257
+ import_node_fs.default.writeFileSync(descriptor, `${JSON.stringify(normalizeState(state), null, 2)}
258
+ `, "utf8");
259
+ import_node_fs.default.fsyncSync(descriptor);
260
+ import_node_fs.default.closeSync(descriptor);
261
+ descriptor = void 0;
262
+ import_node_fs.default.renameSync(temporaryPath, statePath);
263
+ fsyncDirectory(stateRoot);
264
+ } finally {
265
+ if (descriptor !== void 0) import_node_fs.default.closeSync(descriptor);
266
+ import_node_fs.default.rmSync(temporaryPath, { force: true });
267
+ }
268
+ }
269
+ function createBranchTombstone(project, branchName) {
270
+ return {
271
+ id: deletionId({ kind: "branch", projectId: project.projectId, checkoutPathSegments: project.checkoutPathSegments, branchName }),
272
+ kind: "branch",
273
+ projectId: project.projectId,
274
+ checkoutPathSegments: [...project.checkoutPathSegments],
275
+ branchName,
276
+ treePublishedHead: null,
277
+ mirrorRefsDeleted: []
278
+ };
279
+ }
280
+ function createProjectTombstone(project) {
281
+ return {
282
+ id: deletionId({ kind: "project", projectId: project.projectId, checkoutPathSegments: project.checkoutPathSegments }),
283
+ kind: "project",
284
+ projectId: project.projectId,
285
+ checkoutPathSegments: [...project.checkoutPathSegments],
286
+ branchNames: project.branches.map(({ branchName }) => branchName).sort(),
287
+ treePublishedHead: null,
288
+ mirrorRefsDeleted: []
289
+ };
290
+ }
291
+ function addBranchTombstone(tombstones, project, branchName) {
292
+ const projectTombstone = tombstones.find(
293
+ (tombstone) => tombstone.kind === "project" && tombstone.projectId === project.projectId && samePathSegments(tombstone.checkoutPathSegments, project.checkoutPathSegments)
294
+ );
295
+ if (projectTombstone) {
296
+ if (!projectTombstone.branchNames.includes(branchName)) {
297
+ projectTombstone.branchNames.push(branchName);
298
+ projectTombstone.branchNames.sort();
299
+ projectTombstone.treePublishedHead = null;
300
+ }
301
+ return;
302
+ }
303
+ const created = createBranchTombstone(project, branchName);
304
+ if (!tombstones.some(({ id }) => id === created.id)) tombstones.push(created);
305
+ }
306
+ function addProjectTombstone(tombstones, project) {
307
+ const created = createProjectTombstone(project);
308
+ const existing = tombstones.find(({ id }) => id === created.id);
309
+ if (existing) return;
310
+ const supersededBranches = tombstones.filter(
311
+ (tombstone) => tombstone.kind === "branch" && tombstone.projectId === project.projectId && samePathSegments(tombstone.checkoutPathSegments, project.checkoutPathSegments)
312
+ );
313
+ const alreadyDeleted = new Set(supersededBranches.flatMap(({ mirrorRefsDeleted }) => mirrorRefsDeleted));
314
+ created.mirrorRefsDeleted = created.branchNames.filter((branchName) => alreadyDeleted.has(branchName));
315
+ tombstones.splice(
316
+ 0,
317
+ tombstones.length,
318
+ ...tombstones.filter((tombstone) => !supersededBranches.some(({ id }) => id === tombstone.id)),
319
+ created
320
+ );
321
+ }
322
+ function applyPendingCreatedBranches(desiredProjects, pendingCreatedBranches) {
323
+ const seen = /* @__PURE__ */ new Set();
324
+ for (const pending of pendingCreatedBranches) {
325
+ const projectId = validateProjectId(pending.projectId);
326
+ (0, import_managed_paths.validateManagedBranchName)(pending.branchName);
327
+ const key = `${projectId}\0${pending.branchName}`;
328
+ if (seen.has(key)) continue;
329
+ seen.add(key);
330
+ const project = desiredProjects.find((candidate) => candidate.projectId === projectId);
331
+ if (!project) throw new Error(`Pending local branch ${projectId}/${pending.branchName} has no desired project`);
332
+ if (!project.branches.some(({ branchName }) => branchName === pending.branchName)) {
333
+ project.branches.push({ branchName: pending.branchName });
334
+ project.branches = normalizeBranchNames(project.branches, `Project ${projectId}`).map((branchName) => ({ branchName }));
335
+ }
336
+ }
337
+ }
338
+ function pendingCreatedBranchKey(input) {
339
+ return `${input.projectId}\0${input.branchName}`;
340
+ }
341
+ function pruneAuthoritativelyDesiredBranchDeletions(pendingDeletions, desiredProjects) {
342
+ const desiredKeys = new Set(
343
+ desiredProjects.flatMap(
344
+ (project) => project.branches.map(({ branchName }) => pendingCreatedBranchKey({ projectId: project.projectId, branchName }))
345
+ )
346
+ );
347
+ for (const [key, deletion] of pendingDeletions) {
348
+ if (key === pendingCreatedBranchKey(deletion) && desiredKeys.has(key)) pendingDeletions.delete(key);
349
+ }
350
+ }
351
+ function desiredProjectHasBranch(desiredProjects, pending) {
352
+ return Boolean(
353
+ desiredProjects.find(({ projectId }) => projectId === pending.projectId)?.branches.some(({ branchName }) => branchName === pending.branchName)
354
+ );
355
+ }
356
+ function mergePendingCreatedBranches(...groups) {
357
+ const byKey = /* @__PURE__ */ new Map();
358
+ for (const pending of groups.flat()) {
359
+ const normalized = normalizePendingCreatedBranches([pending])[0];
360
+ byKey.set(pendingCreatedBranchKey(normalized), normalized);
361
+ }
362
+ return [...byKey.values()].sort(
363
+ (left, right) => left.projectId.localeCompare(right.projectId) || left.branchName.localeCompare(right.branchName)
364
+ );
365
+ }
366
+ function primaryBranch(project) {
367
+ if (project.branches.some(({ branchName }) => branchName === "main")) return "main";
368
+ if (project.branches.some(({ branchName }) => branchName === project.defaultBranch)) return project.defaultBranch;
369
+ return project.branches[0]?.branchName ?? null;
370
+ }
371
+ function branchIsCurrentlyDesired(desiredProjects, projectId, branchName) {
372
+ return Boolean(
373
+ desiredProjects.find((project) => project.projectId === projectId)?.branches.some((branch) => branch.branchName === branchName)
374
+ );
375
+ }
376
+ function tombstoneTargetIsCurrentlyDesired(desiredProjects, tombstone) {
377
+ const project = desiredProjects.find(
378
+ (candidate) => candidate.projectId === tombstone.projectId && samePathSegments(candidate.checkoutPathSegments, tombstone.checkoutPathSegments)
379
+ );
380
+ if (!project) return false;
381
+ return tombstone.kind === "project" || project.branches.some(({ branchName }) => branchName === tombstone.branchName);
382
+ }
383
+ function tombstoneManagedPath(projectsRoot, tombstone) {
384
+ return tombstone.kind === "branch" ? (0, import_managed_paths.managedBranchPath)(projectsRoot, tombstone.checkoutPathSegments, tombstone.branchName) : (0, import_managed_paths.managedProjectRoot)(projectsRoot, tombstone.checkoutPathSegments);
385
+ }
386
+ function effectiveDesiredProjects(projectsRoot, desiredProjects, tombstones) {
387
+ const blockedPaths = tombstones.map((tombstone) => tombstoneManagedPath(projectsRoot, tombstone));
388
+ return desiredProjects.flatMap((project) => {
389
+ const projectRoot = (0, import_managed_paths.managedProjectRoot)(projectsRoot, project.checkoutPathSegments);
390
+ if (blockedPaths.some((blockedPath) => blockedPath === projectRoot)) return [];
391
+ return [
392
+ {
393
+ ...project,
394
+ checkoutPathSegments: [...project.checkoutPathSegments],
395
+ branches: project.branches.filter(({ branchName }) => {
396
+ const branchPath = (0, import_managed_paths.managedBranchPath)(projectsRoot, project.checkoutPathSegments, branchName);
397
+ return !blockedPaths.some((blockedPath) => pathsOverlap(blockedPath, branchPath));
398
+ })
399
+ }
400
+ ];
401
+ });
402
+ }
403
+ function stateView(projectsRoot, state) {
404
+ const effectiveProjects = effectiveDesiredProjects(projectsRoot, state.desiredProjects, state.tombstones);
405
+ const effectiveByPath = new Map(effectiveProjects.map((project) => [project.checkoutPathSegments.join("/"), project]));
406
+ const pendingTreeDeletions = state.tombstones.flatMap((tombstone) => {
407
+ if (tombstone.treePublishedHead) return [];
408
+ if (tombstone.kind === "project") {
409
+ return [
410
+ {
411
+ tombstoneId: tombstone.id,
412
+ kind: "project",
413
+ projectId: tombstone.projectId,
414
+ checkoutPathSegments: [...tombstone.checkoutPathSegments],
415
+ branchNames: [...tombstone.branchNames],
416
+ managedPath: (0, import_managed_paths.managedProjectRoot)(projectsRoot, tombstone.checkoutPathSegments),
417
+ projectDeleted: !state.desiredProjects.some(({ projectId }) => projectId === tombstone.projectId)
418
+ }
419
+ ];
420
+ }
421
+ return [
422
+ {
423
+ tombstoneId: tombstone.id,
424
+ kind: "branch",
425
+ projectId: tombstone.projectId,
426
+ checkoutPathSegments: [...tombstone.checkoutPathSegments],
427
+ branchName: tombstone.branchName,
428
+ managedPath: (0, import_managed_paths.managedBranchPath)(projectsRoot, tombstone.checkoutPathSegments, tombstone.branchName),
429
+ primaryBranchName: primaryBranch(
430
+ effectiveByPath.get(tombstone.checkoutPathSegments.join("/")) ?? {
431
+ projectId: tombstone.projectId,
432
+ checkoutPathSegments: tombstone.checkoutPathSegments,
433
+ defaultBranch: "main",
434
+ branches: []
435
+ }
436
+ )
437
+ }
438
+ ];
439
+ });
440
+ const pendingMirrorRefDeletions = state.tombstones.flatMap((tombstone) => {
441
+ if (!tombstone.treePublishedHead || tombstone.kind === "project" || branchIsCurrentlyDesired(state.desiredProjects, tombstone.projectId, tombstone.branchName)) {
442
+ return [];
443
+ }
444
+ return tombstoneBranchNames(tombstone).filter((branchName) => !tombstone.mirrorRefsDeleted.includes(branchName)).map((branchName) => ({
445
+ tombstoneId: tombstone.id,
446
+ projectId: tombstone.projectId,
447
+ branchName,
448
+ checkoutPathSegments: [...tombstone.checkoutPathSegments],
449
+ managedPath: (0, import_managed_paths.managedBranchPath)(projectsRoot, tombstone.checkoutPathSegments, branchName),
450
+ primaryBranchName: primaryBranch(
451
+ effectiveByPath.get(tombstone.checkoutPathSegments.join("/")) ?? {
452
+ projectId: tombstone.projectId,
453
+ checkoutPathSegments: tombstone.checkoutPathSegments,
454
+ defaultBranch: "main",
455
+ branches: []
456
+ }
457
+ ),
458
+ treePublishedHead: tombstone.treePublishedHead
459
+ }));
460
+ });
461
+ return {
462
+ desiredProjects: cloneDesiredProjects(state.desiredProjects),
463
+ effectiveDesiredProjects: effectiveProjects,
464
+ locallyPendingCreatedBranches: state.locallyPendingCreatedBranches.map((pending) => ({ ...pending })),
465
+ tombstones: cloneTombstones(state.tombstones),
466
+ pendingTreeDeletions,
467
+ pendingMirrorRefDeletions
468
+ };
469
+ }
470
+ function isRealDirectory(candidate) {
471
+ try {
472
+ return import_node_fs.default.lstatSync(candidate).isDirectory();
473
+ } catch {
474
+ return false;
475
+ }
476
+ }
477
+ function directoryEntries(directory) {
478
+ if (!isRealDirectory(directory)) return null;
479
+ try {
480
+ return import_node_fs.default.readdirSync(directory, { withFileTypes: true });
481
+ } catch {
482
+ return null;
483
+ }
484
+ }
485
+ function canonicalEncodedBranchName(entryName) {
486
+ try {
487
+ const branchName = decodeURIComponent(entryName);
488
+ (0, import_managed_paths.validateManagedBranchName)(branchName);
489
+ return encodeURIComponent(branchName) === entryName ? branchName : null;
490
+ } catch {
491
+ return null;
492
+ }
493
+ }
494
+ function validOuterProjectId(entryName) {
495
+ try {
496
+ return validateProjectId(entryName);
497
+ } catch {
498
+ return null;
499
+ }
500
+ }
501
+ function discoveredOuterProject(projects, projectId) {
502
+ const existing = projects.get(projectId);
503
+ if (existing) return existing;
504
+ const created = {
505
+ projectId,
506
+ projectTreePath: null,
507
+ projectTreeRootIsWhollyManaged: false,
508
+ planTreePath: null,
509
+ planTreeRootIsWhollyManaged: false,
510
+ branches: /* @__PURE__ */ new Map()
511
+ };
512
+ projects.set(projectId, created);
513
+ return created;
514
+ }
515
+ function discoveredOuterBranch(project, branchName) {
516
+ const existing = project.branches.get(branchName);
517
+ if (existing) return existing;
518
+ const created = { branchName, projectTreePath: null, planTreePath: null };
519
+ project.branches.set(branchName, created);
520
+ return created;
521
+ }
522
+ function discoverStaleOuterProjectWorkspaceEntries(input) {
523
+ const workspaceShadowRoot = import_node_path.default.resolve(input.workspaceShadowRoot);
524
+ const desiredProjects = normalizeDesiredProjects(input.desiredProjects);
525
+ applyPendingCreatedBranches(desiredProjects, input.locallyPendingCreatedBranches ?? []);
526
+ const desiredProjectIds = new Set(desiredProjects.map(({ projectId }) => projectId));
527
+ const desiredBranchKeys = new Set(
528
+ desiredProjects.flatMap((project) => project.branches.map(({ branchName }) => `${project.projectId}\0${branchName}`))
529
+ );
530
+ const discovered = /* @__PURE__ */ new Map();
531
+ const projectsContainer = import_node_path.default.join(workspaceShadowRoot, "projects");
532
+ for (const projectEntry of directoryEntries(projectsContainer) ?? []) {
533
+ const projectId = validOuterProjectId(projectEntry.name);
534
+ const projectTreePath = import_node_path.default.join(projectsContainer, projectEntry.name);
535
+ if (!projectId || !projectEntry.isDirectory() || !isRealDirectory(projectTreePath)) continue;
536
+ const project = discoveredOuterProject(discovered, projectId);
537
+ project.projectTreePath = projectTreePath;
538
+ const projectEntries = directoryEntries(projectTreePath);
539
+ const branchesPath = import_node_path.default.join(projectTreePath, "branches");
540
+ const branchEntries = directoryEntries(branchesPath);
541
+ project.projectTreeRootIsWhollyManaged = Boolean(
542
+ projectEntries && projectEntries.every((entry) => entry.name === "branches" && entry.isDirectory()) && branchEntries
543
+ );
544
+ if (!branchEntries) continue;
545
+ for (const branchEntry of branchEntries) {
546
+ const branchName = canonicalEncodedBranchName(branchEntry.name);
547
+ const branchTreePath = import_node_path.default.join(branchesPath, branchEntry.name);
548
+ if (!branchName || !branchEntry.isDirectory() || !isRealDirectory(branchTreePath)) {
549
+ project.projectTreeRootIsWhollyManaged = false;
550
+ continue;
551
+ }
552
+ discoveredOuterBranch(project, branchName).projectTreePath = branchTreePath;
553
+ }
554
+ }
555
+ const plansContainer = import_node_path.default.join(workspaceShadowRoot, "plans");
556
+ for (const projectEntry of directoryEntries(plansContainer) ?? []) {
557
+ const projectId = validOuterProjectId(projectEntry.name);
558
+ const planTreePath = import_node_path.default.join(plansContainer, projectEntry.name);
559
+ if (!projectId || !projectEntry.isDirectory() || !isRealDirectory(planTreePath)) continue;
560
+ const project = discoveredOuterProject(discovered, projectId);
561
+ project.planTreePath = planTreePath;
562
+ const planEntries = directoryEntries(planTreePath);
563
+ project.planTreeRootIsWhollyManaged = Boolean(planEntries);
564
+ if (!planEntries) continue;
565
+ const desiredBranchNames = desiredProjects.find((desiredProject) => desiredProject.projectId === projectId)?.branches.map(({ branchName }) => branchName) ?? [];
566
+ for (const planEntry of planEntries) {
567
+ const branchPlanPath = import_node_path.default.join(planTreePath, planEntry.name);
568
+ const containsDesiredLegacyNestedPlan = desiredBranchNames.some((desiredBranchName) => {
569
+ const segments = desiredBranchName.split("/");
570
+ return segments.length > 1 && segments[0] === planEntry.name && isRealDirectory(import_node_path.default.join(planTreePath, ...segments));
571
+ });
572
+ if (containsDesiredLegacyNestedPlan) continue;
573
+ const branchName = canonicalEncodedBranchName(planEntry.name);
574
+ if (!branchName || !planEntry.isDirectory() || !isRealDirectory(branchPlanPath)) {
575
+ project.planTreeRootIsWhollyManaged = false;
576
+ continue;
577
+ }
578
+ discoveredOuterBranch(project, branchName).planTreePath = branchPlanPath;
579
+ }
580
+ }
581
+ const stale = [];
582
+ for (const project of [...discovered.values()].sort((left, right) => left.projectId.localeCompare(right.projectId))) {
583
+ const branches = [...project.branches.values()].sort((left, right) => left.branchName.localeCompare(right.branchName));
584
+ if (!desiredProjectIds.has(project.projectId)) {
585
+ const projectTreePath = project.projectTreeRootIsWhollyManaged ? project.projectTreePath : null;
586
+ const planTreePath = project.planTreeRootIsWhollyManaged ? project.planTreePath : null;
587
+ const individuallySafeBranches = branches.map((branch) => ({
588
+ branchName: branch.branchName,
589
+ projectTreePath: projectTreePath ? null : branch.projectTreePath,
590
+ planTreePath: planTreePath ? null : branch.planTreePath
591
+ }));
592
+ if (projectTreePath || planTreePath || individuallySafeBranches.some((branch) => branch.projectTreePath || branch.planTreePath)) {
593
+ stale.push({
594
+ kind: "project",
595
+ projectId: project.projectId,
596
+ projectTreePath,
597
+ planTreePath,
598
+ branches: individuallySafeBranches
599
+ });
600
+ }
601
+ continue;
602
+ }
603
+ for (const branch of branches) {
604
+ if (desiredBranchKeys.has(`${project.projectId}\0${branch.branchName}`)) continue;
605
+ stale.push({
606
+ kind: "branch",
607
+ projectId: project.projectId,
608
+ branchName: branch.branchName,
609
+ projectTreePath: branch.projectTreePath,
610
+ planTreePath: branch.planTreePath
611
+ });
612
+ }
613
+ }
614
+ return stale;
615
+ }
616
+ class ProjectWorkspaceStateStore {
617
+ stateRoot;
618
+ projectsRoot;
619
+ statePath;
620
+ constructor(input) {
621
+ this.stateRoot = import_node_path.default.resolve(input.stateRoot);
622
+ this.projectsRoot = import_node_path.default.resolve(input.projectsRoot);
623
+ this.statePath = import_node_path.default.join(this.stateRoot, PROJECT_WORKSPACE_STATE_FILE);
624
+ }
625
+ read() {
626
+ return stateView(this.projectsRoot, readState(this.statePath));
627
+ }
628
+ discoverStaleOuterEntries(workspaceShadowRoot) {
629
+ const state = readState(this.statePath);
630
+ return discoverStaleOuterProjectWorkspaceEntries({
631
+ workspaceShadowRoot,
632
+ desiredProjects: state.desiredProjects,
633
+ locallyPendingCreatedBranches: state.locallyPendingCreatedBranches
634
+ });
635
+ }
636
+ reconcile(input) {
637
+ const previous = readState(this.statePath);
638
+ const authoritativeDesiredProjects = normalizeDesiredProjects(input.desiredProjects);
639
+ const authoritativeProjectIds = new Set(authoritativeDesiredProjects.map(({ projectId }) => projectId));
640
+ const locallyPendingCreatedBranches = mergePendingCreatedBranches(
641
+ previous.locallyPendingCreatedBranches,
642
+ input.locallyPendingCreatedBranches ?? []
643
+ ).filter(
644
+ (pending) => authoritativeProjectIds.has(pending.projectId) && !desiredProjectHasBranch(authoritativeDesiredProjects, pending)
645
+ );
646
+ const desiredProjects = cloneDesiredProjects(authoritativeDesiredProjects);
647
+ applyPendingCreatedBranches(desiredProjects, locallyPendingCreatedBranches);
648
+ const desiredById = new Map(desiredProjects.map((project) => [project.projectId, project]));
649
+ const tombstones = cloneTombstones(previous.tombstones);
650
+ for (const previousProject of previous.desiredProjects) {
651
+ const desiredProject = desiredById.get(previousProject.projectId);
652
+ if (!desiredProject || !samePathSegments(previousProject.checkoutPathSegments, desiredProject.checkoutPathSegments)) {
653
+ addProjectTombstone(tombstones, previousProject);
654
+ continue;
655
+ }
656
+ const desiredBranchNames = new Set(desiredProject.branches.map(({ branchName }) => branchName));
657
+ for (const { branchName } of previousProject.branches) {
658
+ if (!desiredBranchNames.has(branchName)) addBranchTombstone(tombstones, previousProject, branchName);
659
+ }
660
+ }
661
+ tombstones.splice(
662
+ 0,
663
+ tombstones.length,
664
+ ...tombstones.filter(
665
+ (tombstone) => !tombstoneTargetIsCurrentlyDesired(desiredProjects, tombstone) && (!tombstone.treePublishedHead || tombstone.kind === "branch" && !branchIsCurrentlyDesired(desiredProjects, tombstone.projectId, tombstone.branchName))
666
+ )
667
+ );
668
+ const nextState = normalizeState({
669
+ version: PROJECT_WORKSPACE_STATE_VERSION,
670
+ desiredProjects,
671
+ locallyPendingCreatedBranches,
672
+ tombstones
673
+ });
674
+ writeStateAtomically(this.stateRoot, this.statePath, nextState);
675
+ return stateView(this.projectsRoot, nextState);
676
+ }
677
+ /** Persist the create intent before invoking Git so a crash cannot orphan an untracked worktree. */
678
+ recordPendingCreatedBranch(input) {
679
+ const [pending] = normalizePendingCreatedBranches([input]);
680
+ const state = readState(this.statePath);
681
+ if (state.locallyPendingCreatedBranches.some((candidate) => pendingCreatedBranchKey(candidate) === pendingCreatedBranchKey(pending))) {
682
+ return stateView(this.projectsRoot, state);
683
+ }
684
+ const project = state.desiredProjects.find(({ projectId }) => projectId === pending.projectId);
685
+ if (!project) throw new Error(`Cannot record pending branch ${pending.projectId}/${pending.branchName} without a desired project`);
686
+ if (project.branches.some(({ branchName }) => branchName === pending.branchName)) {
687
+ throw new Error(`Cannot record pending branch ${pending.projectId}/${pending.branchName}: branch is already desired`);
688
+ }
689
+ project.branches.push({ branchName: pending.branchName });
690
+ project.branches = normalizeBranchNames(project.branches, `Project ${project.projectId}`).map((branchName) => ({ branchName }));
691
+ state.locallyPendingCreatedBranches.push(pending);
692
+ state.locallyPendingCreatedBranches.sort(
693
+ (left, right) => left.projectId.localeCompare(right.projectId) || left.branchName.localeCompare(right.branchName)
694
+ );
695
+ writeStateAtomically(this.stateRoot, this.statePath, state);
696
+ return stateView(this.projectsRoot, state);
697
+ }
698
+ /** Clear a create intent after an authoritative desired config contains it. */
699
+ clearPendingCreatedBranch(input) {
700
+ const [pending] = normalizePendingCreatedBranches([input]);
701
+ const state = readState(this.statePath);
702
+ const key = pendingCreatedBranchKey(pending);
703
+ if (!state.locallyPendingCreatedBranches.some((candidate) => pendingCreatedBranchKey(candidate) === key)) {
704
+ return stateView(this.projectsRoot, state);
705
+ }
706
+ state.locallyPendingCreatedBranches = state.locallyPendingCreatedBranches.filter(
707
+ (candidate) => pendingCreatedBranchKey(candidate) !== key
708
+ );
709
+ writeStateAtomically(this.stateRoot, this.statePath, state);
710
+ return stateView(this.projectsRoot, state);
711
+ }
712
+ /** Persist a branch tombstone before mutating its linked worktree or local ref. */
713
+ beginBranchDeletion(input) {
714
+ const [target] = normalizePendingCreatedBranches([input]);
715
+ const state = readState(this.statePath);
716
+ const project = state.desiredProjects.find(({ projectId }) => projectId === target.projectId);
717
+ if (!project)
718
+ throw new Error(`Cannot begin branch deletion for ${target.projectId}/${target.branchName}: desired project is missing`);
719
+ const existingTombstone = state.tombstones.find(
720
+ (tombstone) => tombstone.kind === "branch" && tombstone.projectId === target.projectId && samePathSegments(tombstone.checkoutPathSegments, project.checkoutPathSegments) && tombstone.branchName === target.branchName
721
+ );
722
+ if (existingTombstone) return stateView(this.projectsRoot, state);
723
+ addBranchTombstone(state.tombstones, project, target.branchName);
724
+ project.branches = project.branches.filter(({ branchName }) => branchName !== target.branchName);
725
+ const key = pendingCreatedBranchKey(target);
726
+ state.locallyPendingCreatedBranches = state.locallyPendingCreatedBranches.filter(
727
+ (candidate) => pendingCreatedBranchKey(candidate) !== key
728
+ );
729
+ writeStateAtomically(this.stateRoot, this.statePath, state);
730
+ return stateView(this.projectsRoot, state);
731
+ }
732
+ recordTreePublication(input) {
733
+ if (!GIT_OBJECT_ID_PATTERN.test(input.publishedHead)) throw new Error("Invalid published workspace head");
734
+ const state = readState(this.statePath);
735
+ const ids = new Set(input.tombstoneIds);
736
+ let changed = false;
737
+ for (const tombstone of state.tombstones) {
738
+ if (!ids.has(tombstone.id) || tombstone.treePublishedHead) continue;
739
+ tombstone.treePublishedHead = input.publishedHead;
740
+ changed = true;
741
+ }
742
+ state.tombstones = state.tombstones.filter(
743
+ (tombstone) => !tombstone.treePublishedHead || tombstone.kind === "branch" && !branchIsCurrentlyDesired(state.desiredProjects, tombstone.projectId, tombstone.branchName)
744
+ );
745
+ if (changed) writeStateAtomically(this.stateRoot, this.statePath, state);
746
+ return stateView(this.projectsRoot, state);
747
+ }
748
+ recordMirrorRefDeletion(input) {
749
+ (0, import_managed_paths.validateManagedBranchName)(input.branchName);
750
+ const state = readState(this.statePath);
751
+ const tombstone = state.tombstones.find(({ id }) => id === input.tombstoneId);
752
+ if (!tombstone) return stateView(this.projectsRoot, state);
753
+ const branchNames = tombstoneBranchNames(tombstone);
754
+ if (!branchNames.includes(input.branchName)) {
755
+ throw new Error(`Branch ${input.branchName} does not belong to deletion ${input.tombstoneId}`);
756
+ }
757
+ if (!tombstone.treePublishedHead) {
758
+ throw new Error(`Cannot record mirror deletion before workspace tree publication for ${input.tombstoneId}`);
759
+ }
760
+ if (!tombstone.mirrorRefsDeleted.includes(input.branchName)) {
761
+ tombstone.mirrorRefsDeleted.push(input.branchName);
762
+ tombstone.mirrorRefsDeleted.sort();
763
+ }
764
+ if (branchNames.every((branchName) => tombstone.mirrorRefsDeleted.includes(branchName))) {
765
+ state.tombstones = state.tombstones.filter(({ id }) => id !== tombstone.id);
766
+ }
767
+ writeStateAtomically(this.stateRoot, this.statePath, state);
768
+ return stateView(this.projectsRoot, state);
769
+ }
770
+ }
771
+ // Annotate the CommonJS export names for ESM import in node:
772
+ 0 && (module.exports = {
773
+ PROJECT_WORKSPACE_STATE_FILE,
774
+ ProjectWorkspaceStateStore,
775
+ discoverStaleOuterProjectWorkspaceEntries,
776
+ pruneAuthoritativelyDesiredBranchDeletions
777
+ });