Skip to content

Commit 6465d4a

Browse files
claude-code-chat-browser: schema drift detection for upstream JSONL c… (#108)
* claude-code-chat-browser: schema drift detection for upstream JSONL changes (#5) Fingerprint known Claude Code JSONL field paths against a committed schema_baseline.json, warn on drift during parsing, expose GET /api/schema-report, and surface a dismissible amber banner on the session list page. Warnings only - parsing is never blocked. * fix(schema-drift): harden drift tracking and fix banner fetch order (#108) Cache schema_baseline.json with lru_cache and make record_parse_drift non-fatal on baseline I/O or parse errors so parsing never aborts. Fetch /api/schema-report after sessions load so the banner reflects drift from the current parse run. Add vitest coverage for banner rendering and fetch ordering; extend pytest for malformed baseline. Raise benchmark baselines for per-entry field-path fingerprinting. * Address timon's feedback * fix(schema-drift): atomic drift merge, unlimited sample mode, baseline root check (#108) * fix(schema-drift): remove unused expected_type from baseline (#108)
1 parent 4345d69 commit 6465d4a

11 files changed

Lines changed: 882 additions & 8 deletions

File tree

api/schema_report.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
"""Schema drift report endpoint."""
2+
3+
from flask import Blueprint
4+
5+
from api._flask_types import FlaskReturn, json_response
6+
from utils.schema_drift import get_schema_report
7+
8+
schema_report_bp = Blueprint("schema_report", __name__)
9+
10+
11+
@schema_report_bp.route("/api/schema-report")
12+
def schema_report() -> FlaskReturn:
13+
"""Return known/new/missing JSONL field paths from recent parse runs."""
14+
return json_response(get_schema_report())

app.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
from api.export_api import export_bp
1111
from api.projects import projects_bp
12+
from api.schema_report import schema_report_bp
1213
from api.search import search_bp
1314
from api.sessions import sessions_bp
1415
from utils.exclusion_rules import load_rules, resolve_exclusion_rules_path
@@ -101,6 +102,7 @@ def create_app(
101102
app.register_blueprint(sessions_bp)
102103
app.register_blueprint(search_bp)
103104
app.register_blueprint(export_bp)
105+
app.register_blueprint(schema_report_bp)
104106

105107
@app.after_request
106108
def set_security_headers(response):

benchmarks/baselines.json

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,18 @@
11
{
2-
"_note": "Gated means from ubuntu-latest CI benchmark-results.json (PR #97, run 28126772276). Excluded from gate (recorded for reference): test_parse_session_small, test_search_full_corpus (sub-ms CI noise). Memory benchmarks use extra_info.peak_bytes (bytes); latency uses stats.mean (seconds).",
3-
"updated": "2026-06-24T20:15:37Z",
2+
"_note": "Gated means from ubuntu-latest CI benchmark-results.json. PR #108 (schema drift): parse/export latency baselines raised for per-entry field-path fingerprinting. Excluded from gate (recorded for reference): test_parse_session_small, test_search_full_corpus (sub-ms CI noise). Memory benchmarks use extra_info.peak_bytes (bytes); latency uses stats.mean (seconds).",
3+
"updated": "2026-07-03T00:00:00Z",
44
"machine": "Linux",
55
"groups": {
66
"parse": {
77
"test_parse_session_small": 0.00010518068718225604,
8-
"test_parse_session_medium": 0.002991333112179635,
9-
"test_parse_session_large": 0.032311203818181436,
8+
"test_parse_session_medium": 0.004645,
9+
"test_parse_session_large": 0.045401,
1010
"test_parse_large_peak_memory": 2032028.0
1111
},
1212
"export": {
13-
"test_bulk_export_session_count[sessions-10]": 0.0042825538530803925,
14-
"test_bulk_export_session_count[sessions-50]": 0.021406330209302382,
15-
"test_bulk_export_session_count[sessions-100]": 0.04229194749999898,
13+
"test_bulk_export_session_count[sessions-10]": 0.006504,
14+
"test_bulk_export_session_count[sessions-50]": 0.032314,
15+
"test_bulk_export_session_count[sessions-100]": 0.064562,
1616
"test_bulk_export_zip_peak_memory[sessions-10]": 350628.0,
1717
"test_bulk_export_zip_peak_memory[sessions-50]": 506454.0,
1818
"test_bulk_export_zip_peak_memory[sessions-100]": 694088.0

schema_baseline.json

Lines changed: 300 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,300 @@
1+
{
2+
"version": 1,
3+
"description": "Known Claude Code JSONL field paths as of 2026-07-03. Drift detection compares path presence only (added/removed paths); type changes are not detected. Mark required:true only for paths present on every record (currently: type). Update deliberately when upstream format changes.",
4+
"fields": {
5+
"_futureSchemaVersion": {
6+
"required": false
7+
},
8+
"compactMetadata": {
9+
"required": false
10+
},
11+
"compactMetadata.preTokens": {
12+
"required": false
13+
},
14+
"compactMetadata.trigger": {
15+
"required": false
16+
},
17+
"content": {
18+
"required": false
19+
},
20+
"cwd": {
21+
"required": false
22+
},
23+
"data": {
24+
"required": false
25+
},
26+
"data.output": {
27+
"required": false
28+
},
29+
"data.type": {
30+
"required": false
31+
},
32+
"experimentalFlag": {
33+
"required": false
34+
},
35+
"gitBranch": {
36+
"required": false
37+
},
38+
"isApiErrorMessage": {
39+
"required": false
40+
},
41+
"isSidechain": {
42+
"required": false
43+
},
44+
"message": {
45+
"required": false
46+
},
47+
"message.content": {
48+
"required": false
49+
},
50+
"message.content[]": {
51+
"required": false
52+
},
53+
"message.content[].id": {
54+
"required": false
55+
},
56+
"message.content[].input": {
57+
"required": false
58+
},
59+
"message.content[].input.command": {
60+
"required": false
61+
},
62+
"message.content[].input.description": {
63+
"required": false
64+
},
65+
"message.content[].input.file_path": {
66+
"required": false
67+
},
68+
"message.content[].name": {
69+
"required": false
70+
},
71+
"message.content[].text": {
72+
"required": false
73+
},
74+
"message.content[].thinking": {
75+
"required": false
76+
},
77+
"message.content[].type": {
78+
"required": false
79+
},
80+
"message.model": {
81+
"required": false
82+
},
83+
"message.stop_reason": {
84+
"required": false
85+
},
86+
"message.usage": {
87+
"required": false
88+
},
89+
"message.usage.cache_creation": {
90+
"required": false
91+
},
92+
"message.usage.cache_creation.ephemeral_1h_input_tokens": {
93+
"required": false
94+
},
95+
"message.usage.cache_creation.ephemeral_5m_input_tokens": {
96+
"required": false
97+
},
98+
"message.usage.cache_creation_input_tokens": {
99+
"required": false
100+
},
101+
"message.usage.cache_read_input_tokens": {
102+
"required": false
103+
},
104+
"message.usage.input_tokens": {
105+
"required": false
106+
},
107+
"message.usage.output_tokens": {
108+
"required": false
109+
},
110+
"message.usage.service_tier": {
111+
"required": false
112+
},
113+
"parentToolUseID": {
114+
"required": false
115+
},
116+
"parentUuid": {
117+
"required": false
118+
},
119+
"permissionMode": {
120+
"required": false
121+
},
122+
"sessionId": {
123+
"required": false
124+
},
125+
"slug": {
126+
"required": false
127+
},
128+
"snapshot": {
129+
"required": false
130+
},
131+
"snapshot.timestamp": {
132+
"required": false
133+
},
134+
"subtype": {
135+
"required": false
136+
},
137+
"timestamp": {
138+
"required": false
139+
},
140+
"toolUseID": {
141+
"required": false
142+
},
143+
"toolUseResult": {
144+
"required": false
145+
},
146+
"toolUseResult._unknownToolMeta": {
147+
"required": false
148+
},
149+
"toolUseResult._unknownToolMeta.vendor": {
150+
"required": false
151+
},
152+
"toolUseResult.agentId": {
153+
"required": false
154+
},
155+
"toolUseResult.answers": {
156+
"required": false
157+
},
158+
"toolUseResult.answers.q1": {
159+
"required": false
160+
},
161+
"toolUseResult.code": {
162+
"required": false
163+
},
164+
"toolUseResult.content": {
165+
"required": false
166+
},
167+
"toolUseResult.description": {
168+
"required": false
169+
},
170+
"toolUseResult.durationMs": {
171+
"required": false
172+
},
173+
"toolUseResult.exitCode": {
174+
"required": false
175+
},
176+
"toolUseResult.file": {
177+
"required": false
178+
},
179+
"toolUseResult.file.content": {
180+
"required": false
181+
},
182+
"toolUseResult.file.filePath": {
183+
"required": false
184+
},
185+
"toolUseResult.file.numLines": {
186+
"required": false
187+
},
188+
"toolUseResult.filePath": {
189+
"required": false
190+
},
191+
"toolUseResult.filenames": {
192+
"required": false
193+
},
194+
"toolUseResult.filenames[]": {
195+
"required": false
196+
},
197+
"toolUseResult.isAsync": {
198+
"required": false
199+
},
200+
"toolUseResult.message": {
201+
"required": false
202+
},
203+
"toolUseResult.mode": {
204+
"required": false
205+
},
206+
"toolUseResult.newTodos": {
207+
"required": false
208+
},
209+
"toolUseResult.newTodos[]": {
210+
"required": false
211+
},
212+
"toolUseResult.newTodos[].content": {
213+
"required": false
214+
},
215+
"toolUseResult.newTodos[].id": {
216+
"required": false
217+
},
218+
"toolUseResult.numFiles": {
219+
"required": false
220+
},
221+
"toolUseResult.numLines": {
222+
"required": false
223+
},
224+
"toolUseResult.plan": {
225+
"required": false
226+
},
227+
"toolUseResult.plan[]": {
228+
"required": false
229+
},
230+
"toolUseResult.query": {
231+
"required": false
232+
},
233+
"toolUseResult.questions": {
234+
"required": false
235+
},
236+
"toolUseResult.questions[]": {
237+
"required": false
238+
},
239+
"toolUseResult.questions[].id": {
240+
"required": false
241+
},
242+
"toolUseResult.results": {
243+
"required": false
244+
},
245+
"toolUseResult.results[]": {
246+
"required": false
247+
},
248+
"toolUseResult.results[].url": {
249+
"required": false
250+
},
251+
"toolUseResult.retrieval_status": {
252+
"required": false
253+
},
254+
"toolUseResult.status": {
255+
"required": false
256+
},
257+
"toolUseResult.stderr": {
258+
"required": false
259+
},
260+
"toolUseResult.stdout": {
261+
"required": false
262+
},
263+
"toolUseResult.structuredPatch": {
264+
"required": false
265+
},
266+
"toolUseResult.task": {
267+
"required": false
268+
},
269+
"toolUseResult.task.description": {
270+
"required": false
271+
},
272+
"toolUseResult.task.task_id": {
273+
"required": false
274+
},
275+
"toolUseResult.task_id": {
276+
"required": false
277+
},
278+
"toolUseResult.task_type": {
279+
"required": false
280+
},
281+
"toolUseResult.totalDurationMs": {
282+
"required": false
283+
},
284+
"toolUseResult.truncated": {
285+
"required": false
286+
},
287+
"toolUseResult.url": {
288+
"required": false
289+
},
290+
"type": {
291+
"required": true
292+
},
293+
"uuid": {
294+
"required": false
295+
},
296+
"version": {
297+
"required": false
298+
}
299+
}
300+
}

0 commit comments

Comments
 (0)