cursedbelt-core 1.0.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.
Files changed (307) hide show
  1. package/LICENSE +15 -0
  2. package/dist/client/wsWire.d.ts +49 -0
  3. package/dist/client/wsWire.js +57 -0
  4. package/dist/config/appConfig.d.ts +361 -0
  5. package/dist/config/appConfig.js +308 -0
  6. package/dist/core/activity/index.d.ts +8 -0
  7. package/dist/core/activity/index.js +7 -0
  8. package/dist/core/activity/model.d.ts +115 -0
  9. package/dist/core/activity/model.js +63 -0
  10. package/dist/core/analytics/index.d.ts +18 -0
  11. package/dist/core/analytics/index.js +18 -0
  12. package/dist/core/analytics/range.d.ts +186 -0
  13. package/dist/core/analytics/range.js +271 -0
  14. package/dist/core/analytics/schedule.d.ts +301 -0
  15. package/dist/core/analytics/schedule.js +372 -0
  16. package/dist/core/category/index.d.ts +3 -0
  17. package/dist/core/category/index.js +6 -0
  18. package/dist/core/category/memoryStore.d.ts +21 -0
  19. package/dist/core/category/memoryStore.js +97 -0
  20. package/dist/core/category/tree.d.ts +26 -0
  21. package/dist/core/category/tree.js +113 -0
  22. package/dist/core/category/types.d.ts +48 -0
  23. package/dist/core/category/types.js +8 -0
  24. package/dist/core/color.d.ts +14 -0
  25. package/dist/core/color.js +32 -0
  26. package/dist/core/colorScheme/colorSchemeStore.d.ts +70 -0
  27. package/dist/core/colorScheme/colorSchemeStore.js +123 -0
  28. package/dist/core/colorScheme/index.d.ts +7 -0
  29. package/dist/core/colorScheme/index.js +7 -0
  30. package/dist/core/domainOwners.d.ts +19 -0
  31. package/dist/core/domainOwners.js +15 -0
  32. package/dist/core/download.d.ts +14 -0
  33. package/dist/core/download.js +37 -0
  34. package/dist/core/events/ccEventBus.d.ts +44 -0
  35. package/dist/core/events/ccEventBus.js +70 -0
  36. package/dist/core/file-tree/fileTreeModel.d.ts +276 -0
  37. package/dist/core/file-tree/fileTreeModel.js +312 -0
  38. package/dist/core/folder-tree/actions.d.ts +84 -0
  39. package/dist/core/folder-tree/actions.js +119 -0
  40. package/dist/core/folder-tree/index.d.ts +3 -0
  41. package/dist/core/folder-tree/index.js +20 -0
  42. package/dist/core/folder-tree/path.d.ts +76 -0
  43. package/dist/core/folder-tree/path.js +146 -0
  44. package/dist/core/folder-tree/tree.d.ts +86 -0
  45. package/dist/core/folder-tree/tree.js +184 -0
  46. package/dist/core/layout/index.d.ts +1 -0
  47. package/dist/core/layout/index.js +7 -0
  48. package/dist/core/master-lock/index.d.ts +14 -0
  49. package/dist/core/master-lock/index.js +14 -0
  50. package/dist/core/master-lock/kdf.d.ts +63 -0
  51. package/dist/core/master-lock/kdf.js +117 -0
  52. package/dist/core/master-lock/policy.d.ts +32 -0
  53. package/dist/core/master-lock/policy.js +52 -0
  54. package/dist/core/master-lock/presence.d.ts +52 -0
  55. package/dist/core/master-lock/presence.js +61 -0
  56. package/dist/core/master-lock/wire.d.ts +139 -0
  57. package/dist/core/master-lock/wire.js +81 -0
  58. package/dist/core/media/bgRemoval.d.ts +27 -0
  59. package/dist/core/media/bgRemoval.js +101 -0
  60. package/dist/core/media/ffmpeg.d.ts +57 -0
  61. package/dist/core/media/ffmpeg.js +126 -0
  62. package/dist/core/media/format.d.ts +20 -0
  63. package/dist/core/media/format.js +54 -0
  64. package/dist/core/media/index.d.ts +12 -0
  65. package/dist/core/media/index.js +18 -0
  66. package/dist/core/media/mediaItem.d.ts +15 -0
  67. package/dist/core/media/mediaItem.js +34 -0
  68. package/dist/core/media/renderPlan.d.ts +63 -0
  69. package/dist/core/media/renderPlan.js +232 -0
  70. package/dist/core/media/segment.d.ts +79 -0
  71. package/dist/core/media/segment.js +247 -0
  72. package/dist/core/media/size.d.ts +1 -0
  73. package/dist/core/media/size.js +3 -0
  74. package/dist/core/media/trim.d.ts +21 -0
  75. package/dist/core/media/trim.js +40 -0
  76. package/dist/core/media/types.d.ts +148 -0
  77. package/dist/core/media/types.js +11 -0
  78. package/dist/core/media/uploadLimits.d.ts +7 -0
  79. package/dist/core/media/uploadLimits.js +16 -0
  80. package/dist/core/media/validate.d.ts +9 -0
  81. package/dist/core/media/validate.js +121 -0
  82. package/dist/core/media/videoCuts.d.ts +152 -0
  83. package/dist/core/media/videoCuts.js +284 -0
  84. package/dist/core/navigation/breadcrumbCompat.d.ts +24 -0
  85. package/dist/core/navigation/breadcrumbCompat.js +21 -0
  86. package/dist/core/navigation/coverageTest.d.ts +24 -0
  87. package/dist/core/navigation/coverageTest.js +48 -0
  88. package/dist/core/navigation/index.d.ts +6 -0
  89. package/dist/core/navigation/index.js +10 -0
  90. package/dist/core/navigation/migrateNavPreferences.d.ts +55 -0
  91. package/dist/core/navigation/migrateNavPreferences.js +63 -0
  92. package/dist/core/navigation/navigationModel.d.ts +165 -0
  93. package/dist/core/navigation/navigationModel.js +14 -0
  94. package/dist/core/navigation/navigationSelectors.d.ts +63 -0
  95. package/dist/core/navigation/navigationSelectors.js +167 -0
  96. package/dist/core/navigation/searchCatalogue.d.ts +23 -0
  97. package/dist/core/navigation/searchCatalogue.js +76 -0
  98. package/dist/core/net/reconnectSchedule.d.ts +46 -0
  99. package/dist/core/net/reconnectSchedule.js +38 -0
  100. package/dist/core/palette/ShortcutRegistry.d.ts +33 -0
  101. package/dist/core/palette/ShortcutRegistry.js +52 -0
  102. package/dist/core/palette/fuzzyRank.d.ts +18 -0
  103. package/dist/core/palette/fuzzyRank.js +73 -0
  104. package/dist/core/palette/hotkeys.d.ts +28 -0
  105. package/dist/core/palette/hotkeys.js +118 -0
  106. package/dist/core/palette/index.d.ts +3 -0
  107. package/dist/core/palette/index.js +8 -0
  108. package/dist/core/schema/projection.d.ts +84 -0
  109. package/dist/core/schema/projection.js +184 -0
  110. package/dist/core/session/claims.d.ts +145 -0
  111. package/dist/core/session/claims.js +157 -0
  112. package/dist/core/sharing/index.d.ts +5 -0
  113. package/dist/core/sharing/index.js +4 -0
  114. package/dist/core/sharing/model.d.ts +195 -0
  115. package/dist/core/sharing/model.js +150 -0
  116. package/dist/core/site/siteCopy.d.ts +92 -0
  117. package/dist/core/site/siteCopy.js +91 -0
  118. package/dist/core/slots.d.ts +22 -0
  119. package/dist/core/slots.js +31 -0
  120. package/dist/core/styles/constructableSheet.d.ts +51 -0
  121. package/dist/core/styles/constructableSheet.js +80 -0
  122. package/dist/core/tokens/registry.d.ts +119 -0
  123. package/dist/core/tokens/registry.js +447 -0
  124. package/dist/core/uiScale/index.d.ts +8 -0
  125. package/dist/core/uiScale/index.js +8 -0
  126. package/dist/core/uiScale/scale.d.ts +36 -0
  127. package/dist/core/uiScale/scale.js +47 -0
  128. package/dist/core/uiScale/uiScaleStore.d.ts +47 -0
  129. package/dist/core/uiScale/uiScaleStore.js +63 -0
  130. package/dist/core/wire/__fixtures__/rowsMarshalWorker.d.ts +1 -0
  131. package/dist/core/wire/__fixtures__/rowsMarshalWorker.js +23 -0
  132. package/dist/core/wire/index.d.ts +17 -0
  133. package/dist/core/wire/index.js +17 -0
  134. package/dist/core/wire/ingress.d.ts +44 -0
  135. package/dist/core/wire/ingress.js +134 -0
  136. package/dist/core/wire/marshalWorker.d.ts +70 -0
  137. package/dist/core/wire/marshalWorker.js +273 -0
  138. package/dist/core/wire/marshalling.d.ts +122 -0
  139. package/dist/core/wire/marshalling.js +126 -0
  140. package/dist/core/wire/wireHub.d.ts +80 -0
  141. package/dist/core/wire/wireHub.js +116 -0
  142. package/dist/ctgr/base64url.d.ts +8 -0
  143. package/dist/ctgr/base64url.js +39 -0
  144. package/dist/ctgr/chunkAccumulator.d.ts +214 -0
  145. package/dist/ctgr/chunkAccumulator.js +307 -0
  146. package/dist/ctgr/chunkStore.d.ts +24 -0
  147. package/dist/ctgr/chunkStore.js +125 -0
  148. package/dist/ctgr/codec.d.ts +44 -0
  149. package/dist/ctgr/codec.js +153 -0
  150. package/dist/ctgr/compress.d.ts +2 -0
  151. package/dist/ctgr/compress.js +20 -0
  152. package/dist/ctgr/crc32.d.ts +2 -0
  153. package/dist/ctgr/crc32.js +23 -0
  154. package/dist/ctgr/digest.d.ts +1 -0
  155. package/dist/ctgr/digest.js +15 -0
  156. package/dist/ctgr/index.d.ts +9 -0
  157. package/dist/ctgr/index.js +17 -0
  158. package/dist/ctgr/types.d.ts +64 -0
  159. package/dist/ctgr/types.js +21 -0
  160. package/dist/ctgr/v0compat.d.ts +27 -0
  161. package/dist/ctgr/v0compat.js +128 -0
  162. package/dist/index.d.ts +15 -0
  163. package/dist/index.js +17 -0
  164. package/dist/shared/errors.d.ts +101 -0
  165. package/dist/shared/errors.js +99 -0
  166. package/dist/testing/bytes.d.ts +32 -0
  167. package/dist/testing/bytes.js +33 -0
  168. package/dist/testing/domInspect.d.ts +59 -0
  169. package/dist/testing/domInspect.js +115 -0
  170. package/dist/testing/fakeFetch.d.ts +23 -0
  171. package/dist/testing/fakeFetch.js +21 -0
  172. package/dist/testing/index.d.ts +3 -0
  173. package/dist/testing/index.js +11 -0
  174. package/package.json +373 -0
  175. package/src/client/wsWire.ts +83 -0
  176. package/src/config/appConfig.spec.ts +158 -0
  177. package/src/config/appConfig.ts +372 -0
  178. package/src/core/activity/index.ts +21 -0
  179. package/src/core/activity/model.ts +160 -0
  180. package/src/core/analytics/index.ts +18 -0
  181. package/src/core/analytics/range.spec.ts +280 -0
  182. package/src/core/analytics/range.ts +383 -0
  183. package/src/core/analytics/schedule.spec.ts +311 -0
  184. package/src/core/analytics/schedule.ts +624 -0
  185. package/src/core/category/category.spec.ts +144 -0
  186. package/src/core/category/index.ts +19 -0
  187. package/src/core/category/memoryStore.ts +104 -0
  188. package/src/core/category/tree.ts +118 -0
  189. package/src/core/category/types.ts +59 -0
  190. package/src/core/color.spec.ts +35 -0
  191. package/src/core/color.ts +35 -0
  192. package/src/core/colorScheme/colorScheme.spec.ts +127 -0
  193. package/src/core/colorScheme/colorSchemeStore.ts +184 -0
  194. package/src/core/colorScheme/index.ts +23 -0
  195. package/src/core/domainOwners.ts +32 -0
  196. package/src/core/download.spec.ts +38 -0
  197. package/src/core/download.ts +35 -0
  198. package/src/core/events/ccEventBus.spec.ts +90 -0
  199. package/src/core/events/ccEventBus.ts +120 -0
  200. package/src/core/file-tree/fileTreeModel.spec.ts +462 -0
  201. package/src/core/file-tree/fileTreeModel.ts +511 -0
  202. package/src/core/folder-tree/actions.spec.ts +103 -0
  203. package/src/core/folder-tree/actions.ts +153 -0
  204. package/src/core/folder-tree/index.ts +46 -0
  205. package/src/core/folder-tree/path.spec.ts +113 -0
  206. package/src/core/folder-tree/path.ts +148 -0
  207. package/src/core/folder-tree/tree.spec.ts +157 -0
  208. package/src/core/folder-tree/tree.ts +244 -0
  209. package/src/core/layout/index.ts +7 -0
  210. package/src/core/master-lock/index.ts +50 -0
  211. package/src/core/master-lock/kdf.spec.ts +95 -0
  212. package/src/core/master-lock/kdf.ts +145 -0
  213. package/src/core/master-lock/policy.ts +55 -0
  214. package/src/core/master-lock/presence.spec.ts +73 -0
  215. package/src/core/master-lock/presence.ts +71 -0
  216. package/src/core/master-lock/wire.ts +155 -0
  217. package/src/core/media/bgRemoval.spec.ts +120 -0
  218. package/src/core/media/bgRemoval.ts +123 -0
  219. package/src/core/media/ffmpeg.spec.ts +135 -0
  220. package/src/core/media/ffmpeg.ts +162 -0
  221. package/src/core/media/format.spec.ts +41 -0
  222. package/src/core/media/format.ts +58 -0
  223. package/src/core/media/index.ts +105 -0
  224. package/src/core/media/mediaItem.spec.ts +46 -0
  225. package/src/core/media/mediaItem.ts +40 -0
  226. package/src/core/media/renderPlan.spec.ts +140 -0
  227. package/src/core/media/renderPlan.ts +318 -0
  228. package/src/core/media/segment.spec.ts +159 -0
  229. package/src/core/media/segment.ts +352 -0
  230. package/src/core/media/size.spec.ts +19 -0
  231. package/src/core/media/size.ts +3 -0
  232. package/src/core/media/trim.spec.ts +75 -0
  233. package/src/core/media/trim.ts +60 -0
  234. package/src/core/media/types.ts +147 -0
  235. package/src/core/media/uploadLimits.ts +18 -0
  236. package/src/core/media/validate.spec.ts +106 -0
  237. package/src/core/media/validate.ts +131 -0
  238. package/src/core/media/videoCuts.spec.ts +327 -0
  239. package/src/core/media/videoCuts.ts +355 -0
  240. package/src/core/navigation/breadcrumbCompat.ts +44 -0
  241. package/src/core/navigation/coverageTest.ts +54 -0
  242. package/src/core/navigation/index.ts +50 -0
  243. package/src/core/navigation/migrateNavPreferences.ts +119 -0
  244. package/src/core/navigation/navigation.spec.ts +388 -0
  245. package/src/core/navigation/navigationModel.ts +185 -0
  246. package/src/core/navigation/navigationSelectors.ts +206 -0
  247. package/src/core/navigation/searchCatalogue.ts +89 -0
  248. package/src/core/net/reconnectSchedule.spec.ts +49 -0
  249. package/src/core/net/reconnectSchedule.ts +78 -0
  250. package/src/core/palette/ShortcutRegistry.ts +69 -0
  251. package/src/core/palette/fuzzyRank.ts +78 -0
  252. package/src/core/palette/hotkeys.ts +132 -0
  253. package/src/core/palette/index.ts +22 -0
  254. package/src/core/palette/palette.spec.ts +160 -0
  255. package/src/core/schema/projection.spec.ts +122 -0
  256. package/src/core/schema/projection.ts +225 -0
  257. package/src/core/session/claims.ts +181 -0
  258. package/src/core/sharing/index.ts +33 -0
  259. package/src/core/sharing/model.spec.ts +104 -0
  260. package/src/core/sharing/model.ts +296 -0
  261. package/src/core/site/siteCopy.ts +115 -0
  262. package/src/core/slots.ts +38 -0
  263. package/src/core/styles/constructableSheet.ts +86 -0
  264. package/src/core/tokens/registry.spec.ts +160 -0
  265. package/src/core/tokens/registry.ts +536 -0
  266. package/src/core/uiScale/index.ts +32 -0
  267. package/src/core/uiScale/scale.ts +63 -0
  268. package/src/core/uiScale/uiScale.spec.ts +77 -0
  269. package/src/core/uiScale/uiScaleStore.ts +100 -0
  270. package/src/core/wire/__fixtures__/rowsMarshalWorker.ts +24 -0
  271. package/src/core/wire/index.ts +56 -0
  272. package/src/core/wire/ingress.spec.ts +276 -0
  273. package/src/core/wire/ingress.ts +182 -0
  274. package/src/core/wire/marshalWorker.spec.ts +196 -0
  275. package/src/core/wire/marshalWorker.ts +361 -0
  276. package/src/core/wire/marshalling.spec.ts +139 -0
  277. package/src/core/wire/marshalling.ts +238 -0
  278. package/src/core/wire/wireHub.spec.ts +137 -0
  279. package/src/core/wire/wireHub.ts +213 -0
  280. package/src/ctgr/base64url.ts +44 -0
  281. package/src/ctgr/chunkAccumulator.ts +403 -0
  282. package/src/ctgr/chunkStore.spec.ts +101 -0
  283. package/src/ctgr/chunkStore.ts +168 -0
  284. package/src/ctgr/codec.spec.ts +208 -0
  285. package/src/ctgr/codec.ts +202 -0
  286. package/src/ctgr/compress.ts +21 -0
  287. package/src/ctgr/crc32.ts +23 -0
  288. package/src/ctgr/digest.ts +14 -0
  289. package/src/ctgr/index.ts +39 -0
  290. package/src/ctgr/types.ts +80 -0
  291. package/src/ctgr/v0compat.spec.ts +73 -0
  292. package/src/ctgr/v0compat.ts +156 -0
  293. package/src/declaredDepsAreImported.spec.ts +87 -0
  294. package/src/index.ts +25 -0
  295. package/src/leafSubpathsImportNothing.spec.ts +287 -0
  296. package/src/lockfileIsTracked.spec.ts +50 -0
  297. package/src/namedSubpathsResolve.spec.ts +122 -0
  298. package/src/noPathDeps.spec.ts +24 -0
  299. package/src/publishShape.spec.ts +89 -0
  300. package/src/shared/errors.ts +153 -0
  301. package/src/skippedTestsAreAnswered.spec.ts +221 -0
  302. package/src/testTmpRoot.spec.ts +102 -0
  303. package/src/testing/bytes.ts +38 -0
  304. package/src/testing/domInspect.spec.ts +79 -0
  305. package/src/testing/domInspect.ts +127 -0
  306. package/src/testing/fakeFetch.ts +26 -0
  307. package/src/testing/index.ts +11 -0
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Trigger a browser file download from in-memory data — the canonical client-side
3
+ * "save this blob" primitive (there was no shared one; call sites hand-rolled
4
+ * `createObjectURL` + `<a download>` + revoke). DOM-only: it no-ops off the browser
5
+ * so importing modules stay server-safe.
6
+ *
7
+ * ```ts
8
+ * downloadBlob(csvText, 'notes.csv', 'text/csv');
9
+ * downloadBlob(sqliteBytes, 'snapshot.sqlite', 'application/x-sqlite3');
10
+ * ```
11
+ */
12
+ export function downloadBlob(data, filename, mime) {
13
+ if (typeof document === 'undefined' || typeof URL.createObjectURL !== 'function')
14
+ return;
15
+ const url = URL.createObjectURL(new Blob([data], { type: mime }));
16
+ try {
17
+ const a = document.createElement('a');
18
+ a.href = url;
19
+ a.download = filename;
20
+ a.rel = 'noopener';
21
+ document.body.appendChild(a);
22
+ a.click();
23
+ a.remove();
24
+ }
25
+ finally {
26
+ // Revoke on the next tick so the click's navigation has grabbed the URL first.
27
+ setTimeout(() => URL.revokeObjectURL(url), 0);
28
+ }
29
+ }
30
+ /** Decode a base64 payload (e.g. an exported `.sqlite` snapshot) to bytes for {@link downloadBlob}. */
31
+ export function base64ToBytes(base64) {
32
+ const bin = atob(base64);
33
+ const bytes = new Uint8Array(bin.length);
34
+ for (let i = 0; i < bin.length; i++)
35
+ bytes[i] = bin.charCodeAt(i);
36
+ return bytes;
37
+ }
@@ -0,0 +1,44 @@
1
+ /**
2
+ * The global event bus (03 §6.4) — lifecycle notifications only, never data
3
+ * payloads (those ride the port hub).
4
+ *
5
+ * The depth guard is the reason this is a primitive rather than an EventEmitter:
6
+ * handlers emit, and a handler chain that loops — `NOTE_SAVED` → indexer →
7
+ * dashboard → audit interceptor → `NOTE_SAVED` — freezes the main thread with no
8
+ * error and no stack that names the cycle. Each emission inherits
9
+ * `depth = parent.depth + 1` via `causationId`; past `MAX_EVENT_DEPTH` the bus
10
+ * terminates the chain and raises an `EventLoopDiagnostic` that names the whole
11
+ * chain, so the loop is a report instead of a hang.
12
+ *
13
+ * Handlers are async-scheduled, never re-entrant: emitting from inside a handler
14
+ * queues, so a synchronous handler cannot re-enter the dispatcher mid-iteration.
15
+ */
16
+ export interface CCEvent {
17
+ type: string;
18
+ tenantId: string;
19
+ causationId: string;
20
+ depth: number;
21
+ payload: Record<string, unknown>;
22
+ }
23
+ export interface EventLoopDiagnostic {
24
+ chain: string[];
25
+ terminatedAt: string;
26
+ causationId: string;
27
+ }
28
+ export type Unsubscribe = () => void;
29
+ /** Past this depth a chain is a loop, not a workflow (03 §6.4). */
30
+ export declare const MAX_EVENT_DEPTH = 5;
31
+ export interface CCEventBusOptions {
32
+ onLoopDiagnostic?: (diagnostic: EventLoopDiagnostic) => void;
33
+ maxDepth?: number;
34
+ }
35
+ export interface CCEventBus {
36
+ emit(event: Partial<CCEvent> & {
37
+ type: string;
38
+ }): void;
39
+ on(type: string, handler: (event: CCEvent) => void): Unsubscribe;
40
+ /** Resolves once the queue drains — the probe's "is the main thread still responsive?" hook. */
41
+ settled(): Promise<void>;
42
+ diagnostics(): readonly EventLoopDiagnostic[];
43
+ }
44
+ export declare function createCcEventBus(options?: CCEventBusOptions): CCEventBus;
@@ -0,0 +1,70 @@
1
+ /** Past this depth a chain is a loop, not a workflow (03 §6.4). */
2
+ export const MAX_EVENT_DEPTH = 5;
3
+ export function createCcEventBus(options = {}) {
4
+ const maxDepth = options.maxDepth ?? MAX_EVENT_DEPTH;
5
+ const handlers = new Map();
6
+ const diagnostics = [];
7
+ const chains = new Map();
8
+ const queue = [];
9
+ let draining = false;
10
+ let drained = Promise.resolve();
11
+ let resolveDrained = null;
12
+ const drain = async () => {
13
+ draining = true;
14
+ while (queue.length > 0) {
15
+ const event = queue.shift();
16
+ for (const handler of handlers.get(event.type) ?? []) {
17
+ try {
18
+ handler(event);
19
+ }
20
+ catch {
21
+ // A throwing subscriber must not stop the rest of the chain.
22
+ }
23
+ }
24
+ await Promise.resolve(); // yield: handlers are scheduled, never re-entrant
25
+ }
26
+ draining = false;
27
+ resolveDrained?.();
28
+ resolveDrained = null;
29
+ };
30
+ return {
31
+ emit(partial) {
32
+ const causationId = partial.causationId ?? `cause_${chains.size + 1}_${partial.type}`;
33
+ const depth = partial.depth ?? 0;
34
+ const chain = [...(chains.get(causationId) ?? []), partial.type];
35
+ chains.set(causationId, chain);
36
+ if (depth > maxDepth) {
37
+ const diagnostic = { chain, terminatedAt: partial.type, causationId };
38
+ diagnostics.push(diagnostic);
39
+ options.onLoopDiagnostic?.(diagnostic);
40
+ return; // chain terminated — the guard, not a crash
41
+ }
42
+ queue.push({
43
+ type: partial.type,
44
+ tenantId: partial.tenantId ?? 'tenant-a',
45
+ causationId,
46
+ depth,
47
+ payload: partial.payload ?? {},
48
+ });
49
+ if (!draining) {
50
+ drained = new Promise((resolve) => {
51
+ resolveDrained = resolve;
52
+ });
53
+ void drain();
54
+ }
55
+ },
56
+ on(type, handler) {
57
+ const list = handlers.get(type) ?? [];
58
+ list.push(handler);
59
+ handlers.set(type, list);
60
+ return () => {
61
+ const current = handlers.get(type) ?? [];
62
+ const index = current.indexOf(handler);
63
+ if (index >= 0)
64
+ current.splice(index, 1);
65
+ };
66
+ },
67
+ settled: () => (draining ? drained : Promise.resolve()),
68
+ diagnostics: () => diagnostics,
69
+ };
70
+ }
@@ -0,0 +1,276 @@
1
+ /**
2
+ * The arithmetic behind `react/file-tree` — what is in the tree, what is on screen, and
3
+ * which drops are legal. No DOM, so all of it is unit-tested; the pointer handling, the
4
+ * markers and the keyboard live in the component and are not.
5
+ *
6
+ * ── Where this came from, and why it is here rather than in an app ──────────────────────
7
+ * Owner, 2026-09-08: *"the spreadsheet app has a much better file tree sidebar than
8
+ * collections has. Add the file tree viewer from it into cb so apps like collections can
9
+ * use it."* `cursed-embeddable-spreadsheet`'s `FileTree` is the tree he means — it has been
10
+ * carrying that app's whole navigation for weeks. What made it good is mostly in this file:
11
+ * a drop that cannot happen is never OFFERED, so there is no marker, no highlight and no
12
+ * error toast after the fact — the cursor simply says no while the row is still in the air.
13
+ *
14
+ * ── 🔴 Why this is not `core/folder-tree` ──────────────────────────────────────────────
15
+ * `folder-tree` derives a hierarchy from delimited PATH STRINGS that items carry, and its
16
+ * nodes are the result of that derivation. This models a tree that already exists, whatever
17
+ * produced it: a node has an id, a kind and children, and an app may hand over folders
18
+ * derived from paths (collections), rows from a table, or a mix of both under one root. The
19
+ * two are complementary — collections feeds `buildFolderTree`'s output straight into this —
20
+ * and collapsing them would force every consumer of one to adopt the other's storage shape.
21
+ */
22
+ /** What a row IS. `folder` may hold children and accept drops; `file` is a leaf. */
23
+ export type FileTreeNodeKind = 'folder' | 'file';
24
+ /**
25
+ * How visible a node is, generalised from collections' private/locked folders.
26
+ *
27
+ * 🔴 It is a LABEL, never an enforcement. The server decides what a session may see, and a
28
+ * tree that hides a row the server sent (or shows one it did not) is a second, weaker
29
+ * opinion about access that will disagree with the first. collections' server already omits
30
+ * what a locked session may not see — see `albums.ts:folderTree` — so a `private` row here
31
+ * means "you are unlocked and this is one of the protected ones", which is a thing to MARK.
32
+ */
33
+ export type FileTreeAccess = 'normal' | 'private' | 'locked';
34
+ export interface FileTreeNode {
35
+ /** Stable across re-derivation — it is the React key, the selection value and the drag id. */
36
+ id: string;
37
+ name: string;
38
+ kind: FileTreeNodeKind;
39
+ /** Folders only. A folder with `[]` is empty; a file's is ignored. */
40
+ children?: readonly FileTreeNode[];
41
+ /**
42
+ * Whether this folder draws a twisty. **Default `true` for every folder**, empty ones
43
+ * included — pass `false` only for a row that is a folder in name and can never hold
44
+ * anything.
45
+ *
46
+ * 🔴 2026-09-11, the owner: *"Make them use an arrow icon next to a dir even when the dir
47
+ * is empty."* It used to default to `children.length > 0`, and that was wrong twice over.
48
+ * For a LAZY tree it is a trap door — a consumer that fetches a row's contents only while
49
+ * it is open produces a collapsed row with no children, which loses its caret and can
50
+ * never be reopened (`apps/collections` hit exactly that, and had to pass this flag to get
51
+ * out of it). And for an EAGER tree it makes an empty folder look like a file: the arrow
52
+ * is the only thing on the row that says "this is a place", so a folder you have not put
53
+ * anything in yet is drawn as a leaf until the moment it stops being empty.
54
+ *
55
+ * VS Code draws the chevron on every folder and opens an empty one onto nothing, which is
56
+ * what this now matches.
57
+ */
58
+ expandable?: boolean;
59
+ /**
60
+ * A picture of THIS row's contents, drawn in the glyph box in place of its type icon.
61
+ *
62
+ * 🔴 2026-09-11, the owner: *"For media files show a thumbnail instead of the file type
63
+ * icon to the left of the name."* A folder of photographs is a list of rows whose type
64
+ * icons are all identical, so the glyph column says nothing at all — the thumbnail is the
65
+ * only thing in the row that distinguishes one picture from the next before you open it.
66
+ *
67
+ * It takes the SAME 16px box the type icon uses rather than growing the row: the row's
68
+ * height is pinned to the glyph by `fleet/standards/file-tree-geometry.json`, and a tree
69
+ * whose rows change height per file is one you cannot scan. A thumbnail that 404s falls
70
+ * back to the type icon exactly once — see `FileTree`'s `RowGlyph`. Omit it for anything
71
+ * that has no picture, and never point it at the full-size object: this is a 16px box, and
72
+ * a tree of 200 rows pointed at originals is 200 full downloads.
73
+ */
74
+ thumbnailUrl?: string;
75
+ /** Badge on the row — usually "everything at or under here". Omit for none. */
76
+ count?: number;
77
+ access?: FileTreeAccess;
78
+ /**
79
+ * Refuse drops INTO this node even though it is a folder.
80
+ *
81
+ * The consumer's veto, for a rule the tree cannot know: collections uses it so a folder
82
+ * cannot be dragged across album boundaries, which the server has no route for. Defaults
83
+ * to true for folders and is meaningless for files.
84
+ */
85
+ acceptsDrop?: boolean;
86
+ /**
87
+ * Whether this row can be picked up. Default true once `onMove` is wired.
88
+ *
89
+ * Not every row in a tree is a thing you can move: collections puts its ALBUMS at the top
90
+ * level, and albums have no order and no parent to be moved into. A row that cannot go
91
+ * anywhere must not start a drag — otherwise the gesture begins, follows the cursor, and
92
+ * refuses everywhere, which reads as a broken tree rather than a fixed one.
93
+ */
94
+ draggable?: boolean;
95
+ /** Whatever the consumer needs back in a callback. Never read here. */
96
+ meta?: unknown;
97
+ }
98
+ /** Where a drag would put the thing it is carrying. */
99
+ export type FileTreeDrop = {
100
+ kind: 'into';
101
+ parentId: string;
102
+ }
103
+ /**
104
+ * Out of every folder, at the top of the tree.
105
+ *
106
+ * The blank space under the last row is what expresses "get this out of here" — the
107
+ * gesture every file explorer has and the only one that does not need a target row.
108
+ */
109
+ | {
110
+ kind: 'root';
111
+ }
112
+ /** Between two rows, when the consumer stores an order. See {@link ResolveDropOptions}. */
113
+ | {
114
+ kind: 'before';
115
+ siblingId: string;
116
+ parentId: string | null;
117
+ } | {
118
+ kind: 'after';
119
+ siblingId: string;
120
+ parentId: string | null;
121
+ };
122
+ /** A pointer position resolved against the tree: a target, or a refusal with words. */
123
+ export interface DropOffer {
124
+ target: FileTreeDrop | null;
125
+ refused: boolean;
126
+ /**
127
+ * Why, in words, when it is refused.
128
+ *
129
+ * `DESIGN.md`'s law: a control that cannot do a thing says so. A drop that is silently
130
+ * ignored reads as a broken app rather than a refused one.
131
+ */
132
+ reason: string | null;
133
+ }
134
+ /** One row as drawn, top to bottom. */
135
+ export interface FileTreeRow {
136
+ node: FileTreeNode;
137
+ /** How far in it is drawn — 0 at the top level. */
138
+ depth: number;
139
+ parentId: string | null;
140
+ /** Its position among its own siblings, which is what a keyboard move needs. */
141
+ index: number;
142
+ /** True when it is a folder with children (so it gets a twisty). */
143
+ expandable: boolean;
144
+ expanded: boolean;
145
+ }
146
+ /** Every node in the tree, depth-first — the order the rows would be in fully expanded. */
147
+ export declare function walkFileTree(nodes: readonly FileTreeNode[]): FileTreeNode[];
148
+ export declare function findFileTreeNode(nodes: readonly FileTreeNode[], id: string): FileTreeNode | null;
149
+ /**
150
+ * The rows actually on screen, given which folders are shut.
151
+ *
152
+ * 🔴 `collapsed` is the exception list, not `expanded`. A tree derived fresh from the server
153
+ * on every refresh gets new nodes constantly, and an expanded-set model shows each of them
154
+ * shut — so a newly created folder would appear closed and empty at the moment its contents
155
+ * are the thing you are looking for. The exception list defaults everything open, which is
156
+ * also what reads best for the small trees these sidebars hold.
157
+ */
158
+ export declare function flattenFileTree(nodes: readonly FileTreeNode[], collapsed: Iterable<string>): FileTreeRow[];
159
+ /** `id` and everything under it — the set a drag may not land inside. */
160
+ export declare function descendantIdsOf(nodes: readonly FileTreeNode[], id: string): Set<string>;
161
+ /**
162
+ * The id of the folder a node sits in — `null` at the top level, and `null` for a node this
163
+ * tree does not contain.
164
+ *
165
+ * 🔴 The two nulls are deliberately one answer. Every caller is asking "where would a move
166
+ * put this", and for a row the tree no longer holds the honest answer is the top level
167
+ * rather than a throw: a tree re-derived from the server between the press and the release
168
+ * can genuinely lose the row under the pointer.
169
+ */
170
+ export declare function parentIdOf(nodes: readonly FileTreeNode[], id: string): string | null;
171
+ /** The share of a folder row's height that means "into" rather than "between". */
172
+ export declare const INTO_ZONE = 0.5;
173
+ export interface ResolveDropOptions {
174
+ nodes: readonly FileTreeNode[];
175
+ /** The row under the pointer, or null over blank space below the tree. */
176
+ row: FileTreeRow | null;
177
+ /** How far down that row the pointer is, 0 at its top edge and 1 at its bottom. */
178
+ offsetRatio: number;
179
+ /** What is being dragged — a node id from this tree, or null for an external payload. */
180
+ dragId: string | null;
181
+ /**
182
+ * Whether the consumer stores a sibling ORDER.
183
+ *
184
+ * 🔴 Off by default, and collections is why. Its folders are DERIVED from the paths its
185
+ * files carry and are always sorted by name, so there is no order to change — offering a
186
+ * between-rows marker would draw a promise the app cannot keep. Off, a row is only ever
187
+ * dropped INTO something, which is the whole of what that app can do.
188
+ */
189
+ allowReorder?: boolean;
190
+ /** Words for refusing a drop into a folder whose `acceptsDrop` is false. */
191
+ lockedReason?: string;
192
+ /**
193
+ * The consumer's veto, for a refusal that depends on WHAT is being dragged.
194
+ *
195
+ * `acceptsDrop` is a property of the target alone and cannot express "this folder takes
196
+ * files but not folders from another album" — which is collections' actual rule, because
197
+ * its server has a route for re-pathing a folder inside one album and none for moving one
198
+ * across albums. Return the words; return null to allow.
199
+ *
200
+ * 🔴 Consulted BEFORE the target is offered, never after the drop. The whole design of
201
+ * this tree is that an impossible drop is never drawn as possible.
202
+ */
203
+ canDrop?: (dragId: string | null, target: FileTreeDrop) => string | null;
204
+ }
205
+ /**
206
+ * Where the pointer currently is, as a decision.
207
+ *
208
+ * The rule the whole gesture is built around: **a drop that cannot happen is never
209
+ * offered.** Every refusal below returns a reason rather than a null target, so the cursor
210
+ * and the chip can say no while the row is still in the air.
211
+ */
212
+ export declare function resolveDrop({ nodes, row, offsetRatio, dragId, allowReorder, lockedReason, canDrop, }: ResolveDropOptions): DropOffer;
213
+ export interface KeyboardMoveOptions {
214
+ nodes: readonly FileTreeNode[];
215
+ /** The rows on screen — siblings are read off these, which is every sibling by construction. */
216
+ rows: readonly FileTreeRow[];
217
+ /** The row the keyboard is on. */
218
+ row: FileTreeRow;
219
+ /** `in` demotes into the nearest folder above it; `out` promotes to its grandparent. */
220
+ direction: 'in' | 'out';
221
+ lockedReason?: string;
222
+ canDrop?: (dragId: string | null, target: FileTreeDrop) => string | null;
223
+ }
224
+ /**
225
+ * Where ⌥→ / ⌥← would put the focused row — the keyboard's half of the drag.
226
+ *
227
+ * 🔴 It exists because a gesture with no keyboard route is a gesture half the people using
228
+ * it cannot perform, and because TOUCH has no drag here at all: a sidebar row is a small
229
+ * target on a surface whose one constant gesture is the flick, so the pointer drag gives
230
+ * that up and this is what is left. `cursedesk`'s tree has carried it since it was written;
231
+ * this one shipped with the drag alone.
232
+ *
233
+ * The two directions are the only two a keyboard can mean unambiguously. **→ demotes into
234
+ * the nearest folder ABOVE it among its own siblings** — the one place a demotion can go
235
+ * without asking which of several folders was meant — and **← promotes out to the folder its
236
+ * own folder is in**, which is `root` at depth 1.
237
+ *
238
+ * 🔴 Every refusal the DRAG honours is honoured here, from the same fields: `acceptsDrop`,
239
+ * the consumer's `canDrop`, and the descendant rule. A keyboard route that can reach a
240
+ * destination the pointer refuses is a second, weaker opinion about what is legal — and it
241
+ * is the one nobody looks at, because the pointer is what gets tested by hand.
242
+ *
243
+ * There is deliberately no ⌥↑/⌥↓ here. Those would move a row among its siblings, which is
244
+ * only meaningful when the consumer stores an order — and when it does, the sibling before
245
+ * and after are already reachable as `before`/`after` drops. Offering them unconditionally
246
+ * would write a position nothing reads and then draw the row back where it started.
247
+ */
248
+ export declare function resolveKeyboardMove({ nodes, rows, row, direction, lockedReason, canDrop, }: KeyboardMoveOptions): DropOffer;
249
+ /**
250
+ * Where a drop would put the thing being carried, in words — for the chip under the cursor
251
+ * and for the screen reader that cannot see it.
252
+ *
253
+ * 🔴 A drag that says only WHAT is held answers half the question. Owner, of the tree this
254
+ * one was ported from: *"dragging an item from one place to another does not have any
255
+ * indication that it is working until you drop it somewhere new and see it move."* The chip
256
+ * following the cursor answers "it is working"; this answers "and it is going there", which
257
+ * is what stops a wrong drop before the pointer is lifted rather than after.
258
+ *
259
+ * Empty for no target — a refusal has its own words and this must never carry a second "no".
260
+ */
261
+ export declare function describeDrop(nodes: readonly FileTreeNode[], drop: FileTreeDrop | null): string;
262
+ /**
263
+ * The next row whose name starts with `query`, searching from `fromIndex` and wrapping.
264
+ *
265
+ * Typeahead is what makes a long tree usable without a search box — `bud` jumps to
266
+ * `budget-2026` — and wrapping is what stops it dying at the bottom of the list.
267
+ */
268
+ export declare function matchTypeahead(rows: readonly FileTreeRow[], query: string, fromIndex: number): number;
269
+ /**
270
+ * The tree narrowed to what matches, with the folders on the way to a match kept.
271
+ *
272
+ * A folder that matches keeps ALL of its contents — searching for a folder means you want
273
+ * to see what is in it, not a folder that looks empty because its children spell something
274
+ * else.
275
+ */
276
+ export declare function filterFileTree(nodes: readonly FileTreeNode[], query: string): readonly FileTreeNode[];