@trazum/cli 1.10.0 → 1.26.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/src/markdown.ts CHANGED
@@ -8,7 +8,9 @@ import type {
8
8
  PromptProfile,
9
9
  RuleLevel,
10
10
  } from '@trazum/core';
11
- import { formatSignedUsd, formatUsd, getMessages, getModel } from '@trazum/core';
11
+ import { TTL_1H_MS, UNLABELLED, formatSignedUsd, formatUsd, getMessages, getModel, sharesOf } from '@trazum/core';
12
+ import { dayOf, formatGap, median, spanDays } from './time.js';
13
+ import type { AgainstDriver, BillLevers, CacheEconomics, RepriceReport, UsageProfileReport } from '@trazum/core';
12
14
 
13
15
  /**
14
16
  * Markdown for the places a pull request is actually read.
@@ -715,3 +717,415 @@ export function renderBlameMarkdown(input: BlameMarkdownInput): string {
715
717
 
716
718
  return lines.join('\n');
717
719
  }
720
+
721
+ export interface ProfileMarkdownInput {
722
+ report: UsageProfileReport;
723
+ levers: BillLevers;
724
+ cache: CacheEconomics;
725
+ t: CliMessages;
726
+ /**
727
+ * The `--since`/`--until` values as the user typed them, when a window was
728
+ * applied. Passed through rather than re-derived from `timeWindow`'s epoch
729
+ * bounds, because a bare `--until 2026-08-14` includes that whole day —
730
+ * rendering the internal exclusive bound would print the *next* day and
731
+ * disagree with the terminal about which window this was.
732
+ */
733
+ window?: { since: string; until: string };
734
+ /**
735
+ * Passed only when the price table is old enough to matter, so the
736
+ * threshold lives once, beside the terminal's. Rendered loud: staleness
737
+ * does not name its own size the way a skipped line does.
738
+ */
739
+ stalePricing?: { date: string; days: number };
740
+ /**
741
+ * The comparison, when `--against` was given — the section the terminal
742
+ * has had since 1.11 and the markdown did not, so a CI summary reporting
743
+ * on two logs showed only one of them. The drivers arrive computed (core's
744
+ * `driversBetween`) rather than derived here: the sign convention has one
745
+ * implementation, and this is a rendering.
746
+ */
747
+ /**
748
+ * The repricing, when `--what-if` was given. Arrives computed from core's
749
+ * `repriceProfile` rather than derived here: three surfaces must not
750
+ * disagree about what a move would cost, and this is a rendering.
751
+ */
752
+ whatIf?: RepriceReport | null;
753
+ against?: {
754
+ previousTotalUsd: number;
755
+ previousCalls: number;
756
+ labelDrivers: AgainstDriver[];
757
+ modelDrivers: AgainstDriver[];
758
+ /** True when both spans are known and intersect. */
759
+ overlap: { from: string; to: string } | null;
760
+ /** Nothing in the previous log could be priced — its own answer. */
761
+ nothingPriced: boolean;
762
+ };
763
+ }
764
+
765
+ /**
766
+ * `trazum profile` as GitHub-flavoured markdown, for a job summary or a
767
+ * pull-request comment.
768
+ *
769
+ * The terminal report is the source of truth and this reuses its message
770
+ * catalogue line for line, because two renderings of the same finding drift the
771
+ * moment they are worded twice — the sign conventions here (`positive means
772
+ * worse` on the cache delta, ceilings that must be named as ceilings) have each
773
+ * already produced a bug when restated by hand.
774
+ *
775
+ * A finding that only exists in a terminal is a finding the reader's tooling
776
+ * never surfaces; this is the other half of the `--json` lesson, for humans
777
+ * reading CI instead of machines.
778
+ */
779
+ export function renderProfileMarkdown(input: ProfileMarkdownInput): string {
780
+ const { report, levers, cache, t, window, stalePricing, against, whatIf } = input;
781
+ const n = (value: number): string => value.toLocaleString(t.numberLocale);
782
+ const pct = (share: number): string => `${(share * 100).toFixed(1)}%`;
783
+ const shares = sharesOf(report.total);
784
+ const showLabel = (label: string): string =>
785
+ label === UNLABELLED ? t.profile.unlabelled() : label;
786
+
787
+ const lines: string[] = [];
788
+ lines.push(`### ${t.profile.heading()}`);
789
+ lines.push('');
790
+ lines.push(`**${t.profile.spent(t.profile.calls(report.total.calls), formatUsd(report.total.totalUsd))}**`);
791
+ lines.push('');
792
+ // The span, under the same rule as the terminal: stated, never extrapolated,
793
+ // with partial coverage said in the same breath.
794
+ if (report.span !== null) {
795
+ const totalParsed = report.total.calls + report.unpriced.calls;
796
+ const partial =
797
+ report.span.calls < totalParsed
798
+ ? ` ${mdText(t.profile.spanPartial(n(report.span.calls), n(totalParsed)))}`
799
+ : '';
800
+ lines.push(
801
+ `_${mdText(t.profile.spanLine(dayOf(report.span.fromMs), dayOf(report.span.toMs), spanDays(report.span.fromMs, report.span.toMs)))}${partial}_`,
802
+ );
803
+ lines.push('');
804
+ }
805
+ // The window before any figure, and the undated count loud — the same order
806
+ // and the same volume as the terminal, for the same reasons.
807
+ if (report.timeWindow !== null) {
808
+ lines.push(`_${mdText(t.profile.windowLine(window?.since ?? '—', window?.until ?? '—'))}_`);
809
+ lines.push('');
810
+ if (report.timeWindow.undatedExcluded > 0) {
811
+ lines.push(`> ⚠️ ${mdText(t.profile.windowUndated(report.timeWindow.undatedExcluded))}`);
812
+ lines.push('');
813
+ }
814
+ }
815
+ lines.push('| | USD | % | tokens |');
816
+ lines.push('|---|---:|---:|---:|');
817
+ const parts: Array<[string, number, number, number]> = [
818
+ [t.profile.partInput(), report.total.inputUsd, shares.input, report.total.inputTokens],
819
+ [t.profile.partCacheRead(), report.total.cacheReadUsd, shares.cacheRead, report.total.cacheReadTokens],
820
+ [t.profile.partCacheWrite(), report.total.cacheWriteUsd, shares.cacheWrite, report.total.cacheWriteTokens],
821
+ [t.profile.partOutput(), report.total.outputUsd, shares.output, report.total.outputTokens],
822
+ ];
823
+ for (const [name, usd, share, tokens] of parts) {
824
+ // Catalogue text, not user data: no escaping into a code cell needed.
825
+ lines.push(`| ${name} | ${formatUsd(usd)} | ${pct(share)} | ${n(tokens)} |`);
826
+ }
827
+ lines.push('');
828
+
829
+ /**
830
+ * A doubled bill, said before anything above is believed. A CI summary
831
+ * showing a total nobody can trust is worse than one showing no total.
832
+ */
833
+ if (report.duplicateLines.count > 0) {
834
+ lines.push(
835
+ `> ⚠️ ${mdText(t.profile.duplicateLines(report.duplicateLines.count, formatUsd(report.duplicateLines.usd)))}`,
836
+ );
837
+ lines.push('');
838
+ }
839
+
840
+ // The most expensive day against the median day, loud past twice it — the
841
+ // same sentence and the same yardstick the terminal prints.
842
+ if (report.spendByDay.length >= 2) {
843
+ const medianUsd = median(report.spendByDay.map((d) => d.usd));
844
+ const peak = report.spendByDay.reduce((a, b) => (b.usd > a.usd ? b : a));
845
+ if (medianUsd > 0) {
846
+ const sentence = t.profile.dayPeak(peak.day, formatUsd(peak.usd), (peak.usd / medianUsd).toFixed(1));
847
+ const labelClause =
848
+ peak.topLabel !== null && report.byLabel.length > 1
849
+ ? ` ${t.profile.dayPeakLabel(showLabel(peak.topLabel), formatUsd(peak.topLabelUsd))}`
850
+ : '';
851
+ const loud = peak.usd > 2 * medianUsd;
852
+ lines.push(loud ? `> ⚠️ ${mdText(`${sentence}${labelClause}`)}` : `_${mdText(`${sentence}${labelClause}`)}_`);
853
+ lines.push('');
854
+ }
855
+
856
+ /**
857
+ * The series itself, most recent days last — the shape the peak sentence
858
+ * summarises, for the reader who wants to see the week. Capped at 14
859
+ * days with the earlier ones counted out loud: silent truncation reads
860
+ * as "covered everything" when it did not.
861
+ */
862
+ const DAYS_SHOWN = 14;
863
+ const shown = report.spendByDay.slice(-DAYS_SHOWN);
864
+ lines.push(`| ${t.profile.dayTableDay()} | USD | ${t.profile.dayTableCalls()} | ${t.profile.dayTableTop()} |`);
865
+ lines.push('|---|---:|---:|---|');
866
+ for (const day of shown) {
867
+ const top = day.topLabel === null ? '—' : mdTextCell(showLabel(day.topLabel));
868
+ lines.push(`| ${day.day} | ${formatUsd(day.usd)} | ${n(day.calls)} | ${top} |`);
869
+ }
870
+ if (report.spendByDay.length > DAYS_SHOWN) {
871
+ lines.push('');
872
+ lines.push(`_${mdText(t.profile.dayTableEarlier(report.spendByDay.length - DAYS_SHOWN))}_`);
873
+ }
874
+ lines.push('');
875
+ }
876
+
877
+ lines.push(`#### ${t.profile.leversHeading()}`);
878
+ lines.push('');
879
+ if (levers.slices.length === 0) {
880
+ lines.push(mdText(t.profile.leversNone()));
881
+ } else {
882
+ for (const slice of levers.slices.slice(0, 5)) {
883
+ lines.push(
884
+ `- **${mdText(t.profile.leverSlice(showLabel(slice.label), slice.modelName, formatUsd(slice.combinedUsd), pct(slice.shareOfBill)))}** — ${mdText(t.profile.leverCalls(t.profile.calls(slice.calls), formatUsd(slice.spentUsd)))}`,
885
+ );
886
+ if (slice.route) {
887
+ lines.push(` - ${mdText(t.profile.leverRoute(slice.route.candidate.displayName, formatUsd(slice.route.savingUsd)))}`);
888
+ }
889
+ if (slice.batch) {
890
+ lines.push(` - ${mdText(t.profile.leverBatch(formatUsd(slice.batch.savingUsd)))}`);
891
+ }
892
+ }
893
+ }
894
+ lines.push('');
895
+ lines.push(`_${mdText(t.profile.leverPromptCeiling(formatUsd(levers.promptCeilingUsd), pct(levers.promptCeilingShare)))}_`);
896
+ lines.push('');
897
+
898
+ /**
899
+ * The same request sent again — money that bought nothing the call before
900
+ * it had not already paid for. Loud in a summary, and hedged in the same
901
+ * words the terminal uses.
902
+ */
903
+ if (report.repeatedTurns.length > 0) {
904
+ lines.push(`#### ${t.profile.repeatsHeading()}`);
905
+ lines.push('');
906
+ for (const row of report.repeatedTurns.slice(0, 3)) {
907
+ lines.push(
908
+ `> ⚠️ ${mdText(t.profile.repeatsFound(showLabel(row.label), row.modelName, n(row.repeats), n(row.checkedCalls), n(Math.round(row.withinMs / 1000)), formatUsd(row.usd)))}`,
909
+ );
910
+ lines.push('');
911
+ }
912
+ lines.push(`_${mdText(t.profile.repeatsAdvice())}_`);
913
+ lines.push('');
914
+ }
915
+
916
+ /**
917
+ * How big the calls are — the half of the bill the totals table can only
918
+ * name. Same threshold and same two sentences as the terminal, because a
919
+ * CI summary that summarises differently is a second opinion nobody asked
920
+ * for.
921
+ */
922
+ if (report.inputShapes.length > 0) {
923
+ lines.push(`#### ${t.profile.inputShapeHeading()}`);
924
+ lines.push('');
925
+ for (const shape of report.inputShapes.slice(0, 3)) {
926
+ const who = showLabel(shape.label);
927
+ if (shape.medianWithinTokens === null || shape.p95WithinTokens === null || shape.p95OverMedian === null) {
928
+ lines.push(`- ${mdText(t.profile.inputHuge(who, shape.modelName, t.profile.calls(shape.calls), formatUsd(shape.inputUsd)))}`);
929
+ continue;
930
+ }
931
+ const skewed = shape.p95OverMedian >= 4;
932
+ lines.push(
933
+ `- **${mdText(skewed
934
+ ? t.profile.inputSkewed(who, shape.modelName, n(shape.medianWithinTokens), n(shape.p95WithinTokens), shape.p95OverMedian.toFixed(1), formatUsd(shape.inputUsd))
935
+ : t.profile.inputEven(who, shape.modelName, n(shape.medianWithinTokens), n(shape.p95WithinTokens), formatUsd(shape.inputUsd)))}**`,
936
+ );
937
+ lines.push(` - ${mdText(skewed ? t.profile.inputSkewedAdvice() : t.profile.inputEvenAdvice())}`);
938
+ if (shape.cachedShare >= 0.5) {
939
+ lines.push(` - ${mdText(t.profile.inputMostlyCached(pct(shape.cachedShare)))}`);
940
+ } else if (shape.cachedShare < 0.1) {
941
+ lines.push(` - ${mdText(t.profile.inputFullRate())}`);
942
+ }
943
+ }
944
+ lines.push('');
945
+ }
946
+
947
+ /**
948
+ * The repricing, with the assumption above the figure here too — a pull
949
+ * request comment is exactly where a dollar amount with the caveat
950
+ * underneath would be read as a recommendation and merged.
951
+ */
952
+ if (whatIf !== undefined && whatIf !== null) {
953
+ lines.push(`#### ${t.profile.whatIfHeading(whatIf.target.displayName)}`);
954
+ lines.push('');
955
+ lines.push(`_${mdText(t.profile.whatIfAssumption())}_`);
956
+ lines.push('');
957
+ if (whatIf.slices.length === 0) {
958
+ lines.push(mdText(t.profile.whatIfNothingToMove()));
959
+ lines.push('');
960
+ } else {
961
+ lines.push(
962
+ `**${mdText(t.profile.whatIfTotal(formatUsd(whatIf.currentUsd), formatUsd(whatIf.targetUsd), formatUsd(Math.abs(whatIf.deltaUsd))))}**`,
963
+ );
964
+ lines.push('');
965
+ for (const slice of whatIf.slices.slice(0, 5)) {
966
+ lines.push(`- ${mdText(t.profile.whatIfSlice(showLabel(slice.label), slice.model, formatUsd(slice.currentUsd), formatUsd(slice.targetUsd)))}`);
967
+ }
968
+ lines.push('');
969
+ }
970
+ for (const slice of whatIf.overContext.slice(0, 3)) {
971
+ lines.push(
972
+ `> ⚠️ ${mdText(t.profile.whatIfOverContext(showLabel(slice.label), n(slice.maxCallInputTokens), n(whatIf.target.contextWindow), formatUsd(slice.currentUsd)))}`,
973
+ );
974
+ lines.push('');
975
+ }
976
+ if (whatIf.alreadyOnTarget.calls > 0) {
977
+ lines.push(`_${mdText(t.profile.whatIfAlreadyThere(t.profile.calls(whatIf.alreadyOnTarget.calls), formatUsd(whatIf.alreadyOnTarget.usd)))}_`);
978
+ lines.push('');
979
+ }
980
+ if (whatIf.unpricedCalls > 0) {
981
+ lines.push(`_${mdText(t.profile.whatIfUnpriced(t.profile.calls(whatIf.unpricedCalls), whatIf.unpricedModels.join(', ')))}_`);
982
+ lines.push('');
983
+ }
984
+ }
985
+
986
+ // The cache verdict, with the same refusal to answer an unsettled question.
987
+ const unsettled = cache.worstCaseVerdict !== cache.verdict && report.total.assumedWriteTtlCalls > 0;
988
+ if (unsettled) {
989
+ lines.push(`> ⚠️ ${mdText(t.profile.cacheTtlUnsettled(report.total.assumedWriteTtlCalls, formatUsd(-cache.deltaUsd), formatUsd(cache.worstCaseDeltaUsd)))}`);
990
+ lines.push('');
991
+ } else if (cache.verdict === 'lost-money') {
992
+ lines.push(`> ⚠️ ${mdText(t.profile.cacheLost(formatUsd(cache.deltaUsd), n(report.total.cacheWriteTokens), n(report.total.cacheReadTokens)))}`);
993
+ lines.push('');
994
+ } else if (cache.verdict === 'paid-off') {
995
+ lines.push(mdText(t.profile.cachePaidOff(formatUsd(-cache.deltaUsd))));
996
+ lines.push('');
997
+ }
998
+
999
+ // Whether the TTL fits the gaps — the mechanism behind the verdict above,
1000
+ // with the two failing verdicts loud and the rest quiet, as on the terminal.
1001
+ for (const fit of report.cacheTtlFit.slice(0, 3)) {
1002
+ const who = showLabel(fit.label);
1003
+ const gap = formatGap(fit.medianGapMs);
1004
+ const sentence =
1005
+ fit.verdict === 'expires-before-reuse'
1006
+ ? fit.medianGapMs > TTL_1H_MS
1007
+ ? t.profile.ttlFitExpiresBoth(who, fit.modelName, gap)
1008
+ : t.profile.ttlFitExpires(who, fit.modelName, gap)
1009
+ : fit.verdict === 'overlong-ttl'
1010
+ ? t.profile.ttlFitOverlong(who, fit.modelName, gap, formatUsd(fit.overpayUsd))
1011
+ : fit.verdict === 'unsettled'
1012
+ ? t.profile.ttlFitUnsettledGap(who, fit.modelName, gap)
1013
+ : t.profile.ttlFitFits(who, fit.modelName, gap);
1014
+ const loud = fit.verdict === 'expires-before-reuse' || fit.verdict === 'overlong-ttl';
1015
+ lines.push(loud ? `> ⚠️ ${mdText(sentence)}` : `_${mdText(sentence)}_`);
1016
+ lines.push('');
1017
+ }
1018
+
1019
+ // Conversations that never came back: the fact loud, the ceiling quiet —
1020
+ // decided by the slice's own reads, exactly as on the terminal.
1021
+ const readsBySlice = new Map(
1022
+ report.byLabelAndModel.map((r) => [`${r.label}\n${r.model}`, r.breakdown.cacheReadTokens]),
1023
+ );
1024
+ for (const row of report.singleTurnCacheWrites.slice(0, 3)) {
1025
+ const who = showLabel(row.label);
1026
+ const reads = readsBySlice.get(`${row.label}\n${row.model}`) ?? 0;
1027
+ const sentence =
1028
+ reads === 0
1029
+ ? t.profile.singleTurnConfirmed(who, row.modelName, n(row.singleTurnSessions), n(row.sessions), formatUsd(row.singleTurnWriteUsd))
1030
+ : t.profile.singleTurnCeiling(who, row.modelName, n(row.singleTurnSessions), n(row.sessions), formatUsd(row.singleTurnWriteUsd));
1031
+ lines.push(reads === 0 ? `> ⚠️ ${mdText(sentence)}` : `_${mdText(sentence)}_`);
1032
+ lines.push('');
1033
+ }
1034
+
1035
+ for (const growth of report.conversations.slice(0, 3)) {
1036
+ lines.push(
1037
+ `- ${mdText(t.profile.historyGrowth(showLabel(growth.label), growth.modelName, n(Math.round(growth.minTurnTokens)), n(Math.round(growth.maxTurnTokens)), n(growth.longestSession)))} ${mdText(t.profile.historyCeiling(formatUsd(growth.growthUsd), pct(growth.shareOfBill), formatUsd(growth.flatUsd), formatUsd(growth.inputUsd)))}`,
1038
+ );
1039
+ }
1040
+ if (report.conversations.length > 0) lines.push('');
1041
+
1042
+ if (report.total.truncatedCalls > 0 && report.total.outputUsd > 0) {
1043
+ lines.push(
1044
+ `> ⚠️ ${mdText(t.profile.truncatedWaste(t.profile.calls(report.total.truncatedCalls), formatUsd(report.total.truncatedOutputUsd), pct(report.total.truncatedOutputUsd / report.total.outputUsd)))}`,
1045
+ );
1046
+ lines.push('');
1047
+ // The suspects, with the rate over calls that measured — the terminal's
1048
+ // denominator, because a workload logging the field half the time is not
1049
+ // one whose other half completed.
1050
+ const truncatedLabels = report.byLabel
1051
+ .filter((row) => row.breakdown.truncatedCalls > 0)
1052
+ .sort((a, b) => b.breakdown.truncatedOutputUsd - a.breakdown.truncatedOutputUsd);
1053
+ if (truncatedLabels.length > 0 && report.byLabel.length > 1) {
1054
+ for (const row of truncatedLabels.slice(0, 3)) {
1055
+ lines.push(
1056
+ `- ${mdText(t.profile.truncatedBy(showLabel(row.label), n(row.breakdown.truncatedCalls), n(row.breakdown.stopReasonCalls), pct(row.breakdown.truncatedCalls / row.breakdown.stopReasonCalls), formatUsd(row.breakdown.truncatedOutputUsd)))}`,
1057
+ );
1058
+ }
1059
+ lines.push('');
1060
+ }
1061
+ const truncationCeiling = report.outputShapes.find((shape) => shape.p95WithinTokens !== null);
1062
+ if (truncationCeiling !== undefined) {
1063
+ lines.push(`_${mdText(t.profile.truncatedCeiling(n(truncationCeiling.p95WithinTokens!)))}_`);
1064
+ lines.push('');
1065
+ }
1066
+ }
1067
+
1068
+ /**
1069
+ * This bill against the previous one. The convention prints before the
1070
+ * first figure it governs, and the overlap warning between the figure and
1071
+ * the drivers built from it — the terminal's order, for the terminal's
1072
+ * reason: a caveat below a number is a number somebody already acted on.
1073
+ */
1074
+ if (against !== undefined) {
1075
+ lines.push(`#### ${t.profile.againstHeading()}`);
1076
+ lines.push('');
1077
+ if (against.nothingPriced) {
1078
+ lines.push(mdText(t.profile.againstNothingPriced()));
1079
+ lines.push('');
1080
+ } else {
1081
+ const delta = report.total.totalUsd - against.previousTotalUsd;
1082
+ const growthPct =
1083
+ against.previousTotalUsd > 0
1084
+ ? `${delta >= 0 ? '+' : ''}${((delta / against.previousTotalUsd) * 100).toFixed(1)}%`
1085
+ : '—';
1086
+ lines.push(
1087
+ `**${mdText(t.profile.againstTotals(formatUsd(against.previousTotalUsd), formatUsd(report.total.totalUsd), formatSignedUsd(delta), growthPct, t.profile.calls(against.previousCalls), t.profile.calls(report.total.calls)))}**`,
1088
+ );
1089
+ lines.push('');
1090
+ if (against.overlap !== null) {
1091
+ lines.push(`> ⚠️ ${mdText(t.profile.againstOverlap(against.overlap.from, against.overlap.to))}`);
1092
+ lines.push('');
1093
+ }
1094
+ const describe = (driver: AgainstDriver, shown: string): string =>
1095
+ driver.was === null
1096
+ ? t.profile.againstDriverNew(formatSignedUsd(driver.delta), shown)
1097
+ : driver.now === null
1098
+ ? t.profile.againstDriverGone(formatSignedUsd(driver.delta), shown)
1099
+ : t.profile.againstDriver(formatSignedUsd(driver.delta), shown, formatUsd(driver.was), formatUsd(driver.now));
1100
+ for (const driver of against.labelDrivers.slice(0, 5)) {
1101
+ lines.push(`- ${mdText(describe(driver, showLabel(driver.key)))}`);
1102
+ }
1103
+ if (against.modelDrivers.length > 0) {
1104
+ lines.push('');
1105
+ lines.push(`_${mdText(t.profile.againstByModel())}_`);
1106
+ for (const driver of against.modelDrivers.slice(0, 3)) {
1107
+ lines.push(`- ${mdText(describe(driver, driver.key))}`);
1108
+ }
1109
+ }
1110
+ lines.push('');
1111
+ }
1112
+ }
1113
+
1114
+ // The provenance caveat before the data gaps: a stale table qualifies every
1115
+ // dollar above, and it does not name its own size the way a skipped line does.
1116
+ if (stalePricing !== undefined) {
1117
+ lines.push(`> ⚠️ ${mdText(t.profile.pricesStale(stalePricing.date, stalePricing.days))}`);
1118
+ lines.push('');
1119
+ }
1120
+ if (report.unpricedModels.length > 0) {
1121
+ lines.push(`> ⚠️ ${mdText(t.profile.unpriced(report.unpricedModels.join(', '), report.unpriced.calls))}`);
1122
+ lines.push('');
1123
+ }
1124
+ if (report.skippedLines.length > 0) {
1125
+ const shown = report.skippedLines.slice(0, 5).join(', ');
1126
+ lines.push(`_${mdText(t.profile.skipped(report.skippedLines.length, report.skippedLines.length > 5 ? `${shown}…` : shown))}_`);
1127
+ lines.push('');
1128
+ }
1129
+
1130
+ return `${lines.join('\n').trimEnd()}\n`;
1131
+ }
package/src/time.ts ADDED
@@ -0,0 +1,32 @@
1
+ /**
2
+ * The clock's little formatters, shared by the terminal and the markdown
3
+ * renderings so the two cannot drift — the same reason `formatUsd` lives once.
4
+ */
5
+
6
+ /** A gap, in the coarsest unit that keeps one significant figure honest. */
7
+ export function formatGap(ms: number): string {
8
+ if (ms < 90_000) return `${Math.round(ms / 1000)}s`;
9
+ if (ms < 90 * 60_000) return `${Math.round(ms / 60_000)}m`;
10
+ if (ms < 36 * 3_600_000) return `${(ms / 3_600_000).toFixed(1)}h`;
11
+ return `${(ms / 86_400_000).toFixed(1)}d`;
12
+ }
13
+
14
+ /** `YYYY-MM-DD`, UTC — the same bucketing the core's spendByDay uses. */
15
+ export const dayOf = (ms: number): string => new Date(ms).toISOString().slice(0, 10);
16
+
17
+ /** The span's length in days, one decimal. */
18
+ export const spanDays = (fromMs: number, toMs: number): string =>
19
+ ((toMs - fromMs) / 86_400_000).toFixed(1);
20
+
21
+ /**
22
+ * The median of a list, the yardstick a spike cannot inflate.
23
+ *
24
+ * A mean would let the most expensive day raise the bar it is measured
25
+ * against; the median holds still. Returns 0 for an empty list.
26
+ */
27
+ export function median(values: readonly number[]): number {
28
+ if (values.length === 0) return 0;
29
+ const sorted = [...values].sort((a, b) => a - b);
30
+ const mid = Math.floor(sorted.length / 2);
31
+ return sorted.length % 2 === 1 ? sorted[mid]! : (sorted[mid - 1]! + sorted[mid]!) / 2;
32
+ }