@ricsam/r5d-worker 0.0.95 → 0.0.97

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cjs/main.cjs CHANGED
@@ -3824,6 +3824,7 @@ async function startWorker(options) {
3824
3824
  const branchKey = projectBranchKey(project.projectId, branch.branchName);
3825
3825
  const creatorLocalIncarnation = creatorLocalIncarnations.get(branchKey) === branch.branchId;
3826
3826
  const preservesCheckoutPathMove = checkoutPathMoveBranches.has(branchKey);
3827
+ const initialPublicationSourceBranch = creatorLocalIncarnation ? branch.sourceBranchName : null;
3827
3828
  mounts.push({
3828
3829
  id: projectMountId(project.projectId, branch.branchName),
3829
3830
  hydrationIncarnationKey: branch.branchId,
@@ -3833,6 +3834,7 @@ async function startWorker(options) {
3833
3834
  sourceMode: "git",
3834
3835
  hydrateDeletionMode: "git",
3835
3836
  preserveLocalOnInitialOuterAbsence: creatorLocalIncarnation,
3837
+ ...initialPublicationSourceBranch ? { initialPublicationSourceRelativePath: workspaceProjectRelativePath(project.projectId, initialPublicationSourceBranch) } : {},
3836
3838
  preserveLocalOnHydrationBasisChange: preservesCheckoutPathMove,
3837
3839
  busy: () => projectBranchMountBusy(project.projectId, branch.branchName, branchPath),
3838
3840
  mutationToken: () => projectWorkspaceMutationToken(project.projectId, branch.branchName),
@@ -3847,6 +3849,7 @@ async function startWorker(options) {
3847
3849
  sourceMode: "all",
3848
3850
  hydrateDeletionMode: "all",
3849
3851
  preserveLocalOnInitialOuterAbsence: creatorLocalIncarnation,
3852
+ ...initialPublicationSourceBranch ? { initialPublicationSourceRelativePath: workspacePlanRelativePath(project.projectId, initialPublicationSourceBranch) } : {},
3850
3853
  preserveLocalOnHydrationBasisChange: preservesCheckoutPathMove,
3851
3854
  busy: () => projectBranchMountBusy(project.projectId, branch.branchName, branchPath),
3852
3855
  mutationToken: () => projectWorkspaceMutationToken(project.projectId, branch.branchName),
@@ -4446,6 +4449,12 @@ async function startWorker(options) {
4446
4449
  }
4447
4450
  });
4448
4451
  input.assertStillAdmitted?.();
4452
+ if (result.initialPublicationMountIds && result.initialPublicationMountIds.length > 0) {
4453
+ process.stderr.write(
4454
+ `[r5d-worker] published first checkout copies above the automatic diff-size limit, measured against their source subtrees at ${result.diffSizeBytes} bytes: ${result.initialPublicationMountIds.join(", ")}
4455
+ `
4456
+ );
4457
+ }
4449
4458
  if (["no_change", "updated", "pushed"].includes(result.outcome) && !outerRemediation) {
4450
4459
  await applyObservedProjectHeadsAfterInboundWorkspace(
4451
4460
  observedProjectHeads,
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.95",
3
+ "version": "0.0.97",
4
4
  "type": "commonjs"
5
5
  }
@@ -1483,13 +1483,38 @@ function hydrateWorkspaceGitMountsRaw(workspacePath, mounts, options = {}) {
1483
1483
  }
1484
1484
  return { hydratedMountIds: hydratedMountIds.sort(), skippedMountIds: skippedMountIds.sort() };
1485
1485
  }
1486
+ async function selectMountsWithUnchangedOuterSubtree(workspacePath, receipt, head, mounts) {
1487
+ const candidates = mounts.flatMap((mount) => {
1488
+ const basisHead = workspaceHydrationReceiptMountBasisHead(receipt, mount);
1489
+ return basisHead && basisHead !== head ? [{ mount, basisHead }] : [];
1490
+ });
1491
+ if (candidates.length === 0) return /* @__PURE__ */ new Set();
1492
+ const objects = await resolveGitObjectIdsAsync(
1493
+ workspacePath,
1494
+ candidates.flatMap(({ mount, basisHead }) => [
1495
+ workspaceTreeObjectName(basisHead, mount.workspaceRelativePath),
1496
+ workspaceTreeObjectName(head, mount.workspaceRelativePath)
1497
+ ])
1498
+ );
1499
+ const unchanged = /* @__PURE__ */ new Set();
1500
+ for (const { mount, basisHead } of candidates) {
1501
+ const before = objects.get(workspaceTreeObjectName(basisHead, mount.workspaceRelativePath)) ?? null;
1502
+ const after = objects.get(workspaceTreeObjectName(head, mount.workspaceRelativePath)) ?? null;
1503
+ if (before === after) unchanged.add(mount.id);
1504
+ }
1505
+ return unchanged;
1506
+ }
1486
1507
  async function hydrateWorkspaceGitMountsTransactionally(input) {
1487
1508
  const workspacePath = import_node_path.default.resolve(input.workspacePath);
1488
1509
  const durabilityMounts = input.durabilityMounts ?? input.mounts;
1489
1510
  const receiptMounts = input.receiptMounts ?? durabilityMounts;
1490
1511
  const requiredMounts = input.requiredMounts ?? input.mounts;
1512
+ const carryMounts = input.carryMounts ?? [];
1513
+ if (carryMounts.length > 0 && !input.recordCurrentHead) {
1514
+ throw new Error("Workspace hydration can only carry unchanged mounts while recording the current head");
1515
+ }
1491
1516
  const candidateMounts = [
1492
- ...new Map([...input.mounts, ...durabilityMounts, ...requiredMounts].map((mount) => [mount.id, mount])).values()
1517
+ ...new Map([...input.mounts, ...durabilityMounts, ...requiredMounts, ...carryMounts].map((mount) => [mount.id, mount])).values()
1493
1518
  ];
1494
1519
  if (input.recordCurrentHead && !await workspaceCheckoutDurabilityIsCurrentAsync(workspacePath)) {
1495
1520
  throw new Error("Workspace checkout is not durably materialized at its current HEAD");
@@ -1505,12 +1530,13 @@ async function hydrateWorkspaceGitMountsTransactionally(input) {
1505
1530
  ({ id }) => !busyMountIds.has(id) && (requestedIds.has(id) || !requiredIds.has(id))
1506
1531
  );
1507
1532
  const unavailableRequiredIds = requiredMounts.filter(({ id }) => busyMountIds.has(id) || !requestedIds.has(id)).map(({ id }) => id);
1533
+ const carriedMounts = carryMounts.filter(({ id }) => !busyMountIds.has(id));
1508
1534
  return {
1509
1535
  busyMountIds,
1510
1536
  hydrationMounts: hydrationMounts2,
1511
1537
  advancedDurabilityMounts: advancedDurabilityMounts2,
1512
1538
  skippedMountIds: [.../* @__PURE__ */ new Set([...busyMountIds, ...unavailableRequiredIds])].sort(),
1513
- targetReceipt: targetHead ? receiptWithMountsAtHead(receiptBeforeTransaction, targetHead, advancedDurabilityMounts2, receiptMounts) : null
1539
+ targetReceipt: targetHead ? receiptWithMountsAtHead(receiptBeforeTransaction, targetHead, [...advancedDurabilityMounts2, ...carriedMounts], receiptMounts) : null
1514
1540
  };
1515
1541
  };
1516
1542
  const noopCandidate = computeSelection();
@@ -1686,14 +1712,26 @@ async function changedPaths(workspacePath, baseRevision, headRevision) {
1686
1712
  const base = baseRevision ?? emptyTreeHash(workspacePath);
1687
1713
  return (await gitAsync(workspacePath, ["diff", "--name-only", "--no-renames", "-z", base, headRevision], "inspect workspace commit paths")).split("\0").filter(Boolean).sort();
1688
1714
  }
1689
- async function diffSizeBytes(workspacePath, baseRevision, headRevision, limit) {
1715
+ async function diffSizeBytes(workspacePath, baseRevision, headRevision, limit, pathspecs = []) {
1690
1716
  const base = baseRevision ?? emptyTreeHash(workspacePath);
1691
- const subprocess = Bun.spawn(["git", ...NON_RECURSIVE_GIT_CONFIG, "diff", "--binary", "--no-ext-diff", base, headRevision], {
1692
- cwd: workspacePath,
1693
- stdout: "pipe",
1694
- stderr: "pipe",
1695
- env: (0, import_git_process_environment.workerGitProcessEnvironment)()
1696
- });
1717
+ const subprocess = Bun.spawn(
1718
+ [
1719
+ "git",
1720
+ ...NON_RECURSIVE_GIT_CONFIG,
1721
+ "diff",
1722
+ "--binary",
1723
+ "--no-ext-diff",
1724
+ base,
1725
+ headRevision,
1726
+ ...pathspecs.length > 0 ? ["--", ...pathspecs] : []
1727
+ ],
1728
+ {
1729
+ cwd: workspacePath,
1730
+ stdout: "pipe",
1731
+ stderr: "pipe",
1732
+ env: (0, import_git_process_environment.workerGitProcessEnvironment)()
1733
+ }
1734
+ );
1697
1735
  const stderrPromise = new Response(subprocess.stderr).text();
1698
1736
  const reader = subprocess.stdout.getReader();
1699
1737
  let total = 0;
@@ -1713,6 +1751,73 @@ async function diffSizeBytes(workspacePath, baseRevision, headRevision, limit) {
1713
1751
  if (exitCode !== 0) throw new Error(`Measure workspace diff: ${stderr || `git exited ${exitCode}`}`);
1714
1752
  return total;
1715
1753
  }
1754
+ async function resolveGitObjectIdsAsync(workspacePath, names) {
1755
+ const resolved = /* @__PURE__ */ new Map();
1756
+ const unique = [...new Set(names)];
1757
+ if (unique.length === 0) return resolved;
1758
+ const subprocess = Bun.spawn(gitCommandArgs(["cat-file", "--batch-check"]), {
1759
+ cwd: workspacePath,
1760
+ stdin: Buffer.from(`${unique.join("\n")}
1761
+ `),
1762
+ stdout: "pipe",
1763
+ stderr: "pipe",
1764
+ env: (0, import_git_process_environment.workerGitProcessEnvironment)()
1765
+ });
1766
+ const [stdout, stderr, exitCode] = await Promise.all([
1767
+ new Response(subprocess.stdout).text(),
1768
+ new Response(subprocess.stderr).text(),
1769
+ subprocess.exited
1770
+ ]);
1771
+ if (exitCode !== 0) throw new Error(`Resolve workspace objects: ${stderr.trim() || `git exited ${exitCode}`}`);
1772
+ const lines = stdout.split("\n").filter((line) => line.length > 0);
1773
+ if (lines.length !== unique.length) {
1774
+ throw new Error(`Resolve workspace objects: expected ${unique.length} results, received ${lines.length}`);
1775
+ }
1776
+ unique.forEach((name, index) => {
1777
+ const match = /^([0-9a-f]{40,64}) \S+ \d+$/.exec(lines[index]);
1778
+ resolved.set(name, match ? match[1] : null);
1779
+ });
1780
+ return resolved;
1781
+ }
1782
+ function workspaceTreeObjectName(revision, workspaceRelativePath) {
1783
+ return `${revision}:${normalizedWorkspaceMountPath(workspaceRelativePath)}`;
1784
+ }
1785
+ async function measureInitialPublicationDiffSize(input) {
1786
+ const candidates = input.mounts.filter(
1787
+ (mount) => typeof mount.initialPublicationSourceRelativePath === "string"
1788
+ );
1789
+ if (candidates.length === 0) return null;
1790
+ const revisions = [input.localHead, ...input.remoteHead ? [input.remoteHead] : []];
1791
+ const objects = await resolveGitObjectIdsAsync(
1792
+ input.workspacePath,
1793
+ candidates.flatMap(
1794
+ (mount) => revisions.flatMap((revision) => [
1795
+ workspaceTreeObjectName(revision, mount.workspaceRelativePath),
1796
+ workspaceTreeObjectName(revision, mount.initialPublicationSourceRelativePath)
1797
+ ])
1798
+ )
1799
+ );
1800
+ const lookup = (revision, workspaceRelativePath) => revision ? objects.get(workspaceTreeObjectName(revision, workspaceRelativePath)) ?? null : null;
1801
+ const initialPublications = candidates.flatMap((mount) => {
1802
+ const published = lookup(input.localHead, mount.workspaceRelativePath);
1803
+ if (!published || lookup(input.remoteHead, mount.workspaceRelativePath)) return [];
1804
+ const source = lookup(input.localHead, mount.initialPublicationSourceRelativePath) ?? lookup(input.remoteHead, mount.initialPublicationSourceRelativePath);
1805
+ return [{ mount, published, source }];
1806
+ });
1807
+ if (initialPublications.length === 0) return null;
1808
+ let size = await input.measure(
1809
+ input.workspacePath,
1810
+ input.remoteHead,
1811
+ input.localHead,
1812
+ input.limit,
1813
+ initialPublications.map(({ mount }) => `:(exclude,literal)${normalizedWorkspaceMountPath(mount.workspaceRelativePath)}`)
1814
+ );
1815
+ for (const { published, source } of initialPublications) {
1816
+ if (size > input.limit) break;
1817
+ size += await input.measure(input.workspacePath, source, published, input.limit - size);
1818
+ }
1819
+ return { size, mountIds: initialPublications.map(({ mount }) => mount.id).sort() };
1820
+ }
1716
1821
  function conflictPaths(workspacePath) {
1717
1822
  const result = gitResult(workspacePath, ["diff", "--name-only", "--diff-filter=U", "-z"]);
1718
1823
  return result.exitCode === 0 ? result.stdout.split("\0").filter(Boolean).sort() : [];
@@ -1940,12 +2045,19 @@ async function synchronizeWorkspaceGit(input) {
1940
2045
  const uncoveredCompletionBasisMounts = completionReceiptMounts.filter(
1941
2046
  (mount) => !cycleSkippedMountIds.has(mount.id) && !projectionSkippedIds.has(mount.id) && !mountIsAlreadyCovered(mount)
1942
2047
  );
2048
+ const unchangedMountIds = currentHeadBeforeHydration ? await selectMountsWithUnchangedOuterSubtree(
2049
+ workspacePath,
2050
+ currentReceiptBeforeHydration,
2051
+ currentHeadBeforeHydration,
2052
+ uncoveredCompletionMounts
2053
+ ) : /* @__PURE__ */ new Set();
1943
2054
  const hydration = await hydrateWorkspaceGitMountsTransactionally({
1944
2055
  workspacePath,
1945
- mounts: uncoveredCompletionMounts,
1946
- durabilityMounts: uncoveredCompletionBasisMounts,
2056
+ mounts: uncoveredCompletionMounts.filter(({ id }) => !unchangedMountIds.has(id)),
2057
+ durabilityMounts: uncoveredCompletionBasisMounts.filter(({ id }) => !unchangedMountIds.has(id)),
2058
+ carryMounts: uncoveredCompletionMounts.filter(({ id }) => unchangedMountIds.has(id)),
1947
2059
  receiptMounts: completionReceiptMounts,
1948
- requiredMounts: uncoveredRequiredLiveMounts,
2060
+ requiredMounts: uncoveredRequiredLiveMounts.filter(({ id }) => !unchangedMountIds.has(id)),
1949
2061
  recordCurrentHead: true
1950
2062
  });
1951
2063
  for (const id of hydration.skippedMountIds) cycleSkippedMountIds.add(id);
@@ -2095,8 +2207,25 @@ async function synchronizeWorkspaceGit(input) {
2095
2207
  };
2096
2208
  }
2097
2209
  const paths = await changedPaths(workspacePath, remoteHead, localHead);
2098
- const size = paths.length > 0 ? await (input.measureDiffSize ?? diffSizeBytes)(workspacePath, remoteHead, localHead, maxDiffBytes) : 0;
2210
+ const measureDiffSize = input.measureDiffSize ?? diffSizeBytes;
2211
+ let size = paths.length > 0 ? await measureDiffSize(workspacePath, remoteHead, localHead, maxDiffBytes) : 0;
2099
2212
  input.assertStillAdmitted?.();
2213
+ let initialPublicationMountIds;
2214
+ if (paths.length > 0 && size > maxDiffBytes && !input.allowLargeDiff) {
2215
+ const initialPublication = await measureInitialPublicationDiffSize({
2216
+ workspacePath,
2217
+ remoteHead,
2218
+ localHead,
2219
+ mounts: input.mounts,
2220
+ limit: maxDiffBytes,
2221
+ measure: measureDiffSize
2222
+ });
2223
+ input.assertStillAdmitted?.();
2224
+ if (initialPublication && initialPublication.size <= maxDiffBytes) {
2225
+ size = initialPublication.size;
2226
+ initialPublicationMountIds = initialPublication.mountIds;
2227
+ }
2228
+ }
2100
2229
  if (paths.length > 0 && size > maxDiffBytes && !input.allowLargeDiff) {
2101
2230
  const classifiedMounts = classifyMounts([...selected.active, ...selected.tombstones]);
2102
2231
  return {
@@ -2141,7 +2270,9 @@ async function synchronizeWorkspaceGit(input) {
2141
2270
  "push",
2142
2271
  "--no-recurse-submodules"
2143
2272
  ];
2144
- if (input.allowLargeDiff) pushArgs.push(`--push-option=${WORKSPACE_GIT_CONFIRMED_LARGE_DIFF_PUSH_OPTION}`);
2273
+ if (input.allowLargeDiff || initialPublicationMountIds) {
2274
+ pushArgs.push(`--push-option=${WORKSPACE_GIT_CONFIRMED_LARGE_DIFF_PUSH_OPTION}`);
2275
+ }
2145
2276
  pushArgs.push("origin", `HEAD:refs/heads/${WORKSPACE_GIT_BRANCH}`);
2146
2277
  const push = await gitResultAsync(workspacePath, pushArgs);
2147
2278
  if (push.exitCode === 0) {
@@ -2163,6 +2294,7 @@ async function synchronizeWorkspaceGit(input) {
2163
2294
  diffSizeBytes: size,
2164
2295
  affectedPaths: paths,
2165
2296
  ...completedMounts,
2297
+ ...initialPublicationMountIds ? { initialPublicationMountIds } : {},
2166
2298
  ...verifiedAncestorHeads ? { verifiedAncestorHeads } : {},
2167
2299
  ...projectionWarning ? { error: projectionWarning } : {}
2168
2300
  };
@@ -206,6 +206,14 @@ function indexMode(entry) {
206
206
  function indexInfoRecord(mode, objectId, relativePath) {
207
207
  return Buffer.concat([Buffer.from(`${mode} ${objectId} `), Buffer.from(relativePath), Buffer.from([0])]);
208
208
  }
209
+ function basisGitlinks(workspacePath, basis) {
210
+ const listed = gitResult(workspacePath, ["ls-tree", "-r", "-z", `${basis.basisHead}:${basis.workspaceRelativePath}`]);
211
+ if (listed.exitCode !== 0) return [];
212
+ return nulRecords(listed.stdout).flatMap((record) => {
213
+ const match = /^160000 commit ([0-9a-f]{40,64})\t(.+)$/su.exec(record.toString());
214
+ return match ? [{ objectId: match[1], relativePath: match[2] }] : [];
215
+ });
216
+ }
209
217
  function synthesizeMountTree(input) {
210
218
  const sourceRoot = import_node_path.default.resolve(input.sourcePath);
211
219
  const entries = (0, import_working_tree_mirror.inspectWorkingTree)(sourceRoot, input.sourceMode);
@@ -220,6 +228,15 @@ function synthesizeMountTree(input) {
220
228
  const objectId = entry.kind === "file" ? hashRegularFile(input.workspacePath, sourcePath, entry) : hashSymlink(input.workspacePath, sourcePath, entry);
221
229
  records.push(indexInfoRecord(indexMode(entry), objectId, relativePath));
222
230
  }
231
+ if (input.gitlinkBasis && input.sourceMode === "git") {
232
+ for (const gitlink of basisGitlinks(input.workspacePath, input.gitlinkBasis)) {
233
+ const populatedPrefix = `${gitlink.relativePath}/`;
234
+ const populated = [...entries.keys()].some((relativePath) => relativePath.startsWith(populatedPrefix));
235
+ if (entries.get(gitlink.relativePath)?.kind === "directory" && !populated) {
236
+ records.push(indexInfoRecord("160000", gitlink.objectId, gitlink.relativePath));
237
+ }
238
+ }
239
+ }
223
240
  if (records.length > 0) {
224
241
  git(input.workspacePath, ["update-index", "-z", "--index-info"], "populate workspace projection index", {
225
242
  environment,
@@ -284,7 +301,8 @@ function synthesizeOursCommit(input) {
284
301
  const mountTree = synthesizeMountTree({
285
302
  workspacePath: input.workspacePath,
286
303
  sourcePath: input.mount.sourcePath,
287
- sourceMode: input.mount.sourceMode
304
+ sourceMode: input.mount.sourceMode,
305
+ gitlinkBasis: { basisHead: input.basisHead, workspaceRelativePath: input.workspaceRelativePath }
288
306
  });
289
307
  const rootTree = graftMountTree({
290
308
  workspacePath: input.workspacePath,
package/dist/mjs/main.mjs CHANGED
@@ -3831,6 +3831,7 @@ async function startWorker(options) {
3831
3831
  const branchKey = projectBranchKey(project.projectId, branch.branchName);
3832
3832
  const creatorLocalIncarnation = creatorLocalIncarnations.get(branchKey) === branch.branchId;
3833
3833
  const preservesCheckoutPathMove = checkoutPathMoveBranches.has(branchKey);
3834
+ const initialPublicationSourceBranch = creatorLocalIncarnation ? branch.sourceBranchName : null;
3834
3835
  mounts.push({
3835
3836
  id: projectMountId(project.projectId, branch.branchName),
3836
3837
  hydrationIncarnationKey: branch.branchId,
@@ -3840,6 +3841,7 @@ async function startWorker(options) {
3840
3841
  sourceMode: "git",
3841
3842
  hydrateDeletionMode: "git",
3842
3843
  preserveLocalOnInitialOuterAbsence: creatorLocalIncarnation,
3844
+ ...initialPublicationSourceBranch ? { initialPublicationSourceRelativePath: workspaceProjectRelativePath(project.projectId, initialPublicationSourceBranch) } : {},
3843
3845
  preserveLocalOnHydrationBasisChange: preservesCheckoutPathMove,
3844
3846
  busy: () => projectBranchMountBusy(project.projectId, branch.branchName, branchPath),
3845
3847
  mutationToken: () => projectWorkspaceMutationToken(project.projectId, branch.branchName),
@@ -3854,6 +3856,7 @@ async function startWorker(options) {
3854
3856
  sourceMode: "all",
3855
3857
  hydrateDeletionMode: "all",
3856
3858
  preserveLocalOnInitialOuterAbsence: creatorLocalIncarnation,
3859
+ ...initialPublicationSourceBranch ? { initialPublicationSourceRelativePath: workspacePlanRelativePath(project.projectId, initialPublicationSourceBranch) } : {},
3857
3860
  preserveLocalOnHydrationBasisChange: preservesCheckoutPathMove,
3858
3861
  busy: () => projectBranchMountBusy(project.projectId, branch.branchName, branchPath),
3859
3862
  mutationToken: () => projectWorkspaceMutationToken(project.projectId, branch.branchName),
@@ -4453,6 +4456,12 @@ async function startWorker(options) {
4453
4456
  }
4454
4457
  });
4455
4458
  input.assertStillAdmitted?.();
4459
+ if (result.initialPublicationMountIds && result.initialPublicationMountIds.length > 0) {
4460
+ process.stderr.write(
4461
+ `[r5d-worker] published first checkout copies above the automatic diff-size limit, measured against their source subtrees at ${result.diffSizeBytes} bytes: ${result.initialPublicationMountIds.join(", ")}
4462
+ `
4463
+ );
4464
+ }
4456
4465
  if (["no_change", "updated", "pushed"].includes(result.outcome) && !outerRemediation) {
4457
4466
  await applyObservedProjectHeadsAfterInboundWorkspace(
4458
4467
  observedProjectHeads,
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.95",
3
+ "version": "0.0.97",
4
4
  "type": "module"
5
5
  }
@@ -1438,13 +1438,38 @@ function hydrateWorkspaceGitMountsRaw(workspacePath, mounts, options = {}) {
1438
1438
  }
1439
1439
  return { hydratedMountIds: hydratedMountIds.sort(), skippedMountIds: skippedMountIds.sort() };
1440
1440
  }
1441
+ async function selectMountsWithUnchangedOuterSubtree(workspacePath, receipt, head, mounts) {
1442
+ const candidates = mounts.flatMap((mount) => {
1443
+ const basisHead = workspaceHydrationReceiptMountBasisHead(receipt, mount);
1444
+ return basisHead && basisHead !== head ? [{ mount, basisHead }] : [];
1445
+ });
1446
+ if (candidates.length === 0) return /* @__PURE__ */ new Set();
1447
+ const objects = await resolveGitObjectIdsAsync(
1448
+ workspacePath,
1449
+ candidates.flatMap(({ mount, basisHead }) => [
1450
+ workspaceTreeObjectName(basisHead, mount.workspaceRelativePath),
1451
+ workspaceTreeObjectName(head, mount.workspaceRelativePath)
1452
+ ])
1453
+ );
1454
+ const unchanged = /* @__PURE__ */ new Set();
1455
+ for (const { mount, basisHead } of candidates) {
1456
+ const before = objects.get(workspaceTreeObjectName(basisHead, mount.workspaceRelativePath)) ?? null;
1457
+ const after = objects.get(workspaceTreeObjectName(head, mount.workspaceRelativePath)) ?? null;
1458
+ if (before === after) unchanged.add(mount.id);
1459
+ }
1460
+ return unchanged;
1461
+ }
1441
1462
  async function hydrateWorkspaceGitMountsTransactionally(input) {
1442
1463
  const workspacePath = path.resolve(input.workspacePath);
1443
1464
  const durabilityMounts = input.durabilityMounts ?? input.mounts;
1444
1465
  const receiptMounts = input.receiptMounts ?? durabilityMounts;
1445
1466
  const requiredMounts = input.requiredMounts ?? input.mounts;
1467
+ const carryMounts = input.carryMounts ?? [];
1468
+ if (carryMounts.length > 0 && !input.recordCurrentHead) {
1469
+ throw new Error("Workspace hydration can only carry unchanged mounts while recording the current head");
1470
+ }
1446
1471
  const candidateMounts = [
1447
- ...new Map([...input.mounts, ...durabilityMounts, ...requiredMounts].map((mount) => [mount.id, mount])).values()
1472
+ ...new Map([...input.mounts, ...durabilityMounts, ...requiredMounts, ...carryMounts].map((mount) => [mount.id, mount])).values()
1448
1473
  ];
1449
1474
  if (input.recordCurrentHead && !await workspaceCheckoutDurabilityIsCurrentAsync(workspacePath)) {
1450
1475
  throw new Error("Workspace checkout is not durably materialized at its current HEAD");
@@ -1460,12 +1485,13 @@ async function hydrateWorkspaceGitMountsTransactionally(input) {
1460
1485
  ({ id }) => !busyMountIds.has(id) && (requestedIds.has(id) || !requiredIds.has(id))
1461
1486
  );
1462
1487
  const unavailableRequiredIds = requiredMounts.filter(({ id }) => busyMountIds.has(id) || !requestedIds.has(id)).map(({ id }) => id);
1488
+ const carriedMounts = carryMounts.filter(({ id }) => !busyMountIds.has(id));
1463
1489
  return {
1464
1490
  busyMountIds,
1465
1491
  hydrationMounts: hydrationMounts2,
1466
1492
  advancedDurabilityMounts: advancedDurabilityMounts2,
1467
1493
  skippedMountIds: [.../* @__PURE__ */ new Set([...busyMountIds, ...unavailableRequiredIds])].sort(),
1468
- targetReceipt: targetHead ? receiptWithMountsAtHead(receiptBeforeTransaction, targetHead, advancedDurabilityMounts2, receiptMounts) : null
1494
+ targetReceipt: targetHead ? receiptWithMountsAtHead(receiptBeforeTransaction, targetHead, [...advancedDurabilityMounts2, ...carriedMounts], receiptMounts) : null
1469
1495
  };
1470
1496
  };
1471
1497
  const noopCandidate = computeSelection();
@@ -1641,14 +1667,26 @@ async function changedPaths(workspacePath, baseRevision, headRevision) {
1641
1667
  const base = baseRevision ?? emptyTreeHash(workspacePath);
1642
1668
  return (await gitAsync(workspacePath, ["diff", "--name-only", "--no-renames", "-z", base, headRevision], "inspect workspace commit paths")).split("\0").filter(Boolean).sort();
1643
1669
  }
1644
- async function diffSizeBytes(workspacePath, baseRevision, headRevision, limit) {
1670
+ async function diffSizeBytes(workspacePath, baseRevision, headRevision, limit, pathspecs = []) {
1645
1671
  const base = baseRevision ?? emptyTreeHash(workspacePath);
1646
- const subprocess = Bun.spawn(["git", ...NON_RECURSIVE_GIT_CONFIG, "diff", "--binary", "--no-ext-diff", base, headRevision], {
1647
- cwd: workspacePath,
1648
- stdout: "pipe",
1649
- stderr: "pipe",
1650
- env: workerGitProcessEnvironment()
1651
- });
1672
+ const subprocess = Bun.spawn(
1673
+ [
1674
+ "git",
1675
+ ...NON_RECURSIVE_GIT_CONFIG,
1676
+ "diff",
1677
+ "--binary",
1678
+ "--no-ext-diff",
1679
+ base,
1680
+ headRevision,
1681
+ ...pathspecs.length > 0 ? ["--", ...pathspecs] : []
1682
+ ],
1683
+ {
1684
+ cwd: workspacePath,
1685
+ stdout: "pipe",
1686
+ stderr: "pipe",
1687
+ env: workerGitProcessEnvironment()
1688
+ }
1689
+ );
1652
1690
  const stderrPromise = new Response(subprocess.stderr).text();
1653
1691
  const reader = subprocess.stdout.getReader();
1654
1692
  let total = 0;
@@ -1668,6 +1706,73 @@ async function diffSizeBytes(workspacePath, baseRevision, headRevision, limit) {
1668
1706
  if (exitCode !== 0) throw new Error(`Measure workspace diff: ${stderr || `git exited ${exitCode}`}`);
1669
1707
  return total;
1670
1708
  }
1709
+ async function resolveGitObjectIdsAsync(workspacePath, names) {
1710
+ const resolved = /* @__PURE__ */ new Map();
1711
+ const unique = [...new Set(names)];
1712
+ if (unique.length === 0) return resolved;
1713
+ const subprocess = Bun.spawn(gitCommandArgs(["cat-file", "--batch-check"]), {
1714
+ cwd: workspacePath,
1715
+ stdin: Buffer.from(`${unique.join("\n")}
1716
+ `),
1717
+ stdout: "pipe",
1718
+ stderr: "pipe",
1719
+ env: workerGitProcessEnvironment()
1720
+ });
1721
+ const [stdout, stderr, exitCode] = await Promise.all([
1722
+ new Response(subprocess.stdout).text(),
1723
+ new Response(subprocess.stderr).text(),
1724
+ subprocess.exited
1725
+ ]);
1726
+ if (exitCode !== 0) throw new Error(`Resolve workspace objects: ${stderr.trim() || `git exited ${exitCode}`}`);
1727
+ const lines = stdout.split("\n").filter((line) => line.length > 0);
1728
+ if (lines.length !== unique.length) {
1729
+ throw new Error(`Resolve workspace objects: expected ${unique.length} results, received ${lines.length}`);
1730
+ }
1731
+ unique.forEach((name, index) => {
1732
+ const match = /^([0-9a-f]{40,64}) \S+ \d+$/.exec(lines[index]);
1733
+ resolved.set(name, match ? match[1] : null);
1734
+ });
1735
+ return resolved;
1736
+ }
1737
+ function workspaceTreeObjectName(revision, workspaceRelativePath) {
1738
+ return `${revision}:${normalizedWorkspaceMountPath(workspaceRelativePath)}`;
1739
+ }
1740
+ async function measureInitialPublicationDiffSize(input) {
1741
+ const candidates = input.mounts.filter(
1742
+ (mount) => typeof mount.initialPublicationSourceRelativePath === "string"
1743
+ );
1744
+ if (candidates.length === 0) return null;
1745
+ const revisions = [input.localHead, ...input.remoteHead ? [input.remoteHead] : []];
1746
+ const objects = await resolveGitObjectIdsAsync(
1747
+ input.workspacePath,
1748
+ candidates.flatMap(
1749
+ (mount) => revisions.flatMap((revision) => [
1750
+ workspaceTreeObjectName(revision, mount.workspaceRelativePath),
1751
+ workspaceTreeObjectName(revision, mount.initialPublicationSourceRelativePath)
1752
+ ])
1753
+ )
1754
+ );
1755
+ const lookup = (revision, workspaceRelativePath) => revision ? objects.get(workspaceTreeObjectName(revision, workspaceRelativePath)) ?? null : null;
1756
+ const initialPublications = candidates.flatMap((mount) => {
1757
+ const published = lookup(input.localHead, mount.workspaceRelativePath);
1758
+ if (!published || lookup(input.remoteHead, mount.workspaceRelativePath)) return [];
1759
+ const source = lookup(input.localHead, mount.initialPublicationSourceRelativePath) ?? lookup(input.remoteHead, mount.initialPublicationSourceRelativePath);
1760
+ return [{ mount, published, source }];
1761
+ });
1762
+ if (initialPublications.length === 0) return null;
1763
+ let size = await input.measure(
1764
+ input.workspacePath,
1765
+ input.remoteHead,
1766
+ input.localHead,
1767
+ input.limit,
1768
+ initialPublications.map(({ mount }) => `:(exclude,literal)${normalizedWorkspaceMountPath(mount.workspaceRelativePath)}`)
1769
+ );
1770
+ for (const { published, source } of initialPublications) {
1771
+ if (size > input.limit) break;
1772
+ size += await input.measure(input.workspacePath, source, published, input.limit - size);
1773
+ }
1774
+ return { size, mountIds: initialPublications.map(({ mount }) => mount.id).sort() };
1775
+ }
1671
1776
  function conflictPaths(workspacePath) {
1672
1777
  const result = gitResult(workspacePath, ["diff", "--name-only", "--diff-filter=U", "-z"]);
1673
1778
  return result.exitCode === 0 ? result.stdout.split("\0").filter(Boolean).sort() : [];
@@ -1895,12 +2000,19 @@ async function synchronizeWorkspaceGit(input) {
1895
2000
  const uncoveredCompletionBasisMounts = completionReceiptMounts.filter(
1896
2001
  (mount) => !cycleSkippedMountIds.has(mount.id) && !projectionSkippedIds.has(mount.id) && !mountIsAlreadyCovered(mount)
1897
2002
  );
2003
+ const unchangedMountIds = currentHeadBeforeHydration ? await selectMountsWithUnchangedOuterSubtree(
2004
+ workspacePath,
2005
+ currentReceiptBeforeHydration,
2006
+ currentHeadBeforeHydration,
2007
+ uncoveredCompletionMounts
2008
+ ) : /* @__PURE__ */ new Set();
1898
2009
  const hydration = await hydrateWorkspaceGitMountsTransactionally({
1899
2010
  workspacePath,
1900
- mounts: uncoveredCompletionMounts,
1901
- durabilityMounts: uncoveredCompletionBasisMounts,
2011
+ mounts: uncoveredCompletionMounts.filter(({ id }) => !unchangedMountIds.has(id)),
2012
+ durabilityMounts: uncoveredCompletionBasisMounts.filter(({ id }) => !unchangedMountIds.has(id)),
2013
+ carryMounts: uncoveredCompletionMounts.filter(({ id }) => unchangedMountIds.has(id)),
1902
2014
  receiptMounts: completionReceiptMounts,
1903
- requiredMounts: uncoveredRequiredLiveMounts,
2015
+ requiredMounts: uncoveredRequiredLiveMounts.filter(({ id }) => !unchangedMountIds.has(id)),
1904
2016
  recordCurrentHead: true
1905
2017
  });
1906
2018
  for (const id of hydration.skippedMountIds) cycleSkippedMountIds.add(id);
@@ -2050,8 +2162,25 @@ async function synchronizeWorkspaceGit(input) {
2050
2162
  };
2051
2163
  }
2052
2164
  const paths = await changedPaths(workspacePath, remoteHead, localHead);
2053
- const size = paths.length > 0 ? await (input.measureDiffSize ?? diffSizeBytes)(workspacePath, remoteHead, localHead, maxDiffBytes) : 0;
2165
+ const measureDiffSize = input.measureDiffSize ?? diffSizeBytes;
2166
+ let size = paths.length > 0 ? await measureDiffSize(workspacePath, remoteHead, localHead, maxDiffBytes) : 0;
2054
2167
  input.assertStillAdmitted?.();
2168
+ let initialPublicationMountIds;
2169
+ if (paths.length > 0 && size > maxDiffBytes && !input.allowLargeDiff) {
2170
+ const initialPublication = await measureInitialPublicationDiffSize({
2171
+ workspacePath,
2172
+ remoteHead,
2173
+ localHead,
2174
+ mounts: input.mounts,
2175
+ limit: maxDiffBytes,
2176
+ measure: measureDiffSize
2177
+ });
2178
+ input.assertStillAdmitted?.();
2179
+ if (initialPublication && initialPublication.size <= maxDiffBytes) {
2180
+ size = initialPublication.size;
2181
+ initialPublicationMountIds = initialPublication.mountIds;
2182
+ }
2183
+ }
2055
2184
  if (paths.length > 0 && size > maxDiffBytes && !input.allowLargeDiff) {
2056
2185
  const classifiedMounts = classifyMounts([...selected.active, ...selected.tombstones]);
2057
2186
  return {
@@ -2096,7 +2225,9 @@ async function synchronizeWorkspaceGit(input) {
2096
2225
  "push",
2097
2226
  "--no-recurse-submodules"
2098
2227
  ];
2099
- if (input.allowLargeDiff) pushArgs.push(`--push-option=${WORKSPACE_GIT_CONFIRMED_LARGE_DIFF_PUSH_OPTION}`);
2228
+ if (input.allowLargeDiff || initialPublicationMountIds) {
2229
+ pushArgs.push(`--push-option=${WORKSPACE_GIT_CONFIRMED_LARGE_DIFF_PUSH_OPTION}`);
2230
+ }
2100
2231
  pushArgs.push("origin", `HEAD:refs/heads/${WORKSPACE_GIT_BRANCH}`);
2101
2232
  const push = await gitResultAsync(workspacePath, pushArgs);
2102
2233
  if (push.exitCode === 0) {
@@ -2118,6 +2249,7 @@ async function synchronizeWorkspaceGit(input) {
2118
2249
  diffSizeBytes: size,
2119
2250
  affectedPaths: paths,
2120
2251
  ...completedMounts,
2252
+ ...initialPublicationMountIds ? { initialPublicationMountIds } : {},
2121
2253
  ...verifiedAncestorHeads ? { verifiedAncestorHeads } : {},
2122
2254
  ...projectionWarning ? { error: projectionWarning } : {}
2123
2255
  };
@@ -170,6 +170,14 @@ function indexMode(entry) {
170
170
  function indexInfoRecord(mode, objectId, relativePath) {
171
171
  return Buffer.concat([Buffer.from(`${mode} ${objectId} `), Buffer.from(relativePath), Buffer.from([0])]);
172
172
  }
173
+ function basisGitlinks(workspacePath, basis) {
174
+ const listed = gitResult(workspacePath, ["ls-tree", "-r", "-z", `${basis.basisHead}:${basis.workspaceRelativePath}`]);
175
+ if (listed.exitCode !== 0) return [];
176
+ return nulRecords(listed.stdout).flatMap((record) => {
177
+ const match = /^160000 commit ([0-9a-f]{40,64})\t(.+)$/su.exec(record.toString());
178
+ return match ? [{ objectId: match[1], relativePath: match[2] }] : [];
179
+ });
180
+ }
173
181
  function synthesizeMountTree(input) {
174
182
  const sourceRoot = path.resolve(input.sourcePath);
175
183
  const entries = inspectWorkingTree(sourceRoot, input.sourceMode);
@@ -184,6 +192,15 @@ function synthesizeMountTree(input) {
184
192
  const objectId = entry.kind === "file" ? hashRegularFile(input.workspacePath, sourcePath, entry) : hashSymlink(input.workspacePath, sourcePath, entry);
185
193
  records.push(indexInfoRecord(indexMode(entry), objectId, relativePath));
186
194
  }
195
+ if (input.gitlinkBasis && input.sourceMode === "git") {
196
+ for (const gitlink of basisGitlinks(input.workspacePath, input.gitlinkBasis)) {
197
+ const populatedPrefix = `${gitlink.relativePath}/`;
198
+ const populated = [...entries.keys()].some((relativePath) => relativePath.startsWith(populatedPrefix));
199
+ if (entries.get(gitlink.relativePath)?.kind === "directory" && !populated) {
200
+ records.push(indexInfoRecord("160000", gitlink.objectId, gitlink.relativePath));
201
+ }
202
+ }
203
+ }
187
204
  if (records.length > 0) {
188
205
  git(input.workspacePath, ["update-index", "-z", "--index-info"], "populate workspace projection index", {
189
206
  environment,
@@ -248,7 +265,8 @@ function synthesizeOursCommit(input) {
248
265
  const mountTree = synthesizeMountTree({
249
266
  workspacePath: input.workspacePath,
250
267
  sourcePath: input.mount.sourcePath,
251
- sourceMode: input.mount.sourceMode
268
+ sourceMode: input.mount.sourceMode,
269
+ gitlinkBasis: { basisHead: input.basisHead, workspaceRelativePath: input.workspaceRelativePath }
252
270
  });
253
271
  const rootTree = graftMountTree({
254
272
  workspacePath: input.workspacePath,
@@ -26,6 +26,13 @@ export type WorkspaceGitMount = {
26
26
  * This is transient and must clear after that move is published.
27
27
  */
28
28
  preserveLocalOnHydrationBasisChange?: boolean;
29
+ /**
30
+ * Workspace-relative path of the subtree this creator-local mount was copied
31
+ * from (its source branch checkout). While the mount's own outer subtree is
32
+ * still unpublished, the automatic diff-size guard measures it against that
33
+ * source subtree instead of counting the whole copied tree as new content.
34
+ */
35
+ initialPublicationSourceRelativePath?: string;
29
36
  /** Remove the outer-workspace subtree when the mounted source was intentionally deleted. */
30
37
  deleteWhenSourceMissing?: boolean;
31
38
  busy?: () => boolean;
@@ -51,10 +58,16 @@ export type WorkspaceGitSyncResult = {
51
58
  remoteHead: string | null;
52
59
  publishedHead: string | null;
53
60
  rebaseCount: number;
61
+ /**
62
+ * Bytes the automatic publication guard evaluated. A creator-local branch's
63
+ * first publication counts only its divergence from the source subtree.
64
+ */
54
65
  diffSizeBytes: number;
55
66
  affectedPaths: string[];
56
67
  activeMountIds: string[];
57
68
  skippedMountIds: string[];
69
+ /** Creator-local mounts whose first outer publication was measured against their source subtree. */
70
+ initialPublicationMountIds?: string[];
58
71
  conflictPaths?: string[];
59
72
  conflictSnapshotRefs?: {
60
73
  local: string;
@@ -152,7 +165,7 @@ export declare function resetWorkspaceGit(input: {
152
165
  activeMountIds: string[];
153
166
  skippedMountIds: string[];
154
167
  }>;
155
- declare function diffSizeBytes(workspacePath: string, baseRevision: string | null, headRevision: string, limit: number): Promise<number>;
168
+ declare function diffSizeBytes(workspacePath: string, baseRevision: string | null, headRevision: string, limit: number, pathspecs?: readonly string[]): Promise<number>;
156
169
  export declare function synchronizeWorkspaceGit(input: {
157
170
  attemptId?: string;
158
171
  workerLabel: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.95",
3
+ "version": "0.0.97",
4
4
  "type": "module",
5
5
  "main": "./dist/cjs/main.cjs",
6
6
  "module": "./dist/mjs/main.mjs",