@ricsam/r5d-worker 0.0.94 → 0.0.96

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.
@@ -4,6 +4,12 @@ import os from "node:os";
4
4
  import path from "node:path";
5
5
  import { gitCredentialUsernameConfigKey, gitTransportSecurityArgs, workerGitProcessEnvironment } from "./git-process-environment.mjs";
6
6
  import { validateManagedBranchName } from "./managed-paths.mjs";
7
+ import {
8
+ canonicalizeLocalProjectMirrorRefs,
9
+ hashProjectMirrorRefs,
10
+ PROJECT_MIRROR_REF_PREFIX,
11
+ PROJECT_SERVER_SYNCED_REF_PREFIX
12
+ } from "./project-mirror-refs-token.mjs";
7
13
  import { mirrorWorkingTree } from "./working-tree-mirror.mjs";
8
14
  const PROJECT_WORKTREE_SNAPSHOT_PREFIX = "r5d-project-worktrees-";
9
15
  const PROJECT_WORKTREE_SNAPSHOT_MANIFEST = "transaction.json";
@@ -1241,6 +1247,7 @@ ${deleted.stdout}`)) {
1241
1247
  `Delete hidden project mirror for ${input.branchName}: ${deleted.stderr || deleted.stdout || `git exited ${deleted.exitCode}`}`
1242
1248
  );
1243
1249
  }
1250
+ await gitResultAsync(input.gitDirectory, ["update-ref", "-d", `refs/r5d/mirror/${input.branchName}`]);
1244
1251
  }
1245
1252
  async function pushProjectMirrorHeads(input) {
1246
1253
  const results = [];
@@ -1298,6 +1305,7 @@ async function pushProjectMirrorHeads(input) {
1298
1305
  if (pushed.exitCode !== 0) {
1299
1306
  throw new Error(`Push hidden project mirror for ${branchName}: ${pushed.stderr || pushed.stdout || `git exited ${pushed.exitCode}`}`);
1300
1307
  }
1308
+ await gitAsync(checkoutPath, ["update-ref", `refs/r5d/mirror/${branchName}`, head], `record pushed mirror tip ${branchName}`);
1301
1309
  results.push({ branchName, head, pushed: true });
1302
1310
  }
1303
1311
  return results;
@@ -1310,21 +1318,23 @@ function assertProjectMirrorHeadsPushed(results) {
1310
1318
  }
1311
1319
  async function observeProjectMirrorHeads(input) {
1312
1320
  const primaryPath = branchPath(input.projectRoot, input.primaryBranchName);
1313
- const fetch = await gitResultAsync(
1314
- primaryPath,
1315
- [
1316
- ...gitTransportSecurityArgs(input.mirrorUrl, input.credentialHelper, input.credentialUsername),
1317
- "fetch",
1318
- "--no-recurse-submodules",
1319
- "--prune",
1320
- input.mirrorUrl,
1321
- "+refs/heads/*:refs/r5d/mirror/*",
1322
- "+refs/r5d/server-synced/*:refs/r5d/server-synced/*"
1323
- ],
1324
- PROJECT_GIT_TRANSPORT_TIMEOUT_MS
1325
- );
1326
- if (fetch.exitCode !== 0)
1327
- throw new Error(`Refresh project head mirror: ${fetch.stderr || fetch.stdout || `git exited ${fetch.exitCode}`}`);
1321
+ if (input.refreshRemote !== false) {
1322
+ const fetch = await gitResultAsync(
1323
+ primaryPath,
1324
+ [
1325
+ ...gitTransportSecurityArgs(input.mirrorUrl, input.credentialHelper, input.credentialUsername),
1326
+ "fetch",
1327
+ "--no-recurse-submodules",
1328
+ "--prune",
1329
+ input.mirrorUrl,
1330
+ "+refs/heads/*:refs/r5d/mirror/*",
1331
+ "+refs/r5d/server-synced/*:refs/r5d/server-synced/*"
1332
+ ],
1333
+ PROJECT_GIT_TRANSPORT_TIMEOUT_MS
1334
+ );
1335
+ if (fetch.exitCode !== 0)
1336
+ throw new Error(`Refresh project head mirror: ${fetch.stderr || fetch.stdout || `git exited ${fetch.exitCode}`}`);
1337
+ }
1328
1338
  const observations = [];
1329
1339
  for (const branchName of [...input.branchNames].sort()) {
1330
1340
  const checkoutPath = branchPath(input.projectRoot, branchName);
@@ -1370,6 +1380,24 @@ async function observeProjectMirrorHeads(input) {
1370
1380
  }
1371
1381
  return observations;
1372
1382
  }
1383
+ async function computeLocalProjectMirrorRefsToken(input) {
1384
+ const primaryPath = branchPath(input.projectRoot, input.primaryBranchName);
1385
+ const rows = await gitAsync(
1386
+ primaryPath,
1387
+ ["for-each-ref", "--format=%(objectname) %(refname)", PROJECT_MIRROR_REF_PREFIX, PROJECT_SERVER_SYNCED_REF_PREFIX],
1388
+ "list cached project mirror refs"
1389
+ );
1390
+ const refs = rows.split(/\r?\n/).filter(Boolean).map((row) => {
1391
+ const separator = row.indexOf(" ");
1392
+ const objectName = separator < 0 ? "" : row.slice(0, separator);
1393
+ const refname = separator < 0 ? "" : row.slice(separator + 1);
1394
+ if (!/^[0-9a-f]{40,64}$/.test(objectName) || refname.length === 0) {
1395
+ throw new Error(`Invalid cached mirror ref listing row: ${row}`);
1396
+ }
1397
+ return { refname, objectName };
1398
+ });
1399
+ return hashProjectMirrorRefs(canonicalizeLocalProjectMirrorRefs(refs));
1400
+ }
1373
1401
  async function applyObservedProjectMirrorHeads(input) {
1374
1402
  const results = [];
1375
1403
  for (const observation of [...input.observations].sort((left, right) => left.branchName.localeCompare(right.branchName))) {
@@ -1451,6 +1479,7 @@ export {
1451
1479
  ProjectBranchCreationRollbackIncompleteError,
1452
1480
  applyObservedProjectMirrorHeads,
1453
1481
  assertProjectMirrorHeadsPushed,
1482
+ computeLocalProjectMirrorRefsToken,
1454
1483
  createLinkedProjectBranch,
1455
1484
  createOrRetryLinkedProjectBranch,
1456
1485
  deleteLinkedProjectBranch,
@@ -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
  };
@@ -125,6 +125,15 @@ type WorkerServerMessage = {
125
125
  confirmationReason?: string;
126
126
  resetToCanonical?: boolean;
127
127
  requiredAncestorHeads?: string[];
128
+ } | {
129
+ /**
130
+ * Content-derived tokens naming each canonical project repository's
131
+ * observable ref state, keyed by project id. Replace-all: projects
132
+ * absent from a message have no advertised token and observation falls
133
+ * back to fetching. See `project-mirror-fetch-policy.ts`.
134
+ */
135
+ type: "project_mirror_refs_tokens";
136
+ tokens: Record<string, string>;
128
137
  } | {
129
138
  type: "create_project_branch";
130
139
  requestId: string;
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Decides whether a periodic observation must fetch a project's hidden mirror
3
+ * or may reuse the locally cached mirror refs. The server advertises a
4
+ * content-derived token per project (hash of the exact ref namespaces the
5
+ * observation fetch would transfer); when the advertised token equals the
6
+ * token the local refs already cover, the fetch would be a no-op.
7
+ *
8
+ * Skipping is fail-open: any missing or mismatched information fetches. The
9
+ * backstop bounds the damage of a server-side token bug (for example a token
10
+ * computed against the wrong repository path) to one interval; every
11
+ * git-transport writer is observed by the server's on-disk recompute, so the
12
+ * backstop is insurance against bugs, not against known unobserved writers.
13
+ */
14
+ export declare const PROJECT_MIRROR_FETCH_BACKSTOP_MS: number;
15
+ export type ProjectMirrorFetchDecision = {
16
+ fetch: true;
17
+ reason: "skipping_disabled" | "no_advertised_token" | "never_fetched" | "token_changed" | "backstop_elapsed";
18
+ } | {
19
+ fetch: false;
20
+ reason: "token_match";
21
+ };
22
+ export declare function projectMirrorFetchDecision(input: {
23
+ advertisedToken: string | undefined;
24
+ fetchedToken: string | undefined;
25
+ lastFetchAtMs: number | undefined;
26
+ nowMs: number;
27
+ skippingEnabled: boolean;
28
+ backstopMs?: number;
29
+ }): ProjectMirrorFetchDecision;
@@ -0,0 +1,19 @@
1
+ export declare const PROJECT_MIRROR_REF_PREFIX = "refs/r5d/mirror/";
2
+ export declare const PROJECT_SERVER_SYNCED_REF_PREFIX = "refs/r5d/server-synced/";
3
+ export type ProjectMirrorRef = {
4
+ refname: string;
5
+ objectName: string;
6
+ };
7
+ /**
8
+ * Hash a ref listing into a mirror-refs token. Both sides sort here rather
9
+ * than trusting git's listing order: renaming `refs/r5d/mirror/*` back to
10
+ * `refs/heads/*` changes sort neighborhoods.
11
+ */
12
+ export declare function hashProjectMirrorRefs(refs: readonly ProjectMirrorRef[]): string;
13
+ /**
14
+ * Rename locally fetched mirror refs into the namespaces the server hashes.
15
+ * Rows outside the two observed namespaces are dropped; the caller lists only
16
+ * those namespaces, so a dropped row means an unexpected listing, and dropping
17
+ * it can only make tokens differ — which fails open into a fetch.
18
+ */
19
+ export declare function canonicalizeLocalProjectMirrorRefs(refs: readonly ProjectMirrorRef[]): ProjectMirrorRef[];
@@ -212,11 +212,32 @@ export type ProjectMirrorHeadObservation = {
212
212
  * Fetch and pin the exact hidden-mirror tips that may be applied after a
213
213
  * causally later outer-workspace fetch and hydration. Observation deliberately
214
214
  * does not mutate a live branch or its index.
215
+ *
216
+ * With `refreshRemote: false` the network fetch is skipped and the tips come
217
+ * from the locally cached `refs/r5d/mirror/*` / `refs/r5d/server-synced/*`
218
+ * refs of an earlier fetch. That is safe in the stale direction (a newer
219
+ * mirror tip waits for a later cycle, exactly like a tip that lands mid-cycle
220
+ * today) and in the fresh direction (hidden-mirror publication is tree-first,
221
+ * so any locally cached tip's covering outer tree was published before the
222
+ * fetch that cached it, hence before this cycle's outer fetch).
215
223
  */
216
224
  export declare function observeProjectMirrorHeads(input: Omit<ProjectMirrorHeadUpdateInput, "branchMutationAllowed"> & {
217
225
  allowNonFastForward: boolean;
218
226
  branchIds?: ReadonlyMap<string, string>;
227
+ refreshRemote?: boolean;
219
228
  }): Promise<ProjectMirrorHeadObservation[]>;
229
+ /**
230
+ * Compute the mirror-refs token for the locally cached mirror state — the
231
+ * token the server will advertise once it hashes the same refs. After a
232
+ * successful lease push (which updates `refs/r5d/mirror/*` above), this
233
+ * matches the server's next advertisement unless something else also moved
234
+ * the canonical repository, in which case the mismatch fails open into a
235
+ * fetch. Formula twin: `server/git/project-mirror-refs-token.ts`.
236
+ */
237
+ export declare function computeLocalProjectMirrorRefsToken(input: {
238
+ projectRoot: string;
239
+ primaryBranchName: string;
240
+ }): Promise<string>;
220
241
  /** Apply only previously observed object ids; this function never refetches. */
221
242
  export declare function applyObservedProjectMirrorHeads(input: {
222
243
  projectRoot: string;
@@ -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.94",
3
+ "version": "0.0.96",
4
4
  "type": "module",
5
5
  "main": "./dist/cjs/main.cjs",
6
6
  "module": "./dist/mjs/main.mjs",