nilometer 0.0.0-stage → 0.2.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/CHANGELOG.md +68 -0
- package/LICENSE +21 -0
- package/README.md +464 -2
- package/core/pricing/prices.json +348 -0
- package/core/schema/001_ingestion.sql +57 -0
- package/core/schema/002_line_count.sql +7 -0
- package/core/schema/003_derived.sql +70 -0
- package/core/schema/004_requests_dedup.sql +50 -0
- package/core/schema/005_repositories.sql +25 -0
- package/core/schema/006_status_readings.sql +33 -0
- package/core/schema/007_pricing.sql +95 -0
- package/core/schema/008_interruptions.sql +263 -0
- package/core/schema/009_headroom.sql +84 -0
- package/core/schema/010_attribution.sql +106 -0
- package/core/schema/011_projections.sql +83 -0
- package/core/schema/012_unsplit_cache_writes.sql +49 -0
- package/core/schema/013_repository_revalidation.sql +104 -0
- package/core/schema/014_observed_readings.sql +204 -0
- package/core/schema/015_response_observations.sql +127 -0
- package/core/schema/016_windows_repository_paths.sql +41 -0
- package/core/schema/017_unattributed_usage_once.sql +47 -0
- package/core/schema/018_request_time_index.sql +21 -0
- package/core/schema/019_dedup_earliest_of_equals.sql +61 -0
- package/core/schema/020_limit_quota_fields.sql +131 -0
- package/core/schema/021_plan_periods.sql +121 -0
- package/core/schema/022_attachment_walk.sql +223 -0
- package/core/schema/023_auto_continuations.sql +129 -0
- package/core/schema/024_counted_limit_groups.sql +29 -0
- package/core/schema/025_model_fallback.sql +77 -0
- package/core/schema/026_changed_readings.sql +106 -0
- package/core/schema/027_weekly_crossings.sql +32 -0
- package/core/schema/028_stale_readings.sql +196 -0
- package/core/schema/029_lockout_sessions.sql +68 -0
- package/core/schema/030_repository_aliases.sql +81 -0
- package/core/schema/031_cte_cursor_budget.sql +153 -0
- package/core/schema/032_stored_views.sql +233 -0
- package/dist/cli/main.cli.js +39 -0
- package/dist/cli/program.js +881 -0
- package/dist/core/db/database.js +182 -0
- package/dist/core/db/fingerprint.js +71 -0
- package/dist/core/ingest/attribution.js +131 -0
- package/dist/core/ingest/classify.js +382 -0
- package/dist/core/ingest/command.js +95 -0
- package/dist/core/ingest/derive.js +219 -0
- package/dist/core/ingest/discover.js +126 -0
- package/dist/core/ingest/ingest.js +160 -0
- package/dist/core/ingest/reader.js +89 -0
- package/dist/core/ingest/reset-time.js +97 -0
- package/dist/core/ingest/results.js +201 -0
- package/dist/core/ingest/spool.js +165 -0
- package/dist/core/ingest/store.js +99 -0
- package/dist/core/ingest/stored-views.js +65 -0
- package/dist/core/install/install.js +185 -0
- package/dist/core/install/locations.js +89 -0
- package/dist/core/install/private-files.js +259 -0
- package/dist/core/install/record.js +103 -0
- package/dist/core/plans/plan-prices.js +128 -0
- package/dist/core/plans/repository-aliases.js +136 -0
- package/dist/core/pricing/prices.js +147 -0
- package/dist/core/settings/settings-file.js +215 -0
- package/dist/core/settings/statusline.js +240 -0
- package/dist/core/util/commands.js +91 -0
- package/dist/core/util/node-version.js +58 -0
- package/dist/core/util/open-page.js +69 -0
- package/dist/core/util/paths.js +36 -0
- package/dist/core/verify/compare.js +77 -0
- package/dist/core/verify/verify.js +220 -0
- package/dist/core/view/server.js +142 -0
- package/dist/viewer/explain.js +574 -0
- package/dist/viewer/format.js +150 -0
- package/dist/viewer/html-charts.js +1292 -0
- package/dist/viewer/html-contract.js +2 -0
- package/dist/viewer/html-data.js +718 -0
- package/dist/viewer/html.js +1717 -0
- package/dist/viewer/queries.js +128 -0
- package/dist/viewer/render.js +602 -0
- package/dist/viewer/report.js +120 -0
- package/dist/viewer/save.js +110 -0
- package/docs/requirements.md +113 -0
- package/hooks/statusline.sh +104 -0
- package/package.json +61 -4
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file Number, time, and coverage formatting for the viewer (docs/development.md P7.1).
|
|
3
|
+
*
|
|
4
|
+
* The only place values are rounded (CLAUDE.md "Round only at display"). SQL returns unrounded
|
|
5
|
+
* values; these functions turn each into text once. A missing value is always the word "unknown",
|
|
6
|
+
* never 0 or an empty cell (add-metric §1).
|
|
7
|
+
*/
|
|
8
|
+
/** Text shown for a value that isn't known. */
|
|
9
|
+
export const UNKNOWN = "unknown";
|
|
10
|
+
/**
|
|
11
|
+
* Makes a value from the logs safe to print in a terminal (D-050).
|
|
12
|
+
*
|
|
13
|
+
* Model names, repository paths, session IDs, and file names come from data an attacker can
|
|
14
|
+
* influence: a directory or branch name, or a line injected into a session log. Printed raw, a
|
|
15
|
+
* newline forges an extra table row, and an escape sequence can recolor, move, or erase what the
|
|
16
|
+
* user is reading. Control characters are written as escapes instead, so the text stays visible and
|
|
17
|
+
* one cell stays one line. Column widths are measured after this, so alignment holds.
|
|
18
|
+
* @param value - Text from the database.
|
|
19
|
+
* @returns The same text with C0 and C1 control characters written as escapes.
|
|
20
|
+
*/
|
|
21
|
+
export function printable(value) {
|
|
22
|
+
const named = { "\n": "\\n", "\r": "\\r", "\t": "\\t", "\u001b": "\\e" };
|
|
23
|
+
return value.replace(
|
|
24
|
+
// eslint-disable-next-line no-control-regex -- matching control characters is the point.
|
|
25
|
+
/[\u0000-\u001f\u007f-\u009f]/g, (char) => named[char] ?? `\\x${char.charCodeAt(0).toString(16).padStart(2, "0")}`);
|
|
26
|
+
}
|
|
27
|
+
/** Integer formatter with thousands separators, independent of the machine's locale. */
|
|
28
|
+
const INTEGER = new Intl.NumberFormat("en-US", { maximumFractionDigits: 0 });
|
|
29
|
+
/** Up to one decimal place, trailing zero dropped (42.3, 42). */
|
|
30
|
+
const ONE_DECIMAL = new Intl.NumberFormat("en-US", { maximumFractionDigits: 1 });
|
|
31
|
+
/** Dollars and cents with thousands separators. */
|
|
32
|
+
const DOLLARS = new Intl.NumberFormat("en-US", {
|
|
33
|
+
minimumFractionDigits: 2,
|
|
34
|
+
maximumFractionDigits: 2,
|
|
35
|
+
});
|
|
36
|
+
/**
|
|
37
|
+
* Formats one value for display.
|
|
38
|
+
* @param value - The unrounded value, or null/undefined when unknown.
|
|
39
|
+
* @param kind - What the value measures.
|
|
40
|
+
* @returns Display text; {@link UNKNOWN} for null, undefined, or a non-finite number.
|
|
41
|
+
* @example
|
|
42
|
+
* formatNumber(1234567, "tokens"); // "1,234,567"
|
|
43
|
+
* formatNumber(0.004, "usd"); // "less than $0.01"
|
|
44
|
+
* formatNumber(10090.6, "duration"); // "2 h 48 m"
|
|
45
|
+
*/
|
|
46
|
+
export function formatNumber(value, kind) {
|
|
47
|
+
if (value === null || value === undefined || !Number.isFinite(value)) {
|
|
48
|
+
return UNKNOWN;
|
|
49
|
+
}
|
|
50
|
+
switch (kind) {
|
|
51
|
+
case "count":
|
|
52
|
+
case "tokens":
|
|
53
|
+
return INTEGER.format(value);
|
|
54
|
+
case "usd":
|
|
55
|
+
// A non-zero amount that rounds to $0.00 would read as free; say it's under a cent instead.
|
|
56
|
+
return value > 0 && value < 0.005 ? "less than $0.01" : `$${DOLLARS.format(value)}`;
|
|
57
|
+
case "percent":
|
|
58
|
+
return `${ONE_DECIMAL.format(value)}%`;
|
|
59
|
+
case "percentage_points":
|
|
60
|
+
return `${ONE_DECIMAL.format(value)} percentage points`;
|
|
61
|
+
case "rate":
|
|
62
|
+
return `${ONE_DECIMAL.format(value)} percentage points per hour`;
|
|
63
|
+
case "duration":
|
|
64
|
+
return formatDuration(value);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Formats a span of seconds as its two largest units.
|
|
69
|
+
* @param seconds - Non-negative seconds (a negative span is shown by magnitude with a minus sign).
|
|
70
|
+
* @returns E.g. `45 s`, `12 m 5 s`, `2 h 48 m`, `3 d 4 h`.
|
|
71
|
+
*/
|
|
72
|
+
export function formatDuration(seconds) {
|
|
73
|
+
const sign = seconds < 0 ? "-" : "";
|
|
74
|
+
const total = Math.round(Math.abs(seconds));
|
|
75
|
+
const days = Math.floor(total / 86_400);
|
|
76
|
+
const hours = Math.floor((total % 86_400) / 3_600);
|
|
77
|
+
const minutes = Math.floor((total % 3_600) / 60);
|
|
78
|
+
const secs = total % 60;
|
|
79
|
+
if (days > 0) {
|
|
80
|
+
return `${sign}${days} d ${hours} h`;
|
|
81
|
+
}
|
|
82
|
+
if (hours > 0) {
|
|
83
|
+
return `${sign}${hours} h ${minutes} m`;
|
|
84
|
+
}
|
|
85
|
+
if (minutes > 0) {
|
|
86
|
+
return `${sign}${minutes} m ${secs} s`;
|
|
87
|
+
}
|
|
88
|
+
return `${sign}${secs} s`;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Reads an instant's local date and time in a zone.
|
|
92
|
+
* @param iso - ISO-8601 instant.
|
|
93
|
+
* @param timeZone - IANA zone.
|
|
94
|
+
* @returns Date and time parts.
|
|
95
|
+
*/
|
|
96
|
+
function localParts(iso, timeZone) {
|
|
97
|
+
const parts = new Intl.DateTimeFormat("en-US", {
|
|
98
|
+
timeZone,
|
|
99
|
+
year: "numeric",
|
|
100
|
+
month: "2-digit",
|
|
101
|
+
day: "2-digit",
|
|
102
|
+
hour: "2-digit",
|
|
103
|
+
minute: "2-digit",
|
|
104
|
+
hourCycle: "h23",
|
|
105
|
+
}).formatToParts(new Date(iso));
|
|
106
|
+
/**
|
|
107
|
+
* Finds one part's value.
|
|
108
|
+
* @param type - Part type.
|
|
109
|
+
* @returns The value.
|
|
110
|
+
*/
|
|
111
|
+
const part = (type) => parts.find((p) => p.type === type)?.value ?? "";
|
|
112
|
+
return {
|
|
113
|
+
date: `${part("year")}-${part("month")}-${part("day")}`,
|
|
114
|
+
time: `${part("hour")}:${part("minute")}`,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Formats an instant in the display zone.
|
|
119
|
+
* @param iso - ISO-8601 instant, or null.
|
|
120
|
+
* @param timeZone - IANA zone.
|
|
121
|
+
* @returns `YYYY-MM-DD HH:MM`, or {@link UNKNOWN}.
|
|
122
|
+
*/
|
|
123
|
+
export function formatInstant(iso, timeZone) {
|
|
124
|
+
if (iso === null || iso === undefined) {
|
|
125
|
+
return UNKNOWN;
|
|
126
|
+
}
|
|
127
|
+
const { date, time } = localParts(iso, timeZone);
|
|
128
|
+
return `${date} ${time}`;
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Formats the span a metric covers, naming the zone (README principle 5, D-007).
|
|
132
|
+
* @param from - First instant covered, or null when there's no data.
|
|
133
|
+
* @param to - Last instant covered, or null.
|
|
134
|
+
* @param timeZone - IANA zone.
|
|
135
|
+
* @returns `no data yet`, `YYYY-MM-DD HH:MM to HH:MM (zone)` within one local day, or
|
|
136
|
+
* `YYYY-MM-DD HH:MM to YYYY-MM-DD HH:MM (zone)`. "to" rather than an en dash: the same idea was
|
|
137
|
+
* written two ways, and a Windows console on a legacy code page renders the dash as mojibake.
|
|
138
|
+
*/
|
|
139
|
+
export function formatCoverage(from, to, timeZone) {
|
|
140
|
+
if (from === null || from === undefined || to === null || to === undefined) {
|
|
141
|
+
return "no data yet";
|
|
142
|
+
}
|
|
143
|
+
const start = localParts(from, timeZone);
|
|
144
|
+
const end = localParts(to, timeZone);
|
|
145
|
+
if (start.date === end.date) {
|
|
146
|
+
return `${start.date} ${start.time} to ${end.time} (${timeZone})`;
|
|
147
|
+
}
|
|
148
|
+
return `${start.date} ${start.time} to ${end.date} ${end.time} (${timeZone})`;
|
|
149
|
+
}
|
|
150
|
+
//# sourceMappingURL=format.js.map
|