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.
- package/LICENSE +15 -0
- package/dist/client/wsWire.d.ts +49 -0
- package/dist/client/wsWire.js +57 -0
- package/dist/config/appConfig.d.ts +361 -0
- package/dist/config/appConfig.js +308 -0
- package/dist/core/activity/index.d.ts +8 -0
- package/dist/core/activity/index.js +7 -0
- package/dist/core/activity/model.d.ts +115 -0
- package/dist/core/activity/model.js +63 -0
- package/dist/core/analytics/index.d.ts +18 -0
- package/dist/core/analytics/index.js +18 -0
- package/dist/core/analytics/range.d.ts +186 -0
- package/dist/core/analytics/range.js +271 -0
- package/dist/core/analytics/schedule.d.ts +301 -0
- package/dist/core/analytics/schedule.js +372 -0
- package/dist/core/category/index.d.ts +3 -0
- package/dist/core/category/index.js +6 -0
- package/dist/core/category/memoryStore.d.ts +21 -0
- package/dist/core/category/memoryStore.js +97 -0
- package/dist/core/category/tree.d.ts +26 -0
- package/dist/core/category/tree.js +113 -0
- package/dist/core/category/types.d.ts +48 -0
- package/dist/core/category/types.js +8 -0
- package/dist/core/color.d.ts +14 -0
- package/dist/core/color.js +32 -0
- package/dist/core/colorScheme/colorSchemeStore.d.ts +70 -0
- package/dist/core/colorScheme/colorSchemeStore.js +123 -0
- package/dist/core/colorScheme/index.d.ts +7 -0
- package/dist/core/colorScheme/index.js +7 -0
- package/dist/core/domainOwners.d.ts +19 -0
- package/dist/core/domainOwners.js +15 -0
- package/dist/core/download.d.ts +14 -0
- package/dist/core/download.js +37 -0
- package/dist/core/events/ccEventBus.d.ts +44 -0
- package/dist/core/events/ccEventBus.js +70 -0
- package/dist/core/file-tree/fileTreeModel.d.ts +276 -0
- package/dist/core/file-tree/fileTreeModel.js +312 -0
- package/dist/core/folder-tree/actions.d.ts +84 -0
- package/dist/core/folder-tree/actions.js +119 -0
- package/dist/core/folder-tree/index.d.ts +3 -0
- package/dist/core/folder-tree/index.js +20 -0
- package/dist/core/folder-tree/path.d.ts +76 -0
- package/dist/core/folder-tree/path.js +146 -0
- package/dist/core/folder-tree/tree.d.ts +86 -0
- package/dist/core/folder-tree/tree.js +184 -0
- package/dist/core/layout/index.d.ts +1 -0
- package/dist/core/layout/index.js +7 -0
- package/dist/core/master-lock/index.d.ts +14 -0
- package/dist/core/master-lock/index.js +14 -0
- package/dist/core/master-lock/kdf.d.ts +63 -0
- package/dist/core/master-lock/kdf.js +117 -0
- package/dist/core/master-lock/policy.d.ts +32 -0
- package/dist/core/master-lock/policy.js +52 -0
- package/dist/core/master-lock/presence.d.ts +52 -0
- package/dist/core/master-lock/presence.js +61 -0
- package/dist/core/master-lock/wire.d.ts +139 -0
- package/dist/core/master-lock/wire.js +81 -0
- package/dist/core/media/bgRemoval.d.ts +27 -0
- package/dist/core/media/bgRemoval.js +101 -0
- package/dist/core/media/ffmpeg.d.ts +57 -0
- package/dist/core/media/ffmpeg.js +126 -0
- package/dist/core/media/format.d.ts +20 -0
- package/dist/core/media/format.js +54 -0
- package/dist/core/media/index.d.ts +12 -0
- package/dist/core/media/index.js +18 -0
- package/dist/core/media/mediaItem.d.ts +15 -0
- package/dist/core/media/mediaItem.js +34 -0
- package/dist/core/media/renderPlan.d.ts +63 -0
- package/dist/core/media/renderPlan.js +232 -0
- package/dist/core/media/segment.d.ts +79 -0
- package/dist/core/media/segment.js +247 -0
- package/dist/core/media/size.d.ts +1 -0
- package/dist/core/media/size.js +3 -0
- package/dist/core/media/trim.d.ts +21 -0
- package/dist/core/media/trim.js +40 -0
- package/dist/core/media/types.d.ts +148 -0
- package/dist/core/media/types.js +11 -0
- package/dist/core/media/uploadLimits.d.ts +7 -0
- package/dist/core/media/uploadLimits.js +16 -0
- package/dist/core/media/validate.d.ts +9 -0
- package/dist/core/media/validate.js +121 -0
- package/dist/core/media/videoCuts.d.ts +152 -0
- package/dist/core/media/videoCuts.js +284 -0
- package/dist/core/navigation/breadcrumbCompat.d.ts +24 -0
- package/dist/core/navigation/breadcrumbCompat.js +21 -0
- package/dist/core/navigation/coverageTest.d.ts +24 -0
- package/dist/core/navigation/coverageTest.js +48 -0
- package/dist/core/navigation/index.d.ts +6 -0
- package/dist/core/navigation/index.js +10 -0
- package/dist/core/navigation/migrateNavPreferences.d.ts +55 -0
- package/dist/core/navigation/migrateNavPreferences.js +63 -0
- package/dist/core/navigation/navigationModel.d.ts +165 -0
- package/dist/core/navigation/navigationModel.js +14 -0
- package/dist/core/navigation/navigationSelectors.d.ts +63 -0
- package/dist/core/navigation/navigationSelectors.js +167 -0
- package/dist/core/navigation/searchCatalogue.d.ts +23 -0
- package/dist/core/navigation/searchCatalogue.js +76 -0
- package/dist/core/net/reconnectSchedule.d.ts +46 -0
- package/dist/core/net/reconnectSchedule.js +38 -0
- package/dist/core/palette/ShortcutRegistry.d.ts +33 -0
- package/dist/core/palette/ShortcutRegistry.js +52 -0
- package/dist/core/palette/fuzzyRank.d.ts +18 -0
- package/dist/core/palette/fuzzyRank.js +73 -0
- package/dist/core/palette/hotkeys.d.ts +28 -0
- package/dist/core/palette/hotkeys.js +118 -0
- package/dist/core/palette/index.d.ts +3 -0
- package/dist/core/palette/index.js +8 -0
- package/dist/core/schema/projection.d.ts +84 -0
- package/dist/core/schema/projection.js +184 -0
- package/dist/core/session/claims.d.ts +145 -0
- package/dist/core/session/claims.js +157 -0
- package/dist/core/sharing/index.d.ts +5 -0
- package/dist/core/sharing/index.js +4 -0
- package/dist/core/sharing/model.d.ts +195 -0
- package/dist/core/sharing/model.js +150 -0
- package/dist/core/site/siteCopy.d.ts +92 -0
- package/dist/core/site/siteCopy.js +91 -0
- package/dist/core/slots.d.ts +22 -0
- package/dist/core/slots.js +31 -0
- package/dist/core/styles/constructableSheet.d.ts +51 -0
- package/dist/core/styles/constructableSheet.js +80 -0
- package/dist/core/tokens/registry.d.ts +119 -0
- package/dist/core/tokens/registry.js +447 -0
- package/dist/core/uiScale/index.d.ts +8 -0
- package/dist/core/uiScale/index.js +8 -0
- package/dist/core/uiScale/scale.d.ts +36 -0
- package/dist/core/uiScale/scale.js +47 -0
- package/dist/core/uiScale/uiScaleStore.d.ts +47 -0
- package/dist/core/uiScale/uiScaleStore.js +63 -0
- package/dist/core/wire/__fixtures__/rowsMarshalWorker.d.ts +1 -0
- package/dist/core/wire/__fixtures__/rowsMarshalWorker.js +23 -0
- package/dist/core/wire/index.d.ts +17 -0
- package/dist/core/wire/index.js +17 -0
- package/dist/core/wire/ingress.d.ts +44 -0
- package/dist/core/wire/ingress.js +134 -0
- package/dist/core/wire/marshalWorker.d.ts +70 -0
- package/dist/core/wire/marshalWorker.js +273 -0
- package/dist/core/wire/marshalling.d.ts +122 -0
- package/dist/core/wire/marshalling.js +126 -0
- package/dist/core/wire/wireHub.d.ts +80 -0
- package/dist/core/wire/wireHub.js +116 -0
- package/dist/ctgr/base64url.d.ts +8 -0
- package/dist/ctgr/base64url.js +39 -0
- package/dist/ctgr/chunkAccumulator.d.ts +214 -0
- package/dist/ctgr/chunkAccumulator.js +307 -0
- package/dist/ctgr/chunkStore.d.ts +24 -0
- package/dist/ctgr/chunkStore.js +125 -0
- package/dist/ctgr/codec.d.ts +44 -0
- package/dist/ctgr/codec.js +153 -0
- package/dist/ctgr/compress.d.ts +2 -0
- package/dist/ctgr/compress.js +20 -0
- package/dist/ctgr/crc32.d.ts +2 -0
- package/dist/ctgr/crc32.js +23 -0
- package/dist/ctgr/digest.d.ts +1 -0
- package/dist/ctgr/digest.js +15 -0
- package/dist/ctgr/index.d.ts +9 -0
- package/dist/ctgr/index.js +17 -0
- package/dist/ctgr/types.d.ts +64 -0
- package/dist/ctgr/types.js +21 -0
- package/dist/ctgr/v0compat.d.ts +27 -0
- package/dist/ctgr/v0compat.js +128 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.js +17 -0
- package/dist/shared/errors.d.ts +101 -0
- package/dist/shared/errors.js +99 -0
- package/dist/testing/bytes.d.ts +32 -0
- package/dist/testing/bytes.js +33 -0
- package/dist/testing/domInspect.d.ts +59 -0
- package/dist/testing/domInspect.js +115 -0
- package/dist/testing/fakeFetch.d.ts +23 -0
- package/dist/testing/fakeFetch.js +21 -0
- package/dist/testing/index.d.ts +3 -0
- package/dist/testing/index.js +11 -0
- package/package.json +373 -0
- package/src/client/wsWire.ts +83 -0
- package/src/config/appConfig.spec.ts +158 -0
- package/src/config/appConfig.ts +372 -0
- package/src/core/activity/index.ts +21 -0
- package/src/core/activity/model.ts +160 -0
- package/src/core/analytics/index.ts +18 -0
- package/src/core/analytics/range.spec.ts +280 -0
- package/src/core/analytics/range.ts +383 -0
- package/src/core/analytics/schedule.spec.ts +311 -0
- package/src/core/analytics/schedule.ts +624 -0
- package/src/core/category/category.spec.ts +144 -0
- package/src/core/category/index.ts +19 -0
- package/src/core/category/memoryStore.ts +104 -0
- package/src/core/category/tree.ts +118 -0
- package/src/core/category/types.ts +59 -0
- package/src/core/color.spec.ts +35 -0
- package/src/core/color.ts +35 -0
- package/src/core/colorScheme/colorScheme.spec.ts +127 -0
- package/src/core/colorScheme/colorSchemeStore.ts +184 -0
- package/src/core/colorScheme/index.ts +23 -0
- package/src/core/domainOwners.ts +32 -0
- package/src/core/download.spec.ts +38 -0
- package/src/core/download.ts +35 -0
- package/src/core/events/ccEventBus.spec.ts +90 -0
- package/src/core/events/ccEventBus.ts +120 -0
- package/src/core/file-tree/fileTreeModel.spec.ts +462 -0
- package/src/core/file-tree/fileTreeModel.ts +511 -0
- package/src/core/folder-tree/actions.spec.ts +103 -0
- package/src/core/folder-tree/actions.ts +153 -0
- package/src/core/folder-tree/index.ts +46 -0
- package/src/core/folder-tree/path.spec.ts +113 -0
- package/src/core/folder-tree/path.ts +148 -0
- package/src/core/folder-tree/tree.spec.ts +157 -0
- package/src/core/folder-tree/tree.ts +244 -0
- package/src/core/layout/index.ts +7 -0
- package/src/core/master-lock/index.ts +50 -0
- package/src/core/master-lock/kdf.spec.ts +95 -0
- package/src/core/master-lock/kdf.ts +145 -0
- package/src/core/master-lock/policy.ts +55 -0
- package/src/core/master-lock/presence.spec.ts +73 -0
- package/src/core/master-lock/presence.ts +71 -0
- package/src/core/master-lock/wire.ts +155 -0
- package/src/core/media/bgRemoval.spec.ts +120 -0
- package/src/core/media/bgRemoval.ts +123 -0
- package/src/core/media/ffmpeg.spec.ts +135 -0
- package/src/core/media/ffmpeg.ts +162 -0
- package/src/core/media/format.spec.ts +41 -0
- package/src/core/media/format.ts +58 -0
- package/src/core/media/index.ts +105 -0
- package/src/core/media/mediaItem.spec.ts +46 -0
- package/src/core/media/mediaItem.ts +40 -0
- package/src/core/media/renderPlan.spec.ts +140 -0
- package/src/core/media/renderPlan.ts +318 -0
- package/src/core/media/segment.spec.ts +159 -0
- package/src/core/media/segment.ts +352 -0
- package/src/core/media/size.spec.ts +19 -0
- package/src/core/media/size.ts +3 -0
- package/src/core/media/trim.spec.ts +75 -0
- package/src/core/media/trim.ts +60 -0
- package/src/core/media/types.ts +147 -0
- package/src/core/media/uploadLimits.ts +18 -0
- package/src/core/media/validate.spec.ts +106 -0
- package/src/core/media/validate.ts +131 -0
- package/src/core/media/videoCuts.spec.ts +327 -0
- package/src/core/media/videoCuts.ts +355 -0
- package/src/core/navigation/breadcrumbCompat.ts +44 -0
- package/src/core/navigation/coverageTest.ts +54 -0
- package/src/core/navigation/index.ts +50 -0
- package/src/core/navigation/migrateNavPreferences.ts +119 -0
- package/src/core/navigation/navigation.spec.ts +388 -0
- package/src/core/navigation/navigationModel.ts +185 -0
- package/src/core/navigation/navigationSelectors.ts +206 -0
- package/src/core/navigation/searchCatalogue.ts +89 -0
- package/src/core/net/reconnectSchedule.spec.ts +49 -0
- package/src/core/net/reconnectSchedule.ts +78 -0
- package/src/core/palette/ShortcutRegistry.ts +69 -0
- package/src/core/palette/fuzzyRank.ts +78 -0
- package/src/core/palette/hotkeys.ts +132 -0
- package/src/core/palette/index.ts +22 -0
- package/src/core/palette/palette.spec.ts +160 -0
- package/src/core/schema/projection.spec.ts +122 -0
- package/src/core/schema/projection.ts +225 -0
- package/src/core/session/claims.ts +181 -0
- package/src/core/sharing/index.ts +33 -0
- package/src/core/sharing/model.spec.ts +104 -0
- package/src/core/sharing/model.ts +296 -0
- package/src/core/site/siteCopy.ts +115 -0
- package/src/core/slots.ts +38 -0
- package/src/core/styles/constructableSheet.ts +86 -0
- package/src/core/tokens/registry.spec.ts +160 -0
- package/src/core/tokens/registry.ts +536 -0
- package/src/core/uiScale/index.ts +32 -0
- package/src/core/uiScale/scale.ts +63 -0
- package/src/core/uiScale/uiScale.spec.ts +77 -0
- package/src/core/uiScale/uiScaleStore.ts +100 -0
- package/src/core/wire/__fixtures__/rowsMarshalWorker.ts +24 -0
- package/src/core/wire/index.ts +56 -0
- package/src/core/wire/ingress.spec.ts +276 -0
- package/src/core/wire/ingress.ts +182 -0
- package/src/core/wire/marshalWorker.spec.ts +196 -0
- package/src/core/wire/marshalWorker.ts +361 -0
- package/src/core/wire/marshalling.spec.ts +139 -0
- package/src/core/wire/marshalling.ts +238 -0
- package/src/core/wire/wireHub.spec.ts +137 -0
- package/src/core/wire/wireHub.ts +213 -0
- package/src/ctgr/base64url.ts +44 -0
- package/src/ctgr/chunkAccumulator.ts +403 -0
- package/src/ctgr/chunkStore.spec.ts +101 -0
- package/src/ctgr/chunkStore.ts +168 -0
- package/src/ctgr/codec.spec.ts +208 -0
- package/src/ctgr/codec.ts +202 -0
- package/src/ctgr/compress.ts +21 -0
- package/src/ctgr/crc32.ts +23 -0
- package/src/ctgr/digest.ts +14 -0
- package/src/ctgr/index.ts +39 -0
- package/src/ctgr/types.ts +80 -0
- package/src/ctgr/v0compat.spec.ts +73 -0
- package/src/ctgr/v0compat.ts +156 -0
- package/src/declaredDepsAreImported.spec.ts +87 -0
- package/src/index.ts +25 -0
- package/src/leafSubpathsImportNothing.spec.ts +287 -0
- package/src/lockfileIsTracked.spec.ts +50 -0
- package/src/namedSubpathsResolve.spec.ts +122 -0
- package/src/noPathDeps.spec.ts +24 -0
- package/src/publishShape.spec.ts +89 -0
- package/src/shared/errors.ts +153 -0
- package/src/skippedTestsAreAnswered.spec.ts +221 -0
- package/src/testTmpRoot.spec.ts +102 -0
- package/src/testing/bytes.ts +38 -0
- package/src/testing/domInspect.spec.ts +79 -0
- package/src/testing/domInspect.ts +127 -0
- package/src/testing/fakeFetch.ts +26 -0
- 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
|
+
}
|