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,186 @@
1
+ /**
2
+ * The time-range vocabulary every analytics surface shares β€” the presets, the
3
+ * bucket size each one implies, and the bucketing itself.
4
+ *
5
+ * ── Why this is a shared primitive and not per-dashboard code ────────────────
6
+ * Every dashboard needs "today / this week / this month", and every one that
7
+ * hand-rolls it gets the same three things subtly wrong:
8
+ *
9
+ * 1. **"Today" is not "the last 24 hours."** The owner asking *"has the
10
+ * orchestrator done well today?"* means since midnight in THEIR timezone,
11
+ * against comparable earlier days. A rolling 24h window silently mixes last
12
+ * night into this morning and makes every day look average.
13
+ * 2. **The bucket must follow the range.** Hourly buckets over 90 days is 2,160
14
+ * points nobody can read; daily buckets over one day is a single bar. The
15
+ * preset owns both, so a range change cannot leave a mismatched bucket
16
+ * behind.
17
+ * 3. **A comparison needs a baseline of the same shape.** "42 tasks" answers
18
+ * nothing. "42, against a 7-day median of 26" answers the question that was
19
+ * actually asked β€” and the baseline has to be the same length as the range
20
+ * or the comparison is meaningless.
21
+ *
22
+ * Framework-free and DOM-free on purpose: the ENGINE computes with it and the
23
+ * BROWSER renders with it, and they must bucket identically or a chart's x-axis
24
+ * disagrees with its own tooltip.
25
+ *
26
+ * ── Local days, not UTC days ────────────────────────────────────────────────
27
+ * Day boundaries come from the host's own calendar (`new Date(y, m, d)`), so a
28
+ * day is the day the reader lived through. UTC bucketing would move "today" by
29
+ * 5–6 hours in America/Chicago and put a normal evening's work on tomorrow.
30
+ * πŸ”΄ It follows that the ENGINE must be the one to bucket (it shares the
31
+ * owner's timezone) β€” a prod box on UTC bucketing for a Chicago reader
32
+ * reintroduces exactly this bug.
33
+ */
34
+ /**
35
+ * The preset keys. `all` is unbounded-below β€” it starts at the first datum.
36
+ *
37
+ * `custom` is the one key with no fixed width: it takes explicit bounds from the
38
+ * caller (`resolveRange(key, now, { custom })`) and picks its own bucket unit
39
+ * from how wide those bounds are. It exists because the owner asked for it by
40
+ * name β€” *"I want to be able to view all types of times (all time, this year,
41
+ * this month, weeks days, custom times, etc)"* (2026-08-13) β€” and because a
42
+ * dashboard whose narrowest question is "the last 7 days" cannot answer "what
43
+ * happened during that incident on the 14th".
44
+ */
45
+ export type RangeKey = 'today' | 'week' | 'month' | 'quarter' | 'year' | 'all' | 'custom';
46
+ /** How wide one bucket is. `day` is calendar-aligned; `hour` is clock-aligned. */
47
+ export type BucketUnit = 'hour' | 'day';
48
+ export interface RangePreset {
49
+ key: RangeKey;
50
+ /** Control label β€” short, because it sits in a segmented control. */
51
+ label: string;
52
+ /** What the range covers, for a tooltip or a subtitle. */
53
+ description: string;
54
+ unit: BucketUnit;
55
+ /**
56
+ * Whole days back from today, inclusive of today. `null` for `all`.
57
+ * `today` is 1 β€” itself.
58
+ */
59
+ days: number | null;
60
+ }
61
+ export declare const RANGE_PRESETS: readonly RangePreset[];
62
+ /** The presets with a fixed width, in order β€” the ordinary switcher's options. */
63
+ export declare const FIXED_RANGE_KEYS: readonly RangeKey[];
64
+ /** Explicit bounds for the `custom` preset. `to` is exclusive, like `ResolvedRange.to`. */
65
+ export interface CustomRangeBounds {
66
+ from: number;
67
+ to: number;
68
+ }
69
+ export interface ResolveRangeOptions {
70
+ /** The first datum's timestamp β€” bounds the `all` preset. */
71
+ earliest?: number;
72
+ /** Required by `custom`; ignored by every other key. */
73
+ custom?: CustomRangeBounds | null;
74
+ }
75
+ /**
76
+ * Pick the bucket unit for an arbitrary span. Hourly up to two days (a bucket
77
+ * per hour over 48h is 48 points β€” readable); daily past that, because 90 days
78
+ * of hourly buckets is 2,160 points nobody can read, which is the same argument
79
+ * the fixed presets make.
80
+ */
81
+ export declare const unitForSpan: (spanMs: number) => BucketUnit;
82
+ export declare const rangePreset: (key: RangeKey) => RangePreset;
83
+ export declare const HOUR_MS = 3600000;
84
+ export declare const DAY_MS = 86400000;
85
+ /** Local midnight at the start of `t`'s day. */
86
+ export declare const startOfDay: (t: number) => number;
87
+ /** Local start of `t`'s hour. */
88
+ export declare const startOfHour: (t: number) => number;
89
+ /** Start of the bucket `t` falls in, for either unit. */
90
+ export declare const startOfBucket: (t: number, unit: BucketUnit) => number;
91
+ /**
92
+ * Add `n` calendar days, keeping the wall clock. `t + n * DAY_MS` drifts by an
93
+ * hour twice a year and lands on the wrong calendar day, which shows up as a
94
+ * duplicated or missing bar exactly once every spring and autumn β€” the kind of
95
+ * bug nobody reproduces.
96
+ *
97
+ * The time-of-day is carried through rather than normalised to midnight: every
98
+ * caller here passes an already-aligned value, so normalising would be
99
+ * invisible in this file and a trap for the first caller that does not.
100
+ */
101
+ export declare const addDays: (t: number, n: number) => number;
102
+ export interface ResolvedRange {
103
+ key: RangeKey;
104
+ /** Inclusive start (epoch ms), bucket-aligned. */
105
+ from: number;
106
+ /** Exclusive end (epoch ms) β€” `now`, so the newest bucket is partial. */
107
+ to: number;
108
+ unit: BucketUnit;
109
+ /** Every bucket start in the range, oldest β†’ newest. */
110
+ buckets: number[];
111
+ /**
112
+ * The comparable previous window, for a baseline. `null` for `all` (nothing
113
+ * precedes everything) β€” a caller must then say "no baseline", never zero.
114
+ */
115
+ previous: {
116
+ from: number;
117
+ to: number;
118
+ } | null;
119
+ }
120
+ /**
121
+ * Turn a preset into concrete bounds and bucket starts.
122
+ *
123
+ * The third argument is either the `earliest` timestamp (the original,
124
+ * still-supported form β€” `all` is bounded by the first datum, and absent one it
125
+ * falls back to 90 days so a caller can never ask for a bucket array reaching
126
+ * back to the epoch) or an options object, which is how `custom` receives its
127
+ * bounds.
128
+ *
129
+ * πŸ”΄ `custom` with no bounds is NOT an error and must not throw β€” a switcher can
130
+ * legitimately be on Custom before the reader has picked two dates. It degrades
131
+ * to the `month` preset, which is a visible, ordinary window rather than an
132
+ * empty chart the reader has to diagnose.
133
+ */
134
+ export declare function resolveRange(key: RangeKey, now: number, options?: number | ResolveRangeOptions): ResolvedRange;
135
+ /**
136
+ * The three fields it takes to STATE a window β€” deliberately less than a
137
+ * `ResolvedRange`, so a server can put the window on the wire beside an
138
+ * aggregate without also shipping its bucket array. Every `ResolvedRange`
139
+ * satisfies it.
140
+ */
141
+ export type RangeWindow = Pick<ResolvedRange, 'key' | 'from' | 'to'>;
142
+ /**
143
+ * The active window, stated in words β€” what a panel prints so a reader never has
144
+ * to infer which period a number covers.
145
+ *
146
+ * This exists because of one owner report, 2026-08-13: *"what time period are we
147
+ * showing. For instance 'Spend & tokens by model' doesn't say if its per this
148
+ * month or week"*. A figure whose window is unstated is not a smaller version of
149
+ * the same figure β€” it is unreadable, because every reasonable guess gives a
150
+ * different answer.
151
+ */
152
+ export declare function rangeLabel(range: RangeWindow): string;
153
+ /**
154
+ * Sum `weight(item)` into buckets by `at(item)`. Items outside the range are
155
+ * dropped β€” silently, because a caller passing "all events" and asking for
156
+ * today is the normal case, not an error.
157
+ *
158
+ * Returns one number per bucket, index-aligned with `range.buckets`, so a
159
+ * caller can zip it against any other series from the same range without
160
+ * re-deriving the axis.
161
+ */
162
+ export declare function bucketize<T>(items: readonly T[], range: ResolvedRange, at: (item: T) => number, weight?: (item: T) => number): number[];
163
+ /**
164
+ * How much of `[from, to)` overlaps `[windowFrom, windowTo)`. The primitive
165
+ * behind every "…excluding the time it was paused" number: a duration is only
166
+ * honest once the intervals it should not be charged for are subtracted.
167
+ */
168
+ export declare const overlapMs: (from: number, to: number, windowFrom: number, windowTo: number) => number;
169
+ /**
170
+ * `[from, to)` minus every interval in `exclude`. Used for pause-adjusted
171
+ * durations and for the denominator of a rate that must not count downtime.
172
+ *
173
+ * `exclude` may overlap itself and need not be sorted β€” overlapping pause
174
+ * windows (an engine pause inside a usage pause) must not subtract twice, which
175
+ * is the bug a naive sum produces.
176
+ */
177
+ export declare function durationExcluding(from: number, to: number, exclude: readonly {
178
+ from: number;
179
+ to: number;
180
+ }[]): number;
181
+ /** Median of a numeric list. `null` for empty β€” never 0, which is a real value. */
182
+ export declare function median(values: readonly number[]): number | null;
183
+ /** The `p`-th percentile (0–1), nearest-rank. `null` for empty. */
184
+ export declare function percentile(values: readonly number[], p: number): number | null;
185
+ /** A day key (`2026-08-13`) in LOCAL time β€” never `toISOString`, which is UTC. */
186
+ export declare function dayKey(t: number): string;
@@ -0,0 +1,271 @@
1
+ /**
2
+ * The time-range vocabulary every analytics surface shares β€” the presets, the
3
+ * bucket size each one implies, and the bucketing itself.
4
+ *
5
+ * ── Why this is a shared primitive and not per-dashboard code ────────────────
6
+ * Every dashboard needs "today / this week / this month", and every one that
7
+ * hand-rolls it gets the same three things subtly wrong:
8
+ *
9
+ * 1. **"Today" is not "the last 24 hours."** The owner asking *"has the
10
+ * orchestrator done well today?"* means since midnight in THEIR timezone,
11
+ * against comparable earlier days. A rolling 24h window silently mixes last
12
+ * night into this morning and makes every day look average.
13
+ * 2. **The bucket must follow the range.** Hourly buckets over 90 days is 2,160
14
+ * points nobody can read; daily buckets over one day is a single bar. The
15
+ * preset owns both, so a range change cannot leave a mismatched bucket
16
+ * behind.
17
+ * 3. **A comparison needs a baseline of the same shape.** "42 tasks" answers
18
+ * nothing. "42, against a 7-day median of 26" answers the question that was
19
+ * actually asked β€” and the baseline has to be the same length as the range
20
+ * or the comparison is meaningless.
21
+ *
22
+ * Framework-free and DOM-free on purpose: the ENGINE computes with it and the
23
+ * BROWSER renders with it, and they must bucket identically or a chart's x-axis
24
+ * disagrees with its own tooltip.
25
+ *
26
+ * ── Local days, not UTC days ────────────────────────────────────────────────
27
+ * Day boundaries come from the host's own calendar (`new Date(y, m, d)`), so a
28
+ * day is the day the reader lived through. UTC bucketing would move "today" by
29
+ * 5–6 hours in America/Chicago and put a normal evening's work on tomorrow.
30
+ * πŸ”΄ It follows that the ENGINE must be the one to bucket (it shares the
31
+ * owner's timezone) β€” a prod box on UTC bucketing for a Chicago reader
32
+ * reintroduces exactly this bug.
33
+ */
34
+ export const RANGE_PRESETS = [
35
+ {
36
+ key: 'today',
37
+ label: 'Today',
38
+ description: 'Since midnight, by hour',
39
+ unit: 'hour',
40
+ days: 1,
41
+ },
42
+ { key: 'week', label: '7 days', description: 'The last 7 days, by day', unit: 'day', days: 7 },
43
+ {
44
+ key: 'month',
45
+ label: '30 days',
46
+ description: 'The last 30 days, by day',
47
+ unit: 'day',
48
+ days: 30,
49
+ },
50
+ {
51
+ key: 'quarter',
52
+ label: '90 days',
53
+ description: 'The last 90 days, by day',
54
+ unit: 'day',
55
+ days: 90,
56
+ },
57
+ {
58
+ key: 'year',
59
+ label: '1 year',
60
+ description: 'The last 365 days, by day',
61
+ unit: 'day',
62
+ days: 365,
63
+ },
64
+ { key: 'all', label: 'All', description: 'Everything on record, by day', unit: 'day', days: null },
65
+ {
66
+ key: 'custom',
67
+ label: 'Custom',
68
+ description: 'A range you pick',
69
+ unit: 'day',
70
+ // Not a width. `custom` is resolved from the caller's own bounds; `days` is
71
+ // null for the same reason `all`'s is β€” neither has one until it is asked.
72
+ days: null,
73
+ },
74
+ ];
75
+ /** The presets with a fixed width, in order β€” the ordinary switcher's options. */
76
+ export const FIXED_RANGE_KEYS = RANGE_PRESETS.filter((p) => p.key !== 'custom').map((p) => p.key);
77
+ /**
78
+ * Pick the bucket unit for an arbitrary span. Hourly up to two days (a bucket
79
+ * per hour over 48h is 48 points β€” readable); daily past that, because 90 days
80
+ * of hourly buckets is 2,160 points nobody can read, which is the same argument
81
+ * the fixed presets make.
82
+ */
83
+ export const unitForSpan = (spanMs) => (spanMs <= 2 * DAY_MS ? 'hour' : 'day');
84
+ export const rangePreset = (key) => RANGE_PRESETS.find((p) => p.key === key) ?? RANGE_PRESETS[1];
85
+ export const HOUR_MS = 3_600_000;
86
+ export const DAY_MS = 86_400_000;
87
+ /** Local midnight at the start of `t`'s day. */
88
+ export const startOfDay = (t) => {
89
+ const d = new Date(t);
90
+ return new Date(d.getFullYear(), d.getMonth(), d.getDate()).getTime();
91
+ };
92
+ /** Local start of `t`'s hour. */
93
+ export const startOfHour = (t) => {
94
+ const d = new Date(t);
95
+ return new Date(d.getFullYear(), d.getMonth(), d.getDate(), d.getHours()).getTime();
96
+ };
97
+ /** Start of the bucket `t` falls in, for either unit. */
98
+ export const startOfBucket = (t, unit) => unit === 'hour' ? startOfHour(t) : startOfDay(t);
99
+ /**
100
+ * Add `n` calendar days, keeping the wall clock. `t + n * DAY_MS` drifts by an
101
+ * hour twice a year and lands on the wrong calendar day, which shows up as a
102
+ * duplicated or missing bar exactly once every spring and autumn β€” the kind of
103
+ * bug nobody reproduces.
104
+ *
105
+ * The time-of-day is carried through rather than normalised to midnight: every
106
+ * caller here passes an already-aligned value, so normalising would be
107
+ * invisible in this file and a trap for the first caller that does not.
108
+ */
109
+ export const addDays = (t, n) => {
110
+ const d = new Date(t);
111
+ return new Date(d.getFullYear(), d.getMonth(), d.getDate() + n, d.getHours(), d.getMinutes(), d.getSeconds(), d.getMilliseconds()).getTime();
112
+ };
113
+ /**
114
+ * Turn a preset into concrete bounds and bucket starts.
115
+ *
116
+ * The third argument is either the `earliest` timestamp (the original,
117
+ * still-supported form β€” `all` is bounded by the first datum, and absent one it
118
+ * falls back to 90 days so a caller can never ask for a bucket array reaching
119
+ * back to the epoch) or an options object, which is how `custom` receives its
120
+ * bounds.
121
+ *
122
+ * πŸ”΄ `custom` with no bounds is NOT an error and must not throw β€” a switcher can
123
+ * legitimately be on Custom before the reader has picked two dates. It degrades
124
+ * to the `month` preset, which is a visible, ordinary window rather than an
125
+ * empty chart the reader has to diagnose.
126
+ */
127
+ export function resolveRange(key, now, options) {
128
+ const opts = typeof options === 'number' ? { earliest: options } : (options ?? {});
129
+ if (key === 'custom') {
130
+ const bounds = opts.custom;
131
+ if (!bounds || !Number.isFinite(bounds.from) || !Number.isFinite(bounds.to) || bounds.to <= bounds.from) {
132
+ return resolveRange('month', now, { earliest: opts.earliest });
133
+ }
134
+ const unit = unitForSpan(bounds.to - bounds.from);
135
+ const from = startOfBucket(bounds.from, unit);
136
+ const to = bounds.to;
137
+ const buckets = bucketStarts(from, to, unit);
138
+ const span = to - from;
139
+ return { key, from, to, unit, buckets, previous: { from: from - span, to: from } };
140
+ }
141
+ const preset = rangePreset(key);
142
+ const from = preset.days === null
143
+ ? startOfDay(opts.earliest ?? addDays(now, -90))
144
+ : preset.unit === 'hour'
145
+ ? startOfDay(now)
146
+ : addDays(startOfDay(now), -(preset.days - 1));
147
+ const buckets = bucketStarts(from, now, preset.unit);
148
+ const span = now - from;
149
+ return {
150
+ key,
151
+ from,
152
+ to: now,
153
+ unit: preset.unit,
154
+ buckets,
155
+ previous: preset.days === null ? null : { from: from - span, to: from },
156
+ };
157
+ }
158
+ /** Every bucket start in `[from, to]`, oldest β†’ newest. Never empty. */
159
+ function bucketStarts(from, to, unit) {
160
+ const buckets = [];
161
+ if (unit === 'hour') {
162
+ for (let t = from; t <= to; t += HOUR_MS)
163
+ buckets.push(t);
164
+ }
165
+ else {
166
+ for (let t = from; t <= to; t = addDays(t, 1))
167
+ buckets.push(t);
168
+ }
169
+ // A guard, not a formality: a corrupt clock (or an `earliest` in the future)
170
+ // could otherwise produce an empty axis with no explanation.
171
+ if (buckets.length === 0)
172
+ buckets.push(from);
173
+ return buckets;
174
+ }
175
+ /**
176
+ * The active window, stated in words β€” what a panel prints so a reader never has
177
+ * to infer which period a number covers.
178
+ *
179
+ * This exists because of one owner report, 2026-08-13: *"what time period are we
180
+ * showing. For instance 'Spend & tokens by model' doesn't say if its per this
181
+ * month or week"*. A figure whose window is unstated is not a smaller version of
182
+ * the same figure β€” it is unreadable, because every reasonable guess gives a
183
+ * different answer.
184
+ */
185
+ export function rangeLabel(range) {
186
+ if (range.key === 'today')
187
+ return 'Today, since midnight';
188
+ if (range.key === 'all')
189
+ return `All time β€” since ${dayKey(range.from)}`;
190
+ const first = dayKey(range.from);
191
+ const last = dayKey(range.to);
192
+ return first === last ? first : `${first} β†’ ${last}`;
193
+ }
194
+ /**
195
+ * Sum `weight(item)` into buckets by `at(item)`. Items outside the range are
196
+ * dropped β€” silently, because a caller passing "all events" and asking for
197
+ * today is the normal case, not an error.
198
+ *
199
+ * Returns one number per bucket, index-aligned with `range.buckets`, so a
200
+ * caller can zip it against any other series from the same range without
201
+ * re-deriving the axis.
202
+ */
203
+ export function bucketize(items, range, at, weight = () => 1) {
204
+ const out = new Array(range.buckets.length).fill(0);
205
+ const index = new Map(range.buckets.map((t, i) => [t, i]));
206
+ for (const item of items) {
207
+ const t = at(item);
208
+ if (!Number.isFinite(t) || t < range.from || t > range.to)
209
+ continue;
210
+ const i = index.get(startOfBucket(t, range.unit));
211
+ if (i !== undefined)
212
+ out[i] += weight(item);
213
+ }
214
+ return out;
215
+ }
216
+ /**
217
+ * How much of `[from, to)` overlaps `[windowFrom, windowTo)`. The primitive
218
+ * behind every "…excluding the time it was paused" number: a duration is only
219
+ * honest once the intervals it should not be charged for are subtracted.
220
+ */
221
+ export const overlapMs = (from, to, windowFrom, windowTo) => Math.max(0, Math.min(to, windowTo) - Math.max(from, windowFrom));
222
+ /**
223
+ * `[from, to)` minus every interval in `exclude`. Used for pause-adjusted
224
+ * durations and for the denominator of a rate that must not count downtime.
225
+ *
226
+ * `exclude` may overlap itself and need not be sorted β€” overlapping pause
227
+ * windows (an engine pause inside a usage pause) must not subtract twice, which
228
+ * is the bug a naive sum produces.
229
+ */
230
+ export function durationExcluding(from, to, exclude) {
231
+ const total = Math.max(0, to - from);
232
+ if (total === 0 || exclude.length === 0)
233
+ return total;
234
+ const clipped = exclude
235
+ .map((w) => ({ from: Math.max(w.from, from), to: Math.min(w.to, to) }))
236
+ .filter((w) => w.to > w.from)
237
+ .sort((a, b) => a.from - b.from);
238
+ let excluded = 0;
239
+ let cursor = -Infinity;
240
+ for (const w of clipped) {
241
+ const start = Math.max(w.from, cursor);
242
+ if (w.to > start) {
243
+ excluded += w.to - start;
244
+ cursor = w.to;
245
+ }
246
+ }
247
+ return Math.max(0, total - excluded);
248
+ }
249
+ /** Median of a numeric list. `null` for empty β€” never 0, which is a real value. */
250
+ export function median(values) {
251
+ if (values.length === 0)
252
+ return null;
253
+ const s = [...values].sort((a, b) => a - b);
254
+ const mid = s.length >> 1;
255
+ return s.length % 2 === 0 ? (s[mid - 1] + s[mid]) / 2 : s[mid];
256
+ }
257
+ /** The `p`-th percentile (0–1), nearest-rank. `null` for empty. */
258
+ export function percentile(values, p) {
259
+ if (values.length === 0)
260
+ return null;
261
+ const s = [...values].sort((a, b) => a - b);
262
+ const rank = Math.ceil(Math.min(1, Math.max(0, p)) * s.length);
263
+ return s[Math.max(0, rank - 1)];
264
+ }
265
+ /** A day key (`2026-08-13`) in LOCAL time β€” never `toISOString`, which is UTC. */
266
+ export function dayKey(t) {
267
+ const d = new Date(t);
268
+ const mm = String(d.getMonth() + 1).padStart(2, '0');
269
+ const dd = String(d.getDate()).padStart(2, '0');
270
+ return `${d.getFullYear()}-${mm}-${dd}`;
271
+ }