@anionex/dsh-turn-rewind 0.2.2 → 0.3.0

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/src/validate.ts CHANGED
@@ -102,7 +102,8 @@ export function parseManifest(value: unknown): RestorePointManifest {
102
102
  }
103
103
  if (version === LEDGER_FORMAT_VERSION) return { version, ...common }
104
104
  if (restorePointKind !== 'turn') corrupt('Git-native v2 restore points are automatic turn checkpoints')
105
- return { version, ...common, git: parseGitCheckpoint(record.git, id) }
105
+ if (repository.type !== 'git') corrupt('Git-native v2 restore points must fence a Git workspace')
106
+ return { version, ...common, repository, git: parseGitCheckpoint(record.git, id) }
106
107
  }
107
108
 
108
109
  /** Parse an untrusted durable restore-operation journal. */
@@ -158,9 +159,27 @@ export function validateBlobHash(value: string): string {
158
159
  return value
159
160
  }
160
161
 
162
+ /**
163
+ * Parse the durable workspace fence of one manifest.
164
+ *
165
+ * A missing `type` is the only legacy interpretation: manifests written before
166
+ * ordinary-directory support are always Git workspaces and are upgraded in
167
+ * memory. Directory fences must not carry Git-only fields.
168
+ * @param value - persisted `repository` field.
169
+ * @returns the validated workspace state.
170
+ */
161
171
  function parseRepository(value: unknown): RepositoryState {
162
172
  const record = objectRecord(value, 'repository state')
163
173
  const root = absoluteString(record, 'root')
174
+ if (record.type === 'directory') {
175
+ for (const key of ['commonDir', 'head', 'branch', 'operation', 'stagedPaths']) {
176
+ if (record[key] !== undefined) corrupt(`directory workspace state must not contain ${key}`)
177
+ }
178
+ return { type: 'directory', root }
179
+ }
180
+ if (record.type !== undefined && record.type !== 'git') {
181
+ corrupt('workspace state type must be "git" or "directory"')
182
+ }
164
183
  const commonDir = absoluteString(record, 'commonDir')
165
184
  const head = optionalHash(record, 'head')
166
185
  const branch = optionalString(record, 'branch')
@@ -169,6 +188,7 @@ function parseRepository(value: unknown): RepositoryState {
169
188
  if (!Array.isArray(stagedValue)) corrupt('repository stagedPaths must be an array')
170
189
  const stagedPaths = stagedValue.map((path) => validateRelativePath(requireString(path, 'staged path')))
171
190
  return {
191
+ type: 'git',
172
192
  root,
173
193
  commonDir,
174
194
  ...(head === undefined ? {} : { head }),
@@ -0,0 +1,66 @@
1
+ import { discoverDirectory, discoverDirectoryRoot, type DirectorySnapshotSource } from './directory.js'
2
+ import {
3
+ discoverRepositoryOptional,
4
+ discoverRepositoryRootOptional,
5
+ sameRepositoryFence,
6
+ type RepositorySnapshotSource,
7
+ } from './git.js'
8
+ import type { ResolvedChangeLedgerConfig, WorkspaceState } from './types.js'
9
+
10
+ /** Eligible snapshot source for either supported workspace mode. */
11
+ export type WorkspaceSnapshotSource = RepositorySnapshotSource | DirectorySnapshotSource
12
+
13
+ /**
14
+ * Discover the owning Git worktree, or treat `cwd` itself as an ordinary
15
+ * directory when it is not inside any repository.
16
+ * @param cwd - working directory the Session runs in.
17
+ * @param config - capture limits applied to ordinary-directory enumeration.
18
+ * @param signal - optional caller cancellation.
19
+ * @returns the snapshot source for the owning workspace.
20
+ */
21
+ export async function discoverWorkspace(
22
+ cwd: string,
23
+ config: Pick<ResolvedChangeLedgerConfig, 'maxFiles' | 'maxFileBytes'>,
24
+ signal?: AbortSignal,
25
+ ): Promise<WorkspaceSnapshotSource> {
26
+ const repository = await discoverRepositoryOptional(cwd, signal)
27
+ return repository ?? discoverDirectory(cwd, config, signal)
28
+ }
29
+
30
+ /** Workspace mode and root resolved without inventorying any files. */
31
+ export interface WorkspaceIdentity {
32
+ readonly type: WorkspaceState['type']
33
+ readonly root: string
34
+ }
35
+
36
+ /**
37
+ * Resolve the workspace mode and root without inventorying files, for callers
38
+ * that only need to address the workspace (locks, skip markers, storage purge).
39
+ * @param cwd - working directory the Session runs in.
40
+ * @param signal - optional caller cancellation.
41
+ * @returns the workspace mode and canonical root.
42
+ */
43
+ export async function discoverWorkspaceIdentity(cwd: string, signal?: AbortSignal): Promise<WorkspaceIdentity> {
44
+ const root = await discoverRepositoryRootOptional(cwd, signal)
45
+ return root === undefined
46
+ ? { type: 'directory', root: await discoverDirectoryRoot(cwd) }
47
+ : { type: 'git', root }
48
+ }
49
+
50
+ /** Resolve the workspace root without inventorying its files. */
51
+ export async function discoverWorkspaceRoot(cwd: string, signal?: AbortSignal): Promise<string> {
52
+ return (await discoverWorkspaceIdentity(cwd, signal)).root
53
+ }
54
+
55
+ /**
56
+ * Whether two fences still describe the same workspace.
57
+ *
58
+ * Git workspaces keep the full repository fence (HEAD, branch, operation);
59
+ * ordinary directories only own their canonical root, so a moved or recreated
60
+ * root is the only thing that can invalidate them.
61
+ */
62
+ export function sameWorkspaceFence(left: WorkspaceState, right: WorkspaceState): boolean {
63
+ if (left.type !== right.type || left.root !== right.root) return false
64
+ if (left.type === 'directory' || right.type === 'directory') return true
65
+ return sameRepositoryFence(left, right)
66
+ }