@edspencer/paddock 0.71.2 → 0.72.1

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 (106) hide show
  1. package/package.json +1 -1
  2. package/packages/server/dist/app.js +27 -0
  3. package/packages/server/dist/cli/args.d.ts +56 -5
  4. package/packages/server/dist/cli/args.js +193 -31
  5. package/packages/server/dist/cli/config.d.ts +157 -0
  6. package/packages/server/dist/cli/config.js +462 -0
  7. package/packages/server/dist/cli/paddock.js +53 -3
  8. package/packages/server/dist/config.d.ts +36 -1
  9. package/packages/server/dist/config.js +59 -44
  10. package/packages/server/dist/herdctl.d.ts +45 -0
  11. package/packages/server/dist/herdctl.js +58 -4
  12. package/packages/server/dist/instance-config.d.ts +149 -21
  13. package/packages/server/dist/instance-config.js +228 -5
  14. package/packages/server/dist/last-activity.d.ts +21 -0
  15. package/packages/server/dist/last-activity.js +7 -2
  16. package/packages/server/dist/profiles.d.ts +154 -0
  17. package/packages/server/dist/profiles.js +214 -0
  18. package/packages/server/dist/routes/transcripts.d.ts +61 -0
  19. package/packages/server/dist/routes/transcripts.js +676 -0
  20. package/packages/server/dist/routes.d.ts +3 -0
  21. package/packages/server/dist/routes.js +2 -0
  22. package/packages/server/dist/transcript-move.d.ts +62 -0
  23. package/packages/server/dist/transcript-move.js +179 -0
  24. package/packages/server/dist/transcripts-migration-execute.d.ts +144 -0
  25. package/packages/server/dist/transcripts-migration-execute.js +630 -0
  26. package/packages/server/dist/transcripts-migration.d.ts +336 -0
  27. package/packages/server/dist/transcripts-migration.js +917 -0
  28. package/packages/server/dist/transcripts.d.ts +46 -2
  29. package/packages/server/dist/transcripts.js +126 -3
  30. package/packages/server/package.json +1 -1
  31. package/packages/web/dist/assets/DiscoverPage-DhrXvgkl.js +2 -0
  32. package/packages/web/dist/assets/DiscoverView-yPLCp_YE.js +2 -0
  33. package/packages/web/dist/assets/InstanceConfigPage-aOgiBFLq.js +2 -0
  34. package/packages/web/dist/assets/{MarkdownRenderer-DA5CpdGI.js → MarkdownRenderer-BjrB0TDO.js} +1 -1
  35. package/packages/web/dist/assets/ProjectMenu-PhwXTmgn.js +2 -0
  36. package/packages/web/dist/assets/{ProjectRedirect-k10uM_63.js → ProjectRedirect-N4g7WrZm.js} +1 -1
  37. package/packages/web/dist/assets/ProjectView-B-7AIt--.js +13 -0
  38. package/packages/web/dist/assets/{ProjectsGrid-DrzZ7R8M.js → ProjectsGrid-DZF5VHJF.js} +1 -1
  39. package/packages/web/dist/assets/RootHome-C9wEtWHu.js +2 -0
  40. package/packages/web/dist/assets/Surfaces-BidCkENw.js +2 -0
  41. package/packages/web/dist/assets/{arc-BKAVFR26.js → arc-BctfAqLY.js} +1 -1
  42. package/packages/web/dist/assets/architectureDiagram-3BPJPVTR-BJgwSqOO.js +37 -0
  43. package/packages/web/dist/assets/{blockDiagram-GPEHLZMM-Eu4BV7Ea.js → blockDiagram-GPEHLZMM-BD2skmST.js} +1 -1
  44. package/packages/web/dist/assets/{c4Diagram-AAUBKEIU-DR8PZaoa.js → c4Diagram-AAUBKEIU-DckMrhMv.js} +1 -1
  45. package/packages/web/dist/assets/channel-CsZOz8jm.js +2 -0
  46. package/packages/web/dist/assets/{chunk-2J33WTMH-CgvIIrI1.js → chunk-2J33WTMH-CQDUH7rp.js} +1 -1
  47. package/packages/web/dist/assets/{chunk-4BX2VUAB-CyJWrZqN.js → chunk-4BX2VUAB-Bae1J961.js} +1 -1
  48. package/packages/web/dist/assets/{chunk-55IACEB6-DXEnZN1d.js → chunk-55IACEB6-CXxS5wOa.js} +1 -1
  49. package/packages/web/dist/assets/{chunk-727SXJPM-Co89QTmp.js → chunk-727SXJPM-DSNFkPB3.js} +1 -1
  50. package/packages/web/dist/assets/{chunk-AQP2D5EJ-BRxTzfev.js → chunk-AQP2D5EJ-B2mqFyB7.js} +1 -1
  51. package/packages/web/dist/assets/{chunk-FMBD7UC4-DUj18CvF.js → chunk-FMBD7UC4-BfqEy3lu.js} +1 -1
  52. package/packages/web/dist/assets/{chunk-ND2GUHAM-BQMMJobN.js → chunk-ND2GUHAM-Bm6CBVK7.js} +1 -1
  53. package/packages/web/dist/assets/{chunk-QZHKN3VN-COx8LZCe.js → chunk-QZHKN3VN-NmnLtgYc.js} +1 -1
  54. package/packages/web/dist/assets/classDiagram-4FO5ZUOK-DdALkhE9.js +2 -0
  55. package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-DdALkhE9.js +2 -0
  56. package/packages/web/dist/assets/{core-DnpcT0s6.js → core-YKeFSK1q.js} +1 -1
  57. package/packages/web/dist/assets/cose-bilkent-S5V4N54A-qf_mXpa9.js +2 -0
  58. package/packages/web/dist/assets/{dagre-BM42HDAG-CuaYJexd.js → dagre-BM42HDAG-C31v-j3s.js} +1 -1
  59. package/packages/web/dist/assets/{diagram-2AECGRRQ-C9_anxrA.js → diagram-2AECGRRQ-C_TiPHdS.js} +1 -1
  60. package/packages/web/dist/assets/{diagram-5GNKFQAL-Ckf3kFlY.js → diagram-5GNKFQAL-BC8gUFK-.js} +1 -1
  61. package/packages/web/dist/assets/{diagram-KO2AKTUF-BrpUuY5x.js → diagram-KO2AKTUF-vO_QYWxi.js} +1 -1
  62. package/packages/web/dist/assets/{diagram-LMA3HP47-B9lyXZXW.js → diagram-LMA3HP47-vxJ-YZOg.js} +1 -1
  63. package/packages/web/dist/assets/{diagram-OG6HWLK6-DT7ro32m.js → diagram-OG6HWLK6-BNIfJG6l.js} +1 -1
  64. package/packages/web/dist/assets/{erDiagram-TEJ5UH35-ZZn9bHQh.js → erDiagram-TEJ5UH35-B37HsOFM.js} +1 -1
  65. package/packages/web/dist/assets/{flowDiagram-I6XJVG4X-BxQ82PY_.js → flowDiagram-I6XJVG4X-DqA5X81H.js} +1 -1
  66. package/packages/web/dist/assets/ganttDiagram-6RSMTGT7-BMP0zpIg.js +293 -0
  67. package/packages/web/dist/assets/{gitGraphDiagram-PVQCEYII-BlwODdH9.js → gitGraphDiagram-PVQCEYII-P0nXaCzx.js} +1 -1
  68. package/packages/web/dist/assets/index-BkY_Nc_i.css +1 -0
  69. package/packages/web/dist/assets/index-C3MdluUb.js +71 -0
  70. package/packages/web/dist/assets/{infoDiagram-5YYISTIA-nanSBFyG.js → infoDiagram-5YYISTIA-DpKYQ7VK.js} +1 -1
  71. package/packages/web/dist/assets/{ishikawaDiagram-YF4QCWOH-yZHr_s28.js → ishikawaDiagram-YF4QCWOH-BZeRMg3C.js} +1 -1
  72. package/packages/web/dist/assets/{journeyDiagram-JHISSGLW-BKjDqQEQ.js → journeyDiagram-JHISSGLW-Cg2O0qzv.js} +1 -1
  73. package/packages/web/dist/assets/{kanban-definition-UN3LZRKU-QusvHqc6.js → kanban-definition-UN3LZRKU-DBwVxUvI.js} +1 -1
  74. package/packages/web/dist/assets/{linear-4xRoD38Z.js → linear-R8qRwYqx.js} +1 -1
  75. package/packages/web/dist/assets/{mermaid.core-CeulYvDv.js → mermaid.core-jctTs2en.js} +4 -4
  76. package/packages/web/dist/assets/{mindmap-definition-RKZ34NQL-DSjYt2Kp.js → mindmap-definition-RKZ34NQL-CtmsNfpb.js} +1 -1
  77. package/packages/web/dist/assets/{pieDiagram-4H26LBE5-Ci0gO3CA.js → pieDiagram-4H26LBE5-C-UiLFDv.js} +1 -1
  78. package/packages/web/dist/assets/{quadrantDiagram-W4KKPZXB-y2qHkaHP.js → quadrantDiagram-W4KKPZXB-BS4SWa01.js} +1 -1
  79. package/packages/web/dist/assets/{requirementDiagram-4Y6WPE33-D9hiWbad.js → requirementDiagram-4Y6WPE33-LPPAC6Q1.js} +1 -1
  80. package/packages/web/dist/assets/{sankeyDiagram-5OEKKPKP-DA3Mvoze.js → sankeyDiagram-5OEKKPKP-tjF8KyS3.js} +1 -1
  81. package/packages/web/dist/assets/{sequenceDiagram-3UESZ5HK-DWACrljw.js → sequenceDiagram-3UESZ5HK-kmddkdk2.js} +1 -1
  82. package/packages/web/dist/assets/{stateDiagram-AJRCARHV-CFGEhoTb.js → stateDiagram-AJRCARHV-B07mykTY.js} +1 -1
  83. package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-r6WdX0dw.js +2 -0
  84. package/packages/web/dist/assets/{timeline-definition-PNZ67QCA-Cjygpd9e.js → timeline-definition-PNZ67QCA-DrUgi0cT.js} +1 -1
  85. package/packages/web/dist/assets/{vennDiagram-CIIHVFJN-zQnLaxgv.js → vennDiagram-CIIHVFJN-Dz6p0m7l.js} +1 -1
  86. package/packages/web/dist/assets/{wardley-L42UT6IY-DGmt-Lo2.js → wardley-L42UT6IY-DM16lA5x.js} +1 -1
  87. package/packages/web/dist/assets/{wardleyDiagram-YWT4CUSO-Dj3vDukN.js → wardleyDiagram-YWT4CUSO-CCFQkFGB.js} +1 -1
  88. package/packages/web/dist/assets/{xychartDiagram-2RQKCTM6-C0bGbwbL.js → xychartDiagram-2RQKCTM6-DiUI5Dos.js} +1 -1
  89. package/packages/web/dist/index.html +2 -2
  90. package/packages/web/dist/sw.js +1 -1
  91. package/packages/web/dist/assets/Chip-Cp32dDmz.js +0 -2
  92. package/packages/web/dist/assets/DiscoverPage-Cs439wTf.js +0 -2
  93. package/packages/web/dist/assets/DiscoverView-DoGAlUXT.js +0 -2
  94. package/packages/web/dist/assets/InstanceConfigPage-BDypeo_r.js +0 -2
  95. package/packages/web/dist/assets/ProjectMenu-DXXXtXeh.js +0 -2
  96. package/packages/web/dist/assets/ProjectView-BxjcMlSO.js +0 -13
  97. package/packages/web/dist/assets/RootHome-rWodzOC8.js +0 -2
  98. package/packages/web/dist/assets/architectureDiagram-3BPJPVTR-UzIlxaCQ.js +0 -37
  99. package/packages/web/dist/assets/channel-C-sodtn7.js +0 -2
  100. package/packages/web/dist/assets/classDiagram-4FO5ZUOK-D2eBPVdq.js +0 -2
  101. package/packages/web/dist/assets/classDiagram-v2-Q7XG4LA2-D2eBPVdq.js +0 -2
  102. package/packages/web/dist/assets/cose-bilkent-S5V4N54A-B-4NadKI.js +0 -2
  103. package/packages/web/dist/assets/ganttDiagram-6RSMTGT7-BxliloOH.js +0 -293
  104. package/packages/web/dist/assets/index-Cx9y6c3f.js +0 -71
  105. package/packages/web/dist/assets/index-bC5uCujx.css +0 -1
  106. package/packages/web/dist/assets/stateDiagram-v2-BHNVJYJU-CLfbDXLX.js +0 -2
@@ -0,0 +1,336 @@
1
+ import { type TranscriptsMode } from "./transcripts.js";
2
+ /**
3
+ * The three-state classification the modal's table renders, plus the budget
4
+ * escape hatch.
5
+ *
6
+ * `unknown` is not a fourth kind of conflict — it is "we stopped looking", and
7
+ * it is treated as `diverged` (default unchecked) because that is the
8
+ * conservative direction.
9
+ */
10
+ export type MigrationState = "new" | "fast-forward" | "diverged" | "unknown";
11
+ /** Why the banner is not offered. */
12
+ export type MigrationIneligibleReason = "already-host" | "env-shadowed" | "profile-paranoid" | "nothing-pending" | "scan-failed";
13
+ /** Non-fatal conditions the modal should surface. */
14
+ export type MigrationWarningCode = "host-store-unreadable" | "chats-dir-unreadable" | "env-shadowed" | "memory-collision" | "unexpected-entries";
15
+ /** The env var that shadows `claude.transcripts` — see `config.ts:1481`. */
16
+ export declare const TRANSCRIPTS_ENV_VAR = "PADDOCK_CLAUDE_TRANSCRIPTS";
17
+ /**
18
+ * Where unchecked chats are preserved: a SIBLING of `.chats/`, not a child.
19
+ *
20
+ * #882 specified `.chats/pre-migration/`, which cannot work — that directory is
21
+ * "anything", so `pointChatsDirAt` declines the redirect and the migration
22
+ * built to fix #708 ships #708's symptom. Design §5.1 verified it against the
23
+ * real `ensureProjectChats`. Named here because the plan reports the path and
24
+ * the execute half must use the same one.
25
+ */
26
+ export declare const PRESERVE_DIR_NAME = ".chats-pre-migration";
27
+ /**
28
+ * Bytes stage 3 (and the diverged rows' comparison columns) may read across one
29
+ * whole request. The worst case is a *product* of chat count and transcript
30
+ * size and neither is under our control, so it is bounded rather than trusted.
31
+ */
32
+ export declare const DIVERGENCE_SCAN_BUDGET_BYTES: number;
33
+ /** One copy of a chat, for a diverged row's comparison columns. */
34
+ export interface MigrationSide {
35
+ /** Absolute path of this copy. */
36
+ path: string;
37
+ sizeBytes: number;
38
+ /** Filesystem mtime. Present always; NOT a proxy for activity (#863). */
39
+ mtime: string;
40
+ /**
41
+ * Conversation records — `user`/`assistant`, excluding meta and
42
+ * task-notification records (the `last-activity.ts` rule, shared not copied).
43
+ *
44
+ * Present only where a decision needs it: on `diverged` rows, and only while
45
+ * the scan budget lasts. Counting it requires a full parse of the file, so
46
+ * populating it on every row would cost a complete read of every transcript
47
+ * and make the plan endpoint's realistic case ~60× its measured cost. See the
48
+ * PR body for why this deviates from the design's `required` list.
49
+ */
50
+ messageCount?: number;
51
+ /** ISO 8601 timestamp of the last real message; absent when none is datable. */
52
+ lastMessageAt?: string;
53
+ }
54
+ /** One row of the modal's table.
55
+ *
56
+ * `identical` is deliberately NOT a {@link MigrationState}: the plan omits
57
+ * identical chats from its rows entirely, because there is no decision for the
58
+ * user to make about one, and widening the published enum would put a value in
59
+ * the OpenAPI contract that the table can never render. The EXECUTE half still
60
+ * needs to know — an identical chat has a copy on both sides and therefore has
61
+ * a disposition (host's survives, Paddock's is preserved) — so the scan carries
62
+ * it as a flag on the row and {@link buildMigrationPlan} filters those rows out.
63
+ */
64
+ export interface MigrationChatRow {
65
+ sessionId: string;
66
+ /** The chat's set name, else its auto-name. Absent when neither exists. */
67
+ name?: string;
68
+ /** First user message, truncated. Absent when unreadable. */
69
+ preview?: string;
70
+ state: MigrationState;
71
+ /** How the checkbox starts: true for `new`/`fast-forward`. */
72
+ defaultSelected: boolean;
73
+ /** The copy in `.chats/`. Always present. */
74
+ own: MigrationSide;
75
+ /** The copy in the host store. Absent if and only if state is `new`. */
76
+ host?: MigrationSide;
77
+ /** For `fast-forward`, which side is the descendant and will survive. */
78
+ ahead?: "own" | "host";
79
+ /** Sidecars that move with this chat: `<id>/…` and `.reverts/<id>-*.jsonl`. */
80
+ extras: string[];
81
+ /**
82
+ * Byte-identical on both sides. Internal to the scan — stripped from the
83
+ * plan's rows and counted in `totals.identical` instead. See the doc above.
84
+ */
85
+ identical?: boolean;
86
+ }
87
+ /** One project's slice of the plan. */
88
+ export interface MigrationProjectPlan {
89
+ /** Project slug; the empty string is the root workspace. */
90
+ slug: string;
91
+ name: string;
92
+ chatsDir: string;
93
+ hostStore: string;
94
+ preserveDir: string;
95
+ chats: MigrationChatRow[];
96
+ /**
97
+ * Entries of `.chats/` that move with the PROJECT rather than with any one
98
+ * chat — `memory/`, flat `agent-<hex>.jsonl` sidechain transcripts, an
99
+ * orphaned `<id>/` with no transcript.
100
+ *
101
+ * Not in the design's schema. Added because the postcondition moves them and
102
+ * the standing rule is that nothing moves unannounced: without this the
103
+ * completion summary could not say "and 132 memory files", which is the
104
+ * single sharpest item in the design's own inventory (§5).
105
+ */
106
+ projectExtras: string[];
107
+ }
108
+ export interface MigrationWarning {
109
+ code: MigrationWarningCode;
110
+ /** The project it applies to. Absent for instance-wide warnings. */
111
+ slug?: string;
112
+ message: string;
113
+ paths?: string[];
114
+ }
115
+ export interface MigrationTotals {
116
+ chats: number;
117
+ new: number;
118
+ fastForward: number;
119
+ diverged: number;
120
+ unknown: number;
121
+ /** Identical on both sides, omitted from the rows. Reported so a count lower
122
+ * than the user's chat total always has an explanation. */
123
+ identical: number;
124
+ defaultSelected: number;
125
+ }
126
+ export interface MigrationPlan {
127
+ mode: TranscriptsMode;
128
+ configPath: string;
129
+ configVersion: string | null;
130
+ projects: MigrationProjectPlan[];
131
+ sweepers: {
132
+ stores: number;
133
+ chats: number;
134
+ };
135
+ totals: MigrationTotals;
136
+ scanBudgetExhausted: boolean;
137
+ warnings: MigrationWarning[];
138
+ }
139
+ export interface MigrationProbe {
140
+ mode: TranscriptsMode;
141
+ eligible: boolean;
142
+ reason?: MigrationIneligibleReason;
143
+ envVar?: string;
144
+ /**
145
+ * Transcripts that would migrate. A LOWER BOUND by contract — see
146
+ * {@link probeMigration} for why this implementation can afford to make it
147
+ * exact, and for the one case where a pending project reports 0.
148
+ */
149
+ pendingChats: number;
150
+ pendingProjects: number;
151
+ scannedProjects: number;
152
+ computedAt: string;
153
+ }
154
+ /** The subset of a project this module needs — kept minimal so it is testable
155
+ * against plain fixtures rather than a whole `ProjectStore`. */
156
+ export interface MigrationProjectRef {
157
+ slug: string;
158
+ name: string;
159
+ /** The METADATA dir — where `.chats/` lives. */
160
+ dir: string;
161
+ /** The agent's cwd — what the host store's encoded name is built from. NOT
162
+ * `dir` for a repo-backed or linked project (#187/#206). */
163
+ workingDir: string;
164
+ }
165
+ export interface MigrationInput {
166
+ /** The transcripts mode this process resolved at boot. */
167
+ mode: TranscriptsMode;
168
+ /** The resolved posture profile — `paranoid` suppresses the banner (§10.4). */
169
+ profile: string;
170
+ /** True when `PADDOCK_CLAUDE_TRANSCRIPTS` is set: the config write is inert. */
171
+ envShadowed: boolean;
172
+ projects: MigrationProjectRef[];
173
+ /** The USER's own `~/.claude` — the destination store's root. */
174
+ userHome: string;
175
+ /** Absolute path of the file the POST would write to. */
176
+ configPath: string;
177
+ /** Fingerprint of that file as read for this response. */
178
+ configVersion: string | null;
179
+ /** Sweeper working dirs (`<dataDir>/sweepers/<slug>/`), by slug. */
180
+ sweeperDirs?: Map<string, string>;
181
+ /** Per-session display name/preview, when the caller has them cheaply. */
182
+ names?: Map<string, {
183
+ name?: string;
184
+ preview?: string;
185
+ }>;
186
+ /**
187
+ * Override the per-request divergence-scan budget. Exists so a test can
188
+ * exhaust it with kilobytes instead of the 256 MB of pathological transcripts
189
+ * the real bound would need — the exhaustion path is the one that must not be
190
+ * silent, so it has to be exercised rather than reasoned about.
191
+ */
192
+ scanBudgetBytes?: number;
193
+ }
194
+ interface ChatsDirListing {
195
+ /** Session ids with a `<id>.jsonl` in `.chats/`. */
196
+ chats: string[];
197
+ /** Session ids with a `<id>/` sidecar directory. */
198
+ chatDirs: Set<string>;
199
+ /** Entries that move with the project rather than with a chat. */
200
+ projectExtras: string[];
201
+ /** Entries matching no known pattern. */
202
+ unexpected: string[];
203
+ hasReverts: boolean;
204
+ hasMemory: boolean;
205
+ /** Total entries — the probe's whole question. */
206
+ entryCount: number;
207
+ }
208
+ /** Drop the memoised probe answers (tests; a config write). */
209
+ export declare function resetMigrationProbeCache(): void;
210
+ /**
211
+ * "Is there anything to migrate?" — one `readdir` per project, no transcript
212
+ * read and no host-store read at all (§10.1).
213
+ *
214
+ * Memoised per project on `mtimeMs:size` of `.chats/` plus the config version,
215
+ * which costs one `stat` to validate — that is what lets the banner be fetched
216
+ * on every page load. The host-store term the design's §3.4 cache key carried
217
+ * is gone with the host-store read it was there for.
218
+ *
219
+ * ### `pendingChats` is a lower bound that this implementation makes exact
220
+ *
221
+ * The design frames it as a lower bound because the probe "stops counting once
222
+ * the answer is known". `fs.readdir` hands back the whole list in one call, so
223
+ * there is no per-entry short-circuit to be had and counting the `*.jsonl`
224
+ * entries is free — the number is exact. It is still *reported* as a lower
225
+ * bound, because that is the contract clients were given and narrowing it later
226
+ * is a breaking change; but the field is `>= 0` rather than `∈ {0, 1}`.
227
+ *
228
+ * The one wrinkle the corrected predicate introduces: eligibility is about
229
+ * ENTRIES and `pendingChats` counts TRANSCRIPTS, so a `.chats/` holding only
230
+ * `memory/` is `eligible: true` with `pendingChats: 0`. That combination is
231
+ * correct, not a contradiction — there is real work to do and no chat to do it
232
+ * to — and it makes the schema's "exact only when 0" gloss wrong in one
233
+ * direction. Callers must key the banner off `eligible`, never off the count.
234
+ *
235
+ * ### `mode === "host"` is not on its own a reason to refuse
236
+ *
237
+ * The predicate is the same under both modes, which is the #882 ruling (§2)
238
+ * superseding the design's §7.2. `host` shipped as an unconditional refusal on
239
+ * the reasoning "already migrated, nothing to offer", and that is wrong for the
240
+ * one instance that most needs the offer: flipping to `host` while `.chats/`
241
+ * still held anything is #708's STRANDED state. `pointChatsDirAt` declines the
242
+ * redirect against a non-empty real directory (`transcripts.ts:145`), so the old
243
+ * transcripts sit in a real `.chats/` that nothing reads — and the probe told
244
+ * exactly those users there was nothing to do.
245
+ *
246
+ * The distinction is the DIRECTORY, and it is why {@link readChatsDir} `lstat`s:
247
+ *
248
+ * | `.chats/` under `host` | Eligible | Why |
249
+ * |---|---|---|
250
+ * | symlink into the host store | no — `already-host` | migrated; the redirect is planted |
251
+ * | empty real directory | no — `already-host` | the next boot plants it by itself |
252
+ * | non-empty real directory | **yes** | stranded: the redirect was declined |
253
+ *
254
+ * The first two are the overwhelmingly common case and must stay ineligible —
255
+ * showing a migration banner to everyone who has already migrated is worse than
256
+ * the bug this narrows. Under `host` the execute path moves the files and skips
257
+ * the config write (`configWritten: false`, `restartRequired: true`); the next
258
+ * boot then finds an empty `.chats/`, plants the redirect, and the stranded
259
+ * chats reappear. That closes the `own → host` half of #708.
260
+ */
261
+ export declare function probeMigration(input: MigrationInput): Promise<MigrationProbe>;
262
+ /**
263
+ * One project, classified — everything both halves of the feature need, read
264
+ * from disk exactly once.
265
+ *
266
+ * Shared rather than duplicated because the execute half's step 4 is
267
+ * "re-enumerate from disk and reconcile the plan against reality" (§4.1): it
268
+ * needs the *same* classifier, not a similar one. A second enumeration written
269
+ * against the same directory shapes would drift, and the direction it would
270
+ * drift in is "execute moves a file the preview promised it would not".
271
+ */
272
+ export interface MigrationProjectScan {
273
+ ref: MigrationProjectRef;
274
+ chatsDir: string;
275
+ hostStore: string;
276
+ preserveDir: string;
277
+ listing: ChatsDirListing;
278
+ /** False when the host store could not be read; every row is then `unknown`. */
279
+ hostReadable: boolean;
280
+ /** `.reverts/` BASE NAMES owned by each chat (not paths — the move helper
281
+ * rebuilds both sides from the store roots). */
282
+ revertsByChat: Map<string, string[]>;
283
+ /** Every chat, INCLUDING the identical ones the plan omits. */
284
+ rows: MigrationChatRow[];
285
+ projectExtras: string[];
286
+ warnings: MigrationWarning[];
287
+ candidates: ScanCandidate[];
288
+ }
289
+ /** A conflicted pair that stage 2 could not settle, deferred to stage 3. */
290
+ interface ScanCandidate {
291
+ row: MigrationChatRow;
292
+ /** Path of the longer side — the one that would be scanned. */
293
+ longerPath: string;
294
+ longerBytes: number;
295
+ shorterPath: string;
296
+ shorterBytes: number;
297
+ /** Which side is longer, i.e. which would be `ahead` on a hit. */
298
+ longerSide: "own" | "host";
299
+ /** The ancestor-candidate uuid, or undefined when the tail read found none. */
300
+ uuid?: string;
301
+ }
302
+ /**
303
+ * Build the modal's plan: every chat in every project's `.chats/`, classified
304
+ * against its host store.
305
+ *
306
+ * Chats identical on both sides are omitted from the rows — there is no
307
+ * decision to make — but they are counted in `totals.identical` so a row count
308
+ * lower than the user's chat total always has an explanation. They still
309
+ * migrate: the postcondition is about `.chats/` being empty, not about which
310
+ * chats the user picked.
311
+ */
312
+ export declare function buildMigrationPlan(input: MigrationInput): Promise<MigrationPlan>;
313
+ /**
314
+ * Classify one project's `.chats/` against its host store.
315
+ *
316
+ * Returns `null` when there is nothing there to migrate: no `.chats/`, an
317
+ * already-planted symlink, or an empty directory. Everything else — including
318
+ * an unreadable store — comes back as a scan carrying its own warnings, because
319
+ * the execute half has to decide what to do about it rather than have it
320
+ * swallowed here.
321
+ *
322
+ * The stage-3 candidates are returned UNSETTLED. Settling them is one shared
323
+ * budget across the whole request, so the caller has to own it — see
324
+ * {@link settleCandidates}.
325
+ */
326
+ export declare function scanProject(project: MigrationProjectRef, input: MigrationInput): Promise<MigrationProjectScan | null>;
327
+ /**
328
+ * Stage 3 over the deferred candidates, smallest-first against one shared
329
+ * budget. Ascending order means the budget settles as many rows as it can
330
+ * before it runs out, and — because it is ascending — the first row that does
331
+ * not fit proves every later one does not either.
332
+ *
333
+ * Returns whether any row came back `unknown`.
334
+ */
335
+ export declare function settleCandidates(candidates: ScanCandidate[], budgetBytes: number): Promise<boolean>;
336
+ export {};