@ricsam/r5d-worker 0.0.131 → 0.0.133

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 (47) hide show
  1. package/dist/cjs/atomic-rename.cjs +303 -0
  2. package/dist/cjs/git-blob-hash.cjs +41 -0
  3. package/dist/cjs/main.cjs +187 -38
  4. package/dist/cjs/package.json +1 -1
  5. package/dist/cjs/three-way-merge.cjs +346 -0
  6. package/dist/cjs/working-tree-mirror.cjs +1049 -64
  7. package/dist/cjs/workspace-command-sync-policy.cjs +8 -4
  8. package/dist/cjs/workspace-command-targets.cjs +63 -0
  9. package/dist/cjs/workspace-filesystem-job-types.cjs +11 -1
  10. package/dist/cjs/workspace-filesystem-jobs.cjs +2 -0
  11. package/dist/cjs/workspace-git-sync.cjs +846 -61
  12. package/dist/cjs/workspace-hydration-ledger.cjs +66 -0
  13. package/dist/cjs/workspace-hydration-merge.cjs +433 -0
  14. package/dist/cjs/workspace-hydration-recovery-state.cjs +53 -0
  15. package/dist/cjs/workspace-merge-projection.cjs +81 -10
  16. package/dist/cjs/workspace-project-config-policy.cjs +19 -12
  17. package/dist/mjs/atomic-rename.mjs +261 -0
  18. package/dist/mjs/git-blob-hash.mjs +16 -0
  19. package/dist/mjs/main.mjs +196 -39
  20. package/dist/mjs/package.json +1 -1
  21. package/dist/mjs/three-way-merge.mjs +318 -0
  22. package/dist/mjs/working-tree-mirror.mjs +1035 -64
  23. package/dist/mjs/workspace-command-sync-policy.mjs +8 -4
  24. package/dist/mjs/workspace-command-targets.mjs +37 -0
  25. package/dist/mjs/workspace-filesystem-job-types.mjs +11 -1
  26. package/dist/mjs/workspace-filesystem-jobs.mjs +4 -0
  27. package/dist/mjs/workspace-git-sync.mjs +854 -62
  28. package/dist/mjs/workspace-hydration-ledger.mjs +42 -0
  29. package/dist/mjs/workspace-hydration-merge.mjs +399 -0
  30. package/dist/mjs/workspace-hydration-recovery-state.mjs +29 -0
  31. package/dist/mjs/workspace-merge-projection.mjs +85 -11
  32. package/dist/mjs/workspace-project-config-policy.mjs +16 -10
  33. package/dist/types/atomic-rename.d.ts +78 -0
  34. package/dist/types/git-blob-hash.d.ts +10 -0
  35. package/dist/types/main.d.ts +21 -2
  36. package/dist/types/three-way-merge.d.ts +77 -0
  37. package/dist/types/working-tree-mirror.d.ts +270 -7
  38. package/dist/types/workspace-command-sync-policy.d.ts +12 -6
  39. package/dist/types/workspace-command-targets.d.ts +37 -0
  40. package/dist/types/workspace-filesystem-job-types.d.ts +46 -4
  41. package/dist/types/workspace-git-sync.d.ts +125 -3
  42. package/dist/types/workspace-hydration-ledger.d.ts +43 -0
  43. package/dist/types/workspace-hydration-merge.d.ts +95 -0
  44. package/dist/types/workspace-hydration-recovery-state.d.ts +10 -0
  45. package/dist/types/workspace-merge-projection.d.ts +19 -1
  46. package/dist/types/workspace-project-config-policy.d.ts +17 -3
  47. package/package.json +2 -2
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Declared additional targets of a command, PTY or one-shot exec.
3
+ *
4
+ * A server that forwards a session's live `checkout` claims attaches the
5
+ * claimed sibling checkouts as `additionalTargets`. The worker treats every
6
+ * listed project branch exactly like the command's own target for admission
7
+ * (every mount is reserved and waits on the hold fence), busy detection and
8
+ * mutation tokens, for the whole lifetime of the process: the list is fixed
9
+ * when the command is admitted and only the process exit removes it. The
10
+ * primary target alone keeps environment attribution, the plans-mount
11
+ * predicate and credential-bearing process-group ownership.
12
+ */
13
+ export type WorkspaceCommandProjectTarget = {
14
+ type: "project";
15
+ projectId: string;
16
+ branchName: string;
17
+ };
18
+ export type WorkspaceCommandAnyTarget = WorkspaceCommandProjectTarget | {
19
+ type: "workspace";
20
+ ownerUserId: string;
21
+ rootProfile: "visible_projects" | "canonical_sync";
22
+ };
23
+ export type WorkspaceCommandTargetValidators = {
24
+ validateProjectId: (projectId: string) => void;
25
+ validateBranchName: (branchName: string) => void;
26
+ };
27
+ /**
28
+ * The additional targets a message declares, checked and deduplicated: only
29
+ * project branches, each structurally valid, none equal to the primary
30
+ * target. A malformed entry is a protocol violation and fails the command
31
+ * before admission rather than silently narrowing its fence.
32
+ */
33
+ export declare function normalizeCommandAdditionalTargets(primary: WorkspaceCommandAnyTarget, additional: unknown, validators: WorkspaceCommandTargetValidators): WorkspaceCommandProjectTarget[];
34
+ /** Every target a command fences, primary first. */
35
+ export declare function commandTargets<T extends WorkspaceCommandAnyTarget>(primary: T, additional: readonly WorkspaceCommandProjectTarget[]): Array<T | WorkspaceCommandProjectTarget>;
36
+ /** Log suffix naming the additional checkouts a command fences; empty when there are none. */
37
+ export declare function describeCommandAdditionalTargets(additional: readonly WorkspaceCommandProjectTarget[]): string;
@@ -1,6 +1,7 @@
1
1
  import type { ProjectWorktreeBranch, ProjectWorktreeSnapshot, ProjectWorktreeSnapshotProgress, ProjectWorktreeSnapshotScope, ProjectWorktreeWorkingTreeMode } from "./project-worktrees";
2
- import type { WorkingTreeGitlink } from "./working-tree-mirror";
3
- import type { WorkspaceGitMountData, WorkspaceHydrationReceipt } from "./workspace-git-sync";
2
+ import type { ProjectedWorkingTreeEntry, WorkingTreeGitlink } from "./working-tree-mirror";
3
+ import type { WorkspaceGitMountData, WorkspaceHydrationReceipt, WorkspaceHydrationRecoveryRequired, WorkspaceHydrationPreservationResult, WorkspaceHydrationMountSkip } from "./workspace-git-sync";
4
+ import type { WorkspaceHydrationMergeBasis } from "./workspace-hydration-merge";
4
5
  import type { WorkspaceMergeProjectionMount, WorkspaceMergeProjectionResult } from "./workspace-merge-projection";
5
6
  import type { ProjectCheckoutPathMoveBranch } from "./workspace-path-move";
6
7
  /**
@@ -28,16 +29,43 @@ export type WorkspaceFilesystemJobTable = {
28
29
  };
29
30
  result: null;
30
31
  };
31
- /** The complete hydration transaction: snapshot, hydrate, fsync, commit marker, receipt; restore on failure. */
32
+ /** Read-only pending guarded transaction evidence for configuration admission. */
33
+ hydration_recovery_inspect: {
34
+ input: {
35
+ workspacePath: string;
36
+ };
37
+ result: WorkspaceHydrationRecoveryRequired | null;
38
+ };
39
+ /** Explicit exact-identity recovery: preserve visible data and archive all interrupted guarded transaction evidence. */
40
+ hydration_preserve_interrupted: {
41
+ input: {
42
+ workspacePath: string;
43
+ expectedTransactionId: string;
44
+ };
45
+ result: WorkspaceHydrationPreservationResult;
46
+ };
47
+ /**
48
+ * The complete hydration transaction: snapshot, hydrate, fsync, commit
49
+ * marker, receipt; restore on failure. A mount listed in `mergeBases` is
50
+ * merge-hydrated (docs/merge-hydration-design.md): files changed in the
51
+ * checkout since the projection read are three-way merged instead of
52
+ * overwritten, and a conflict or a write racing the hydration pins the
53
+ * mount at its previous receipt entry (`skippedMounts`) instead of
54
+ * failing the transaction.
55
+ */
32
56
  hydration_transaction: {
33
57
  input: {
34
58
  workspacePath: string;
35
59
  hydrationMounts: WorkspaceGitMountData[];
36
60
  advancedDurabilityMounts: WorkspaceGitMountData[];
37
61
  targetReceipt: WorkspaceHydrationReceipt | null;
62
+ mergeBases?: Record<string, WorkspaceHydrationMergeBasis>;
38
63
  };
39
64
  result: {
40
65
  hydratedMountIds: string[];
66
+ skippedMounts: WorkspaceHydrationMountSkip[];
67
+ /** Per merge-hydrated mount, the blob each replaced path held before hydration (the stale-rewrite detector's input). */
68
+ hydratedPreBlobs: Record<string, Record<string, string>>;
41
69
  };
42
70
  };
43
71
  /** Pre-clone hydration of visible mounts without a transaction (no outer Git directory yet). */
@@ -79,17 +107,30 @@ export type WorkspaceFilesystemJobTable = {
79
107
  basisHead: string;
80
108
  currentHead: string;
81
109
  attemptId: string;
110
+ /** Blob ids the last merge hydration replaced, by mount-relative path; a changed file hashing to one is a stale rewrite. */
111
+ staleRewriteBlobs?: Record<string, string>;
82
112
  };
83
113
  result: WorkspaceMergeProjectionResult;
84
114
  };
85
- /** Byte mirror of one visible mount into the outer checkout. */
115
+ /**
116
+ * Byte mirror of one visible mount into the outer checkout. Returns what it
117
+ * read for every file (merge hydration's stat basis), or the paths that
118
+ * hold a stale rewrite, in which case nothing was mirrored.
119
+ */
86
120
  projection_mirror: {
87
121
  input: {
88
122
  workspacePath: string;
89
123
  mount: WorkspaceGitMountData;
124
+ staleRewriteBlobs?: Record<string, string>;
90
125
  };
91
126
  result: {
92
127
  gitlinks: WorkingTreeGitlink[];
128
+ projectedFiles: Record<string, ProjectedWorkingTreeEntry>;
129
+ /** Wall-clock time the mirror finished reading; projected stats younger than this minus a window are not trusted. */
130
+ readAtMs: number;
131
+ staleRewritePaths: string[];
132
+ /** Ledger paths whose checkout content moved on from both the replaced and the hydrated bytes. */
133
+ staleRewriteCleared: string[];
93
134
  };
94
135
  };
95
136
  /** Outer-tree edits after the mirrors: tombstone removal, merged subtree materialization, deferred restores. */
@@ -216,6 +257,7 @@ export type WorkspaceFilesystemSerializedError = {
216
257
  code?: string;
217
258
  branchMayExist?: true;
218
259
  cleanupFailures?: string[];
260
+ hydrationRecovery?: WorkspaceHydrationRecoveryRequired;
219
261
  errors?: WorkspaceFilesystemSerializedError[];
220
262
  cause?: WorkspaceFilesystemSerializedError;
221
263
  };
@@ -1,10 +1,26 @@
1
- import { type WorkingTreeGitlink, type WorkingTreeSourceMode } from "./working-tree-mirror";
1
+ import { type PreparedWorkingTreeMirror, type ProjectedWorkingTreeEntry, type WorkingTreeGitlink, type WorkingTreeMirrorPlan, type WorkingTreeMirrorScope, type WorkingTreeSourceMode } from "./working-tree-mirror";
2
+ import { type WorkspaceHydrationMergeBasis, type WorkspaceHydrationMergeDecision, type WorkspaceHydrationMountSkip as WorkspaceHydrationMountSkipBase } from "./workspace-hydration-merge";
3
+ import { WorkspaceHydrationPreBlobLedger } from "./workspace-hydration-ledger";
2
4
  export declare const WORKSPACE_GIT_BRANCH = "main";
3
5
  export declare const MAX_WORKSPACE_GIT_DIFF_BYTES: number;
4
6
  export declare const WORKSPACE_GIT_CONFIRMED_LARGE_DIFF_PUSH_OPTION = "r5d-confirm-large-diff-v1";
5
7
  export declare const WORKSPACE_GIT_HYDRATED_RECEIPT = "r5d/workspace-hydrated-head";
6
8
  export declare const WORKSPACE_GIT_HYDRATION_TRANSACTION = "r5d/workspace-hydration-transaction";
9
+ export declare const WORKSPACE_GIT_HYDRATION_RECOVERIES = "r5d/workspace-hydration-recoveries";
7
10
  export declare const WORKSPACE_GIT_CHECKOUT_DURABILITY = "r5d/workspace-checkout-durability";
11
+ /** `R5D_MERGE_HYDRATION=0` restores the plain byte mirror for post-publication hydration (docs/merge-hydration-design.md §4). */
12
+ export declare const WORKSPACE_MERGE_HYDRATION_SWITCH = "R5D_MERGE_HYDRATION";
13
+ declare function hydrationPreBlobLedger(workspacePath: string): WorkspaceHydrationPreBlobLedger;
14
+ export type WorkspaceHydrationMountSkip = WorkspaceHydrationMountSkipBase & {
15
+ retainedPaths?: string[];
16
+ };
17
+ export type WorkspaceMountSkipReason = WorkspaceHydrationMountSkip["reason"] | "stale_rewrite_detected";
18
+ /** Why a mount was left at its recorded basis this cycle, with the paths involved; a diagnostic beside `skippedMountIds`. */
19
+ export type WorkspaceMountSkipReasons = Record<string, {
20
+ reason: WorkspaceMountSkipReason;
21
+ paths: string[];
22
+ retainedPaths?: string[];
23
+ }>;
8
24
  type WorkspaceHydrationMountBasis = {
9
25
  id: string;
10
26
  incarnationKey: string;
@@ -26,6 +42,57 @@ export type WorkspaceHydrationReceipt = {
26
42
  /** Non-enumerable read-compat marker used only to finish a v1/v2 crash transition. */
27
43
  legacyHead?: string;
28
44
  };
45
+ type WorkspaceHydrationTransactionManifest = {
46
+ /** Version 5 makes guarded recovery explicit before any visible mutation. */
47
+ version: 3 | 4 | 5;
48
+ transactionId?: string;
49
+ targetReceipt: WorkspaceHydrationReceipt | null;
50
+ receiptBefore: WorkspaceHydrationReceipt | null;
51
+ mounts: Array<{
52
+ id: string;
53
+ incarnationKey: string;
54
+ sourcePath: string;
55
+ durabilityRootPath: string;
56
+ workspaceRelativePath: string;
57
+ existed: boolean;
58
+ snapshotName: string;
59
+ /**
60
+ * Present when the snapshot holds only the entries hydration replaces or
61
+ * removes, with the paths it creates listed. Without it the snapshot is a
62
+ * complete copy of the mount.
63
+ */
64
+ scope?: WorkingTreeMirrorScope;
65
+ /** Required in v5: guarded mounts never enter unguarded snapshot restore. */
66
+ recovery?: "snapshot" | "guarded";
67
+ }>;
68
+ /** Settled rollback scopes remain evidence; they are never restored again. */
69
+ preservedMounts?: WorkspaceHydrationTransactionManifest["mounts"];
70
+ };
71
+ export type WorkspaceHydrationRecoveryRequired = {
72
+ transactionId: string;
73
+ transactionPath: string;
74
+ mounts: Array<Pick<WorkspaceHydrationTransactionManifest["mounts"][number], "id" | "incarnationKey" | "sourcePath" | "durabilityRootPath" | "workspaceRelativePath"> & {
75
+ retainedPaths?: string[];
76
+ }>;
77
+ receiptBefore: WorkspaceHydrationReceipt | null;
78
+ };
79
+ export declare class WorkspaceHydrationRecoveryRequiredError extends Error {
80
+ readonly hydrationRecovery: WorkspaceHydrationRecoveryRequired;
81
+ readonly code = "hydration_recovery_required";
82
+ constructor(hydrationRecovery: WorkspaceHydrationRecoveryRequired, options?: ErrorOptions);
83
+ }
84
+ export type WorkspaceHydrationPreservationResult = {
85
+ transactionId: string;
86
+ archivePath: string;
87
+ receiptBefore: WorkspaceHydrationReceipt | null;
88
+ preservedMountIds: string[];
89
+ alreadyPreserved: boolean;
90
+ retainedEntries?: Array<{
91
+ mountId: string;
92
+ sourcePath: string;
93
+ archivePath: string;
94
+ }>;
95
+ };
29
96
  export type WorkspaceGitMount = {
30
97
  id: string;
31
98
  /** Durable logical incarnation (for example the project-branch row id). */
@@ -57,6 +124,8 @@ export type WorkspaceGitMount = {
57
124
  /** Remove the outer-workspace subtree when the mounted source was intentionally deleted. */
58
125
  deleteWhenSourceMissing?: boolean;
59
126
  busy?: () => boolean;
127
+ /** Control-thread diagnostics only; never sent to a filesystem job. */
128
+ describeHolders?: () => string;
60
129
  /** Monotonic/opaque visible-mutation token used to catch short writes that begin and end between busy checks. */
61
130
  mutationToken?: () => string;
62
131
  /**
@@ -130,6 +199,14 @@ export type WorkspaceGitSyncResult = {
130
199
  affectedPaths: string[];
131
200
  activeMountIds: string[];
132
201
  skippedMountIds: string[];
202
+ /**
203
+ * Mounts merge hydration pinned this cycle (a conflict between a write
204
+ * that landed after the projection read and the inbound change, or a write
205
+ * racing the hydration) and mounts the stale-rewrite detector refused to
206
+ * project, each with the paths involved. Worker-internal until the server
207
+ * contract carries it.
208
+ */
209
+ mountSkipReasons?: WorkspaceMountSkipReasons;
133
210
  /** Token each active mount carried when this cycle projected it; recorded by the caller to skip unchanged mounts later. */
134
211
  projectedMutationTokens?: Record<string, string>;
135
212
  /** Creator-local mounts whose first outer publication was measured against their source subtree. */
@@ -179,6 +256,40 @@ export declare function runCheckoutDurabilityRecoveryJob(input: {
179
256
  export declare function runOuterCheckoutFsyncJob(input: {
180
257
  workspacePath: string;
181
258
  }): null;
259
+ /** Read-only admission inspection, including before incident configuration fast paths. */
260
+ export declare function runHydrationRecoveryInspectJob(input: {
261
+ workspacePath: string;
262
+ }): WorkspaceHydrationRecoveryRequired | null;
263
+ /**
264
+ * Explicitly preserve an interrupted guarded transaction. The caller owns
265
+ * workspace admission/exclusion. No visible file is restored or deleted and
266
+ * no mount is receipted at the partially applied target head. The entire
267
+ * transaction directory is archived atomically, including snapshots and any
268
+ * interrupted manifest writes, before ordinary merge projection may resume.
269
+ */
270
+ export declare function runHydrationPreserveInterruptedJob(input: {
271
+ workspacePath: string;
272
+ expectedTransactionId: string;
273
+ }): WorkspaceHydrationPreservationResult;
274
+ /** Everything the apply phase needs for a merge-hydrated mount, decided before the manifest was written. */
275
+ type WorkspaceHydrationMergePlan = {
276
+ prepared: PreparedWorkingTreeMirror;
277
+ decision: Extract<WorkspaceHydrationMergeDecision, {
278
+ kind: "apply";
279
+ }>;
280
+ plan: WorkingTreeMirrorPlan;
281
+ /** The mount's snapshot directory once the transaction directory is published. */
282
+ snapshotPath: string;
283
+ basis: WorkspaceHydrationMergeBasis;
284
+ };
285
+ declare function beginHydrationTransaction(workspacePath: string, mounts: readonly WorkspaceGitMount[], targetReceipt: WorkspaceHydrationReceipt | null, mergeBases?: Readonly<Record<string, WorkspaceHydrationMergeBasis>>): {
286
+ manifest: WorkspaceHydrationTransactionManifest;
287
+ durabilityScopes: Map<string, readonly string[]>;
288
+ mergePlans: Map<string, WorkspaceHydrationMergePlan>;
289
+ /** Mounts merge hydration decided not to touch; they have no snapshot, no manifest entry and keep their receipt entry. */
290
+ skippedMounts: WorkspaceHydrationMountSkip[];
291
+ targetReceipt: WorkspaceHydrationReceipt | null;
292
+ };
182
293
  declare function configureWorkspaceRepository(input: {
183
294
  workspacePath: string;
184
295
  remoteUrl: string;
@@ -231,7 +342,7 @@ type ProjectedMountGitlinks = {
231
342
  * arrives as an empty directory plus a gitlink the caller must stage with
232
343
  * `stageWorkspaceGitlinks` before `git add -A`, which can never create one.
233
344
  */
234
- declare function mirrorMountsToWorkspace(workspacePath: string, mounts: readonly WorkspaceGitMount[]): ProjectedMountGitlinks[];
345
+ declare function mirrorMountsToWorkspace(workspacePath: string, mounts: readonly WorkspaceGitMount[], projectedFiles?: Map<string, ProjectedWorkingTreeEntry>): ProjectedMountGitlinks[];
235
346
  /**
236
347
  * Executor job body: the complete hydration transaction. Snapshot exactly
237
348
  * what hydration replaces, hydrate, cross the durability barrier, write the
@@ -244,8 +355,11 @@ export declare function runHydrationTransactionJob(input: {
244
355
  hydrationMounts: readonly WorkspaceGitMountData[];
245
356
  advancedDurabilityMounts: readonly WorkspaceGitMountData[];
246
357
  targetReceipt: WorkspaceHydrationReceipt | null;
358
+ mergeBases?: Readonly<Record<string, WorkspaceHydrationMergeBasis>>;
247
359
  }): {
248
360
  hydratedMountIds: string[];
361
+ skippedMounts: WorkspaceHydrationMountSkip[];
362
+ hydratedPreBlobs: Record<string, Record<string, string>>;
249
363
  };
250
364
  /** Executor job body: hydrate visible mounts before an outer clone exists (no transaction to record). */
251
365
  export declare function runHydrationRawJob(input: {
@@ -340,12 +454,17 @@ export type SynchronizeWorkspaceGitInput = {
340
454
  hydrationHooks?: WorkspaceHydrationTransactionHooks;
341
455
  };
342
456
  export declare function synchronizeWorkspaceGit(input: SynchronizeWorkspaceGitInput): Promise<WorkspaceGitSyncResult>;
343
- /** Executor job body: byte-mirror one visible mount into the outer checkout. */
457
+ /** Executor job body: byte-mirror one visible mount into the outer checkout, recording what it read. */
344
458
  export declare function runProjectionMirrorJob(input: {
345
459
  workspacePath: string;
346
460
  mount: WorkspaceGitMountData;
461
+ staleRewriteBlobs?: Record<string, string>;
347
462
  }): {
348
463
  gitlinks: WorkingTreeGitlink[];
464
+ projectedFiles: Record<string, ProjectedWorkingTreeEntry>;
465
+ readAtMs: number;
466
+ staleRewritePaths: string[];
467
+ staleRewriteCleared: string[];
349
468
  };
350
469
  /** Executor job body: outer-tree edits that follow the mirrors, in the cycle's order. */
351
470
  export declare function runProjectionOuterApplyJob(input: {
@@ -364,10 +483,13 @@ export declare function runProjectionReceiptJob(input: {
364
483
  receipt: WorkspaceHydrationReceipt;
365
484
  }): null;
366
485
  export declare const workspaceGitSyncTestHarness: {
486
+ beginHydrationTransaction: typeof beginHydrationTransaction;
367
487
  commandArgs: typeof gitCommandArgs;
368
488
  workspaceCloneCommandArgs: typeof workspaceCloneCommandArgs;
369
489
  configureWorkspaceRepository: typeof configureWorkspaceRepository;
370
490
  mirrorMountsToWorkspace: typeof mirrorMountsToWorkspace;
371
491
  hydrateMountsFromWorkspace: typeof hydrateWorkspaceGitMounts;
492
+ hydrationPreBlobLedger: typeof hydrationPreBlobLedger;
493
+ resetHydrationPreBlobLedgers(): void;
372
494
  };
373
495
  export {};
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Blob ids of the bytes merge hydration replaced in a checkout, kept in
3
+ * memory per mount incarnation until a later hydration of the same path
4
+ * replaces them or the mount is hydrated outside a cycle (recovery, reset,
5
+ * configuration), which rewrites the checkout without recording anything.
6
+ *
7
+ * The projection reads it as the stale-rewrite detector
8
+ * (docs/merge-hydration-design.md §3.3): a process that read a file before a
9
+ * hydration and re-emits the same bytes afterwards would otherwise publish a
10
+ * revert of the inbound change as an ordinary local edit. A changed file whose
11
+ * content hashes to the blob hydration replaced is exactly that, so the mount
12
+ * is pinned instead of published, loudly.
13
+ *
14
+ * Deliberately not persisted: a restart loses the entries and the detector is
15
+ * best effort. Phase 2 of the design turns this into a durable ledger if
16
+ * telemetry warrants it.
17
+ */
18
+ export declare class WorkspaceHydrationPreBlobLedger {
19
+ private readonly mounts;
20
+ private static key;
21
+ /** Blob ids by mount-relative path for the mount, or an empty map. */
22
+ preBlobs(mount: {
23
+ id: string;
24
+ hydrationIncarnationKey: string;
25
+ }): ReadonlyMap<string, string>;
26
+ /** Record the pre-hydration blob of every path a merge hydration replaced; earlier entries for other paths are kept. */
27
+ record(mount: {
28
+ id: string;
29
+ hydrationIncarnationKey: string;
30
+ }, preBlobs: ReadonlyMap<string, string> | Readonly<Record<string, string>>): void;
31
+ /** Forget the mount: its checkout was rewritten by something this ledger did not observe. */
32
+ clearMount(mount: {
33
+ id: string;
34
+ hydrationIncarnationKey: string;
35
+ }): void;
36
+ /** Forget paths whose checkout content moved on from the hydrated bytes: a later return to the old bytes is a deliberate edit. */
37
+ forget(mount: {
38
+ id: string;
39
+ hydrationIncarnationKey: string;
40
+ }, paths: readonly string[]): void;
41
+ clear(): void;
42
+ get size(): number;
43
+ }
@@ -0,0 +1,95 @@
1
+ import fs from "node:fs";
2
+ import { type PreparedWorkingTreeMirror, type ProjectedWorkingTreeEntry, type WorkingTreeMirrorOverride, type WorkingTreeMirrorSelection } from "./working-tree-mirror";
3
+ /**
4
+ * Where a mount's bytes were read this cycle (docs/merge-hydration-design.md
5
+ * §2.1). `projectedCommit` is the commit whose subtree at the mount path is
6
+ * exactly what projection read: the projected head for a mirrored mount, the
7
+ * synthesized ours commit for a merge-projected one. `projectedFiles` is
8
+ * what the mirror or synthesis saw for each entry, or null when the mount was
9
+ * not read this cycle (its token was unchanged), in which case a changed
10
+ * file is recognized by hashing it against the projected blob.
11
+ */
12
+ export type WorkspaceHydrationMergeBasis = {
13
+ projectedCommit: string;
14
+ projectedFiles: Record<string, ProjectedWorkingTreeEntry> | null;
15
+ /**
16
+ * Wall-clock time the projection finished reading. A projected stat whose
17
+ * timestamps fall within `RACY_PROJECTION_WINDOW_MS` of it is not trusted
18
+ * to prove the file unchanged (git's racy-clean rule: a rewrite of the
19
+ * same size in the same coarse timestamp tick leaves the stat intact), so
20
+ * such a file is hashed against the projected blob instead.
21
+ */
22
+ readAtMs?: number;
23
+ };
24
+ export declare const RACY_PROJECTION_WINDOW_MS = 2000;
25
+ export type WorkspaceHydrationSkipReason = "hydration_merge_conflict" | "hydration_target_changed";
26
+ /** A mount merge hydration left at its previous receipt entry, and why. */
27
+ export type WorkspaceHydrationMountSkip = {
28
+ mountId: string;
29
+ reason: WorkspaceHydrationSkipReason;
30
+ paths: string[];
31
+ };
32
+ /** One path whose blob differs between the projected subtree (base) and the hydration head's subtree (theirs). */
33
+ export type WorkspaceSubtreeChange = {
34
+ base: {
35
+ mode: string;
36
+ objectId: string;
37
+ } | null;
38
+ theirs: {
39
+ mode: string;
40
+ objectId: string;
41
+ } | null;
42
+ };
43
+ export type WorkspaceHydrationMergeDecision = {
44
+ kind: "conflict";
45
+ paths: string[];
46
+ } | {
47
+ kind: "target_changed";
48
+ paths: string[];
49
+ } | {
50
+ kind: "apply";
51
+ selection: WorkingTreeMirrorSelection;
52
+ overrides: Map<string, WorkingTreeMirrorOverride>;
53
+ /** The target `lstat` (or absence) every mutated path must still show when it is mutated. */
54
+ expectations: Map<string, fs.Stats | null>;
55
+ /** Blob of the bytes each written or removed path held before hydration; the stale-rewrite detector's input. */
56
+ preBlobs: Map<string, string>;
57
+ /** Paths whose content was three-way merged. */
58
+ mergedPaths: string[];
59
+ /** Paths the checkout changed since projection that hydration leaves as they are. */
60
+ keptPaths: string[];
61
+ };
62
+ /**
63
+ * Decide, per path the hydration head changed relative to what projection
64
+ * read, whether hydration writes theirs (the checkout is unchanged there),
65
+ * leaves the checkout alone (theirs did not change there), writes a
66
+ * three-way merge, or cannot proceed (docs/merge-hydration-design.md §2.2,
67
+ * §2.6). Every path the checkout changed since projection that the head did
68
+ * not change survives untouched, including files created since. Nothing is
69
+ * written here; the result drives `applyWorkingTreeMirror` under guards that
70
+ * re-check each path's `lstat` immediately before mutating it.
71
+ */
72
+ export declare function decideWorkspaceHydrationMerge(input: {
73
+ prepared: PreparedWorkingTreeMirror;
74
+ changes: ReadonlyMap<string, WorkspaceSubtreeChange>;
75
+ projectedFiles: Readonly<Record<string, ProjectedWorkingTreeEntry>> | null;
76
+ readAtMs?: number;
77
+ /** Blob contents by object id, in one batch. */
78
+ readBaseBlobs: (objectIds: readonly string[]) => Map<string, Buffer>;
79
+ /** Paths marked -diff on either side must never enter the text merger. */
80
+ readBinaryPaths?: (relativePaths: readonly string[]) => ReadonlySet<string>;
81
+ }): WorkspaceHydrationMergeDecision;
82
+ /** Batch attribute reads, with NUL delimiters so arbitrary Git filenames stay intact. */
83
+ export declare function readHydrationBinaryPaths(input: {
84
+ checkoutRoot: string;
85
+ outerRoot: string;
86
+ relativePaths: readonly string[];
87
+ }): Set<string>;
88
+ /**
89
+ * Parse `git diff-tree -r -z --no-renames --raw <base tree> <theirs tree>`
90
+ * output into per-path changes. Records come in pairs: the status line
91
+ * (`:<base mode> <theirs mode> <base id> <theirs id> <status>`) and the path.
92
+ */
93
+ export declare function parseWorkspaceSubtreeChanges(raw: Buffer): Map<string, WorkspaceSubtreeChange>;
94
+ /** The mount-relative path of a checkout entry as its absolute path, for diagnostics. */
95
+ export declare function describeHydrationSkipPaths(mountSourcePath: string, paths: readonly string[]): string;
@@ -0,0 +1,10 @@
1
+ /** Wire evidence owned by the standalone worker package; no API runtime dependency. */
2
+ export type WorkerHydrationRecovery = {
3
+ transactionId: string;
4
+ transactionPath: string;
5
+ mountIds: string[];
6
+ previousHeads: Record<string, string>;
7
+ retainedPaths?: string[];
8
+ };
9
+ /** Extract durable recovery evidence even through executor/AggregateError wrapping. */
10
+ export declare function findHydrationRecovery(error: unknown): WorkerHydrationRecovery | undefined;
@@ -1,4 +1,4 @@
1
- import { type WorkingTreeSourceMode } from "./working-tree-mirror";
1
+ import { type ProjectedWorkingTreeEntry, type WorkingTreeSourceMode } from "./working-tree-mirror";
2
2
  export type WorkspaceMergeProjectionSupport = {
3
3
  supported: true;
4
4
  } | {
@@ -11,15 +11,32 @@ export type WorkspaceMergeProjectionMount = {
11
11
  workspaceRelativePath: string;
12
12
  sourceMode: WorkingTreeSourceMode;
13
13
  };
14
+ /**
15
+ * `projectedFiles` records what the synthesis read for every visible entry
16
+ * (merge hydration's basis for "changed since projection"). A `stale_rewrite`
17
+ * result means the checkout holds, at the listed paths, exactly the bytes the
18
+ * last hydration replaced; nothing was merged and the mount must stay pinned.
19
+ */
14
20
  export type WorkspaceMergeProjectionResult = {
15
21
  kind: "clean";
16
22
  resultTree: string;
17
23
  oursCommit: string;
24
+ projectedFiles: Record<string, ProjectedWorkingTreeEntry>;
25
+ readAtMs: number;
26
+ /** Ledger paths whose checkout content moved on from both the replaced and the basis bytes (or that are gone). */
27
+ staleRewriteCleared: string[];
18
28
  } | {
19
29
  kind: "conflict";
20
30
  oursCommit: string;
21
31
  conflictPaths: string[];
22
32
  error: string;
33
+ projectedFiles: Record<string, ProjectedWorkingTreeEntry>;
34
+ readAtMs: number;
35
+ staleRewriteCleared: string[];
36
+ } | {
37
+ kind: "stale_rewrite";
38
+ paths: string[];
39
+ staleRewriteCleared: string[];
23
40
  };
24
41
  declare function gitVersionIsSupported(version: string): boolean;
25
42
  export declare function workspaceMergeProjectionSupport(): WorkspaceMergeProjectionSupport;
@@ -29,6 +46,7 @@ export declare function mergeWorkspaceProjectionMount(input: {
29
46
  basisHead: string;
30
47
  currentHead: string;
31
48
  attemptId: string;
49
+ staleRewriteBlobs?: Readonly<Record<string, string>>;
32
50
  }): WorkspaceMergeProjectionResult;
33
51
  export declare function materializeWorkspaceProjectionTree(input: {
34
52
  workspacePath: string;
@@ -10,25 +10,39 @@ type ProjectConfiguration = {
10
10
  projectId: string;
11
11
  };
12
12
  type ProjectConfigurationWithBranches = ProjectConfiguration & {
13
+ executionDisabled?: boolean;
13
14
  branches: readonly {
14
15
  branchName: string;
15
16
  }[];
16
17
  };
18
+ /**
19
+ * Every executable checkout of a configuration whose reconciliation did not
20
+ * run. Frozen projects expose neither ready nor pending execution targets, so
21
+ * the report stays inside the server's executable checkout ledger.
22
+ */
17
23
  export declare function deferredProjectConfigurationPendingBranches(projects: readonly ProjectConfigurationWithBranches[]): Array<{
18
24
  projectId: string;
19
25
  branchName: string;
20
26
  }>;
27
+ export type BusyProjectConfigurationChangeReason = "added" | "removed" | "initializing" | "changed";
28
+ export type BusyProjectConfigurationChange = {
29
+ projectId: string;
30
+ reason: BusyProjectConfigurationChangeReason;
31
+ };
21
32
  /**
22
33
  * Identify configuration changes that would touch a project while visible
23
34
  * checkout activity is active or reserved. A visible-projects target covers
24
- * every project; canonical-sync activity is isolated in the outer clone.
35
+ * every project; canonical-sync activity is isolated in the outer clone. The
36
+ * reason names why the busy project cannot take the configuration now, so a
37
+ * deferral can be diagnosed without reconstructing both generations.
25
38
  */
26
- export declare function busyProjectConfigurationChangeIds<TProject extends ProjectConfiguration>(input: {
39
+ export declare function busyProjectConfigurationChanges<TProject extends ProjectConfiguration>(input: {
27
40
  currentProjects: readonly TProject[];
28
41
  incomingProjects: readonly TProject[];
29
42
  activeTargets: Iterable<ProjectTarget | WorkspaceTarget>;
30
43
  currentFingerprint: (project: TProject) => string;
31
44
  incomingFingerprint: (project: TProject) => string;
32
45
  currentProjectReady: (project: TProject) => boolean;
33
- }): string[];
46
+ }): BusyProjectConfigurationChange[];
47
+ export declare function describeBusyProjectConfigurationChanges(changes: readonly BusyProjectConfigurationChange[]): string;
34
48
  export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ricsam/r5d-worker",
3
- "version": "0.0.131",
3
+ "version": "0.0.133",
4
4
  "type": "module",
5
5
  "main": "./dist/cjs/main.cjs",
6
6
  "module": "./dist/mjs/main.mjs",
@@ -21,7 +21,7 @@
21
21
  "r5d-worker": "dist/cjs/main.cjs"
22
22
  },
23
23
  "dependencies": {
24
- "@ricsam/r5d-api": "^0.0.131",
24
+ "@ricsam/r5d-api": "^0.0.133",
25
25
  "node-pty": "^1.1.0",
26
26
  "picomatch": "^4.0.3"
27
27
  },