@ricsam/r5d-worker 0.0.45 → 0.0.46

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.
@@ -53,6 +53,9 @@ const MAX_REPORTED_WORKSPACE_PATHS = 2e3;
53
53
  const MAX_REPORTED_WORKSPACE_STATUS_BYTES = 256 * 1024;
54
54
  const MAX_MIRROR_COMPARISON_CACHE_ENTRIES = 2e5;
55
55
  const MIRROR_COMPARE_BUFFER_BYTES = 64 * 1024;
56
+ const DESTRUCTIVE_CHECKOUT_MIN_TRACKED_FILES = 20;
57
+ const DESTRUCTIVE_CHECKOUT_REMOVAL_RATIO = 0.8;
58
+ const ACTIVE_GIT_LOCK_PATHS = ["index.lock", "HEAD.lock", "packed-refs.lock", "shallow.lock"];
56
59
  const mirrorComparisonCache = /* @__PURE__ */ new Map();
57
60
  function gitArgs(input, args) {
58
61
  return input.authHeader ? ["git", "-c", `http.extraHeader=${input.authHeader}`, ...args] : ["git", ...args];
@@ -128,6 +131,36 @@ function listGitEligibleFiles(checkoutPath) {
128
131
  }
129
132
  }).sort();
130
133
  }
134
+ function checkoutGitSnapshot(checkoutPath) {
135
+ const gitPaths = Bun.spawnSync(
136
+ ["git", "rev-parse", "--git-path", "index", ...ACTIVE_GIT_LOCK_PATHS.flatMap((lock) => ["--git-path", lock])],
137
+ {
138
+ cwd: checkoutPath,
139
+ stdout: "pipe",
140
+ stderr: "ignore",
141
+ env: { ...process.env, GIT_TERMINAL_PROMPT: "0" }
142
+ }
143
+ );
144
+ if (gitPaths.exitCode !== 0) return null;
145
+ const [indexPath, ...lockPaths] = gitPaths.stdout.toString().trim().split(/\r?\n/).map((gitPath) => import_node_path.default.resolve(checkoutPath, gitPath));
146
+ if (!indexPath || lockPaths.length !== ACTIVE_GIT_LOCK_PATHS.length || lockPaths.some((lockPath) => import_node_fs.default.existsSync(lockPath))) {
147
+ return null;
148
+ }
149
+ const head = Bun.spawnSync(["git", "rev-parse", "--verify", "HEAD"], {
150
+ cwd: checkoutPath,
151
+ stdout: "pipe",
152
+ stderr: "ignore",
153
+ env: { ...process.env, GIT_TERMINAL_PROMPT: "0" }
154
+ });
155
+ let indexSignature = "missing";
156
+ try {
157
+ const stat = import_node_fs.default.statSync(indexPath);
158
+ indexSignature = entrySignature(stat);
159
+ } catch {
160
+ }
161
+ if (lockPaths.some((lockPath) => import_node_fs.default.existsSync(lockPath))) return null;
162
+ return `${head.exitCode === 0 ? head.stdout.toString().trim() : "unborn"}\0${indexSignature}`;
163
+ }
131
164
  function listShadowTrackedFiles(input, relativeRoot) {
132
165
  const normalizedRoot = relativeRoot.split(import_node_path.default.sep).join("/").replace(/^\/+|\/+$/g, "");
133
166
  const output = runGit(input, input.shadowRoot, ["ls-files", "-z", "--cached", "--", normalizedRoot], "list canonical workspace files");
@@ -257,11 +290,28 @@ function mirrorFileSet(sourceRoot, targetRoot, sourceFiles, targetFiles) {
257
290
  }
258
291
  removeEmptyDirectories(targetRoot);
259
292
  }
293
+ function restoreShadowProjectSnapshot(input, relativeRoot) {
294
+ const shadowRoot = import_node_path.default.join(input.shadowRoot, ...relativeRoot.split("/"));
295
+ const headFiles = runGitResult(input, input.shadowRoot, ["ls-tree", "-r", "--name-only", "-z", "HEAD", "--", relativeRoot]);
296
+ if (headFiles.exitCode === 0 && headFiles.stdout) {
297
+ runGit(input, input.shadowRoot, ["restore", "--source=HEAD", "--worktree", "--", relativeRoot], "restore deferred checkout scan");
298
+ runGit(input, input.shadowRoot, ["clean", "-fd", "--", relativeRoot], "clean deferred checkout scan");
299
+ return;
300
+ }
301
+ import_node_fs.default.rmSync(shadowRoot, { recursive: true, force: true });
302
+ }
260
303
  function mirrorVisibleProjectToShadow(input, manifest, branchName) {
261
304
  const visibleRoot = visibleProjectBranchPath(input.projectsRoot, manifest, branchName);
262
305
  if (!import_node_fs.default.existsSync(import_node_path.default.join(visibleRoot, ".git"))) return;
263
- const shadowRoot = import_node_path.default.join(input.shadowRoot, ...workspaceProjectBranchRelativePath(manifest.projectId, branchName).split("/"));
264
- mirrorFileSet(visibleRoot, shadowRoot, listGitEligibleFiles(visibleRoot), listFilesRecursively(shadowRoot));
306
+ const beforeSnapshot = checkoutGitSnapshot(visibleRoot);
307
+ if (!beforeSnapshot) return;
308
+ const visibleFiles = listGitEligibleFiles(visibleRoot);
309
+ const relativeRoot = workspaceProjectBranchRelativePath(manifest.projectId, branchName);
310
+ const shadowRoot = import_node_path.default.join(input.shadowRoot, ...relativeRoot.split("/"));
311
+ mirrorFileSet(visibleRoot, shadowRoot, visibleFiles, listFilesRecursively(shadowRoot));
312
+ if (checkoutGitSnapshot(visibleRoot) !== beforeSnapshot) {
313
+ restoreShadowProjectSnapshot(input, relativeRoot);
314
+ }
265
315
  }
266
316
  function mirrorShadowProjectToVisible(input, manifest, branchName) {
267
317
  const visibleRoot = visibleProjectBranchPath(input.projectsRoot, manifest, branchName);
@@ -290,29 +340,7 @@ function mirrorShadowPlansToLocal(input, manifest, branchName) {
290
340
  listFilesRecursively(targetRoot, planFilter)
291
341
  );
292
342
  }
293
- function pruneDirectoryChildren(root, allowedNames) {
294
- if (!import_node_fs.default.existsSync(root)) return;
295
- for (const entry of import_node_fs.default.readdirSync(root)) {
296
- if (allowedNames.has(entry)) continue;
297
- const target = import_node_path.default.resolve(root, entry);
298
- assertInside(root, target, "Workspace manifest prune path");
299
- import_node_fs.default.rmSync(target, { recursive: true, force: true });
300
- }
301
- }
302
- function pruneShadowToManifest(input) {
303
- const projectIds = new Set(input.projects.map((project) => project.projectId));
304
- const projectsRoot = import_node_path.default.join(input.shadowRoot, "projects");
305
- const plansRoot = import_node_path.default.join(input.shadowRoot, "plans");
306
- pruneDirectoryChildren(projectsRoot, projectIds);
307
- pruneDirectoryChildren(plansRoot, projectIds);
308
- for (const manifest of input.projects) {
309
- const encodedBranches = new Set(manifest.branches.map(encodeWorkspaceBranch));
310
- pruneDirectoryChildren(import_node_path.default.join(projectsRoot, manifest.projectId, "branches"), encodedBranches);
311
- pruneDirectoryChildren(import_node_path.default.join(plansRoot, manifest.projectId), encodedBranches);
312
- }
313
- }
314
343
  function mirrorVisibleWorkspaceToShadow(input, excludedCheckouts = /* @__PURE__ */ new Set()) {
315
- pruneShadowToManifest(input);
316
344
  for (const manifest of [...input.projects].sort((left, right) => left.projectId.localeCompare(right.projectId))) {
317
345
  for (const branchName of [...new Set(manifest.branches)].sort()) {
318
346
  if (excludedCheckouts.has(`${manifest.projectId}\0${branchName}`)) continue;
@@ -326,13 +354,13 @@ function reconcileNewVisibleCheckouts(input) {
326
354
  const manifest = input.projects.find((project) => project.projectId === target.projectId);
327
355
  if (!manifest || !manifest.branches.includes(target.branchName)) continue;
328
356
  const projectRoot = workspaceProjectBranchRelativePath(target.projectId, target.branchName);
329
- if (listShadowTrackedFiles(input, projectRoot).length > 0) {
357
+ if (listShadowTrackedFiles(input, projectRoot).length > 0 || restoreShadowRootFromRemote(input, projectRoot)) {
330
358
  mirrorShadowProjectToVisible(input, manifest, target.branchName);
331
359
  } else {
332
360
  mirrorVisibleProjectToShadow(input, manifest, target.branchName);
333
361
  }
334
362
  const plansRoot = workspacePlansRelativePath(target.projectId, target.branchName);
335
- if (listShadowTrackedFiles(input, plansRoot).length > 0) {
363
+ if (listShadowTrackedFiles(input, plansRoot).length > 0 || restoreShadowRootFromRemote(input, plansRoot)) {
336
364
  mirrorShadowPlansToLocal(input, manifest, target.branchName);
337
365
  } else {
338
366
  mirrorLocalPlansToShadow(input, manifest, target.branchName);
@@ -379,11 +407,103 @@ function revParse(input, revision) {
379
407
  function gitStatus(input) {
380
408
  return runGit(input, input.shadowRoot, ["status", "--porcelain=v1", "--untracked-files=all"], "read workspace status");
381
409
  }
382
- function stagedPaths(input) {
383
- return runGit(input, input.shadowRoot, ["diff", "--cached", "--name-only", "-z"], "list workspace changes").split("\0").filter(Boolean).sort();
410
+ function resetUncommittedShadowSnapshot(input) {
411
+ if (!gitStatus(input)) return;
412
+ if (tryGit(input, input.shadowRoot, ["rev-parse", "--verify", "HEAD"])) {
413
+ runGit(input, input.shadowRoot, ["reset", "--hard", "HEAD"], "reset interrupted workspace snapshot");
414
+ } else {
415
+ runGit(input, input.shadowRoot, ["read-tree", "--empty"], "reset interrupted unborn workspace snapshot");
416
+ }
417
+ runGit(input, input.shadowRoot, ["clean", "-fd"], "clean interrupted workspace snapshot");
418
+ }
419
+ function fastForwardCleanShadowForNewCheckouts(input) {
420
+ if (!input.newVisibleCheckouts?.length) return;
421
+ runGit(input, input.shadowRoot, ["add", "-A"], "stage existing workspace changes before checkout hydration");
422
+ if (gitStatus(input)) return;
423
+ const localHead = revParse(input, "HEAD");
424
+ const remoteHead = revParse(input, `origin/${WORKSPACE_BRANCH}`);
425
+ if (!localHead || !remoteHead || localHead === remoteHead) return;
426
+ if (!tryGit(input, input.shadowRoot, ["merge-base", "--is-ancestor", localHead, remoteHead])) return;
427
+ runGit(input, input.shadowRoot, ["reset", "--hard", remoteHead], "fast-forward before checkout hydration");
428
+ }
429
+ function restoreShadowRootFromRemote(input, relativeRoot) {
430
+ const remoteRevision = `origin/${WORKSPACE_BRANCH}`;
431
+ const remoteFiles = runGitResult(input, input.shadowRoot, ["ls-tree", "-r", "--name-only", "-z", remoteRevision, "--", relativeRoot]);
432
+ if (remoteFiles.exitCode !== 0 || !remoteFiles.stdout) return false;
433
+ runGit(
434
+ input,
435
+ input.shadowRoot,
436
+ ["restore", `--source=${remoteRevision}`, "--staged", "--worktree", "--", relativeRoot],
437
+ `hydrate ${relativeRoot} from canonical workspace`
438
+ );
439
+ return true;
384
440
  }
385
- async function stagedDiffSizeBytes(input) {
386
- const subprocess = Bun.spawn(gitArgs(input, ["diff", "--cached", "--binary", "--no-ext-diff"]), {
441
+ function candidateBaseRevision(input) {
442
+ const localHead = revParse(input, "HEAD");
443
+ if (!localHead) return null;
444
+ const remoteHead = revParse(input, `origin/${WORKSPACE_BRANCH}`);
445
+ if (!remoteHead) {
446
+ const emptyTree = Bun.spawnSync(gitArgs(input, ["mktree"]), {
447
+ cwd: input.shadowRoot,
448
+ stdin: Buffer.alloc(0),
449
+ stdout: "pipe",
450
+ stderr: "pipe",
451
+ env: { ...process.env, GIT_TERMINAL_PROMPT: "0" }
452
+ });
453
+ if (emptyTree.exitCode !== 0) {
454
+ throw new Error(
455
+ `create empty workspace comparison tree: ${emptyTree.stderr.toString().trim() || `git exited ${emptyTree.exitCode}`}`
456
+ );
457
+ }
458
+ return emptyTree.stdout.toString().trim();
459
+ }
460
+ const mergeBase = runGitResult(input, input.shadowRoot, ["merge-base", localHead, remoteHead]);
461
+ return mergeBase.exitCode === 0 && mergeBase.stdout ? mergeBase.stdout : remoteHead;
462
+ }
463
+ function stagedPaths(input, baseRevision) {
464
+ return runGit(
465
+ input,
466
+ input.shadowRoot,
467
+ ["diff", "--cached", "--name-only", "-z", ...baseRevision ? [baseRevision] : []],
468
+ "list workspace changes"
469
+ ).split("\0").filter(Boolean).sort();
470
+ }
471
+ function stagedDeletedPaths(input, baseRevision) {
472
+ return runGit(
473
+ input,
474
+ input.shadowRoot,
475
+ ["diff", "--cached", "--diff-filter=D", "--name-only", "-z", ...baseRevision ? [baseRevision] : []],
476
+ "list workspace deletions"
477
+ ).split("\0").filter(Boolean).sort();
478
+ }
479
+ function managedCheckoutRoot(filePath) {
480
+ const projectMatch = /^projects\/([^/]+)\/branches\/([^/]+)\//.exec(filePath);
481
+ return projectMatch ? `projects/${projectMatch[1]}/branches/${projectMatch[2]}` : null;
482
+ }
483
+ function revisionTrackedFileCount(input, revision, relativeRoot) {
484
+ if (!revision) return 0;
485
+ const result = runGitResult(input, input.shadowRoot, ["ls-tree", "-r", "--name-only", "-z", revision, "--", relativeRoot]);
486
+ if (result.exitCode !== 0) return 0;
487
+ return result.stdout.split("\0").filter(Boolean).length;
488
+ }
489
+ function destructiveCheckoutReductions(input, baseRevision) {
490
+ const roots = new Set(
491
+ stagedDeletedPaths(input, baseRevision).map(managedCheckoutRoot).filter((root) => Boolean(root))
492
+ );
493
+ const destructive = [];
494
+ for (const root of roots) {
495
+ const trackedBefore = revisionTrackedFileCount(input, baseRevision, root);
496
+ if (trackedBefore === 0) continue;
497
+ const trackedAfter = listShadowTrackedFiles(input, root).length;
498
+ const removedRatio = (trackedBefore - trackedAfter) / trackedBefore;
499
+ if (trackedAfter === 0 || trackedBefore >= DESTRUCTIVE_CHECKOUT_MIN_TRACKED_FILES && removedRatio >= DESTRUCTIVE_CHECKOUT_REMOVAL_RATIO) {
500
+ destructive.push({ root, trackedBefore, trackedAfter });
501
+ }
502
+ }
503
+ return destructive.sort((left, right) => left.root.localeCompare(right.root));
504
+ }
505
+ async function stagedDiffSizeBytes(input, baseRevision) {
506
+ const subprocess = Bun.spawn(gitArgs(input, ["diff", "--cached", "--binary", "--no-ext-diff", ...baseRevision ? [baseRevision] : []]), {
387
507
  cwd: input.shadowRoot,
388
508
  stdout: "pipe",
389
509
  stderr: "pipe",
@@ -486,13 +606,17 @@ async function synchronizeWorkspace(rawInput) {
486
606
  };
487
607
  }
488
608
  if (!input.skipVisibleMirror) {
609
+ resetUncommittedShadowSnapshot(input);
489
610
  const newCheckoutKeys = new Set((input.newVisibleCheckouts ?? []).map((target) => `${target.projectId}\0${target.branchName}`));
490
611
  mirrorVisibleWorkspaceToShadow(input, newCheckoutKeys);
612
+ fastForwardCleanShadowForNewCheckouts(input);
491
613
  reconcileNewVisibleCheckouts(input);
492
614
  }
493
615
  runGit(input, input.shadowRoot, ["add", "-A"], "stage workspace changes");
494
- const paths = stagedPaths(input);
495
- const diffSizeBytes = await stagedDiffSizeBytes(input);
616
+ const stagedWorkingPaths = stagedPaths(input);
617
+ const baseRevision = candidateBaseRevision(input);
618
+ const paths = stagedPaths(input, baseRevision);
619
+ const diffSizeBytes = await stagedDiffSizeBytes(input, baseRevision);
496
620
  const statusBeforeCommit = reportedStatus(gitStatus(input));
497
621
  const observed = {
498
622
  ...result,
@@ -501,10 +625,23 @@ async function synchronizeWorkspace(rawInput) {
501
625
  affectedPaths: reportedPaths(paths),
502
626
  affectedProjects: affectedProjects(paths)
503
627
  };
628
+ const destructiveReductions = destructiveCheckoutReductions(input, baseRevision);
629
+ if (destructiveReductions.length > 0 && !input.confirmedLargeDiff) {
630
+ const summary = destructiveReductions.map(({ root, trackedBefore, trackedAfter }) => `${root} (${trackedBefore} -> ${trackedAfter} tracked files)`).join(", ");
631
+ return {
632
+ ...observed,
633
+ outcome: "large_diff_blocked",
634
+ error: `Workspace safety guard blocked a destructive checkout reduction: ${summary}`
635
+ };
636
+ }
504
637
  if (paths.length > 0 && diffSizeBytes > MAX_WORKSPACE_SYNC_DIFF_BYTES && !input.confirmedLargeDiff) {
505
- return { ...observed, outcome: "large_diff_blocked" };
638
+ return {
639
+ ...observed,
640
+ outcome: "large_diff_blocked",
641
+ error: `Workspace safety guard blocked a ${diffSizeBytes}-byte diff above the ${MAX_WORKSPACE_SYNC_DIFF_BYTES}-byte automatic publication limit.`
642
+ };
506
643
  }
507
- if (paths.length > 0) {
644
+ if (stagedWorkingPaths.length > 0) {
508
645
  runGit(input, input.shadowRoot, ["commit", "-m", commitMessage(input)], "commit workspace changes");
509
646
  }
510
647
  const candidateHead = revParse(input, "HEAD");
@@ -597,7 +734,10 @@ async function synchronizeWorkspace(rawInput) {
597
734
  }
598
735
  function calculateWorkspaceDiffFingerprint(input) {
599
736
  ensureShadowWorkspace(input);
600
- if (!input.skipVisibleMirror) mirrorVisibleWorkspaceToShadow(input);
737
+ if (!input.skipVisibleMirror) {
738
+ resetUncommittedShadowSnapshot(input);
739
+ mirrorVisibleWorkspaceToShadow(input);
740
+ }
601
741
  runGit(input, input.shadowRoot, ["add", "-A"], "stage workspace fingerprint");
602
742
  const stagedTree = runGit(input, input.shadowRoot, ["write-tree"], "write workspace fingerprint tree");
603
743
  const headTree = revParse(input, "HEAD^{tree}");
@@ -610,27 +750,25 @@ function calculateWorkspaceDiffFingerprint(input) {
610
750
  }
611
751
  class WorkspaceSyncSingleFlight {
612
752
  queue = Promise.resolve();
613
- run(input) {
614
- const queued = this.queue.then(
615
- () => synchronizeWorkspace(input),
616
- () => synchronizeWorkspace(input)
617
- );
753
+ enqueue(task) {
754
+ const queued = this.queue.then(task, task);
618
755
  this.queue = queued.then(
619
756
  () => void 0,
620
757
  () => void 0
621
758
  );
622
759
  return queued;
623
760
  }
761
+ run(input) {
762
+ return this.enqueue(() => synchronizeWorkspace(input));
763
+ }
764
+ runPrepared(prepare) {
765
+ return this.enqueue(() => synchronizeWorkspace(prepare()));
766
+ }
624
767
  fingerprint(input) {
625
- const queued = this.queue.then(
626
- () => calculateWorkspaceDiffFingerprint(input),
627
- () => calculateWorkspaceDiffFingerprint(input)
628
- );
629
- this.queue = queued.then(
630
- () => void 0,
631
- () => void 0
632
- );
633
- return queued;
768
+ return this.enqueue(() => calculateWorkspaceDiffFingerprint(input));
769
+ }
770
+ fingerprintPrepared(prepare) {
771
+ return this.enqueue(() => calculateWorkspaceDiffFingerprint(prepare()));
634
772
  }
635
773
  afterCurrent() {
636
774
  return this.queue;