@ai-matrx/kit 0.12.0 → 0.13.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +35 -0
- package/README.md +1 -0
- package/dist/content-transfer.cjs +1323 -0
- package/dist/content-transfer.cjs.map +1 -0
- package/dist/content-transfer.d.cts +356 -0
- package/dist/content-transfer.d.ts +356 -0
- package/dist/content-transfer.js +1290 -0
- package/dist/content-transfer.js.map +1 -0
- package/dist/format.cjs +11 -1
- package/dist/format.cjs.map +1 -1
- package/dist/format.d.cts +20 -8
- package/dist/format.d.ts +20 -8
- package/dist/format.js +11 -1
- package/dist/format.js.map +1 -1
- package/dist/index.cjs +1174 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +356 -1
- package/dist/index.d.ts +356 -1
- package/dist/index.js +1174 -3
- package/dist/index.js.map +1 -1
- package/package.json +14 -2
package/dist/format.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/format.ts"],"sourcesContent":["/**\n * @ai-matrx/kit/format — THE one home for the tiny display formatters that\n * every Matrx surface, in every repo, needs and that nothing owns.\n *\n * WHY THIS FILE EXISTS (the H1 finding, 2026-09-07 duplication census). These\n * formatters were duplicated *between packages*: `@ai-matrx/meet` and\n * `@ai-matrx/messaging` each defined `formatDuration` + `getInitials`,\n * `@ai-matrx/associations` and `@ai-matrx/diff` each defined\n * `formatRelativeTime`, and `@ai-matrx/media` hid `formatFileSize` inside a\n * React upload component. Because the fleet's packages disagreed about who\n * owned them, ~65 host twins across matrx-frontend / matrx-extend /\n * matrx-local / matrx-games / aidream's two apps had *no correct package to\n * point at* — a settings page cannot take a dependency on the video-calling\n * package to print a duration.\n *\n * WHY KIT. `@ai-matrx/kit` has ZERO sibling dependencies (verified with\n * `node scripts/check_ts_sibling_graph.mjs --order`), so it sits at the very\n * bottom of the sibling DAG: meet, messaging, associations, diff, media and\n * print can all depend on it without creating a cycle, and so can every host.\n * It is also, by charter, the \"little primitives every Matrx app speaks\"\n * package. This is not a new package — new packages need Arman's approval.\n *\n * THE UNIT LAW. A duration formatter that takes a bare `number` is a bug\n * waiting to happen: the fleet's twins variously took milliseconds, seconds\n * and minutes, and two of them were wrong by 1000×-worth of confusion. There\n * is deliberately NO `formatDuration` here. The unit is in the name:\n * `formatDurationMs`, `formatDurationSeconds`, `formatDurationMinutes`.\n *\n * Everything in this module is pure, synchronous, and free of React, DOM and\n * `Intl` implicit-locale surprises except where a style explicitly asks for\n * `Intl.RelativeTimeFormat`. Every time-dependent function takes an explicit\n * `now` so tests never depend on the clock.\n */\n\n// ───────────────────────── timestamps ─────────────────────────\n\nexport type TimestampInput = string | number | Date | null | undefined;\n\n/** True when a string already carries a timezone designator (Z or ±offset). */\nfunction hasTimezoneDesignator(value: string): boolean {\n if (/[zZ]$/.test(value)) return true;\n if (/[+-]\\d{2}(:?\\d{2})?$/.test(value)) return true;\n if (/\\b(GMT|UTC)\\b/i.test(value)) return true;\n return false;\n}\n\n/**\n * Normalise a raw timestamp string so `new Date()` parses it as an absolute\n * instant.\n *\n * THE CORRECTION THIS CARRIES (from `@ai-matrx/diff`, the richest of the\n * twins). Postgres serialises `timestamp with time zone` as\n * \"2026-06-13T16:32:26+00:00\" (parses correctly) and `timestamp without time\n * zone` as \"2026-06-13T16:32:26\" — which, per the ES spec, `new Date()` parses\n * as LOCAL time. Our backend writes those columns in UTC, so a bare\n * `new Date(...)` is wrong by the viewer's offset: the classic \"times are off\n * by N hours\" report. A zone-less string that HAS a time component is treated\n * as UTC; date-only strings are left untouched (a calendar day is not a\n * timestamp).\n */\nfunction normalizeTimestampString(raw: string): string {\n const value = raw.trim();\n if (!value) return value;\n if (!/\\d{1,2}:\\d{2}/.test(value)) return value;\n if (hasTimezoneDesignator(value)) return value;\n return `${value.replace(\" \", \"T\")}Z`;\n}\n\n/** Parse any backend timestamp into a Date, or `null` when unparseable. */\nexport function parseTimestamp(value: TimestampInput): Date | null {\n if (value === null || value === undefined) return null;\n if (value instanceof Date) {\n return Number.isNaN(value.getTime()) ? null : value;\n }\n if (typeof value === \"number\") {\n const fromNumber = new Date(value);\n return Number.isNaN(fromNumber.getTime()) ? null : fromNumber;\n }\n if (typeof value !== \"string\") return null;\n const normalized = normalizeTimestampString(value);\n if (!normalized) return null;\n const parsed = new Date(normalized);\n return Number.isNaN(parsed.getTime()) ? null : parsed;\n}\n\nconst DEFAULT_ABSOLUTE_OPTIONS: Intl.DateTimeFormatOptions = {\n year: \"numeric\",\n month: \"short\",\n day: \"numeric\",\n hour: \"numeric\",\n minute: \"2-digit\",\n};\n\n/** Absolute local date+time, e.g. \"Jun 13, 2026, 9:32 AM\". */\nexport function formatAbsoluteDate(\n value: TimestampInput,\n options: Intl.DateTimeFormatOptions = DEFAULT_ABSOLUTE_OPTIONS,\n fallback = \"—\",\n): string {\n const parsed = parseTimestamp(value);\n if (!parsed) return fallback;\n return parsed.toLocaleString(undefined, options);\n}\n\n// ───────────────────────── relative time ─────────────────────────\n\nconst RELATIVE_UNITS: readonly {\n limit: number;\n divisor: number;\n short: string;\n long: string;\n}[] = [\n { limit: 60_000, divisor: 1000, short: \"s\", long: \"second\" },\n { limit: 3_600_000, divisor: 60_000, short: \"m\", long: \"minute\" },\n { limit: 86_400_000, divisor: 3_600_000, short: \"h\", long: \"hour\" },\n { limit: 604_800_000, divisor: 86_400_000, short: \"d\", long: \"day\" },\n { limit: 2_592_000_000, divisor: 604_800_000, short: \"w\", long: \"week\" },\n { limit: 31_536_000_000, divisor: 2_592_000_000, short: \"mo\", long: \"month\" },\n];\n\nconst INTL_UNITS: readonly { unit: Intl.RelativeTimeFormatUnit; ms: number }[] = [\n { unit: \"year\", ms: 365 * 24 * 60 * 60 * 1000 },\n { unit: \"month\", ms: 30 * 24 * 60 * 60 * 1000 },\n { unit: \"week\", ms: 7 * 24 * 60 * 60 * 1000 },\n { unit: \"day\", ms: 24 * 60 * 60 * 1000 },\n { unit: \"hour\", ms: 60 * 60 * 1000 },\n { unit: \"minute\", ms: 60 * 1000 },\n];\n\n/**\n * The three relative-time voices the fleet actually speaks. They are display\n * decisions, not implementations — one body serves all three.\n *\n * - `\"short\"` — \"2m ago\", \"3d ago\". The dense default (from `@ai-matrx/diff`).\n * - `\"long\"` — \"2 minutes ago\", \"3 days ago\".\n * - `\"intl\"` — `Intl.RelativeTimeFormat` with `numeric: \"auto\"`, so the\n * viewer's locale conventions apply and \"yesterday\" reads as \"yesterday\"\n * (from `@ai-matrx/associations`). Sub-minute reads \"just now\".\n */\nexport type RelativeTimeStyle = \"short\" | \"long\" | \"intl\";\n\nexport interface RelativeTimeOptions {\n /** Default `\"short\"`. */\n style?: RelativeTimeStyle | undefined;\n /**\n * THE BARE CONTRACT. `false` drops the trailing \" ago\" — \"2m\", \"3d\",\n * \"2 minutes\" — for a dense table cell whose COLUMN HEADER already says\n * \"Age\" or \"Last seen\". Default `true`.\n *\n * WHY AN OPTION AND NOT A `\"bare\"` STYLE (decided 2026-09-11, after three\n * surfaces — an exposure audit, a conversation sidebar and a batch Age\n * column — each lost their deliberate bare stamp to a collapse). `short` /\n * `long` / `intl` are three different VOICES; suffixed-vs-bare is a\n * different axis entirely, and you can want a bare `long` (\"2 minutes\") as\n * readily as a bare `short` (\"2m\"). Folding the axis into the style enum\n * would force `\"bare-short\"` / `\"bare-long\"` and double the enum every time\n * either axis grows. Orthogonal beats combinatorial.\n *\n * IT ALSO CARRIES THE DENSE-CONTEXT FALLBACK, on purpose. Past a year the\n * suffixed voices fall back to `formatAbsoluteDate` — \"Aug 7, 2025,\n * 11:14 PM\" — which is right in prose and ruinous in a narrow column. Asking\n * for the bare form IS the statement \"this is a dense cell\", so the bare\n * form's long-ago fallback is the short numeric date, \"8/7/2025\", which is\n * what every hand-rolled twin printed there. One option, one context, two\n * consistent consequences.\n *\n * `\"intl\"` IGNORES THIS, loudly documented rather than silently half-done:\n * `Intl.RelativeTimeFormat` composes the whole string in the viewer's\n * locale, and there is no correct locale-independent way to amputate its\n * suffix. Ask for `\"short\"` or `\"long\"` when you need bare.\n */\n suffix?: boolean | undefined;\n /** Injected clock. Default `Date.now()` — pass it in tests. */\n now?: number | undefined;\n /** Returned for null / unparseable input. Default `\"—\"`. */\n fallback?: string | undefined;\n /**\n * Echo the raw input instead of `fallback` when it cannot be parsed —\n * degraded, never broken. What the comments face wants: an odd server string\n * is more useful on screen than an em-dash that hides it.\n */\n fallbackToInput?: boolean | undefined;\n}\n\n/**\n * \"2m ago\" / \"2 minutes ago\" / a locale-aware \"2 minutes ago\", falling back to\n * an absolute local date past a year. Timezone-agnostic by construction (a\n * pure epoch difference over `parseTimestamp`'s corrected instant).\n *\n * Future timestamps read \"just now\" in `\"short\"`/`\"long\"` (a clock skew of a\n * few seconds must not print \"in 3 seconds\"); `\"intl\"` formats them properly\n * (\"in 5 minutes\") because that is the whole point of asking for `Intl`.\n */\nexport function formatRelativeTime(\n value: TimestampInput,\n options: RelativeTimeOptions = {},\n): string {\n const style = options.style ?? \"short\";\n const bare = options.suffix === false;\n const now = options.now ?? Date.now();\n const parsed = parseTimestamp(value);\n if (!parsed) {\n if (options.fallbackToInput === true && typeof value === \"string\") return value;\n return options.fallback ?? \"—\";\n }\n\n if (style === \"intl\") {\n const delta = parsed.getTime() - now;\n const magnitude = Math.abs(delta);\n if (magnitude < 60_000) return \"just now\";\n const rtf = new Intl.RelativeTimeFormat(undefined, { numeric: \"auto\" });\n for (const { unit, ms } of INTL_UNITS) {\n if (magnitude >= ms) return rtf.format(Math.trunc(delta / ms), unit);\n }\n return \"just now\";\n }\n\n const elapsed = now - parsed.getTime();\n if (elapsed < 0) return \"just now\";\n const ago = bare ? \"\" : \" ago\";\n for (const unit of RELATIVE_UNITS) {\n if (elapsed < unit.limit) {\n const count = Math.max(1, Math.floor(elapsed / unit.divisor));\n return style === \"long\"\n ? `${count} ${unit.long}${count === 1 ? \"\" : \"s\"}${ago}`\n : `${count}${unit.short}${ago}`;\n }\n }\n // Past a year: prose gets the full local date+time, a bare/dense cell gets\n // the short numeric date. See `suffix` above for why one option decides both.\n return bare ? parsed.toLocaleDateString() : formatAbsoluteDate(parsed);\n}\n\n// ───────────────────────── durations ─────────────────────────\n\n/**\n * The four duration voices the fleet actually speaks. Picked once here so a\n * call duration reads the same in the meeting, the history list and the\n * notification.\n *\n * - `\"clock\"` (default) — `0:00`, `9:04`, `1:02:33`. For anything a human\n * watches tick: recordings, calls, media players, timers.\n * - `\"compact\"` — `250ms`, `5.2s`, `5m 30s`, `1h 02m`. For elapsed work:\n * job runs, request timings, step durations. Sub-second is honest about\n * being sub-second rather than collapsing to `0:00`.\n * - `\"coarse\"` — `45 min`, `1h 30m`, `3d 4h`. For durations read at a glance\n * in a dense cell: podcast episodes, lesson lengths, estimates, uptime,\n * sandbox time-remaining. Never prints seconds.\n * - `\"long\"` — `45 minutes`, `3 hours`, `1 day`, `3 days`. THE PROSE VOICE:\n * the one you put INSIDE a sentence a person reads. \"Nothing has been\n * delivered for 3 days.\" \"8 save attempts have failed over the last\n * 5 minutes.\" \"Expires in 3 days.\"\n *\n * WHY `\"long\"` EXISTS (added 2026-09-11). THREE independent collapse lanes,\n * which never spoke to each other, hit the same wall in one day: six files\n * across matrx-frontend render a duration into English prose, and the package\n * had no voice for it. `coarse` renders three days as `72h` and \"45 minutes\"\n * as \"45 min\", both of which are wrong in a sentence — and a collapse that\n * degrades a screen is not a win. The name is deliberately the SAME WORD as\n * {@link RelativeTimeStyle}'s `\"long\"`, which already means \"spell the unit\n * out in full, correctly pluralised\" (\"2 minutes ago\"). One module, one word,\n * one meaning.\n *\n * WHY THERE IS NO FIFTH, ABBREVIATED-SPELLED-OUT VOICE. A lane proposed a\n * third register — `45 sec` / `12 min` / `3 hr` / `2 days` — for countdown\n * copy. It was refused: it is `\"long\"` with three words truncated, it gives\n * the reader nothing `\"long\"` does not, and its only distinguishing feature\n * (\"hr\", \"sec\") is jargon in exactly the sentences that wanted prose. The one\n * caller that spoke it now speaks `\"long\"`. Two registers — dense (`coarse`)\n * and prose (`\"long\"`) — cover every site the fleet has.\n *\n * WHY `\"coarse\"` GREW A DAY TIER rather than `\"long\"` absorbing those callers:\n * `coarse` stopping at hours was simply a hole. It printed `77h` for a\n * three-day container uptime, which no dense cell wants either; every site\n * that changes is a site that was already reading badly.\n */\nexport type DurationStyle = \"clock\" | \"compact\" | \"coarse\" | \"long\";\n\nexport interface DurationOptions {\n /** Default `\"clock\"`. */\n style?: DurationStyle | undefined;\n /**\n * Returned for `null` / `undefined` / non-finite input. Default `\"0:00\"` for\n * `\"clock\"` and `\"—\"` for `\"compact\"` / `\"coarse\"` / `\"long\"`.\n */\n fallback?: string | undefined;\n /**\n * How a tier's leading number is derived from the remainder below it.\n * `\"nearest\"` (default) is right for a MEASURED or ESTIMATED span — a\n * 44-minute-31-second podcast reads \"45 min\". `\"down\"` is mandatory for a\n * COUNTDOWN: with 5 minutes 30 seconds left, \"6 min\" promises the user half\n * a minute they do not have. (Real regression, sandbox time-remaining,\n * 2026-09-11.)\n *\n * `\"clock\"` and `\"long\"` are ALREADY `\"down\"` by construction and ignore\n * this — a clock never rounds `0:59` up to `1:00`, and prose \"for 3 hours\"\n * asserts at-least-three-hours. It changes `\"coarse\"` and `\"compact\"`.\n */\n round?: \"nearest\" | \"down\" | undefined;\n /**\n * Keep the sign on a negative duration (`-1:23`) instead of clamping to\n * zero. Default `false` — a negative elapsed time is nearly always a clock\n * bug, and `0:00` is the honest reading. Opt in where a signed offset is the\n * actual quantity (a field format for a stored `interval`, say).\n */\n signed?: boolean | undefined;\n}\n\nconst pad2 = (value: number): string => value.toString().padStart(2, \"0\");\n\nfunction clockBody(totalSeconds: number): string {\n const seconds = totalSeconds % 60;\n const minutes = Math.floor(totalSeconds / 60) % 60;\n const hours = Math.floor(totalSeconds / 3600);\n return hours > 0\n ? `${hours}:${pad2(minutes)}:${pad2(seconds)}`\n : `${minutes}:${pad2(seconds)}`;\n}\n\ntype Rounding = \"nearest\" | \"down\";\n\nconst reduce = (value: number, mode: Rounding): number =>\n mode === \"down\" ? Math.floor(value) : Math.round(value);\n\nfunction compactBody(ms: number, round: Rounding): string {\n if (ms < 1000) return `${reduce(ms, round)}ms`;\n const totalSeconds = ms / 1000;\n if (totalSeconds < 60) {\n // One decimal under 10s (0.1s is a visible difference at that scale),\n // whole seconds above it (nobody reads \"43.7s\" as more precise than \"44s\").\n //\n // A TRAILING `.0` IS STRIPPED (2026-09-11). The rule above exists to show\n // TENTHS WHEN THERE ARE TENTHS; `5.0s` spends a character saying \"there\n // are none\", and a game countdown ticking \"9.0s, 8.0s, 7.0s\" reads worse\n // than \"9s, 8s, 7s\" for no gain. The usual objection — column jitter —\n // does not apply to this voice: `compact` already swings between `250ms`,\n // `5.2s`, `44s` and `1m 30s`, so it was never a fixed-width column format.\n // `\"clock\"` is the fixed-width one.\n if (totalSeconds >= 10) return `${reduce(totalSeconds, round)}s`;\n const tenths = round === \"down\"\n ? Math.floor(totalSeconds * 10) / 10\n : Math.round(totalSeconds * 10) / 10;\n return `${Number.isInteger(tenths) ? tenths : tenths.toFixed(1)}s`;\n }\n const whole = reduce(totalSeconds, round);\n const minutes = Math.floor(whole / 60);\n if (minutes < 60) return `${minutes}m ${pad2(whole % 60)}s`;\n const hours = Math.floor(minutes / 60);\n return `${hours}h ${pad2(minutes % 60)}m`;\n}\n\nfunction coarseBody(ms: number, round: Rounding): string {\n const minutes = reduce(ms / 60_000, round);\n if (minutes < 1) return \"< 1 min\";\n if (minutes < 60) return `${minutes} min`;\n if (minutes < 1440) {\n const hours = Math.floor(minutes / 60);\n const rest = minutes % 60;\n return rest > 0 ? `${hours}h ${rest}m` : `${hours}h`;\n }\n // Day tier, added 2026-09-11: this voice used to print `77h` for a\n // three-day container uptime. It keeps `coarse`'s own shape — abbreviated\n // units, at most two of them, largest first — so `3d 4h`, `3d`, never\n // `3d 4h 12m`.\n const days = Math.floor(minutes / 1440);\n const restHours = Math.floor((minutes % 1440) / 60);\n return restHours > 0 ? `${days}d ${restHours}h` : `${days}d`;\n}\n\nconst plural = (count: number, unit: string): string =>\n `${count} ${unit}${count === 1 ? \"\" : \"s\"}`;\n\n/**\n * The prose voice. Always FLOORS: \"for 3 hours\" and \"expires in 3 days\" both\n * assert at-least, which is the only reading that cannot mislead in either\n * direction (an elapsed time never overstates, a countdown never over-promises).\n *\n * ONE TIER ONLY — \"3 hours\", never \"3 hours 12 minutes\". This voice goes\n * inside a sentence, and a sentence carrying two magnitudes reads like a\n * stopwatch readout. A caller that genuinely needs both wants `coarse`.\n *\n * Sub-second says so in words rather than printing a floored \"0 seconds\",\n * which reads as \"nothing happened\" — the same reasoning as `coarse`'s\n * \"< 1 min\".\n *\n * It stops at days. Weeks and months are calendar units whose length depends\n * on WHICH week and WHICH month, so a duration — a pure span with no anchor —\n * cannot honestly speak them. \"45 days\" is exact; \"1.5 months\" is a guess.\n * {@link formatRelativeTime}, which HAS an anchor, is where weeks and months\n * belong.\n */\nfunction longBody(ms: number): string {\n if (ms < 1_000) return \"less than a second\";\n if (ms < 60_000) return plural(Math.floor(ms / 1_000), \"second\");\n if (ms < 3_600_000) return plural(Math.floor(ms / 60_000), \"minute\");\n if (ms < 86_400_000) return plural(Math.floor(ms / 3_600_000), \"hour\");\n return plural(Math.floor(ms / 86_400_000), \"day\");\n}\n\n/**\n * THE canonical duration formatter. Milliseconds in, a chosen voice out.\n *\n * `null`, `undefined`, `NaN` and `Infinity` all take the fallback — never\n * `NaN:NaN`, which is what a null start time produces, and never a confident\n * `0:00` for \"we do not know\".\n */\nexport function formatDurationMs(\n ms: number | null | undefined,\n options: DurationOptions = {},\n): string {\n const style = options.style ?? \"clock\";\n const fallback = options.fallback ?? (style === \"clock\" ? \"0:00\" : \"—\");\n if (ms === null || ms === undefined || !Number.isFinite(ms)) return fallback;\n\n const negative = ms < 0;\n if (negative && options.signed !== true) {\n return style === \"clock\" ? clockBody(0) : fallback;\n }\n const magnitude = Math.abs(ms);\n const round = options.round ?? \"nearest\";\n const body =\n style === \"clock\"\n ? clockBody(Math.floor(magnitude / 1000))\n : style === \"compact\"\n ? compactBody(magnitude, round)\n : style === \"coarse\"\n ? coarseBody(magnitude, round)\n : longBody(magnitude);\n return negative ? `-${body}` : body;\n}\n\n/** Seconds in. See {@link formatDurationMs}. */\nexport function formatDurationSeconds(\n seconds: number | null | undefined,\n options: DurationOptions = {},\n): string {\n if (seconds === null || seconds === undefined || !Number.isFinite(seconds)) {\n return formatDurationMs(seconds as number | null | undefined, options);\n }\n return formatDurationMs(seconds * 1000, options);\n}\n\n/** Minutes in. See {@link formatDurationMs}. */\nexport function formatDurationMinutes(\n minutes: number | null | undefined,\n options: DurationOptions = {},\n): string {\n if (minutes === null || minutes === undefined || !Number.isFinite(minutes)) {\n return formatDurationMs(minutes as number | null | undefined, options);\n }\n return formatDurationMs(minutes * 60_000, options);\n}\n\n/**\n * Duration between two timestamps, in milliseconds — `null` when the start is\n * missing or either end is unparseable. An absent `to` means \"still running\",\n * so it measures to `now`. Pair with {@link formatDurationMs}.\n */\nexport function durationMsBetween(\n from: TimestampInput,\n to: TimestampInput,\n now: number = Date.now(),\n): number | null {\n const start = parseTimestamp(from);\n if (!start) return null;\n if (to === null || to === undefined) return Math.max(0, now - start.getTime());\n const end = parseTimestamp(to);\n if (!end) return null;\n return Math.max(0, end.getTime() - start.getTime());\n}\n\n// ───────────────────────── byte sizes ─────────────────────────\n\nconst SIZE_UNITS = [\"B\", \"KB\", \"MB\", \"GB\", \"TB\", \"PB\"] as const;\n\nexport interface FileSizeOptions {\n /** Returned for `null` / `undefined` / non-finite / negative. Default `\"—\"`. */\n fallback?: string | undefined;\n}\n\n/**\n * `512` → `\"512 B\"`, `2048` → `\"2.0 KB\"`, `15360` → `\"15 KB\"`.\n *\n * THE DISPLAY DECISIONS, made once (unified 2026-09-07 from six host twins and\n * `@ai-matrx/media`'s copy, which was the richest):\n *\n * 1. Binary units (1024), labelled with the short SI-ish names the whole fleet\n * already used — `B`/`KB`/`MB`/`GB`/`TB`, never `\"Bytes\"`.\n * 2. One decimal below 10 in a unit (`2.0 KB`, `9.9 MB`), whole numbers at or\n * above it (`15 KB`, `340 MB`) — three significant figures is the most a\n * file size is ever worth, and `1.5 GB` versus `1536.0 MB` is the whole\n * point of the unit.\n * 3. Whole bytes below 1 KB — `\"512 B\"`, never `\"512.0 B\"`.\n * 4. `null` / `undefined` / `NaN` / `Infinity` / negative all collapse to an\n * em-dash, NOT to `\"0 B\"`. A corrupt or mid-upload `size` row rendering as\n * a confident, wrong `\"0 B\"` is a screen telling a lie; \"size unknown\" is\n * the honest reading. (`0` itself is a real size and prints `\"0 B\"`.)\n */\nexport function formatFileSize(\n bytes: number | null | undefined,\n options: FileSizeOptions = {},\n): string {\n const fallback = options.fallback ?? \"—\";\n if (bytes === null || bytes === undefined || !Number.isFinite(bytes) || bytes < 0) {\n return fallback;\n }\n if (bytes < 1024) return `${Math.round(bytes)} B`;\n let value = bytes;\n let unit = 0;\n while (value >= 1024 && unit < SIZE_UNITS.length - 1) {\n value /= 1024;\n unit += 1;\n }\n const rendered = value >= 10 ? String(Math.round(value)) : value.toFixed(1);\n return `${rendered} ${SIZE_UNITS[unit]}`;\n}\n\n// ───────────────────────── people ─────────────────────────\n\nexport interface InitialsOptions {\n /** Returned when nothing usable is left. Default `\"?\"`. */\n fallback?: string | undefined;\n}\n\n/**\n * `\"Ana Rivera\"` → `\"AR\"`, `\"Ana\"` → `\"A\"`, `\"Ana Maria Rivera\"` → `\"AR\"`,\n * `\"ana@example.com\"` → `\"A\"`, `\" \"` → `\"?\"`.\n *\n * THE DISPLAY DECISION, made once: a multi-part name takes FIRST + LAST, not\n * first + second. `\"Ana Maria Rivera\"` is `AR`, because the family name is the\n * half a reader recognises. (Six matrx-frontend twins took first + second and\n * printed `AM`; the two package copies both took first + last, and both were\n * tested. First + last wins.)\n *\n * Pass the email as the value when there is no name — a single token yields\n * its first character, which is exactly what the host twins did by hand.\n */\nexport function getInitials(\n value: string | null | undefined,\n options: InitialsOptions = {},\n): string {\n const fallback = options.fallback ?? \"?\";\n if (typeof value !== \"string\") return fallback;\n const parts = value.trim().split(/\\s+/).filter((part) => part.length > 0);\n if (parts.length === 0) return fallback;\n const first = parts[0]?.charAt(0) ?? \"\";\n const last = parts.length > 1 ? (parts[parts.length - 1]?.charAt(0) ?? \"\") : \"\";\n return `${first}${last}`.toUpperCase() || fallback;\n}\n\n/**\n * A stable palette index for an avatar with no image. Deterministic on the\n * seed, so the same person is the same colour on every device and every\n * reload — a random colour per render is a surprisingly loud bug.\n */\nexport function avatarPaletteIndex(seed: string, buckets = 8): number {\n let hash = 0;\n for (let index = 0; index < seed.length; index += 1) {\n hash = (hash * 31 + seed.charCodeAt(index)) | 0;\n }\n return Math.abs(hash) % buckets;\n}\n\n// ───────────────────── honest numbers ─────────────────────\n\n/**\n * THE LYING-SCREEN CLASS, and the primitives that close it.\n *\n * WHAT HAPPENED (matrx-frontend, 2026-09-12). `$${(app.total_cost ?? 0)\n * .toFixed(4)}` rendered \"$0.0000\" for a cost NOBODY MEASURED, and a reader\n * walked away believing the run was free. The same `?? 0` then went into a\n * SUM, so an unmeasured cost was invisible inside a real-looking analytics\n * total — and those totals rode into an agent payload, which means agents\n * were handed fabricated cost numbers as fact. Sibling masks: a division with\n * a zero denominator printing \"NaN% used\", and a red FAILED icon asserting\n * failure when the truth was that the success rate had never been measured.\n *\n * THE CONVENTION IS ALREADY OURS. {@link formatFileSize} decided it in\n * 2026-09-07: unknown reads as an em-dash, never a confident \"0 B\". These\n * carry the identical contract to money, counts and percentages — the same\n * null / undefined / NaN / Infinity honesty, exported under\n * {@link UNKNOWN_DISPLAY} so the whole fleet spells \"unknown\" one way.\n *\n * THE OTHER HALF OF THE LAW, and the easy half to break while fixing the\n * first: a zero that is REALLY a zero still prints as zero. `0` is a\n * measurement; `null` is the absence of one. Nothing here collapses them.\n *\n * WHY `?? 0` IS NO LONGER THE EASY PATH. It was never the formatter that\n * tempted anyone — it was the AGGREGATE, where a nullable value has to become\n * a number before it can be added. {@link sumKnown} is that step, and it\n * hands back the count of values it could NOT measure alongside the total, so\n * a caller cannot present a total as complete without having been shown what\n * is missing from it.\n */\n\n/** What every Matrx surface prints when a value is unknown. */\nexport const UNKNOWN_DISPLAY = \"—\";\n\n/**\n * True only for a real, usable measurement. `0` passes; `null`, `undefined`,\n * `NaN` and `Infinity` do not. The predicate that makes `?? 0` unnecessary.\n */\nexport function isKnownNumber(value: unknown): value is number {\n return typeof value === \"number\" && Number.isFinite(value);\n}\n\nexport interface UnknownOption {\n /** Override the em-dash that stands in for an unmeasured value. */\n unknown?: string | undefined;\n}\n\n/**\n * `12.5` → `\"$12.50\"`, `0` → `\"$0.00\"`, `null` → `\"—\"`.\n *\n * THE NAME IS `Usd`, NOT `Currency`, under the same UNIT LAW that forbids a\n * bare `formatDuration`: this prepends a literal `$` and applies no locale or\n * currency conversion, so calling it `formatCurrency` would promise a\n * capability it does not have — and a EUR amount rendered with a `$` is the\n * same class of lie this section exists to stop.\n *\n * `digits` defaults to 2 (a price a person reads). Per-token and per-call\n * costs pass 4 — fractions of a cent are the whole quantity there.\n */\nexport function formatUsd(\n value: number | null | undefined,\n options: { digits?: number | undefined } & UnknownOption = {},\n): string {\n const { digits = 2, unknown = UNKNOWN_DISPLAY } = options;\n if (!isKnownNumber(value)) return unknown;\n return `$${value.toFixed(digits)}`;\n}\n\n/**\n * `1234567` → `\"1,234,567\"`, `0` → `\"0\"`, `null` → `\"—\"`.\n *\n * Locale-grouped, because an ungrouped nine-digit token count is read wrong\n * as often as it is read. There is deliberately no compact (\"1.2M\") voice\n * here yet: the call sites that want one are real but were not part of the\n * evidence this section was built from, and a display voice invented ahead of\n * its callers is how a formatter ends up with options nobody should use.\n */\nexport function formatCount(\n value: number | null | undefined,\n options: UnknownOption = {},\n): string {\n const { unknown = UNKNOWN_DISPLAY } = options;\n if (!isKnownNumber(value)) return unknown;\n return value.toLocaleString();\n}\n\n/**\n * A ratio, or `null` when it is INDETERMINATE — the guard the \"NaN% used\"\n * defect was missing.\n *\n * `0 / 0` is not zero, it is unknown, and so is any division by a zero or\n * unmeasured denominator. Returning `null` rather than a number forces the\n * caller through {@link formatPercentFromFraction}'s unknown branch instead\n * of letting `NaN` reach the screen.\n */\nexport function safeRatio(\n numerator: number | null | undefined,\n denominator: number | null | undefined,\n): number | null {\n if (!isKnownNumber(numerator) || !isKnownNumber(denominator)) return null;\n if (denominator === 0) return null;\n const ratio = numerator / denominator;\n return Number.isFinite(ratio) ? ratio : null;\n}\n\n/**\n * `0.25` → `\"25%\"`, `0` → `\"0%\"`, `null` → `\"—\"`.\n *\n * THE UNIT IS IN THE NAME, deliberately at the cost of a long one. Half the\n * fleet's percentage twins took a 0..1 fraction and half took a 0..100\n * percentage, and the two are indistinguishable at a call site — a 100×\n * error that renders plausibly. `FromFraction` makes the wrong call visible\n * while it is being written. Pair it with {@link safeRatio}.\n *\n * NOTHING IS CLAMPED. A percentage over 100 is usually a real signal — over\n * quota, over budget, over capacity — and hiding it is its own lie.\n */\nexport function formatPercentFromFraction(\n fraction: number | null | undefined,\n options: { digits?: number | undefined } & UnknownOption = {},\n): string {\n const { digits = 0, unknown = UNKNOWN_DISPLAY } = options;\n if (!isKnownNumber(fraction)) return unknown;\n return `${(fraction * 100).toFixed(digits)}%`;\n}\n\n/** What {@link sumKnown} measured, and what it could not. */\nexport interface KnownSum {\n /** Sum of the measured values ONLY. `0` when nothing was measurable. */\n total: number;\n /** How many values were real measurements. */\n known: number;\n /** How many were `null` / `undefined` / `NaN` / `Infinity`. */\n unknown: number;\n}\n\n/**\n * Add up a column that may contain unmeasured values, and SAY SO.\n *\n * THIS IS THE FUNCTION THAT CLOSES THE CLASS. Every formatter above refuses\n * to invent a value, but `?? 0` was never typed in front of a formatter — it\n * was typed in front of `+`, because a nullable number has to become a number\n * before it can be added, and `0` is the obvious way to make it one. That\n * silently folds \"we never measured this\" into \"this cost nothing\", and the\n * resulting total looks exactly like a complete one.\n *\n * So the aggregate step returns the counts with the number. A caller holding\n * a `KnownSum` has been handed the fact that three of forty rows were\n * unmeasured; it can still render `total`, but it cannot claim completeness\n * without ignoring a field it was given. Render `unknown > 0` as a caveat\n * beside the total — never as a footnote nobody reads, and never as nothing.\n */\nexport function sumKnown(\n values: Iterable<number | null | undefined>,\n): KnownSum {\n let total = 0;\n let known = 0;\n let unknown = 0;\n for (const value of values) {\n if (isKnownNumber(value)) {\n total += value;\n known += 1;\n } else {\n unknown += 1;\n }\n }\n return { total, known, unknown };\n}\n"],"mappings":"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","names":[]}
|
|
1
|
+
{"version":3,"sources":["../src/format.ts"],"sourcesContent":["/**\n * @ai-matrx/kit/format — THE one home for the tiny display formatters that\n * every Matrx surface, in every repo, needs and that nothing owns.\n *\n * WHY THIS FILE EXISTS (the H1 finding, 2026-09-07 duplication census). These\n * formatters were duplicated *between packages*: `@ai-matrx/meet` and\n * `@ai-matrx/messaging` each defined `formatDuration` + `getInitials`,\n * `@ai-matrx/associations` and `@ai-matrx/diff` each defined\n * `formatRelativeTime`, and `@ai-matrx/media` hid `formatFileSize` inside a\n * React upload component. Because the fleet's packages disagreed about who\n * owned them, ~65 host twins across matrx-frontend / matrx-extend /\n * matrx-local / matrx-games / aidream's two apps had *no correct package to\n * point at* — a settings page cannot take a dependency on the video-calling\n * package to print a duration.\n *\n * WHY KIT. `@ai-matrx/kit` has ZERO sibling dependencies (verified with\n * `node scripts/check_ts_sibling_graph.mjs --order`), so it sits at the very\n * bottom of the sibling DAG: meet, messaging, associations, diff, media and\n * print can all depend on it without creating a cycle, and so can every host.\n * It is also, by charter, the \"little primitives every Matrx app speaks\"\n * package. This is not a new package — new packages need Arman's approval.\n *\n * THE UNIT LAW. A duration formatter that takes a bare `number` is a bug\n * waiting to happen: the fleet's twins variously took milliseconds, seconds\n * and minutes, and two of them were wrong by 1000×-worth of confusion. There\n * is deliberately NO `formatDuration` here. The unit is in the name:\n * `formatDurationMs`, `formatDurationSeconds`, `formatDurationMinutes`.\n *\n * Everything in this module is pure, synchronous, and free of React, DOM and\n * `Intl` implicit-locale surprises except where a style explicitly asks for\n * `Intl.RelativeTimeFormat`. Every time-dependent function takes an explicit\n * `now` so tests never depend on the clock.\n */\n\n// ───────────────────────── timestamps ─────────────────────────\n\nexport type TimestampInput = string | number | Date | null | undefined;\n\n/** True when a string already carries a timezone designator (Z or ±offset). */\nfunction hasTimezoneDesignator(value: string): boolean {\n if (/[zZ]$/.test(value)) return true;\n if (/[+-]\\d{2}(:?\\d{2})?$/.test(value)) return true;\n if (/\\b(GMT|UTC)\\b/i.test(value)) return true;\n return false;\n}\n\n/**\n * Normalise a raw timestamp string so `new Date()` parses it as an absolute\n * instant.\n *\n * THE CORRECTION THIS CARRIES (from `@ai-matrx/diff`, the richest of the\n * twins). Postgres serialises `timestamp with time zone` as\n * \"2026-06-13T16:32:26+00:00\" (parses correctly) and `timestamp without time\n * zone` as \"2026-06-13T16:32:26\" — which, per the ES spec, `new Date()` parses\n * as LOCAL time. Our backend writes those columns in UTC, so a bare\n * `new Date(...)` is wrong by the viewer's offset: the classic \"times are off\n * by N hours\" report. A zone-less string that HAS a time component is treated\n * as UTC; date-only strings are left untouched (a calendar day is not a\n * timestamp).\n */\nfunction normalizeTimestampString(raw: string): string {\n const value = raw.trim();\n if (!value) return value;\n if (!/\\d{1,2}:\\d{2}/.test(value)) return value;\n if (hasTimezoneDesignator(value)) return value;\n return `${value.replace(\" \", \"T\")}Z`;\n}\n\n/** Parse any backend timestamp into a Date, or `null` when unparseable. */\nexport function parseTimestamp(value: TimestampInput): Date | null {\n if (value === null || value === undefined) return null;\n if (value instanceof Date) {\n return Number.isNaN(value.getTime()) ? null : value;\n }\n if (typeof value === \"number\") {\n const fromNumber = new Date(value);\n return Number.isNaN(fromNumber.getTime()) ? null : fromNumber;\n }\n if (typeof value !== \"string\") return null;\n const normalized = normalizeTimestampString(value);\n if (!normalized) return null;\n const parsed = new Date(normalized);\n return Number.isNaN(parsed.getTime()) ? null : parsed;\n}\n\nconst DEFAULT_ABSOLUTE_OPTIONS: Intl.DateTimeFormatOptions = {\n year: \"numeric\",\n month: \"short\",\n day: \"numeric\",\n hour: \"numeric\",\n minute: \"2-digit\",\n};\n\n/** Absolute local date+time, e.g. \"Jun 13, 2026, 9:32 AM\". */\nexport function formatAbsoluteDate(\n value: TimestampInput,\n options: Intl.DateTimeFormatOptions = DEFAULT_ABSOLUTE_OPTIONS,\n fallback = \"—\",\n): string {\n const parsed = parseTimestamp(value);\n if (!parsed) return fallback;\n return parsed.toLocaleString(undefined, options);\n}\n\n// ───────────────────────── relative time ─────────────────────────\n\nconst RELATIVE_UNITS: readonly {\n limit: number;\n divisor: number;\n short: string;\n long: string;\n}[] = [\n { limit: 60_000, divisor: 1000, short: \"s\", long: \"second\" },\n { limit: 3_600_000, divisor: 60_000, short: \"m\", long: \"minute\" },\n { limit: 86_400_000, divisor: 3_600_000, short: \"h\", long: \"hour\" },\n { limit: 604_800_000, divisor: 86_400_000, short: \"d\", long: \"day\" },\n { limit: 2_592_000_000, divisor: 604_800_000, short: \"w\", long: \"week\" },\n { limit: 31_536_000_000, divisor: 2_592_000_000, short: \"mo\", long: \"month\" },\n];\n\nconst INTL_UNITS: readonly { unit: Intl.RelativeTimeFormatUnit; ms: number }[] = [\n { unit: \"year\", ms: 365 * 24 * 60 * 60 * 1000 },\n { unit: \"month\", ms: 30 * 24 * 60 * 60 * 1000 },\n { unit: \"week\", ms: 7 * 24 * 60 * 60 * 1000 },\n { unit: \"day\", ms: 24 * 60 * 60 * 1000 },\n { unit: \"hour\", ms: 60 * 60 * 1000 },\n { unit: \"minute\", ms: 60 * 1000 },\n];\n\n/**\n * The three relative-time voices the fleet actually speaks. They are display\n * decisions, not implementations — one body serves all three.\n *\n * - `\"short\"` — \"2m ago\", \"3d ago\". The dense default (from `@ai-matrx/diff`).\n * - `\"long\"` — \"2 minutes ago\", \"3 days ago\".\n * - `\"intl\"` — `Intl.RelativeTimeFormat` with `numeric: \"auto\"`, so the\n * viewer's locale conventions apply and \"yesterday\" reads as \"yesterday\"\n * (from `@ai-matrx/associations`). Sub-minute reads \"just now\".\n */\nexport type RelativeTimeStyle = \"short\" | \"long\" | \"intl\";\n\nexport interface RelativeTimeOptions {\n /** Default `\"short\"`. */\n style?: RelativeTimeStyle | undefined;\n /**\n * THE BARE CONTRACT. `false` drops the trailing \" ago\" — \"2m\", \"3d\",\n * \"2 minutes\" — for a dense table cell whose COLUMN HEADER already says\n * \"Age\" or \"Last seen\". Default `true`.\n *\n * WHY AN OPTION AND NOT A `\"bare\"` STYLE (decided 2026-09-11, after three\n * surfaces — an exposure audit, a conversation sidebar and a batch Age\n * column — each lost their deliberate bare stamp to a collapse). `short` /\n * `long` / `intl` are three different VOICES; suffixed-vs-bare is a\n * different axis entirely, and you can want a bare `long` (\"2 minutes\") as\n * readily as a bare `short` (\"2m\"). Folding the axis into the style enum\n * would force `\"bare-short\"` / `\"bare-long\"` and double the enum every time\n * either axis grows. Orthogonal beats combinatorial.\n *\n * IT ALSO CARRIES THE DENSE-CONTEXT FALLBACK, on purpose. Past a year the\n * suffixed voices fall back to `formatAbsoluteDate` — \"Aug 7, 2025,\n * 11:14 PM\" — which is right in prose and ruinous in a narrow column. Asking\n * for the bare form IS the statement \"this is a dense cell\", so the bare\n * form's long-ago fallback is the short numeric date, \"8/7/2025\", which is\n * what every hand-rolled twin printed there. One option, one context, two\n * consistent consequences.\n *\n * `\"intl\"` IGNORES THIS, loudly documented rather than silently half-done:\n * `Intl.RelativeTimeFormat` composes the whole string in the viewer's\n * locale, and there is no correct locale-independent way to amputate its\n * suffix. Ask for `\"short\"` or `\"long\"` when you need bare.\n */\n suffix?: boolean | undefined;\n /** Injected clock. Default `Date.now()` — pass it in tests. */\n now?: number | undefined;\n /** Returned for null / unparseable input. Default `\"—\"`. */\n fallback?: string | undefined;\n /**\n * Echo the raw input instead of `fallback` when it cannot be parsed —\n * degraded, never broken. What the comments face wants: an odd server string\n * is more useful on screen than an em-dash that hides it.\n */\n fallbackToInput?: boolean | undefined;\n}\n\n/**\n * \"2m ago\" / \"2 minutes ago\" / a locale-aware \"2 minutes ago\", falling back to\n * an absolute local date past a year. Timezone-agnostic by construction (a\n * pure epoch difference over `parseTimestamp`'s corrected instant).\n *\n * Future timestamps read \"just now\" in `\"short\"`/`\"long\"` (a clock skew of a\n * few seconds must not print \"in 3 seconds\"); `\"intl\"` formats them properly\n * (\"in 5 minutes\") because that is the whole point of asking for `Intl`.\n */\nexport function formatRelativeTime(\n value: TimestampInput,\n options: RelativeTimeOptions = {},\n): string {\n const style = options.style ?? \"short\";\n const bare = options.suffix === false;\n const now = options.now ?? Date.now();\n const parsed = parseTimestamp(value);\n if (!parsed) {\n if (options.fallbackToInput === true && typeof value === \"string\") return value;\n return options.fallback ?? \"—\";\n }\n\n if (style === \"intl\") {\n const delta = parsed.getTime() - now;\n const magnitude = Math.abs(delta);\n if (magnitude < 60_000) return \"just now\";\n const rtf = new Intl.RelativeTimeFormat(undefined, { numeric: \"auto\" });\n for (const { unit, ms } of INTL_UNITS) {\n if (magnitude >= ms) return rtf.format(Math.trunc(delta / ms), unit);\n }\n return \"just now\";\n }\n\n const elapsed = now - parsed.getTime();\n if (elapsed < 0) return \"just now\";\n const ago = bare ? \"\" : \" ago\";\n for (const unit of RELATIVE_UNITS) {\n if (elapsed < unit.limit) {\n const count = Math.max(1, Math.floor(elapsed / unit.divisor));\n return style === \"long\"\n ? `${count} ${unit.long}${count === 1 ? \"\" : \"s\"}${ago}`\n : `${count}${unit.short}${ago}`;\n }\n }\n // Past a year: prose gets the full local date+time, a bare/dense cell gets\n // the short numeric date. See `suffix` above for why one option decides both.\n return bare ? parsed.toLocaleDateString() : formatAbsoluteDate(parsed);\n}\n\n// ───────────────────────── durations ─────────────────────────\n\n/**\n * The four duration voices the fleet actually speaks. Picked once here so a\n * call duration reads the same in the meeting, the history list and the\n * notification.\n *\n * - `\"clock\"` (default) — `0:00`, `9:04`, `1:02:33`. For anything a human\n * watches tick: recordings, calls, media players, timers.\n * - `\"compact\"` — `250ms`, `5.2s`, `5m 30s`, `1h 02m`. For elapsed work:\n * job runs, request timings, step durations. Sub-second is honest about\n * being sub-second rather than collapsing to `0:00`.\n * - `\"coarse\"` — `45 min`, `1h 30m`, `3d 4h`. For durations read at a glance\n * in a dense cell: podcast episodes, lesson lengths, estimates, uptime,\n * sandbox time-remaining. Never prints seconds.\n * - `\"long\"` — `45 minutes`, `3 hours`, `1 day`, `3 days`. THE PROSE VOICE:\n * the one you put INSIDE a sentence a person reads. \"Nothing has been\n * delivered for 3 days.\" \"8 save attempts have failed over the last\n * 5 minutes.\" \"Expires in 3 days.\"\n *\n * WHY `\"long\"` EXISTS (added 2026-09-11). THREE independent collapse lanes,\n * which never spoke to each other, hit the same wall in one day: six files\n * across matrx-frontend render a duration into English prose, and the package\n * had no voice for it. `coarse` renders three days as `72h` and \"45 minutes\"\n * as \"45 min\", both of which are wrong in a sentence — and a collapse that\n * degrades a screen is not a win. The name is deliberately the SAME WORD as\n * {@link RelativeTimeStyle}'s `\"long\"`, which already means \"spell the unit\n * out in full, correctly pluralised\" (\"2 minutes ago\"). One module, one word,\n * one meaning.\n *\n * WHY THERE IS NO FIFTH, ABBREVIATED-SPELLED-OUT VOICE. A lane proposed a\n * third register — `45 sec` / `12 min` / `3 hr` / `2 days` — for countdown\n * copy. It was refused: it is `\"long\"` with three words truncated, it gives\n * the reader nothing `\"long\"` does not, and its only distinguishing feature\n * (\"hr\", \"sec\") is jargon in exactly the sentences that wanted prose. The one\n * caller that spoke it now speaks `\"long\"`. Two registers — dense (`coarse`)\n * and prose (`\"long\"`) — cover every site the fleet has.\n *\n * WHY `\"coarse\"` GREW A DAY TIER rather than `\"long\"` absorbing those callers:\n * `coarse` stopping at hours was simply a hole. It printed `77h` for a\n * three-day container uptime, which no dense cell wants either; every site\n * that changes is a site that was already reading badly.\n */\nexport type DurationStyle = \"clock\" | \"compact\" | \"coarse\" | \"long\";\n\nexport interface DurationOptions {\n /** Default `\"clock\"`. */\n style?: DurationStyle | undefined;\n /**\n * Returned for `null` / `undefined` / non-finite input. Default `\"0:00\"` for\n * `\"clock\"` and `\"—\"` for `\"compact\"` / `\"coarse\"` / `\"long\"`.\n */\n fallback?: string | undefined;\n /**\n * How a tier's leading number is derived from the remainder below it.\n * `\"nearest\"` (default) is right for a MEASURED or ESTIMATED span — a\n * 44-minute-31-second podcast reads \"45 min\". `\"down\"` is mandatory for a\n * COUNTDOWN: with 5 minutes 30 seconds left, \"6 min\" promises the user half\n * a minute they do not have. (Real regression, sandbox time-remaining,\n * 2026-09-11.)\n *\n * `\"clock\"` and `\"long\"` are ALREADY `\"down\"` by construction and ignore\n * this — a clock never rounds `0:59` up to `1:00`, and prose \"for 3 hours\"\n * asserts at-least-three-hours. It changes `\"coarse\"` and `\"compact\"`.\n */\n round?: \"nearest\" | \"down\" | undefined;\n /**\n * Keep the sign on a negative duration (`-1:23`) instead of clamping to\n * zero. Default `false` — a negative elapsed time is nearly always a clock\n * bug, and `0:00` is the honest reading. Opt in where a signed offset is the\n * actual quantity (a field format for a stored `interval`, say).\n */\n signed?: boolean | undefined;\n}\n\nconst pad2 = (value: number): string => value.toString().padStart(2, \"0\");\n\nfunction clockBody(totalSeconds: number): string {\n const seconds = totalSeconds % 60;\n const minutes = Math.floor(totalSeconds / 60) % 60;\n const hours = Math.floor(totalSeconds / 3600);\n return hours > 0\n ? `${hours}:${pad2(minutes)}:${pad2(seconds)}`\n : `${minutes}:${pad2(seconds)}`;\n}\n\ntype Rounding = \"nearest\" | \"down\";\n\nconst reduce = (value: number, mode: Rounding): number =>\n mode === \"down\" ? Math.floor(value) : Math.round(value);\n\nfunction compactBody(ms: number, round: Rounding): string {\n if (ms < 1000) return `${reduce(ms, round)}ms`;\n const totalSeconds = ms / 1000;\n if (totalSeconds < 60) {\n // One decimal under 10s (0.1s is a visible difference at that scale),\n // whole seconds above it (nobody reads \"43.7s\" as more precise than \"44s\").\n //\n // A TRAILING `.0` IS STRIPPED (2026-09-11). The rule above exists to show\n // TENTHS WHEN THERE ARE TENTHS; `5.0s` spends a character saying \"there\n // are none\", and a game countdown ticking \"9.0s, 8.0s, 7.0s\" reads worse\n // than \"9s, 8s, 7s\" for no gain. The usual objection — column jitter —\n // does not apply to this voice: `compact` already swings between `250ms`,\n // `5.2s`, `44s` and `1m 30s`, so it was never a fixed-width column format.\n // `\"clock\"` is the fixed-width one.\n if (totalSeconds >= 10) return `${reduce(totalSeconds, round)}s`;\n const tenths = round === \"down\"\n ? Math.floor(totalSeconds * 10) / 10\n : Math.round(totalSeconds * 10) / 10;\n return `${Number.isInteger(tenths) ? tenths : tenths.toFixed(1)}s`;\n }\n const whole = reduce(totalSeconds, round);\n const minutes = Math.floor(whole / 60);\n if (minutes < 60) return `${minutes}m ${pad2(whole % 60)}s`;\n const hours = Math.floor(minutes / 60);\n return `${hours}h ${pad2(minutes % 60)}m`;\n}\n\nfunction coarseBody(ms: number, round: Rounding): string {\n const minutes = reduce(ms / 60_000, round);\n if (minutes < 1) return \"< 1 min\";\n if (minutes < 60) return `${minutes} min`;\n if (minutes < 1440) {\n const hours = Math.floor(minutes / 60);\n const rest = minutes % 60;\n return rest > 0 ? `${hours}h ${rest}m` : `${hours}h`;\n }\n // Day tier, added 2026-09-11: this voice used to print `77h` for a\n // three-day container uptime. It keeps `coarse`'s own shape — abbreviated\n // units, at most two of them, largest first — so `3d 4h`, `3d`, never\n // `3d 4h 12m`.\n const days = Math.floor(minutes / 1440);\n const restHours = Math.floor((minutes % 1440) / 60);\n return restHours > 0 ? `${days}d ${restHours}h` : `${days}d`;\n}\n\nconst plural = (count: number, unit: string): string =>\n `${count} ${unit}${count === 1 ? \"\" : \"s\"}`;\n\n/**\n * The prose voice. Always FLOORS: \"for 3 hours\" and \"expires in 3 days\" both\n * assert at-least, which is the only reading that cannot mislead in either\n * direction (an elapsed time never overstates, a countdown never over-promises).\n *\n * ONE TIER ONLY — \"3 hours\", never \"3 hours 12 minutes\". This voice goes\n * inside a sentence, and a sentence carrying two magnitudes reads like a\n * stopwatch readout. A caller that genuinely needs both wants `coarse`.\n *\n * Sub-second says so in words rather than printing a floored \"0 seconds\",\n * which reads as \"nothing happened\" — the same reasoning as `coarse`'s\n * \"< 1 min\".\n *\n * It stops at days. Weeks and months are calendar units whose length depends\n * on WHICH week and WHICH month, so a duration — a pure span with no anchor —\n * cannot honestly speak them. \"45 days\" is exact; \"1.5 months\" is a guess.\n * {@link formatRelativeTime}, which HAS an anchor, is where weeks and months\n * belong.\n */\nfunction longBody(ms: number): string {\n if (ms < 1_000) return \"less than a second\";\n if (ms < 60_000) return plural(Math.floor(ms / 1_000), \"second\");\n if (ms < 3_600_000) return plural(Math.floor(ms / 60_000), \"minute\");\n if (ms < 86_400_000) return plural(Math.floor(ms / 3_600_000), \"hour\");\n return plural(Math.floor(ms / 86_400_000), \"day\");\n}\n\n/**\n * THE canonical duration formatter. Milliseconds in, a chosen voice out.\n *\n * `null`, `undefined`, `NaN` and `Infinity` all take the fallback — never\n * `NaN:NaN`, which is what a null start time produces, and never a confident\n * `0:00` for \"we do not know\".\n */\nexport function formatDurationMs(\n ms: number | null | undefined,\n options: DurationOptions = {},\n): string {\n const style = options.style ?? \"clock\";\n const fallback = options.fallback ?? (style === \"clock\" ? \"0:00\" : \"—\");\n if (ms === null || ms === undefined || !Number.isFinite(ms)) return fallback;\n\n const negative = ms < 0;\n if (negative && options.signed !== true) {\n return style === \"clock\" ? clockBody(0) : fallback;\n }\n const magnitude = Math.abs(ms);\n const round = options.round ?? \"nearest\";\n const body =\n style === \"clock\"\n ? clockBody(Math.floor(magnitude / 1000))\n : style === \"compact\"\n ? compactBody(magnitude, round)\n : style === \"coarse\"\n ? coarseBody(magnitude, round)\n : longBody(magnitude);\n return negative ? `-${body}` : body;\n}\n\n/** Seconds in. See {@link formatDurationMs}. */\nexport function formatDurationSeconds(\n seconds: number | null | undefined,\n options: DurationOptions = {},\n): string {\n if (seconds === null || seconds === undefined || !Number.isFinite(seconds)) {\n return formatDurationMs(seconds as number | null | undefined, options);\n }\n return formatDurationMs(seconds * 1000, options);\n}\n\n/** Minutes in. See {@link formatDurationMs}. */\nexport function formatDurationMinutes(\n minutes: number | null | undefined,\n options: DurationOptions = {},\n): string {\n if (minutes === null || minutes === undefined || !Number.isFinite(minutes)) {\n return formatDurationMs(minutes as number | null | undefined, options);\n }\n return formatDurationMs(minutes * 60_000, options);\n}\n\n/**\n * Duration between two timestamps, in milliseconds — `null` when the start is\n * missing or either end is unparseable. An absent `to` means \"still running\",\n * so it measures to `now`. Pair with {@link formatDurationMs}.\n */\nexport function durationMsBetween(\n from: TimestampInput,\n to: TimestampInput,\n now: number = Date.now(),\n): number | null {\n const start = parseTimestamp(from);\n if (!start) return null;\n if (to === null || to === undefined) return Math.max(0, now - start.getTime());\n const end = parseTimestamp(to);\n if (!end) return null;\n return Math.max(0, end.getTime() - start.getTime());\n}\n\n// ───────────────────────── byte sizes ─────────────────────────\n\nconst SIZE_UNITS = [\"B\", \"KB\", \"MB\", \"GB\", \"TB\", \"PB\"] as const;\n\nexport interface FileSizeOptions {\n /** Returned for `null` / `undefined` / non-finite / negative. Default `\"—\"`. */\n fallback?: string | undefined;\n}\n\n/**\n * `512` → `\"512 B\"`, `2048` → `\"2.0 KB\"`, `15360` → `\"15 KB\"`.\n *\n * THE DISPLAY DECISIONS, made once (unified 2026-09-07 from six host twins and\n * `@ai-matrx/media`'s copy, which was the richest):\n *\n * 1. Binary units (1024), labelled with the short SI-ish names the whole fleet\n * already used — `B`/`KB`/`MB`/`GB`/`TB`, never `\"Bytes\"`.\n * 2. One decimal below 10 in a unit (`2.0 KB`, `9.9 MB`), whole numbers at or\n * above it (`15 KB`, `340 MB`) — three significant figures is the most a\n * file size is ever worth, and `1.5 GB` versus `1536.0 MB` is the whole\n * point of the unit.\n * 3. Whole bytes below 1 KB — `\"512 B\"`, never `\"512.0 B\"`.\n * 4. `null` / `undefined` / `NaN` / `Infinity` / negative all collapse to an\n * em-dash, NOT to `\"0 B\"`. A corrupt or mid-upload `size` row rendering as\n * a confident, wrong `\"0 B\"` is a screen telling a lie; \"size unknown\" is\n * the honest reading. (`0` itself is a real size and prints `\"0 B\"`.)\n */\nexport function formatFileSize(\n bytes: number | null | undefined,\n options: FileSizeOptions = {},\n): string {\n const fallback = options.fallback ?? \"—\";\n if (bytes === null || bytes === undefined || !Number.isFinite(bytes) || bytes < 0) {\n return fallback;\n }\n if (bytes < 1024) return `${Math.round(bytes)} B`;\n let value = bytes;\n let unit = 0;\n while (value >= 1024 && unit < SIZE_UNITS.length - 1) {\n value /= 1024;\n unit += 1;\n }\n const rendered = value >= 10 ? String(Math.round(value)) : value.toFixed(1);\n return `${rendered} ${SIZE_UNITS[unit]}`;\n}\n\n// ───────────────────────── people ─────────────────────────\n\nexport interface InitialsOptions {\n /** Returned when nothing usable is left. Default `\"?\"`. */\n fallback?: string | undefined;\n}\n\n/**\n * `\"Ana Rivera\"` → `\"AR\"`, `\"Ana\"` → `\"A\"`, `\"Ana Maria Rivera\"` → `\"AR\"`,\n * `\"ana@example.com\"` → `\"A\"`, `\" \"` → `\"?\"`.\n *\n * THE DISPLAY DECISION, made once: a multi-part name takes FIRST + LAST, not\n * first + second. `\"Ana Maria Rivera\"` is `AR`, because the family name is the\n * half a reader recognises. (Six matrx-frontend twins took first + second and\n * printed `AM`; the two package copies both took first + last, and both were\n * tested. First + last wins.)\n *\n * Pass the email as the value when there is no name — a single token yields\n * its first character, which is exactly what the host twins did by hand.\n */\nexport function getInitials(\n value: string | null | undefined,\n options: InitialsOptions = {},\n): string {\n const fallback = options.fallback ?? \"?\";\n if (typeof value !== \"string\") return fallback;\n const parts = value.trim().split(/\\s+/).filter((part) => part.length > 0);\n if (parts.length === 0) return fallback;\n const first = parts[0]?.charAt(0) ?? \"\";\n const last = parts.length > 1 ? (parts[parts.length - 1]?.charAt(0) ?? \"\") : \"\";\n return `${first}${last}`.toUpperCase() || fallback;\n}\n\n/**\n * A stable palette index for an avatar with no image. Deterministic on the\n * seed, so the same person is the same colour on every device and every\n * reload — a random colour per render is a surprisingly loud bug.\n */\nexport function avatarPaletteIndex(seed: string, buckets = 8): number {\n let hash = 0;\n for (let index = 0; index < seed.length; index += 1) {\n hash = (hash * 31 + seed.charCodeAt(index)) | 0;\n }\n return Math.abs(hash) % buckets;\n}\n\n// ───────────────────── honest numbers ─────────────────────\n\n/**\n * THE LYING-SCREEN CLASS, and the primitives that close it.\n *\n * WHAT HAPPENED (matrx-frontend, 2026-09-12). `$${(app.total_cost ?? 0)\n * .toFixed(4)}` rendered \"$0.0000\" for a cost NOBODY MEASURED, and a reader\n * walked away believing the run was free. The same `?? 0` then went into a\n * SUM, so an unmeasured cost was invisible inside a real-looking analytics\n * total — and those totals rode into an agent payload, which means agents\n * were handed fabricated cost numbers as fact. Sibling masks: a division with\n * a zero denominator printing \"NaN% used\", and a red FAILED icon asserting\n * failure when the truth was that the success rate had never been measured.\n *\n * THE CONVENTION IS ALREADY OURS. {@link formatFileSize} decided it in\n * 2026-09-07: unknown reads as an em-dash, never a confident \"0 B\". These\n * carry the identical contract to money, counts and percentages — the same\n * null / undefined / NaN / Infinity honesty, exported under\n * {@link UNKNOWN_DISPLAY} so the whole fleet spells \"unknown\" one way.\n *\n * THE OTHER HALF OF THE LAW, and the easy half to break while fixing the\n * first: a zero that is REALLY a zero still prints as zero. `0` is a\n * measurement; `null` is the absence of one. Nothing here collapses them.\n *\n * WHY `?? 0` IS NO LONGER THE EASY PATH. It was never the formatter that\n * tempted anyone — it was the AGGREGATE, where a nullable value has to become\n * a number before it can be added. {@link sumKnown} is that step, and it\n * hands back the count of values it could NOT measure alongside the total, so\n * a caller cannot present a total as complete without having been shown what\n * is missing from it.\n */\n\n/** What every Matrx surface prints when a value is unknown. */\nexport const UNKNOWN_DISPLAY = \"—\";\n\n/**\n * True only for a real, usable measurement. `0` passes; `null`, `undefined`,\n * `NaN` and `Infinity` do not. The predicate that makes `?? 0` unnecessary.\n */\nexport function isKnownNumber(value: unknown): value is number {\n return typeof value === \"number\" && Number.isFinite(value);\n}\n\nexport interface UnknownOption {\n /** Override the em-dash that stands in for an unmeasured value. */\n unknown?: string | undefined;\n}\n\n/**\n * `12.5` → `\"$12.50\"`, `1234.5` → `\"$1,234.50\"`, `0` → `\"$0.00\"`,\n * `null` → `\"—\"`.\n *\n * THE NAME IS `Usd`, NOT `Currency`, under the same UNIT LAW that forbids a\n * bare `formatDuration`: this prepends a literal `$` and applies no currency\n * conversion, so calling it `formatCurrency` would promise a capability it\n * does not have — and a EUR amount rendered with a `$` is the same class of\n * lie this section exists to stop.\n *\n * THOUSANDS ARE GROUPED (0.12.1). `$1234.57` is read wrong often enough that\n * five of the six host twins this replaced grouped by hand; an ungrouped\n * seven-figure spend total is the same failure as an ungrouped token count.\n * The digits are still fixed, so the decimal place does not wander down a\n * column.\n *\n * `digits` defaults to 2 (a price a person reads). Pass a number for a fixed\n * precision, or `\"adaptive\"` for per-token and per-call costs, where a fixed\n * 2 rounds the entire quantity away to `\"$0.00\"` — the confident-wrong-number\n * failure this section exists to stop, wearing a rounding mask. Adaptive\n * gives 2 decimals at a dollar or more, 4 down to a cent, and 6 below that,\n * which is the union of what the two independent twins that derived it\n * needed.\n */\nexport function formatUsd(\n value: number | null | undefined,\n options: { digits?: number | \"adaptive\" | undefined } & UnknownOption = {},\n): string {\n const { digits = 2, unknown = UNKNOWN_DISPLAY } = options;\n if (!isKnownNumber(value)) return unknown;\n let places: number;\n if (digits === \"adaptive\") {\n const magnitude = Math.abs(value);\n places = magnitude === 0 || magnitude >= 1 ? 2 : magnitude >= 0.01 ? 4 : 6;\n } else {\n places = digits;\n }\n return `$${value.toLocaleString(\"en-US\", {\n minimumFractionDigits: places,\n maximumFractionDigits: places,\n })}`;\n}\n\n/**\n * `1234567` → `\"1,234,567\"`, `0` → `\"0\"`, `null` → `\"—\"`.\n *\n * Locale-grouped, because an ungrouped nine-digit token count is read wrong\n * as often as it is read. There is deliberately no compact (\"1.2M\") voice\n * here yet: the call sites that want one are real but were not part of the\n * evidence this section was built from, and a display voice invented ahead of\n * its callers is how a formatter ends up with options nobody should use.\n */\nexport function formatCount(\n value: number | null | undefined,\n options: UnknownOption = {},\n): string {\n const { unknown = UNKNOWN_DISPLAY } = options;\n if (!isKnownNumber(value)) return unknown;\n return value.toLocaleString();\n}\n\n/**\n * A ratio, or `null` when it is INDETERMINATE — the guard the \"NaN% used\"\n * defect was missing.\n *\n * `0 / 0` is not zero, it is unknown, and so is any division by a zero or\n * unmeasured denominator. Returning `null` rather than a number forces the\n * caller through {@link formatPercentFromFraction}'s unknown branch instead\n * of letting `NaN` reach the screen.\n */\nexport function safeRatio(\n numerator: number | null | undefined,\n denominator: number | null | undefined,\n): number | null {\n if (!isKnownNumber(numerator) || !isKnownNumber(denominator)) return null;\n if (denominator === 0) return null;\n const ratio = numerator / denominator;\n return Number.isFinite(ratio) ? ratio : null;\n}\n\n/**\n * `0.25` → `\"25%\"`, `0` → `\"0%\"`, `null` → `\"—\"`.\n *\n * THE UNIT IS IN THE NAME, deliberately at the cost of a long one. Half the\n * fleet's percentage twins took a 0..1 fraction and half took a 0..100\n * percentage, and the two are indistinguishable at a call site — a 100×\n * error that renders plausibly. `FromFraction` makes the wrong call visible\n * while it is being written. Pair it with {@link safeRatio}.\n *\n * NOTHING IS CLAMPED. A percentage over 100 is usually a real signal — over\n * quota, over budget, over capacity — and hiding it is its own lie.\n */\nexport function formatPercentFromFraction(\n fraction: number | null | undefined,\n options: { digits?: number | undefined } & UnknownOption = {},\n): string {\n const { digits = 0, unknown = UNKNOWN_DISPLAY } = options;\n if (!isKnownNumber(fraction)) return unknown;\n return `${(fraction * 100).toFixed(digits)}%`;\n}\n\n/** What {@link sumKnown} measured, and what it could not. */\nexport interface KnownSum {\n /** Sum of the measured values ONLY. `0` when nothing was measurable. */\n total: number;\n /** How many values were real measurements. */\n known: number;\n /** How many were `null` / `undefined` / `NaN` / `Infinity`. */\n unknown: number;\n}\n\n/**\n * Add up a column that may contain unmeasured values, and SAY SO.\n *\n * THIS IS THE FUNCTION THAT CLOSES THE CLASS. Every formatter above refuses\n * to invent a value, but `?? 0` was never typed in front of a formatter — it\n * was typed in front of `+`, because a nullable number has to become a number\n * before it can be added, and `0` is the obvious way to make it one. That\n * silently folds \"we never measured this\" into \"this cost nothing\", and the\n * resulting total looks exactly like a complete one.\n *\n * So the aggregate step returns the counts with the number. A caller holding\n * a `KnownSum` has been handed the fact that three of forty rows were\n * unmeasured; it can still render `total`, but it cannot claim completeness\n * without ignoring a field it was given. Render `unknown > 0` as a caveat\n * beside the total — never as a footnote nobody reads, and never as nothing.\n */\nexport function sumKnown(\n values: Iterable<number | null | undefined>,\n): KnownSum {\n let total = 0;\n let known = 0;\n let unknown = 0;\n for (const value of values) {\n if (isKnownNumber(value)) {\n total += value;\n known += 1;\n } else {\n unknown += 1;\n }\n }\n return { total, known, unknown };\n}\n"],"mappings":"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","names":[]}
|