@mohndoe/pi-atlas 0.1.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.
Files changed (66) hide show
  1. package/.pi/extensions/guardrails.json +10 -0
  2. package/.pi/extensions/guardrails.v0.json +8 -0
  3. package/AGENTS.md +13 -0
  4. package/CONTEXT.md +119 -0
  5. package/LICENSE +21 -0
  6. package/README.md +40 -0
  7. package/bun.lock +325 -0
  8. package/docs/ARCHITECTURE.md +66 -0
  9. package/docs/adr/0001-global-session-project-map.md +9 -0
  10. package/docs/adr/0002-precomputed-summaries.md +9 -0
  11. package/docs/agents/domain.md +42 -0
  12. package/docs/agents/issue-tracker.md +22 -0
  13. package/docs/agents/triage-labels.md +14 -0
  14. package/package.json +49 -0
  15. package/src/__tests__/cache.test.ts +388 -0
  16. package/src/__tests__/components.fixtures.ts +54 -0
  17. package/src/__tests__/compute.fixtures.ts +49 -0
  18. package/src/__tests__/compute.test.ts +336 -0
  19. package/src/__tests__/e2e.test.ts +182 -0
  20. package/src/__tests__/format.test.ts +232 -0
  21. package/src/__tests__/parser.test.ts +1396 -0
  22. package/src/cache.ts +178 -0
  23. package/src/colorPalette.ts +119 -0
  24. package/src/components/BarChart.ts +288 -0
  25. package/src/components/Dashboard.ts +222 -0
  26. package/src/components/Header.ts +40 -0
  27. package/src/components/KpiCards.ts +104 -0
  28. package/src/components/LoadingView.ts +38 -0
  29. package/src/components/MarqueeText.ts +79 -0
  30. package/src/components/RangeSelector.ts +63 -0
  31. package/src/components/RankedBarList.ts +71 -0
  32. package/src/components/SortedTable.ts +221 -0
  33. package/src/components/StatCard.ts +64 -0
  34. package/src/components/TabBar.ts +59 -0
  35. package/src/components/UsageRow.ts +55 -0
  36. package/src/components/__tests__/Bar.test.ts +66 -0
  37. package/src/components/__tests__/BarChart.test.ts +224 -0
  38. package/src/components/__tests__/Dashboard.test.ts +452 -0
  39. package/src/components/__tests__/KpiCards.test.ts +83 -0
  40. package/src/components/__tests__/LoadingView.test.ts +26 -0
  41. package/src/components/__tests__/MarqueeText.test.ts +75 -0
  42. package/src/components/__tests__/RangeSelector.test.ts +34 -0
  43. package/src/components/__tests__/RankedBarList.test.ts +110 -0
  44. package/src/components/__tests__/SortedTable.integration.test.ts +228 -0
  45. package/src/components/__tests__/SortedTable.test.ts +723 -0
  46. package/src/components/__tests__/TabBar.test.ts +62 -0
  47. package/src/components/__tests__/cells.test.ts +193 -0
  48. package/src/components/cells.ts +108 -0
  49. package/src/components/shared/Bar.ts +22 -0
  50. package/src/components/shared/GridRow.ts +22 -0
  51. package/src/compute.ts +210 -0
  52. package/src/format.ts +219 -0
  53. package/src/index.ts +88 -0
  54. package/src/parser.ts +363 -0
  55. package/src/tabs/Languages.ts +102 -0
  56. package/src/tabs/Models.ts +108 -0
  57. package/src/tabs/Overview.ts +152 -0
  58. package/src/tabs/Projects.ts +92 -0
  59. package/src/tabs/Usage.ts +181 -0
  60. package/src/tabs/__tests__/Languages.test.ts +158 -0
  61. package/src/tabs/__tests__/Models.test.ts +143 -0
  62. package/src/tabs/__tests__/Overview.test.ts +92 -0
  63. package/src/tabs/__tests__/Projects.test.ts +142 -0
  64. package/src/tabs/__tests__/Usage.test.ts +174 -0
  65. package/src/types.ts +99 -0
  66. package/tsconfig.json +30 -0
@@ -0,0 +1,232 @@
1
+ import { describe, expect, it } from "bun:test";
2
+ import {
3
+ MONTH_NAMES,
4
+ dateFromISOString,
5
+ formatCacheTimestamp,
6
+ formatCost,
7
+ formatModelName,
8
+ formatNumber,
9
+ langFromPath,
10
+ projectNameFromCwd,
11
+ } from "../format";
12
+
13
+ describe("formatModelName", () => {
14
+ it("handles standard model names", () => {
15
+ expect(formatModelName("deepseek-v4-pro")).toBe("Deepseek V4 Pro");
16
+ expect(formatModelName("llama-3-70b")).toBe("Llama 3 70b");
17
+ expect(formatModelName("claude-haiku-3.5")).toBe("Claude Haiku 3.5");
18
+ expect(formatModelName("gemini-2.5-pro")).toBe("Gemini 2.5 Pro");
19
+ });
20
+
21
+ it("strips 8-digit date suffix", () => {
22
+ expect(formatModelName("claude-opus-4-20250514")).toBe("Claude Opus 4");
23
+ });
24
+
25
+ it("strips YYYY-MM-DD date suffix", () => {
26
+ expect(formatModelName("some-model-2025-05-14")).toBe("Some Model");
27
+ });
28
+ });
29
+
30
+ describe("langFromPath", () => {
31
+ it("maps .ts to TypeScript", () => {
32
+ expect(langFromPath("/src/foo.ts")).toBe("TypeScript");
33
+ });
34
+
35
+ it("maps .rs to Rust", () => {
36
+ expect(langFromPath("/src/lib.rs")).toBe("Rust");
37
+ });
38
+
39
+ it("maps .py to Python", () => {
40
+ expect(langFromPath("/app/main.py")).toBe("Python");
41
+ });
42
+
43
+ it("maps common web extensions", () => {
44
+ expect(langFromPath("/src/App.tsx")).toBe("TypeScript");
45
+ expect(langFromPath("/src/App.jsx")).toBe("JavaScript");
46
+ expect(langFromPath("/styles.css")).toBe("CSS");
47
+ expect(langFromPath("/index.html")).toBe("HTML");
48
+ expect(langFromPath("/data.json")).toBe("JSON");
49
+ expect(langFromPath("/config.yaml")).toBe("YAML");
50
+ expect(langFromPath("/README.md")).toBe("Markdown");
51
+ });
52
+
53
+ it("handles files without extension as 'Other'", () => {
54
+ expect(langFromPath("/src/Makefile")).toBe("Other");
55
+ expect(langFromPath("/src/justfile")).toBe("Other");
56
+ });
57
+
58
+ it("handles unknown extensions as 'Other'", () => {
59
+ expect(langFromPath("/data/file.xyz")).toBe("Other");
60
+ expect(langFromPath("/data/file.abcdef")).toBe("Other");
61
+ });
62
+
63
+ it("handles Dockerfile extension correctly", () => {
64
+ expect(langFromPath("/app/Dockerfile")).toBe("Dockerfile");
65
+ });
66
+
67
+ it("is case-insensitive for extension lookup", () => {
68
+ expect(langFromPath("/src/Foo.TS")).toBe("TypeScript");
69
+ expect(langFromPath("/src/Foo.PY")).toBe("Python");
70
+ });
71
+ });
72
+
73
+ describe("projectNameFromCwd", () => {
74
+ it("extracts basename from Unix path", () => {
75
+ expect(projectNameFromCwd("/home/mohndoe/Work/pi-atlas")).toBe("pi-atlas");
76
+ });
77
+
78
+ it("handles single-level path", () => {
79
+ expect(projectNameFromCwd("/my-project")).toBe("my-project");
80
+ });
81
+
82
+ it("strips trailing slash like basename", () => {
83
+ expect(projectNameFromCwd("/home/doe/proj/")).toBe("proj");
84
+ });
85
+ });
86
+
87
+ describe("dateFromISOString", () => {
88
+ it("extracts YYYY-MM-DD from ISO timestamp", () => {
89
+ expect(dateFromISOString("2026-06-08T17:37:04.122Z")).toBe("2026-06-08");
90
+ });
91
+
92
+ it("works on date-only", () => {
93
+ expect(dateFromISOString("2026-12-31")).toBe("2026-12-31");
94
+ });
95
+ });
96
+
97
+ describe("formatNumber", () => {
98
+ it("formats numbers below 1000 as-is", () => {
99
+ expect(formatNumber(0)).toBe("0");
100
+ expect(formatNumber(1)).toBe("1");
101
+ expect(formatNumber(999)).toBe("999");
102
+ });
103
+
104
+ it("formats thousands with k", () => {
105
+ expect(formatNumber(1000)).toBe("1.0k");
106
+ expect(formatNumber(1500)).toBe("1.5k");
107
+ expect(formatNumber(999999)).toBe("1000.0k");
108
+ });
109
+
110
+ it("formats millions with M", () => {
111
+ expect(formatNumber(1_000_000)).toBe("1.00M");
112
+ expect(formatNumber(2_500_000)).toBe("2.50M");
113
+ });
114
+
115
+ it("formats billions with B", () => {
116
+ expect(formatNumber(1_000_000_000)).toBe("1.00B");
117
+ expect(formatNumber(2_500_000_000)).toBe("2.50B");
118
+ });
119
+ });
120
+
121
+ describe("formatCost", () => {
122
+ it("formats small costs with $ and two decimals", () => {
123
+ expect(formatCost(0)).toBe("$0.00");
124
+ expect(formatCost(1.5)).toBe("$1.50");
125
+ expect(formatCost(999.99)).toBe("$999.99");
126
+ });
127
+
128
+ it("formats thousands with k", () => {
129
+ expect(formatCost(1000)).toBe("$1.0k");
130
+ expect(formatCost(1500)).toBe("$1.5k");
131
+ });
132
+
133
+ it("formats millions with M", () => {
134
+ expect(formatCost(1_000_000)).toBe("$1.0M");
135
+ expect(formatCost(2_500_000)).toBe("$2.5M");
136
+ });
137
+ });
138
+
139
+ describe("formatCacheTimestamp", () => {
140
+ it("shows time only for same day", () => {
141
+ const now = new Date();
142
+ const iso = now.toISOString();
143
+ const result = formatCacheTimestamp(iso);
144
+ expect(result).toMatch(/\d{1,2}:\d{2}/);
145
+ expect(result).not.toContain("Yesterday");
146
+ expect(result).not.toContain(",");
147
+ });
148
+
149
+ it("shows 'Yesterday' for previous day", () => {
150
+ const yesterday = new Date();
151
+ yesterday.setUTCDate(new Date().getUTCDate() - 1);
152
+ const iso = yesterday.toISOString();
153
+ const result = formatCacheTimestamp(iso);
154
+ expect(result).toMatch(/^Yesterday/);
155
+ });
156
+
157
+ it("shows date for older dates this year", () => {
158
+ const old = new Date("2026-01-15T14:30:00Z");
159
+ const iso = old.toISOString();
160
+ const result = formatCacheTimestamp(iso);
161
+ expect(result).toMatch(/^Jan 15,/);
162
+ });
163
+
164
+ it("shows date with year for previous year", () => {
165
+ const old = new Date("2025-06-10T09:15:00Z");
166
+ const iso = old.toISOString();
167
+ const result = formatCacheTimestamp(iso);
168
+ expect(result).toMatch(/2025/);
169
+ });
170
+
171
+ // ---- Timezone-awareness tests ----
172
+
173
+ it("formats time in local timezone (not UTC)", () => {
174
+ const d = new Date("2026-06-15T07:30:00Z");
175
+ const iso = d.toISOString();
176
+ const result = formatCacheTimestamp(iso);
177
+
178
+ const localHr = d.getHours();
179
+ const localMin = d.getMinutes();
180
+ const utcHr = d.getUTCHours();
181
+
182
+ const h12 = localHr % 12 || 12;
183
+ const ampm = localHr >= 12 ? "PM" : "AM";
184
+ const expectedLocal = `${h12}:${String(localMin).padStart(2, "0")} ${ampm}`;
185
+
186
+ expect(result).toContain(expectedLocal);
187
+
188
+ // When local time differs from UTC, verify UTC time is NOT shown
189
+ if (localHr !== utcHr) {
190
+ const utcH12 = utcHr % 12 || 12;
191
+ const utcAmpm = utcHr >= 12 ? "PM" : "AM";
192
+ expect(result).not.toContain(`${utcH12}:${String(localMin).padStart(2, "0")} ${utcAmpm}`);
193
+ }
194
+ });
195
+
196
+ it("uses local month/day (not UTC) for older date display", () => {
197
+ const d = new Date("2026-01-15T12:00:00Z");
198
+ const iso = d.toISOString();
199
+ const result = formatCacheTimestamp(iso);
200
+
201
+ const localMonth = d.getMonth();
202
+ const localDay = d.getDate();
203
+ const utcMonth = d.getUTCMonth();
204
+ const utcDay = d.getUTCDate();
205
+
206
+ // Output should contain local month/day
207
+ expect(result).toMatch(new RegExp(`${MONTH_NAMES[localMonth]} ${localDay},`));
208
+
209
+ // If local date differs from UTC date, verify UTC date is NOT shown
210
+ if (localMonth !== utcMonth || localDay !== utcDay) {
211
+ expect(result).not.toMatch(new RegExp(`${MONTH_NAMES[utcMonth]} ${utcDay},`));
212
+ }
213
+ });
214
+
215
+ it("formats time in local timezone for yesterday dates", () => {
216
+ const yesterday = new Date();
217
+ yesterday.setUTCDate(new Date().getUTCDate() - 1);
218
+ yesterday.setUTCHours(7, 30, 0, 0);
219
+ const iso = yesterday.toISOString();
220
+ const result = formatCacheTimestamp(iso);
221
+
222
+ const localHr = yesterday.getHours();
223
+ const localMin = yesterday.getMinutes();
224
+
225
+ const h12 = localHr % 12 || 12;
226
+ const ampm = localHr >= 12 ? "PM" : "AM";
227
+ const expectedLocal = `${h12}:${String(localMin).padStart(2, "0")} ${ampm}`;
228
+
229
+ expect(result).toMatch(/^Yesterday/);
230
+ expect(result).toContain(expectedLocal);
231
+ });
232
+ });