nilometer 0.0.0-stage → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (81) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/LICENSE +21 -0
  3. package/README.md +464 -2
  4. package/core/pricing/prices.json +348 -0
  5. package/core/schema/001_ingestion.sql +57 -0
  6. package/core/schema/002_line_count.sql +7 -0
  7. package/core/schema/003_derived.sql +70 -0
  8. package/core/schema/004_requests_dedup.sql +50 -0
  9. package/core/schema/005_repositories.sql +25 -0
  10. package/core/schema/006_status_readings.sql +33 -0
  11. package/core/schema/007_pricing.sql +95 -0
  12. package/core/schema/008_interruptions.sql +263 -0
  13. package/core/schema/009_headroom.sql +84 -0
  14. package/core/schema/010_attribution.sql +106 -0
  15. package/core/schema/011_projections.sql +83 -0
  16. package/core/schema/012_unsplit_cache_writes.sql +49 -0
  17. package/core/schema/013_repository_revalidation.sql +104 -0
  18. package/core/schema/014_observed_readings.sql +204 -0
  19. package/core/schema/015_response_observations.sql +127 -0
  20. package/core/schema/016_windows_repository_paths.sql +41 -0
  21. package/core/schema/017_unattributed_usage_once.sql +47 -0
  22. package/core/schema/018_request_time_index.sql +21 -0
  23. package/core/schema/019_dedup_earliest_of_equals.sql +61 -0
  24. package/core/schema/020_limit_quota_fields.sql +131 -0
  25. package/core/schema/021_plan_periods.sql +121 -0
  26. package/core/schema/022_attachment_walk.sql +223 -0
  27. package/core/schema/023_auto_continuations.sql +129 -0
  28. package/core/schema/024_counted_limit_groups.sql +29 -0
  29. package/core/schema/025_model_fallback.sql +77 -0
  30. package/core/schema/026_changed_readings.sql +106 -0
  31. package/core/schema/027_weekly_crossings.sql +32 -0
  32. package/core/schema/028_stale_readings.sql +196 -0
  33. package/core/schema/029_lockout_sessions.sql +68 -0
  34. package/core/schema/030_repository_aliases.sql +81 -0
  35. package/core/schema/031_cte_cursor_budget.sql +153 -0
  36. package/core/schema/032_stored_views.sql +233 -0
  37. package/dist/cli/main.cli.js +39 -0
  38. package/dist/cli/program.js +881 -0
  39. package/dist/core/db/database.js +182 -0
  40. package/dist/core/db/fingerprint.js +71 -0
  41. package/dist/core/ingest/attribution.js +131 -0
  42. package/dist/core/ingest/classify.js +382 -0
  43. package/dist/core/ingest/command.js +95 -0
  44. package/dist/core/ingest/derive.js +219 -0
  45. package/dist/core/ingest/discover.js +126 -0
  46. package/dist/core/ingest/ingest.js +160 -0
  47. package/dist/core/ingest/reader.js +89 -0
  48. package/dist/core/ingest/reset-time.js +97 -0
  49. package/dist/core/ingest/results.js +201 -0
  50. package/dist/core/ingest/spool.js +165 -0
  51. package/dist/core/ingest/store.js +99 -0
  52. package/dist/core/ingest/stored-views.js +65 -0
  53. package/dist/core/install/install.js +185 -0
  54. package/dist/core/install/locations.js +89 -0
  55. package/dist/core/install/private-files.js +259 -0
  56. package/dist/core/install/record.js +103 -0
  57. package/dist/core/plans/plan-prices.js +128 -0
  58. package/dist/core/plans/repository-aliases.js +136 -0
  59. package/dist/core/pricing/prices.js +147 -0
  60. package/dist/core/settings/settings-file.js +215 -0
  61. package/dist/core/settings/statusline.js +240 -0
  62. package/dist/core/util/commands.js +91 -0
  63. package/dist/core/util/node-version.js +58 -0
  64. package/dist/core/util/open-page.js +69 -0
  65. package/dist/core/util/paths.js +36 -0
  66. package/dist/core/verify/compare.js +77 -0
  67. package/dist/core/verify/verify.js +220 -0
  68. package/dist/core/view/server.js +142 -0
  69. package/dist/viewer/explain.js +574 -0
  70. package/dist/viewer/format.js +150 -0
  71. package/dist/viewer/html-charts.js +1292 -0
  72. package/dist/viewer/html-contract.js +2 -0
  73. package/dist/viewer/html-data.js +718 -0
  74. package/dist/viewer/html.js +1717 -0
  75. package/dist/viewer/queries.js +128 -0
  76. package/dist/viewer/render.js +602 -0
  77. package/dist/viewer/report.js +120 -0
  78. package/dist/viewer/save.js +110 -0
  79. package/docs/requirements.md +113 -0
  80. package/hooks/statusline.sh +104 -0
  81. package/package.json +61 -4
@@ -0,0 +1,348 @@
1
+ {
2
+ "comment": "Anthropic API list prices, USD per million tokens. Every row was read from source_url on verified_on. effective_from 0000-01-01 means in effect since release as far as can be verified (D-020). A price change adds a row; never edit a past row. See the pricing-table skill.",
3
+ "rows": [
4
+ {
5
+ "model_id": "claude-fable-5-1",
6
+ "effective_from": "0000-01-01",
7
+ "input": 10,
8
+ "output": 50,
9
+ "cache_write_5m": 12.5,
10
+ "cache_write_1h": 20,
11
+ "cache_read": 0.25,
12
+ "fast": null,
13
+ "standard_rate_max_input_tokens": null,
14
+ "source_url": "https://platform.claude.com/docs/en/about-claude/pricing",
15
+ "verified_on": "2026-09-13"
16
+ },
17
+ {
18
+ "model_id": "claude-fable-5",
19
+ "effective_from": "0000-01-01",
20
+ "input": 10,
21
+ "output": 50,
22
+ "cache_write_5m": 12.5,
23
+ "cache_write_1h": 20,
24
+ "cache_read": 1,
25
+ "fast": null,
26
+ "standard_rate_max_input_tokens": null,
27
+ "source_url": "https://platform.claude.com/docs/en/about-claude/pricing",
28
+ "verified_on": "2026-09-13"
29
+ },
30
+ {
31
+ "model_id": "claude-mythos-5-1",
32
+ "effective_from": "0000-01-01",
33
+ "input": 10,
34
+ "output": 50,
35
+ "cache_write_5m": 12.5,
36
+ "cache_write_1h": 20,
37
+ "cache_read": 0.25,
38
+ "fast": null,
39
+ "standard_rate_max_input_tokens": null,
40
+ "source_url": "https://platform.claude.com/docs/en/about-claude/pricing",
41
+ "verified_on": "2026-09-24"
42
+ },
43
+ {
44
+ "model_id": "claude-mythos-5",
45
+ "effective_from": "0000-01-01",
46
+ "input": 10,
47
+ "output": 50,
48
+ "cache_write_5m": 12.5,
49
+ "cache_write_1h": 20,
50
+ "cache_read": 1,
51
+ "fast": null,
52
+ "standard_rate_max_input_tokens": null,
53
+ "source_url": "https://platform.claude.com/docs/en/about-claude/pricing",
54
+ "verified_on": "2026-09-24"
55
+ },
56
+ {
57
+ "model_id": "claude-opus-5-5",
58
+ "effective_from": "0000-01-01",
59
+ "input": 4,
60
+ "output": 20,
61
+ "cache_write_5m": 5,
62
+ "cache_write_1h": 8,
63
+ "cache_read": 0.2,
64
+ "fast": {
65
+ "input": 8,
66
+ "output": 40,
67
+ "cache_write_5m": 10,
68
+ "cache_write_1h": 16,
69
+ "cache_read": 0.4
70
+ },
71
+ "standard_rate_max_input_tokens": null,
72
+ "source_url": "https://platform.claude.com/docs/en/about-claude/pricing",
73
+ "verified_on": "2026-09-24"
74
+ },
75
+ {
76
+ "model_id": "claude-opus-5",
77
+ "effective_from": "0000-01-01",
78
+ "input": 5,
79
+ "output": 25,
80
+ "cache_write_5m": 6.25,
81
+ "cache_write_1h": 10,
82
+ "cache_read": 0.5,
83
+ "fast": {
84
+ "input": 10,
85
+ "output": 50,
86
+ "cache_write_5m": 12.5,
87
+ "cache_write_1h": 20,
88
+ "cache_read": 1
89
+ },
90
+ "standard_rate_max_input_tokens": null,
91
+ "source_url": "https://platform.claude.com/docs/en/about-claude/pricing",
92
+ "verified_on": "2026-09-13"
93
+ },
94
+ {
95
+ "model_id": "claude-opus-4-8",
96
+ "effective_from": "0000-01-01",
97
+ "input": 5,
98
+ "output": 25,
99
+ "cache_write_5m": 6.25,
100
+ "cache_write_1h": 10,
101
+ "cache_read": 0.5,
102
+ "fast": {
103
+ "input": 10,
104
+ "output": 50,
105
+ "cache_write_5m": 12.5,
106
+ "cache_write_1h": 20,
107
+ "cache_read": 1
108
+ },
109
+ "standard_rate_max_input_tokens": null,
110
+ "source_url": "https://platform.claude.com/docs/en/about-claude/pricing",
111
+ "verified_on": "2026-09-13"
112
+ },
113
+ {
114
+ "model_id": "claude-opus-4-7",
115
+ "effective_from": "0000-01-01",
116
+ "input": 5,
117
+ "output": 25,
118
+ "cache_write_5m": 6.25,
119
+ "cache_write_1h": 10,
120
+ "cache_read": 0.5,
121
+ "fast": null,
122
+ "standard_rate_max_input_tokens": null,
123
+ "source_url": "https://platform.claude.com/docs/en/about-claude/pricing",
124
+ "verified_on": "2026-09-13"
125
+ },
126
+ {
127
+ "model_id": "claude-opus-4-6",
128
+ "effective_from": "0000-01-01",
129
+ "input": 5,
130
+ "output": 25,
131
+ "cache_write_5m": 6.25,
132
+ "cache_write_1h": 10,
133
+ "cache_read": 0.5,
134
+ "fast": null,
135
+ "standard_rate_max_input_tokens": null,
136
+ "source_url": "https://platform.claude.com/docs/en/about-claude/pricing",
137
+ "verified_on": "2026-09-13"
138
+ },
139
+ {
140
+ "model_id": "claude-opus-4-5",
141
+ "effective_from": "0000-01-01",
142
+ "input": 5,
143
+ "output": 25,
144
+ "cache_write_5m": 6.25,
145
+ "cache_write_1h": 10,
146
+ "cache_read": 0.5,
147
+ "fast": null,
148
+ "standard_rate_max_input_tokens": 200000,
149
+ "source_url": "https://platform.claude.com/docs/en/about-claude/pricing",
150
+ "verified_on": "2026-09-24"
151
+ },
152
+ {
153
+ "model_id": "claude-opus-4-5-20251101",
154
+ "effective_from": "0000-01-01",
155
+ "input": 5,
156
+ "output": 25,
157
+ "cache_write_5m": 6.25,
158
+ "cache_write_1h": 10,
159
+ "cache_read": 0.5,
160
+ "fast": null,
161
+ "standard_rate_max_input_tokens": 200000,
162
+ "source_url": "https://platform.claude.com/docs/en/about-claude/pricing",
163
+ "verified_on": "2026-09-24"
164
+ },
165
+ {
166
+ "model_id": "claude-opus-4-1",
167
+ "effective_from": "0000-01-01",
168
+ "input": 15,
169
+ "output": 75,
170
+ "cache_write_5m": 18.75,
171
+ "cache_write_1h": 30,
172
+ "cache_read": 1.5,
173
+ "fast": null,
174
+ "standard_rate_max_input_tokens": 200000,
175
+ "source_url": "https://platform.claude.com/docs/en/about-claude/pricing",
176
+ "verified_on": "2026-09-24"
177
+ },
178
+ {
179
+ "model_id": "claude-opus-4-1-20250805",
180
+ "effective_from": "0000-01-01",
181
+ "input": 15,
182
+ "output": 75,
183
+ "cache_write_5m": 18.75,
184
+ "cache_write_1h": 30,
185
+ "cache_read": 1.5,
186
+ "fast": null,
187
+ "standard_rate_max_input_tokens": 200000,
188
+ "source_url": "https://platform.claude.com/docs/en/about-claude/pricing",
189
+ "verified_on": "2026-09-24"
190
+ },
191
+ {
192
+ "model_id": "claude-opus-4-0",
193
+ "effective_from": "0000-01-01",
194
+ "input": 15,
195
+ "output": 75,
196
+ "cache_write_5m": 18.75,
197
+ "cache_write_1h": 30,
198
+ "cache_read": 1.5,
199
+ "fast": null,
200
+ "standard_rate_max_input_tokens": 200000,
201
+ "source_url": "https://platform.claude.com/docs/en/about-claude/pricing",
202
+ "verified_on": "2026-09-24"
203
+ },
204
+ {
205
+ "model_id": "claude-opus-4-20250514",
206
+ "effective_from": "0000-01-01",
207
+ "input": 15,
208
+ "output": 75,
209
+ "cache_write_5m": 18.75,
210
+ "cache_write_1h": 30,
211
+ "cache_read": 1.5,
212
+ "fast": null,
213
+ "standard_rate_max_input_tokens": 200000,
214
+ "source_url": "https://platform.claude.com/docs/en/about-claude/pricing",
215
+ "verified_on": "2026-09-24"
216
+ },
217
+ {
218
+ "model_id": "claude-sonnet-5-5",
219
+ "effective_from": "0000-01-01",
220
+ "input": 2,
221
+ "output": 10,
222
+ "cache_write_5m": 2.5,
223
+ "cache_write_1h": 4,
224
+ "cache_read": 0.2,
225
+ "fast": null,
226
+ "standard_rate_max_input_tokens": null,
227
+ "source_url": "https://platform.claude.com/docs/en/about-claude/pricing",
228
+ "verified_on": "2026-10-04"
229
+ },
230
+ {
231
+ "model_id": "claude-sonnet-5",
232
+ "effective_from": "0000-01-01",
233
+ "input": 2,
234
+ "output": 10,
235
+ "cache_write_5m": 2.5,
236
+ "cache_write_1h": 4,
237
+ "cache_read": 0.2,
238
+ "fast": null,
239
+ "standard_rate_max_input_tokens": null,
240
+ "source_url": "https://platform.claude.com/docs/en/about-claude/pricing",
241
+ "verified_on": "2026-09-13"
242
+ },
243
+ {
244
+ "model_id": "claude-sonnet-4-6",
245
+ "effective_from": "0000-01-01",
246
+ "input": 3,
247
+ "output": 15,
248
+ "cache_write_5m": 3.75,
249
+ "cache_write_1h": 6,
250
+ "cache_read": 0.3,
251
+ "fast": null,
252
+ "standard_rate_max_input_tokens": null,
253
+ "source_url": "https://platform.claude.com/docs/en/about-claude/pricing",
254
+ "verified_on": "2026-09-13"
255
+ },
256
+ {
257
+ "model_id": "claude-sonnet-4-5",
258
+ "effective_from": "0000-01-01",
259
+ "input": 3,
260
+ "output": 15,
261
+ "cache_write_5m": 3.75,
262
+ "cache_write_1h": 6,
263
+ "cache_read": 0.3,
264
+ "fast": null,
265
+ "standard_rate_max_input_tokens": 200000,
266
+ "source_url": "https://platform.claude.com/docs/en/about-claude/pricing",
267
+ "verified_on": "2026-09-24"
268
+ },
269
+ {
270
+ "model_id": "claude-sonnet-4-5-20250929",
271
+ "effective_from": "0000-01-01",
272
+ "input": 3,
273
+ "output": 15,
274
+ "cache_write_5m": 3.75,
275
+ "cache_write_1h": 6,
276
+ "cache_read": 0.3,
277
+ "fast": null,
278
+ "standard_rate_max_input_tokens": 200000,
279
+ "source_url": "https://platform.claude.com/docs/en/about-claude/pricing",
280
+ "verified_on": "2026-09-24"
281
+ },
282
+ {
283
+ "model_id": "claude-sonnet-4-0",
284
+ "effective_from": "0000-01-01",
285
+ "input": 3,
286
+ "output": 15,
287
+ "cache_write_5m": 3.75,
288
+ "cache_write_1h": 6,
289
+ "cache_read": 0.3,
290
+ "fast": null,
291
+ "standard_rate_max_input_tokens": 200000,
292
+ "source_url": "https://platform.claude.com/docs/en/about-claude/pricing",
293
+ "verified_on": "2026-09-24"
294
+ },
295
+ {
296
+ "model_id": "claude-sonnet-4-20250514",
297
+ "effective_from": "0000-01-01",
298
+ "input": 3,
299
+ "output": 15,
300
+ "cache_write_5m": 3.75,
301
+ "cache_write_1h": 6,
302
+ "cache_read": 0.3,
303
+ "fast": null,
304
+ "standard_rate_max_input_tokens": 200000,
305
+ "source_url": "https://platform.claude.com/docs/en/about-claude/pricing",
306
+ "verified_on": "2026-09-24"
307
+ },
308
+ {
309
+ "model_id": "claude-haiku-4-5-20251001",
310
+ "effective_from": "0000-01-01",
311
+ "input": 1,
312
+ "output": 5,
313
+ "cache_write_5m": 1.25,
314
+ "cache_write_1h": 2,
315
+ "cache_read": 0.1,
316
+ "fast": null,
317
+ "standard_rate_max_input_tokens": 200000,
318
+ "source_url": "https://platform.claude.com/docs/en/about-claude/pricing",
319
+ "verified_on": "2026-09-13"
320
+ },
321
+ {
322
+ "model_id": "claude-haiku-4-5",
323
+ "effective_from": "0000-01-01",
324
+ "input": 1,
325
+ "output": 5,
326
+ "cache_write_5m": 1.25,
327
+ "cache_write_1h": 2,
328
+ "cache_read": 0.1,
329
+ "fast": null,
330
+ "standard_rate_max_input_tokens": 200000,
331
+ "source_url": "https://platform.claude.com/docs/en/about-claude/pricing",
332
+ "verified_on": "2026-09-13"
333
+ },
334
+ {
335
+ "model_id": "claude-3-5-haiku-20241022",
336
+ "effective_from": "0000-01-01",
337
+ "input": 0.8,
338
+ "output": 4,
339
+ "cache_write_5m": 1,
340
+ "cache_write_1h": 1.6,
341
+ "cache_read": 0.08,
342
+ "fast": null,
343
+ "standard_rate_max_input_tokens": 200000,
344
+ "source_url": "https://platform.claude.com/docs/en/about-claude/pricing",
345
+ "verified_on": "2026-09-24"
346
+ }
347
+ ]
348
+ }
@@ -0,0 +1,57 @@
1
+ -- 001_ingestion: raw storage for session logs and the status line spool.
2
+ --
3
+ -- Implements D-002 (store every raw line; everything else is derived and rebuildable) and D-003
4
+ -- (incremental reading by byte offset of the last complete line). Rows in raw_lines are never
5
+ -- updated or deleted: a log file rewritten by Claude Code keeps its earlier lines here, because
6
+ -- those requests happened (fixtures/README.md, cumulative runs).
7
+ --
8
+ -- Column comments: "source:" names the log, payload, or filesystem value a column holds;
9
+ -- "derived:" marks values computed by ingestion.
10
+
11
+ CREATE TABLE ingest_runs (
12
+ id INTEGER PRIMARY KEY, -- derived: run identity, increasing
13
+ started_at TEXT NOT NULL, -- derived: ISO-8601 UTC time the run began
14
+ finished_at TEXT, -- derived: ISO-8601 UTC time the run ended; NULL while running or after a crash
15
+ mode TEXT NOT NULL, -- derived: 'incremental' reads from stored offsets; 'full' rereads every file
16
+ CHECK (mode IN ('incremental', 'full'))
17
+ ) STRICT;
18
+
19
+ CREATE TABLE source_files (
20
+ id INTEGER PRIMARY KEY, -- derived: file identity
21
+ kind TEXT NOT NULL, -- derived: 'log' for session JSONL, 'spool' for the status line spool
22
+ root TEXT NOT NULL, -- source: discovery root (a CLAUDE_CONFIG_DIR entry, ~/.claude, or the data dir)
23
+ relative_path TEXT NOT NULL, -- source: path below the root, e.g. projects/<encoded>/<session>.jsonl
24
+ byte_offset INTEGER NOT NULL, -- derived: position just past the last complete line consumed (D-003)
25
+ size INTEGER NOT NULL, -- source: file size in bytes at the last read (stat)
26
+ inode INTEGER, -- source: inode at the last read (stat); NULL where the platform has none
27
+ first_run_id INTEGER NOT NULL REFERENCES ingest_runs (id), -- derived: run that first saw the file
28
+ last_run_id INTEGER NOT NULL REFERENCES ingest_runs (id), -- derived: run that last read the file
29
+ UNIQUE (root, relative_path),
30
+ CHECK (kind IN ('log', 'spool')),
31
+ CHECK (byte_offset >= 0 AND byte_offset <= size)
32
+ ) STRICT;
33
+
34
+ CREATE TABLE raw_lines (
35
+ id INTEGER PRIMARY KEY, -- derived: row identity; not an ordering (use file, first run, line)
36
+ source_file_id INTEGER NOT NULL REFERENCES source_files (id), -- derived: file the line came from
37
+ line_number INTEGER NOT NULL, -- source: 1-based line number where the line was first seen
38
+ byte_offset INTEGER NOT NULL, -- source: byte position of the line's first byte when first seen
39
+ content_hash TEXT NOT NULL, -- derived: SHA-256 of bytes, lowercase hex; with the file, the line's identity
40
+ bytes BLOB NOT NULL, -- source: the exact line bytes, without the terminating newline
41
+ first_run_id INTEGER NOT NULL REFERENCES ingest_runs (id), -- derived: run that first stored the line
42
+ UNIQUE (source_file_id, content_hash),
43
+ CHECK (line_number >= 1),
44
+ CHECK (byte_offset >= 0),
45
+ CHECK (length(content_hash) = 64)
46
+ ) STRICT;
47
+
48
+ -- The canonical order every derived list uses: file, then first run, then line.
49
+ CREATE INDEX raw_lines_order ON raw_lines (source_file_id, first_run_id, line_number);
50
+
51
+ CREATE TABLE ingest_report (
52
+ id INTEGER PRIMARY KEY, -- derived: report row identity
53
+ run_id INTEGER NOT NULL REFERENCES ingest_runs (id), -- derived: run that found the problem
54
+ source_file_id INTEGER REFERENCES source_files (id), -- derived: file involved; NULL for run-level problems
55
+ problem TEXT NOT NULL, -- derived: problem code, e.g. 'file_rewritten', 'file_unreadable'
56
+ detail TEXT -- derived: human-readable detail; never a full log line
57
+ ) STRICT;
@@ -0,0 +1,7 @@
1
+ -- 002_line_count: remember how many complete lines precede each file's stored byte offset.
2
+ --
3
+ -- Incremental reads start mid-file (D-003). Without this count, the first new line couldn't be
4
+ -- given its true line number, and fixtures/README.md defines a line's number as where it was
5
+ -- first seen. Reset to 0 together with byte_offset when a rewrite is detected.
6
+
7
+ ALTER TABLE source_files ADD COLUMN line_count INTEGER NOT NULL DEFAULT 0 CHECK (line_count >= 0); -- derived: complete lines before byte_offset
@@ -0,0 +1,70 @@
1
+ -- 003_derived: tables derived from raw_lines by core/ingest/derive.ts.
2
+ --
3
+ -- Everything here is rebuildable (D-002): deleting these rows and deriving again from raw_lines
4
+ -- must produce identical contents. derive_meta records the parser version that produced them, so
5
+ -- a parser change triggers a rebuild instead of mixing old and new interpretations.
6
+ --
7
+ -- Classification and field rules: fixtures/README.md (D-001, D-004, D-007, D-019).
8
+ -- The token tables are not STRICT on purpose: a JSON number that isn't an integer is stored as
9
+ -- written rather than rejected, matching "a token value counts if it's a JSON number".
10
+
11
+ CREATE TABLE derive_meta (
12
+ key TEXT PRIMARY KEY, -- derived: setting name, e.g. 'parser_version'
13
+ value TEXT NOT NULL -- derived: setting value
14
+ ) STRICT;
15
+
16
+ CREATE TABLE parsed_lines (
17
+ raw_line_id INTEGER PRIMARY KEY REFERENCES raw_lines (id), -- derived: the line these fields came from
18
+ class TEXT NOT NULL, -- derived: classification, first matching rule wins
19
+ session_id TEXT, -- source: sessionId ('<missing>' if absent or not a string); NULL when malformed
20
+ type TEXT, -- source: type ('<missing>' if absent or not a string); NULL when malformed
21
+ subtype TEXT, -- source: subtype when it's a string, else NULL
22
+ timestamp_raw TEXT, -- source: timestamp as JSON text of its value; NULL when absent or malformed
23
+ timestamp_utc TEXT, -- derived: timestamp normalized to UTC with milliseconds; NULL when it doesn't parse
24
+ cwd TEXT, -- source: cwd when it's a string, else NULL
25
+ git_branch TEXT, -- source: gitBranch when it's a string, else NULL
26
+ cc_version TEXT, -- source: version (Claude Code version) when it's a string, else NULL
27
+ is_sidechain INTEGER NOT NULL, -- source: isSidechain === true (1) or anything else (0)
28
+ CHECK (class IN ('malformed', 'limit_hit', 'api_error', 'synthetic_other', 'request', 'retry_notice', 'ignored_type')),
29
+ CHECK (is_sidechain IN (0, 1))
30
+ ) STRICT;
31
+
32
+ CREATE TABLE requests (
33
+ raw_line_id INTEGER PRIMARY KEY REFERENCES raw_lines (id), -- derived: the request line
34
+ dedup_key TEXT, -- derived: D-001 key; NULL when the line has neither ID (unkeyed)
35
+ message_id TEXT, -- source: message.id when it's a string
36
+ request_id TEXT, -- source: requestId when it's a string
37
+ model TEXT NOT NULL, -- source: message.model ('<missing>' if absent or not a string)
38
+ input_tokens NUMERIC NOT NULL, -- source: message.usage.input_tokens (0 when not a number)
39
+ output_tokens NUMERIC NOT NULL, -- source: message.usage.output_tokens (0 when not a number)
40
+ cache_read_tokens NUMERIC NOT NULL, -- source: message.usage.cache_read_input_tokens (0 when not a number)
41
+ cache_write_5m_tokens NUMERIC, -- source: message.usage.cache_creation.ephemeral_5m_input_tokens; NULL without the split object
42
+ cache_write_1h_tokens NUMERIC, -- source: message.usage.cache_creation.ephemeral_1h_input_tokens; NULL without the split object
43
+ cache_write_unsplit_tokens NUMERIC, -- source: message.usage.cache_creation_input_tokens; only when the split object is absent
44
+ speed TEXT -- source: message.usage.speed when it's a string
45
+ );
46
+
47
+ -- Requests grouped for dedup: each key's candidates are read together.
48
+ CREATE INDEX requests_dedup_key ON requests (dedup_key);
49
+
50
+ CREATE TABLE events (
51
+ raw_line_id INTEGER PRIMARY KEY REFERENCES raw_lines (id), -- derived: the event line
52
+ class TEXT NOT NULL, -- derived: 'limit_hit', 'api_error', 'synthetic_other', or 'retry_notice'
53
+ error TEXT, -- source: error when it's a string
54
+ api_error_status NUMERIC, -- source: apiErrorStatus, or error.status for retry notices
55
+ window TEXT, -- derived: 'five_hour' or 'seven_day' parsed from limit-hit text (D-004)
56
+ reset_text TEXT, -- derived: text after 'resets ' in limit-hit text (D-004)
57
+ unknown_error_key TEXT, -- derived: report label for an api_error's unrecognized or missing error
58
+ retry_rate_limits_present INTEGER NOT NULL, -- derived: 1 when a retry notice's error.rateLimits is present and not null (D-019)
59
+ CHECK (class IN ('limit_hit', 'api_error', 'synthetic_other', 'retry_notice'))
60
+ );
61
+
62
+ CREATE TABLE line_problems (
63
+ id INTEGER PRIMARY KEY, -- derived: row identity
64
+ raw_line_id INTEGER NOT NULL REFERENCES raw_lines (id), -- derived: the line with the problem
65
+ problem TEXT NOT NULL, -- derived: 'missing_field', 'unparsed_timestamp', or 'non_message_iteration'
66
+ detail TEXT, -- derived: the field name, raw timestamp JSON, or iteration type
67
+ CHECK (problem IN ('missing_field', 'unparsed_timestamp', 'non_message_iteration'))
68
+ ) STRICT;
69
+
70
+ CREATE INDEX line_problems_line ON line_problems (raw_line_id);
@@ -0,0 +1,50 @@
1
+ -- 004_requests_dedup: the deduplicated request set, as a view over derived rows (D-001).
2
+ --
3
+ -- One API response is written as several cumulative log lines. Each dedup key keeps one winner:
4
+ -- largest output_tokens, then the non-sidechain copy, then the later line in (file, first run, line)
5
+ -- order. Unkeyed requests (neither message.id nor requestId) pass through as themselves.
6
+ -- Rows carry their source position so every number built on this view can be traced to a line.
7
+
8
+ -- Observed: one row per distinct API response (winners) plus every unkeyed request line.
9
+ CREATE VIEW requests_dedup AS
10
+ WITH ranked AS (
11
+ SELECT
12
+ r.raw_line_id,
13
+ r.dedup_key,
14
+ r.message_id,
15
+ r.request_id,
16
+ r.model,
17
+ r.input_tokens,
18
+ r.output_tokens,
19
+ r.cache_read_tokens,
20
+ r.cache_write_5m_tokens,
21
+ r.cache_write_1h_tokens,
22
+ r.cache_write_unsplit_tokens,
23
+ r.speed,
24
+ p.session_id,
25
+ p.timestamp_raw,
26
+ p.timestamp_utc,
27
+ p.cwd,
28
+ p.is_sidechain,
29
+ f.root,
30
+ f.relative_path,
31
+ l.first_run_id,
32
+ l.line_number,
33
+ ROW_NUMBER() OVER (
34
+ PARTITION BY r.dedup_key
35
+ ORDER BY r.output_tokens DESC, p.is_sidechain ASC,
36
+ f.root DESC, f.relative_path DESC, l.first_run_id DESC, l.line_number DESC
37
+ ) AS candidate_rank
38
+ FROM requests r
39
+ JOIN parsed_lines p ON p.raw_line_id = r.raw_line_id
40
+ JOIN raw_lines l ON l.id = r.raw_line_id
41
+ JOIN source_files f ON f.id = l.source_file_id
42
+ )
43
+ SELECT
44
+ raw_line_id, dedup_key, message_id, request_id, model,
45
+ input_tokens, output_tokens, cache_read_tokens,
46
+ cache_write_5m_tokens, cache_write_1h_tokens, cache_write_unsplit_tokens, speed,
47
+ session_id, timestamp_raw, timestamp_utc, cwd, is_sidechain,
48
+ root, relative_path, first_run_id, line_number
49
+ FROM ranked
50
+ WHERE dedup_key IS NULL OR candidate_rank = 1;
@@ -0,0 +1,25 @@
1
+ -- 005_repositories: which repository each working directory belongs to (D-010).
2
+ --
3
+ -- Filled by core/ingest/attribution.ts, once per distinct cwd, using git's common directory, so
4
+ -- worktrees and subdirectories of one repository share a repo_root. Unlike the derived tables in
5
+ -- 003, this depends on the filesystem at resolution time (a directory can disappear later), so it
6
+ -- is a cache and is not part of the rebuild-identical set.
7
+
8
+ CREATE TABLE repositories (
9
+ cwd TEXT PRIMARY KEY, -- source: cwd from log lines
10
+ kind TEXT NOT NULL, -- derived: 'repo', 'not_git' (git found no repository), or 'missing' (path gone)
11
+ repo_root TEXT, -- derived: parent of git's common dir (the dir itself for bare repos); NULL unless kind = 'repo'
12
+ resolved_at TEXT NOT NULL, -- derived: ISO-8601 UTC time of resolution
13
+ CHECK (kind IN ('repo', 'not_git', 'missing')),
14
+ CHECK ((kind = 'repo') = (repo_root IS NOT NULL))
15
+ ) STRICT;
16
+
17
+ -- Observed: each request's repository, or its working directory when it has none.
18
+ CREATE VIEW request_repositories AS
19
+ SELECT
20
+ d.raw_line_id,
21
+ d.cwd,
22
+ COALESCE(r.kind, 'unresolved') AS repo_kind,
23
+ COALESCE(r.repo_root, d.cwd) AS repository
24
+ FROM requests_dedup d
25
+ LEFT JOIN repositories r ON r.cwd = d.cwd;
@@ -0,0 +1,33 @@
1
+ -- 006_status_readings: status line readings decoded from the hook's spool (D-008, D-009, D-018).
2
+ --
3
+ -- Spool lines are stored in raw_lines like log lines (source_files.kind = 'spool') and derived
4
+ -- here instead of into parsed_lines. Values are stored as written and flagged, never clamped: an
5
+ -- out-of-range percentage is evidence of a payload problem, not something to repair.
6
+ -- Rebuildable from raw_lines, like the tables in 003.
7
+
8
+ CREATE TABLE status_readings (
9
+ raw_line_id INTEGER PRIMARY KEY REFERENCES raw_lines (id), -- derived: the spool line
10
+ status TEXT NOT NULL, -- derived: 'ok', 'malformed_line', 'malformed_payload_encoding', or 'malformed_payload'
11
+ captured_at_s NUMERIC, -- source: spool captured_at_s (epoch seconds, from the hook's clock)
12
+ hook_version NUMERIC, -- source: spool hook_version
13
+ session_id TEXT, -- source: payload session_id
14
+ transcript_path TEXT, -- source: payload transcript_path; links the reading to its session log
15
+ model_id TEXT, -- source: payload model.id
16
+ cc_version TEXT, -- source: payload version (Claude Code version)
17
+ cost_total_usd NUMERIC, -- source: payload cost.total_cost_usd; a cross-check only, never a source (CLAUDE.md)
18
+ has_rate_limits INTEGER NOT NULL, -- derived: 1 when payload rate_limits is an object
19
+ CHECK (status IN ('ok', 'malformed_line', 'malformed_payload_encoding', 'malformed_payload')),
20
+ CHECK (has_rate_limits IN (0, 1))
21
+ );
22
+
23
+ CREATE TABLE rate_limit_windows (
24
+ raw_line_id INTEGER NOT NULL REFERENCES status_readings (raw_line_id), -- derived: the reading
25
+ window TEXT NOT NULL, -- source: key under payload rate_limits, e.g. 'five_hour'
26
+ used_percentage NUMERIC, -- source: rate_limits.<window>.used_percentage when it's a number
27
+ resets_at NUMERIC, -- source: rate_limits.<window>.resets_at when it's a number (epoch seconds)
28
+ validity TEXT NOT NULL, -- derived: 'valid' or the first problem found
29
+ PRIMARY KEY (raw_line_id, window),
30
+ CHECK (validity IN ('valid', 'invalid_percentage', 'invalid_epoch_in_percentage', 'invalid_resets_at', 'unknown_window'))
31
+ );
32
+
33
+ CREATE INDEX rate_limit_windows_window ON rate_limit_windows (window, validity);