@indigoai-us/hq-cloud 6.14.50 → 6.15.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 (340) hide show
  1. package/dist/bin/sync-mutation.d.ts +14 -0
  2. package/dist/bin/sync-mutation.d.ts.map +1 -0
  3. package/dist/bin/sync-mutation.js +60 -0
  4. package/dist/bin/sync-mutation.js.map +1 -0
  5. package/dist/bin/sync-mutation.test.d.ts +2 -0
  6. package/dist/bin/sync-mutation.test.d.ts.map +1 -0
  7. package/dist/bin/sync-mutation.test.js +64 -0
  8. package/dist/bin/sync-mutation.test.js.map +1 -0
  9. package/dist/bin/sync-runner-company.d.ts +8 -0
  10. package/dist/bin/sync-runner-company.d.ts.map +1 -1
  11. package/dist/bin/sync-runner-company.js +16 -0
  12. package/dist/bin/sync-runner-company.js.map +1 -1
  13. package/dist/bin/sync-runner-company.test.d.ts +2 -0
  14. package/dist/bin/sync-runner-company.test.d.ts.map +1 -0
  15. package/dist/bin/sync-runner-company.test.js +36 -0
  16. package/dist/bin/sync-runner-company.test.js.map +1 -0
  17. package/dist/bin/sync-runner-watch-loop.d.ts.map +1 -1
  18. package/dist/bin/sync-runner-watch-loop.js +98 -8
  19. package/dist/bin/sync-runner-watch-loop.js.map +1 -1
  20. package/dist/bin/sync-runner.d.ts +17 -0
  21. package/dist/bin/sync-runner.d.ts.map +1 -1
  22. package/dist/bin/sync-runner.js.map +1 -1
  23. package/dist/bin/sync-runner.test.js +109 -0
  24. package/dist/bin/sync-runner.test.js.map +1 -1
  25. package/dist/cli/conflict-recovery.test.d.ts +2 -0
  26. package/dist/cli/conflict-recovery.test.d.ts.map +1 -0
  27. package/dist/cli/conflict-recovery.test.js +201 -0
  28. package/dist/cli/conflict-recovery.test.js.map +1 -0
  29. package/dist/cli/conflict.d.ts +60 -0
  30. package/dist/cli/conflict.d.ts.map +1 -1
  31. package/dist/cli/conflict.js +333 -0
  32. package/dist/cli/conflict.js.map +1 -1
  33. package/dist/cli/reindex-knowledge.d.ts +7 -0
  34. package/dist/cli/reindex-knowledge.d.ts.map +1 -0
  35. package/dist/cli/reindex-knowledge.js +389 -0
  36. package/dist/cli/reindex-knowledge.js.map +1 -0
  37. package/dist/cli/reindex-knowledge.test.d.ts +12 -0
  38. package/dist/cli/reindex-knowledge.test.d.ts.map +1 -0
  39. package/dist/cli/reindex-knowledge.test.js +260 -0
  40. package/dist/cli/reindex-knowledge.test.js.map +1 -0
  41. package/dist/cli/reindex.d.ts.map +1 -1
  42. package/dist/cli/reindex.js +8 -0
  43. package/dist/cli/reindex.js.map +1 -1
  44. package/dist/cli/rescue-settings-reconcile.test.js +47 -0
  45. package/dist/cli/rescue-settings-reconcile.test.js.map +1 -1
  46. package/dist/cli/sync.d.ts +27 -0
  47. package/dist/cli/sync.d.ts.map +1 -1
  48. package/dist/cli/sync.js +52 -0
  49. package/dist/cli/sync.js.map +1 -1
  50. package/dist/cli/sync.test.js +31 -1
  51. package/dist/cli/sync.test.js.map +1 -1
  52. package/dist/index.d.ts +2 -0
  53. package/dist/index.d.ts.map +1 -1
  54. package/dist/index.js +1 -0
  55. package/dist/index.js.map +1 -1
  56. package/dist/skill-telemetry.d.ts +6 -0
  57. package/dist/skill-telemetry.d.ts.map +1 -1
  58. package/dist/skill-telemetry.js +14 -2
  59. package/dist/skill-telemetry.js.map +1 -1
  60. package/dist/skill-telemetry.test.js +79 -0
  61. package/dist/skill-telemetry.test.js.map +1 -1
  62. package/dist/sync/candidate-uploader.d.ts +88 -0
  63. package/dist/sync/candidate-uploader.d.ts.map +1 -0
  64. package/dist/sync/candidate-uploader.js +212 -0
  65. package/dist/sync/candidate-uploader.js.map +1 -0
  66. package/dist/sync/candidate-uploader.test.d.ts +2 -0
  67. package/dist/sync/candidate-uploader.test.d.ts.map +1 -0
  68. package/dist/sync/candidate-uploader.test.js +132 -0
  69. package/dist/sync/candidate-uploader.test.js.map +1 -0
  70. package/dist/sync/delta-client.d.ts +73 -0
  71. package/dist/sync/delta-client.d.ts.map +1 -0
  72. package/dist/sync/delta-client.js +201 -0
  73. package/dist/sync/delta-client.js.map +1 -0
  74. package/dist/sync/delta-client.test.d.ts +2 -0
  75. package/dist/sync/delta-client.test.d.ts.map +1 -0
  76. package/dist/sync/delta-client.test.js +97 -0
  77. package/dist/sync/delta-client.test.js.map +1 -0
  78. package/dist/sync/durable-apply.d.ts +76 -0
  79. package/dist/sync/durable-apply.d.ts.map +1 -0
  80. package/dist/sync/durable-apply.js +530 -0
  81. package/dist/sync/durable-apply.js.map +1 -0
  82. package/dist/sync/durable-apply.test.d.ts +2 -0
  83. package/dist/sync/durable-apply.test.d.ts.map +1 -0
  84. package/dist/sync/durable-apply.test.js +180 -0
  85. package/dist/sync/durable-apply.test.js.map +1 -0
  86. package/dist/sync/event-sync.d.ts +33 -1
  87. package/dist/sync/event-sync.d.ts.map +1 -1
  88. package/dist/sync/event-sync.js +149 -1
  89. package/dist/sync/event-sync.js.map +1 -1
  90. package/dist/sync/event-sync.test.js +142 -1
  91. package/dist/sync/event-sync.test.js.map +1 -1
  92. package/dist/sync/index.d.ts +2 -0
  93. package/dist/sync/index.d.ts.map +1 -1
  94. package/dist/sync/index.js +1 -0
  95. package/dist/sync/index.js.map +1 -1
  96. package/dist/sync/multipart-uploader.d.ts +99 -0
  97. package/dist/sync/multipart-uploader.d.ts.map +1 -0
  98. package/dist/sync/multipart-uploader.js +447 -0
  99. package/dist/sync/multipart-uploader.js.map +1 -0
  100. package/dist/sync/multipart-uploader.test.d.ts +2 -0
  101. package/dist/sync/multipart-uploader.test.d.ts.map +1 -0
  102. package/dist/sync/multipart-uploader.test.js +119 -0
  103. package/dist/sync/multipart-uploader.test.js.map +1 -0
  104. package/dist/sync/mutation-client.d.ts +82 -0
  105. package/dist/sync/mutation-client.d.ts.map +1 -0
  106. package/dist/sync/mutation-client.js +221 -0
  107. package/dist/sync/mutation-client.js.map +1 -0
  108. package/dist/sync/mutation-client.test.d.ts +2 -0
  109. package/dist/sync/mutation-client.test.d.ts.map +1 -0
  110. package/dist/sync/mutation-client.test.js +51 -0
  111. package/dist/sync/mutation-client.test.js.map +1 -0
  112. package/dist/sync/push-receiver.d.ts +45 -0
  113. package/dist/sync/push-receiver.d.ts.map +1 -1
  114. package/dist/sync/push-receiver.js +101 -0
  115. package/dist/sync/push-receiver.js.map +1 -1
  116. package/dist/sync/push-receiver.test.js +54 -2
  117. package/dist/sync/push-receiver.test.js.map +1 -1
  118. package/dist/sync/scope-inventory-client.d.ts +69 -0
  119. package/dist/sync/scope-inventory-client.d.ts.map +1 -0
  120. package/dist/sync/scope-inventory-client.js +210 -0
  121. package/dist/sync/scope-inventory-client.js.map +1 -0
  122. package/dist/sync/scope-inventory-client.test.d.ts +2 -0
  123. package/dist/sync/scope-inventory-client.test.d.ts.map +1 -0
  124. package/dist/sync/scope-inventory-client.test.js +94 -0
  125. package/dist/sync/scope-inventory-client.test.js.map +1 -0
  126. package/dist/sync/snapshot-client.d.ts +98 -0
  127. package/dist/sync/snapshot-client.d.ts.map +1 -0
  128. package/dist/sync/snapshot-client.js +402 -0
  129. package/dist/sync/snapshot-client.js.map +1 -0
  130. package/dist/sync/snapshot-client.test.d.ts +2 -0
  131. package/dist/sync/snapshot-client.test.d.ts.map +1 -0
  132. package/dist/sync/snapshot-client.test.js +169 -0
  133. package/dist/sync/snapshot-client.test.js.map +1 -0
  134. package/dist/sync/uploader-finalization.d.ts +97 -0
  135. package/dist/sync/uploader-finalization.d.ts.map +1 -0
  136. package/dist/sync/uploader-finalization.js +273 -0
  137. package/dist/sync/uploader-finalization.js.map +1 -0
  138. package/dist/sync/uploader-finalization.test.d.ts +2 -0
  139. package/dist/sync/uploader-finalization.test.d.ts.map +1 -0
  140. package/dist/sync/uploader-finalization.test.js +92 -0
  141. package/dist/sync/uploader-finalization.test.js.map +1 -0
  142. package/dist/telemetry.d.ts +11 -1
  143. package/dist/telemetry.d.ts.map +1 -1
  144. package/dist/telemetry.js +21 -2
  145. package/dist/telemetry.js.map +1 -1
  146. package/dist/telemetry.test.js +80 -0
  147. package/dist/telemetry.test.js.map +1 -1
  148. package/package.json +6 -1
  149. package/.claude/policies/hq-cloud-esm-cannot-spy-fs-builtins.md +0 -30
  150. package/.claude/policies/hq-cloud-strip-types-no-parameter-properties.md +0 -22
  151. package/.github/workflows/ci.yml +0 -84
  152. package/.github/workflows/publish.yml +0 -56
  153. package/.github/workflows/unreleased-commits-nag.yml +0 -256
  154. package/eslint.config.js +0 -67
  155. package/pnpm-workspace.yaml +0 -2
  156. package/scripts/presign-transport-e2e.mjs +0 -250
  157. package/scripts/vault-rebaseline.sh +0 -323
  158. package/scripts/vault-rescue.sh +0 -332
  159. package/src/active-company.test.ts +0 -188
  160. package/src/active-company.ts +0 -168
  161. package/src/agent-codex-instructions.test.ts +0 -332
  162. package/src/agent-codex-instructions.ts +0 -309
  163. package/src/auth.ts +0 -146
  164. package/src/backup-prune.test.ts +0 -98
  165. package/src/backup-prune.ts +0 -182
  166. package/src/bin/backup-prune-runner.ts +0 -33
  167. package/src/bin/rescue-runner.ts +0 -25
  168. package/src/bin/sync-runner-company.ts +0 -695
  169. package/src/bin/sync-runner-events.test.ts +0 -143
  170. package/src/bin/sync-runner-events.ts +0 -55
  171. package/src/bin/sync-runner-planning.test.ts +0 -311
  172. package/src/bin/sync-runner-planning.ts +0 -258
  173. package/src/bin/sync-runner-rollup.test.ts +0 -37
  174. package/src/bin/sync-runner-rollup.ts +0 -97
  175. package/src/bin/sync-runner-telemetry.ts +0 -15
  176. package/src/bin/sync-runner-watch-loop.ts +0 -1235
  177. package/src/bin/sync-runner-watch-routes.test.ts +0 -71
  178. package/src/bin/sync-runner-watch-routes.ts +0 -184
  179. package/src/bin/sync-runner.test.ts +0 -8767
  180. package/src/bin/sync-runner.ts +0 -2190
  181. package/src/cli/accept.ts +0 -124
  182. package/src/cli/conflict.ts +0 -119
  183. package/src/cli/doctor.test.ts +0 -581
  184. package/src/cli/doctor.ts +0 -642
  185. package/src/cli/index.ts +0 -49
  186. package/src/cli/invite.test.ts +0 -250
  187. package/src/cli/invite.ts +0 -214
  188. package/src/cli/promote.ts +0 -157
  189. package/src/cli/reindex.test.ts +0 -957
  190. package/src/cli/reindex.ts +0 -970
  191. package/src/cli/rescue-classify-ordering.test.ts +0 -548
  192. package/src/cli/rescue-clone-diagnostics.test.ts +0 -120
  193. package/src/cli/rescue-core.ts +0 -3011
  194. package/src/cli/rescue-drift-reconcile.test.ts +0 -179
  195. package/src/cli/rescue-drop-dir-symlink.test.ts +0 -224
  196. package/src/cli/rescue-exec-bit-preserve.test.ts +0 -187
  197. package/src/cli/rescue-hq-root-guard.test.ts +0 -232
  198. package/src/cli/rescue-journal-reconcile.test.ts +0 -215
  199. package/src/cli/rescue-mtime-preserve.test.ts +0 -203
  200. package/src/cli/rescue-settings-reconcile.test.ts +0 -583
  201. package/src/cli/rescue-snapshot.test.ts +0 -57
  202. package/src/cli/rescue-snapshot.ts +0 -51
  203. package/src/cli/rescue.reindex.test.ts +0 -63
  204. package/src/cli/rescue.test.ts +0 -131
  205. package/src/cli/rescue.ts +0 -182
  206. package/src/cli/share.test.ts +0 -7843
  207. package/src/cli/share.ts +0 -3663
  208. package/src/cli/sync-scope.test.ts +0 -652
  209. package/src/cli/sync.test.ts +0 -5207
  210. package/src/cli/sync.ts +0 -3470
  211. package/src/cli/tombstones.ts +0 -106
  212. package/src/cli/watch-event-push-conflict.test.ts +0 -234
  213. package/src/client-info.test.ts +0 -214
  214. package/src/client-info.ts +0 -121
  215. package/src/cognito-auth.test.ts +0 -712
  216. package/src/cognito-auth.ts +0 -1422
  217. package/src/company-resolver.test.ts +0 -618
  218. package/src/company-resolver.ts +0 -521
  219. package/src/context.test.ts +0 -583
  220. package/src/context.ts +0 -378
  221. package/src/daemon-worker.ts +0 -26
  222. package/src/daemon.ts +0 -99
  223. package/src/entity-resolver.test.ts +0 -315
  224. package/src/entity-resolver.ts +0 -180
  225. package/src/ignore.test.ts +0 -466
  226. package/src/ignore.ts +0 -469
  227. package/src/index.ts +0 -439
  228. package/src/journal.test.ts +0 -968
  229. package/src/journal.ts +0 -765
  230. package/src/lib/cloud-authoritative.test.ts +0 -45
  231. package/src/lib/cloud-authoritative.ts +0 -59
  232. package/src/lib/conflict-file.ts +0 -86
  233. package/src/lib/conflict-index.ts +0 -289
  234. package/src/lib/conflict.test.ts +0 -348
  235. package/src/lib/describe-error.test.ts +0 -100
  236. package/src/lib/describe-error.ts +0 -58
  237. package/src/lib/exit-codes.ts +0 -24
  238. package/src/lib/machine-id.test.ts +0 -231
  239. package/src/lib/machine-id.ts +0 -175
  240. package/src/lib/net-errors.test.ts +0 -65
  241. package/src/lib/net-errors.ts +0 -86
  242. package/src/lib/readlink-safe.test.ts +0 -43
  243. package/src/lib/readlink-safe.ts +0 -29
  244. package/src/local-path-codec.test.ts +0 -138
  245. package/src/local-path-codec.ts +0 -161
  246. package/src/machine-auth.test.ts +0 -1323
  247. package/src/manifest-reconcile.test.ts +0 -1123
  248. package/src/manifest-reconcile.ts +0 -518
  249. package/src/object-io.test.ts +0 -1221
  250. package/src/object-io.ts +0 -1306
  251. package/src/operation-lock.test.ts +0 -484
  252. package/src/operation-lock.ts +0 -680
  253. package/src/outcome-telemetry.test.ts +0 -498
  254. package/src/outcome-telemetry.ts +0 -639
  255. package/src/personal-vault-exclusions.test.ts +0 -308
  256. package/src/personal-vault-exclusions.ts +0 -354
  257. package/src/personal-vault.test.ts +0 -756
  258. package/src/personal-vault.ts +0 -496
  259. package/src/prefix-coalesce.test.ts +0 -240
  260. package/src/prefix-coalesce.ts +0 -273
  261. package/src/public-surface.test.ts +0 -117
  262. package/src/qmd-reindex.test.ts +0 -877
  263. package/src/qmd-reindex.ts +0 -842
  264. package/src/read-only-state-dir.test.ts +0 -188
  265. package/src/remote-pull.test.ts +0 -1130
  266. package/src/remote-pull.ts +0 -618
  267. package/src/s3.symlink-materialize.test.ts +0 -492
  268. package/src/s3.test.ts +0 -1789
  269. package/src/s3.ts +0 -1532
  270. package/src/schemas/signal-types.test.ts +0 -82
  271. package/src/schemas/signal-types.ts +0 -38
  272. package/src/schemas/source-channels.test.ts +0 -82
  273. package/src/schemas/source-channels.ts +0 -53
  274. package/src/scope-shrink.test.ts +0 -633
  275. package/src/scope-shrink.ts +0 -481
  276. package/src/signals/get.test.ts +0 -310
  277. package/src/signals/get.ts +0 -75
  278. package/src/signals/internals.ts +0 -195
  279. package/src/signals/list.test.ts +0 -420
  280. package/src/signals/list.ts +0 -79
  281. package/src/signals/parse.ts +0 -8
  282. package/src/signals/types.ts +0 -91
  283. package/src/skill-telemetry.test.ts +0 -1825
  284. package/src/skill-telemetry.ts +0 -1439
  285. package/src/sources/get.test.ts +0 -293
  286. package/src/sources/get.ts +0 -66
  287. package/src/sources/internals.ts +0 -198
  288. package/src/sources/list.test.ts +0 -402
  289. package/src/sources/list.ts +0 -84
  290. package/src/sources/parse.ts +0 -43
  291. package/src/sources/types.ts +0 -84
  292. package/src/sync/event-sync.test.ts +0 -594
  293. package/src/sync/event-sync.ts +0 -545
  294. package/src/sync/feature-flags.test.ts +0 -378
  295. package/src/sync/feature-flags.ts +0 -62
  296. package/src/sync/index.ts +0 -76
  297. package/src/sync/lease-client.test.ts +0 -128
  298. package/src/sync/lease-client.ts +0 -207
  299. package/src/sync/logger.test.ts +0 -242
  300. package/src/sync/logger.ts +0 -79
  301. package/src/sync/metrics.test.ts +0 -462
  302. package/src/sync/metrics.ts +0 -213
  303. package/src/sync/pull-scope.ts +0 -265
  304. package/src/sync/push-event.test.ts +0 -266
  305. package/src/sync/push-event.ts +0 -224
  306. package/src/sync/push-receiver.test.ts +0 -566
  307. package/src/sync/push-receiver.ts +0 -1048
  308. package/src/sync/push-transport.ts +0 -231
  309. package/src/sync/realtime-rollout.test.ts +0 -86
  310. package/src/sync/realtime-rollout.ts +0 -262
  311. package/src/sync/state-store.test.ts +0 -194
  312. package/src/sync/state-store.ts +0 -727
  313. package/src/sync-core.ts +0 -58
  314. package/src/sync-progress.test.ts +0 -94
  315. package/src/sync-progress.ts +0 -140
  316. package/src/telemetry-events.test.ts +0 -88
  317. package/src/telemetry-events.ts +0 -205
  318. package/src/telemetry.test.ts +0 -1280
  319. package/src/telemetry.ts +0 -1109
  320. package/src/types.ts +0 -314
  321. package/src/vault-client.test.ts +0 -1380
  322. package/src/vault-client.ts +0 -1694
  323. package/src/version.ts +0 -24
  324. package/src/watch-roots.test.ts +0 -278
  325. package/src/watch-roots.ts +0 -162
  326. package/src/watcher-event-gate.test.ts +0 -212
  327. package/src/watcher.test.ts +0 -1079
  328. package/src/watcher.ts +0 -1741
  329. package/test/e2e/sync/cross-tenant-isolation.test.ts +0 -630
  330. package/test/e2e/sync/skill-telemetry-oversized-transcript.test.ts +0 -124
  331. package/test/e2e/sync/transient-company-leg.test.ts +0 -384
  332. package/test/e2e/sync/windows-unreadable-link-leg.test.ts +0 -191
  333. package/test/e2e/watcher-real-chokidar.test.ts +0 -165
  334. package/test/e2e/watcher-recursive-backend.test.ts +0 -181
  335. package/test/e2e/watcher-scoped-coverage.test.ts +0 -381
  336. package/test/invite-flow.integration.test.ts +0 -244
  337. package/test/joiner-manifest-reconcile.integration.test.ts +0 -322
  338. package/test/share-sync.integration.test.ts +0 -213
  339. package/tsconfig.json +0 -19
  340. package/vitest.config.ts +0 -22
@@ -1,481 +0,0 @@
1
- /**
2
- * Scope-shrink detection + classification + clean removal (US-005).
3
- *
4
- * Implements the "hybrid scope-change contract" decided in US-000 Task 3
5
- * (see companies/indigo/projects/hq-sync-browse-vs-sync/references.md):
6
- *
7
- * - Compare the current pull's `prefixSet` against the last `PullRecord`'s
8
- * `prefixSet` for the same company. Files in the journal covered by the
9
- * previous scope but NOT covered by the new scope are **orphans**.
10
- * - Classify each orphan **clean** (safe to silently delete) or **dirty**
11
- * (locally modified — sacred, never silently delete).
12
- * - The remote-pull caller drives:
13
- * * default mode: abort the leg if any dirty orphan exists;
14
- * * `--force-scope-shrink`: continue, leave dirty files on disk,
15
- * tombstone their journal entries.
16
- *
17
- * The pure-detection layer here intentionally does NOT touch disk for the
18
- * tombstone write — that lives in `journal.ts`. It DOES touch disk for the
19
- * orphan classification (hash + stat) because cleanliness is a function of
20
- * the file's current on-disk state vs the journal.
21
- *
22
- * PUSH-ONLY PREFIXES ARE NEVER PRUNED HERE (US-006). Session transcripts under
23
- * `sessions/{personUid}/...` are push-only: pushed into the vault, then
24
- * subtracted from every pull scope (incl. `all`/owner) via
25
- * `resolvePullScope().excludePrefixes`. That exclude set is applied ONLY to the
26
- * download filter (`computePullPlan`) and is DELIBERATELY NOT passed into this
27
- * module's `currentPrefixSet`. So a session file that was authored locally rides
28
- * the `direction:"up"` skip below, and one materialized on demand (`hq files
29
- * get`) rides the pin union already folded into the caller's inclusion
30
- * `prefixSet` — either way it is never pulled AND never orphaned. Feeding the
31
- * exclude set into `currentPrefixSet` would prune exactly those files; callers
32
- * must not.
33
- */
34
-
35
- import * as fs from "fs";
36
- import * as path from "path";
37
- import type {
38
- JournalEntry,
39
- PullRecord,
40
- SyncJournal,
41
- } from "./types.js";
42
- import { hashFile, tombstoneEntry } from "./journal.js";
43
- import { localPathForVaultKey } from "./local-path-codec.js";
44
- import {
45
- isCoveredByAny,
46
- type ScopePrefixInput,
47
- } from "./prefix-coalesce.js";
48
-
49
- export interface OrphanClassification {
50
- /** Relative path (journal key). */
51
- path: string;
52
- /** Journal entry as of last sync. */
53
- entry: JournalEntry;
54
- /** True iff the local file is provably unchanged since last sync. */
55
- clean: boolean;
56
- /** Why we called it dirty — surfaced in the abort error for operators. */
57
- dirtyReason?:
58
- | "modified-after-sync"
59
- | "hash-mismatch"
60
- | "stat-error";
61
- }
62
-
63
- export interface ScopeShrinkPlan {
64
- /** Set of files covered by `lastPrefixSet` but not by `currentPrefixSet`. */
65
- orphans: OrphanClassification[];
66
- /** Subset of `orphans` with `clean === true`. */
67
- clean: OrphanClassification[];
68
- /** Subset of `orphans` with `clean === false`. */
69
- dirty: OrphanClassification[];
70
- /** True iff at least one orphan was found. */
71
- scopeChangeDetected: boolean;
72
- }
73
-
74
- export interface BuildScopeShrinkPlanInput {
75
- journal: SyncJournal;
76
- hqRoot: string;
77
- /** Coalesced prefixes used by the LAST pull for this company. */
78
- lastPrefixSet: readonly ScopePrefixInput[];
79
- /** Coalesced prefixes the CURRENT pull will use. */
80
- currentPrefixSet: readonly ScopePrefixInput[];
81
- /**
82
- * The caller's own Cognito `sub`. When set, a file the caller authored
83
- * (`entry.createdBySub === callerSub`) is NEVER orphaned by a scope shrink —
84
- * regardless of mode. This is the core of the authorship contract: sync mode
85
- * governs whether you mirror *other people's* files; it must never disown
86
- * your own work. Owners hold their whole vault by role-bypass, so without
87
- * this guard a `shared`/`custom` scope would treat their own un-granted
88
- * content as "someone else's file I happen to see" and prune it.
89
- */
90
- callerSub?: string;
91
- /**
92
- * When `true`, an orphan whose authorship is unknown (`createdBySub`
93
- * undefined — a legacy entry predating author stamping, or an object
94
- * uploaded without author metadata) is also retained rather than pruned.
95
- * The automatic background pull sets this so a routine sync never makes a
96
- * destructive guess about pre-stamp content; the explicit `hq sync narrow`
97
- * ritual (which carries its own confirmation + dirty gate) leaves it off so
98
- * a deliberately-confirmed narrow can still reclaim legacy files.
99
- */
100
- protectUnknownAuthors?: boolean;
101
- }
102
-
103
- /**
104
- * Build a scope-shrink plan: find orphans, classify each clean/dirty.
105
- * Pure given the journal + filesystem state — no network, no journal
106
- * mutation.
107
- *
108
- * **Tombstone-aware:** journal entries that already carry a `removedAt`
109
- * marker are skipped — they represent a prior scope-shrink prune and must
110
- * not be re-flagged as orphans on each subsequent pull (that's the whole
111
- * point of the tombstone retention window).
112
- *
113
- * **Direction-aware:** only `direction: "down"` entries (and pre-ETag
114
- * legacy entries without an explicit direction marker) participate in
115
- * shrink detection. Push-only files (`direction: "up"`) represent local
116
- * authorship — they aren't in scope-as-pulled, so a scope change doesn't
117
- * orphan them.
118
- */
119
- export function buildScopeShrinkPlan(
120
- input: BuildScopeShrinkPlanInput,
121
- ): ScopeShrinkPlan {
122
- const { journal, hqRoot, lastPrefixSet, currentPrefixSet } = input;
123
- const { callerSub, protectUnknownAuthors } = input;
124
- const orphans: OrphanClassification[] = [];
125
-
126
- for (const [relPath, entry] of Object.entries(journal.files)) {
127
- if (entry.removedAt) continue; // tombstone — already pruned
128
- if (entry.direction !== "down") continue;
129
- if (!isCoveredByAny(relPath, lastPrefixSet)) continue;
130
- if (isCoveredByAny(relPath, currentPrefixSet)) continue;
131
- // Authorship guard: sync mode decides whether you mirror OTHER people's
132
- // files — it must never disown your own. A file the caller authored is
133
- // sacred and never orphaned, even out of the current prefix scope. When
134
- // `protectUnknownAuthors` is set (the automatic pull path), a legacy
135
- // entry with no recorded author is also retained — a routine background
136
- // sync should never make a destructive guess about pre-stamp content.
137
- if (callerSub && entry.createdBySub === callerSub) continue;
138
- if (protectUnknownAuthors && entry.createdBySub === undefined) continue;
139
- orphans.push(classifyOrphan(relPath, entry, hqRoot));
140
- }
141
-
142
- const clean = orphans.filter((o) => o.clean);
143
- const dirty = orphans.filter((o) => !o.clean);
144
- return {
145
- orphans,
146
- clean,
147
- dirty,
148
- scopeChangeDetected: orphans.length > 0,
149
- };
150
- }
151
-
152
- /**
153
- * Classify a single orphan:
154
- *
155
- * - **Clean** when:
156
- * * the local file is missing (already removed by the user — harmless), OR
157
- * * `sha256(localFile) === entry.hash` AND `stat.mtime ≤ entry.syncedAt`.
158
- * - **Dirty** otherwise.
159
- *
160
- * Symlinks: we don't re-hash with the symlink-target convention here —
161
- * the safer default is to treat any symlink whose lstat exists but whose
162
- * target hash doesn't match `entry.hash` (via `hashFile` reading the
163
- * target) as dirty. In practice symlinks materialize through the same
164
- * code path as files; a stale-target symlink is correctly flagged dirty
165
- * and the operator-facing message points at the path either way.
166
- */
167
- function classifyOrphan(
168
- relPath: string,
169
- entry: JournalEntry,
170
- hqRoot: string,
171
- ): OrphanClassification {
172
- const absPath = localPathForVaultKey(hqRoot, relPath);
173
- let stat: fs.Stats;
174
- try {
175
- stat = fs.lstatSync(absPath);
176
- } catch (err) {
177
- const code = (err as NodeJS.ErrnoException).code;
178
- if (code === "ENOENT") {
179
- return { path: relPath, entry, clean: true };
180
- }
181
- return {
182
- path: relPath,
183
- entry,
184
- clean: false,
185
- dirtyReason: "stat-error",
186
- };
187
- }
188
-
189
- // mtime guard: a local edit moves mtime past syncedAt. We use ≤ because
190
- // a download stamps syncedAt at the close of the write; mtime is set by
191
- // the OS before that, so an unmodified pulled file has mtime ≤ syncedAt.
192
- const mtimeMs = stat.mtimeMs;
193
- const syncedAtMs = Date.parse(entry.syncedAt);
194
- if (!Number.isNaN(syncedAtMs) && mtimeMs > syncedAtMs + 1000) {
195
- // 1s grace for filesystem clock jitter.
196
- return {
197
- path: relPath,
198
- entry,
199
- clean: false,
200
- dirtyReason: "modified-after-sync",
201
- };
202
- }
203
-
204
- // Hash check — final word. If the content matches the journaled hash,
205
- // the file is provably what the last pull left there.
206
- let actualHash: string;
207
- try {
208
- actualHash = hashFile(absPath);
209
- } catch {
210
- return {
211
- path: relPath,
212
- entry,
213
- clean: false,
214
- dirtyReason: "stat-error",
215
- };
216
- }
217
- if (actualHash !== entry.hash) {
218
- return {
219
- path: relPath,
220
- entry,
221
- clean: false,
222
- dirtyReason: "hash-mismatch",
223
- };
224
- }
225
- return { path: relPath, entry, clean: true };
226
- }
227
-
228
- /**
229
- * Where a scope-shrink error is going to be rendered, so the structured error
230
- * can carry advice that is ACTUALLY FOLLOWABLE from that entry point.
231
- *
232
- * - `"cli"` — a human at a terminal running `hq sync pull|now`. They can
233
- * re-run with `--force-scope-shrink` or run the guided
234
- * `hq sync narrow --apply` ritual.
235
- * - `"runner"` — the menubar's `hq-sync-runner`. It accepts NO such flag
236
- * (DEV-1768 fix #2: the old "pass --force-scope-shrink" advice
237
- * was impossible to follow from here), so the only followable
238
- * action is to open a terminal and run `hq sync narrow --apply`.
239
- * In practice the runner pulls with `scopeShrinkPolicy:
240
- * "auto-recover"` and never throws this — but the context keeps
241
- * the message honest if it ever surfaces.
242
- * - `"engine"` — unknown/library caller; generic advice.
243
- */
244
- export type ScopeShrinkAdviceContext = "cli" | "runner" | "engine";
245
-
246
- /** Followable next-step advice for a blocked scope shrink, per entry point. */
247
- function scopeShrinkAdvice(ctx: ScopeShrinkAdviceContext): string {
248
- switch (ctx) {
249
- case "cli":
250
- return (
251
- "Re-run with `--force-scope-shrink` to disown them now (dirty files " +
252
- "are KEPT on disk, only un-tracked from sync), or run " +
253
- "`hq sync narrow --apply` to migrate with a confirmation prompt."
254
- );
255
- case "runner":
256
- return (
257
- "The menubar sync cannot take this flag — open a terminal and run " +
258
- "`hq sync narrow --apply` to migrate this membership (you confirm the " +
259
- "file list), or `hq sync now --force-scope-shrink` once to proceed " +
260
- "(dirty files are KEPT on disk, only un-tracked)."
261
- );
262
- default:
263
- return (
264
- "Run `hq sync narrow --apply` to migrate with confirmation, or pass " +
265
- "`forceScopeShrink` (dirty files are kept on disk, only un-tracked)."
266
- );
267
- }
268
- }
269
-
270
- /**
271
- * Structured error thrown when the engine refuses to proceed because a scope
272
- * shrink would orphan dirty files. The CLI catches this and renders the
273
- * operator-facing message; the engine never prints directly.
274
- *
275
- * `adviceContext` makes the message FOLLOWABLE from each entry point — the old
276
- * fixed "pass --force-scope-shrink" line was impossible to act on from the
277
- * menubar runner, which rejects that flag (DEV-1768 fix #2).
278
- */
279
- export class ScopeShrinkBlockedError extends Error {
280
- readonly code = "SCOPE_SHRINK_BLOCKED";
281
- constructor(
282
- public readonly companyUid: string,
283
- public readonly fromMode: PullRecord["syncMode"] | "unknown",
284
- public readonly toMode: PullRecord["syncMode"],
285
- public readonly dirty: OrphanClassification[],
286
- public readonly clean: OrphanClassification[],
287
- public readonly adviceContext: ScopeShrinkAdviceContext = "engine",
288
- ) {
289
- super(
290
- `Sync scope shrank for ${companyUid} (${fromMode} → ${toMode}); ` +
291
- `${dirty.length} locally-modified file(s) outside the new scope ` +
292
- `would be un-tracked from sync. ${scopeShrinkAdvice(adviceContext)}`,
293
- );
294
- this.name = "ScopeShrinkBlockedError";
295
- }
296
- }
297
-
298
- /**
299
- * Structured error thrown when an AUTOMATIC scope shrink would prune more
300
- * CLEAN local files than the configured safety cap in a single pull. This is
301
- * the bulk-delete guard: a routine background sync should never silently
302
- * delete a large local tree — whether from a deliberate-but-abrupt first
303
- * narrow, a server bug returning an unexpectedly small grant set, or a
304
- * mis-resolved scope. The operator runs the explicit `hq sync narrow` ritual
305
- * (which has its own confirmation + dirty gate) or passes `--force-scope-shrink`
306
- * to proceed. The engine never deletes anything when it throws this.
307
- */
308
- export class ScopeShrinkLargePruneError extends Error {
309
- readonly code = "SCOPE_SHRINK_LARGE_PRUNE";
310
- constructor(
311
- public readonly companyUid: string,
312
- public readonly toMode: PullRecord["syncMode"],
313
- public readonly cleanCount: number,
314
- public readonly cap: number,
315
- public readonly adviceContext: ScopeShrinkAdviceContext = "engine",
316
- ) {
317
- super(
318
- `Refusing to auto-move ${cleanCount} local file(s) for ${companyUid} ` +
319
- `(${toMode} scope) in one sync — exceeds the safety cap of ${cap}. ` +
320
- `Raise HQ_SYNC_MAX_AUTO_PRUNE, or ${scopeShrinkAdvice(adviceContext)}`,
321
- );
322
- this.name = "ScopeShrinkLargePruneError";
323
- }
324
- }
325
-
326
- /**
327
- * Disposition for CLEAN orphans (files provably unchanged since the last sync)
328
- * that fall outside the new scope:
329
- *
330
- * - `"delete"` — `unlink` the local file. The legacy behavior; reserved
331
- * for the explicit `hq sync narrow --apply` ritual, which
332
- * already confirms the file list with the operator.
333
- * - `"quarantine"` — MOVE the file into `quarantineRoot` instead of deleting
334
- * it, so it stays recoverable. The conservative default
335
- * for the automatic pull path: a background sync must
336
- * never silently PURGE local files (DEV-1768 fix #3).
337
- */
338
- export type CleanOrphanDisposition = "delete" | "quarantine";
339
-
340
- export interface ApplyScopeShrinkInput {
341
- journal: SyncJournal;
342
- plan: ScopeShrinkPlan;
343
- hqRoot: string;
344
- /**
345
- * When `true`, dirty files are LEFT ON DISK and their journal entries are
346
- * tombstoned anyway. When `false` (default), the caller should have
347
- * already aborted on dirty orphans — this function still tombstones any
348
- * dirty entries handed to it, on the assumption the caller knows what
349
- * it's doing.
350
- */
351
- forceScopeShrink: boolean;
352
- reason?: "scope_shrink" | "narrow_apply" | "manual";
353
- /**
354
- * How to dispose of CLEAN orphans. Defaults to `"delete"` so existing
355
- * callers (and the confirmed `narrow --apply` ritual) keep their behavior;
356
- * the automatic pull path passes `"quarantine"`.
357
- */
358
- cleanDisposition?: CleanOrphanDisposition;
359
- /**
360
- * Absolute directory clean orphans are relocated into when
361
- * `cleanDisposition === "quarantine"`. Each orphan moves to
362
- * `<quarantineRoot>/<orphan.path>` (parent dirs created). REQUIRED when
363
- * quarantining; if absent, the function falls back to `"delete"` so it can
364
- * never get stuck unable to make progress.
365
- */
366
- quarantineRoot?: string;
367
- }
368
-
369
- export interface ApplyScopeShrinkResult {
370
- /** Clean orphans `unlink`ed from disk (only when disposition is `delete`). */
371
- cleanRemoved: number;
372
- /** Clean orphans MOVED to quarantine (only when disposition is `quarantine`). */
373
- cleanQuarantined: number;
374
- /** Dirty orphans tombstoned in the journal (file LEFT on disk). */
375
- dirtyTombstoned: number;
376
- /** Named paths deleted — for explicit, non-silent operator reporting. */
377
- removedPaths: string[];
378
- /** Named paths moved to quarantine — for explicit reporting. */
379
- quarantinedPaths: string[];
380
- /** Named dirty paths un-tracked but KEPT on disk — for explicit reporting. */
381
- dirtyKeptPaths: string[];
382
- /** Absolute quarantine directory used (when anything was quarantined). */
383
- quarantineRoot?: string;
384
- }
385
-
386
- /**
387
- * Move a clean orphan from the working tree into the quarantine tree,
388
- * preserving its relative path. Same-device `rename` first (cheap, atomic);
389
- * cross-device falls back to copy+unlink. A missing source is a no-op (the
390
- * user already removed it — harmless). Returns true iff the file was relocated
391
- * (or was already absent), false only on an unexpected error the caller should
392
- * surface.
393
- */
394
- function quarantineOrphan(
395
- srcAbs: string,
396
- destAbs: string,
397
- ): void {
398
- fs.mkdirSync(path.dirname(destAbs), { recursive: true });
399
- try {
400
- fs.renameSync(srcAbs, destAbs);
401
- } catch (err) {
402
- const code = (err as NodeJS.ErrnoException).code;
403
- if (code === "ENOENT") return; // source already gone — nothing to move
404
- if (code === "EXDEV") {
405
- // Cross-device move: copy then unlink. cpSync handles files + symlinks.
406
- fs.cpSync(srcAbs, destAbs, { recursive: true, verbatimSymlinks: true });
407
- fs.rmSync(srcAbs, { recursive: true, force: true });
408
- return;
409
- }
410
- throw err;
411
- }
412
- }
413
-
414
- /**
415
- * Apply a scope-shrink plan: dispose of clean orphans (delete OR quarantine)
416
- * + tombstone their journal entries. With `forceScopeShrink: true`, dirty
417
- * orphans are PRESERVED on disk and only their journal entries are tombstoned
418
- * (so they stop being re-flagged on every pull — the idempotent recovery seam).
419
- *
420
- * Returns counts AND named paths so the caller can report exactly what moved /
421
- * was un-tracked — never a silent purge (DEV-1768 fix #3).
422
- */
423
- export function applyScopeShrink(
424
- input: ApplyScopeShrinkInput,
425
- ): ApplyScopeShrinkResult {
426
- const { journal, plan, hqRoot, forceScopeShrink } = input;
427
- const reason = input.reason ?? "scope_shrink";
428
- // Quarantine only when explicitly asked AND a destination is provided;
429
- // otherwise fall back to delete so we always make progress.
430
- const quarantining =
431
- input.cleanDisposition === "quarantine" && !!input.quarantineRoot;
432
- let cleanRemoved = 0;
433
- let cleanQuarantined = 0;
434
- let dirtyTombstoned = 0;
435
- const removedPaths: string[] = [];
436
- const quarantinedPaths: string[] = [];
437
- const dirtyKeptPaths: string[] = [];
438
-
439
- for (const orphan of plan.clean) {
440
- const absPath = localPathForVaultKey(hqRoot, orphan.path);
441
- if (quarantining) {
442
- const destAbs = localPathForVaultKey(input.quarantineRoot!, orphan.path);
443
- quarantineOrphan(absPath, destAbs);
444
- tombstoneEntry(journal, orphan.path, reason);
445
- cleanQuarantined++;
446
- quarantinedPaths.push(orphan.path);
447
- } else {
448
- try {
449
- fs.unlinkSync(absPath);
450
- } catch (err) {
451
- const code = (err as NodeJS.ErrnoException).code;
452
- if (code !== "ENOENT") throw err; // missing-on-disk is fine; anything else escalates
453
- }
454
- tombstoneEntry(journal, orphan.path, reason);
455
- cleanRemoved++;
456
- removedPaths.push(orphan.path);
457
- }
458
- }
459
-
460
- if (forceScopeShrink) {
461
- for (const orphan of plan.dirty) {
462
- // Do NOT delete the file — keep dirty content on disk, prune only the
463
- // journal entry so it stops being re-flagged as an orphan on each pull.
464
- tombstoneEntry(journal, orphan.path, reason);
465
- dirtyTombstoned++;
466
- dirtyKeptPaths.push(orphan.path);
467
- }
468
- }
469
-
470
- return {
471
- cleanRemoved,
472
- cleanQuarantined,
473
- dirtyTombstoned,
474
- removedPaths,
475
- quarantinedPaths,
476
- dirtyKeptPaths,
477
- ...(quarantining && quarantinedPaths.length > 0
478
- ? { quarantineRoot: input.quarantineRoot }
479
- : {}),
480
- };
481
- }