-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathCODE_REVIEW_ISSUES.json
More file actions
506 lines (506 loc) · 31.5 KB
/
CODE_REVIEW_ISSUES.json
File metadata and controls
506 lines (506 loc) · 31.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
{
"meta": {
"project": "LazyTail",
"review_date": "2026-03-12",
"reviewers": ["Uncle Bob (Robert C. Martin)", "Kent Beck"],
"overall_grade": "B-",
"total_issues": 44
},
"issues": [
{
"id": "C1",
"severity": "critical",
"category": "god-class",
"title": "App struct is a God Class",
"file": "src/app/mod.rs",
"lines": "155-2957",
"description": "App struct has 25+ fields and 2957 lines mixing 6 responsibilities: tab management, input handling, filter control, source panel navigation, UI rendering state, and event dispatching. apply_event() alone is 132 lines with 40+ match arms.",
"task": "Extract InputController (input_buffer, input_cursor, input_mode), FilterController (current_filter_mode, regex_error, query_error, pending_filter_at), SourcePanelController (source_panel, side_panel_width), and TabManager (tabs, active_tab, combined_tabs). Target: App < 500 lines, each controller < 300 lines.",
"status": "done"
},
{
"id": "C2",
"severity": "critical",
"category": "god-class",
"title": "query.rs is a monolithic 1864-line file",
"file": "src/filter/query.rs",
"lines": "1-1864",
"description": "Contains parser, AST types, filter implementation, and validation all in one file. This is 26% of the entire filter module.",
"task": "Split into query/ directory: query/mod.rs (re-exports), query/ast.rs (FilterQuery, Aggregation types), query/parser.rs (text query parsing), query/validate.rs (validation logic). Each file should be 400-500 lines.",
"status": "done"
},
{
"id": "C3",
"severity": "critical",
"category": "god-class",
"title": "MCP tools.rs is a God Object",
"file": "src/mcp/tools.rs",
"lines": "1-2233",
"description": "All 6 MCP tool implementations plus helpers, response stripping, and tests in a single 2233-line file. Three nearly identical ANSI-stripping functions violate DRY.",
"task": "Split into per-tool files: search_impl.rs, lines_impl.rs, stats_impl.rs, render_impl.rs. Extract trait AnsiStrippable for response stripping to eliminate the three duplicate strip_*_response() functions.",
"status": "done"
},
{
"id": "C4",
"severity": "critical",
"category": "bug",
"title": "Theme loading has infinite recursion vulnerability",
"file": "src/theme/loader.rs",
"lines": "66",
"description": "The resolve_named() function accepts a visited: &mut Vec<String> parameter for cycle detection but never checks it before recursing. If a theme references itself via 'base: my-theme-name', infinite recursion and stack overflow occur.",
"task": "Add visited.contains(name) check at the top of resolve_named() before the recursive call to load_theme_file(). Return a ConfigError::CycleDetected if the theme name is already in the visited list.",
"status": "open"
},
{
"id": "H1",
"severity": "high",
"category": "long-method",
"title": "main.rs has multiple oversized functions",
"file": "src/main.rs",
"lines": "118-1191",
"description": "main() is ~258 lines handling 5 concerns. collect_file_events() is ~135 lines. collect_input_events() is ~110 lines. run_app_with_discovery() is ~113 lines. Raw Duration and Instant values scattered as timing state (primitive obsession).",
"task": "Extract main() into setup_cli(), detect_mode(), run_mode(). Create EventLoop or TimeTracker struct to hold timing state (last_status_refresh, last_file_poll). Extract collect_file_events() into smaller focused functions. Target: main() < 50 lines, no function > 80 lines.",
"status": "open"
},
{
"id": "H2",
"severity": "high",
"category": "long-method",
"title": "render_sources_list() is a 246-line God Function with DRY violations",
"file": "src/tui/side_panel.rs",
"lines": "109-354",
"description": "Handles category headers, combined items, regular items, AND overflow overlay. Contains duplicated line-building logic between combined items (lines 181-209) and regular items (lines 261-288) — identical truncation and metadata appending patterns.",
"task": "Extract append_truncated_metadata() helper to eliminate DRY violation. Split into render_category_header(), render_combined_item(), render_regular_item(), render_overflow_overlay(). Target: each function < 60 lines.",
"status": "open"
},
{
"id": "H3",
"severity": "high",
"category": "fragile-code",
"title": "Span index management uses magic numbers",
"file": "src/tui/log_view.rs",
"lines": "414-452",
"description": "apply_expanded_bg() uses hardcoded indices (0, 1, 2, 3) to locate spans built by build_item(). Values like sep_idx = if has_source_tag { 2 } else { 1 } are brittle — if span insertion order changes in build_item(), this breaks silently with no compiler warning.",
"task": "Introduce a SpanRole enum (SourceTag, LineNumber, Separator, Content) or a LineSpans struct that tracks span positions semantically. Replace index arithmetic with role-based lookups in apply_expanded_bg().",
"status": "open"
},
{
"id": "H4",
"severity": "high",
"category": "long-method",
"title": "preset.rs compile() is 175 lines mixing concerns",
"file": "src/renderer/preset.rs",
"lines": "150-325",
"description": "Single function mixes parser resolution, layout entry compilation, and field validation. Multiple nested match statements. High cognitive load and hard to test individual compilation phases.",
"task": "Split compile() into compile_parser(), compile_layout(), and validate_fields(). Each should be independently testable. Target: compile() becomes a 20-line orchestrator calling sub-functions.",
"status": "open"
},
{
"id": "H5",
"severity": "high",
"category": "long-method",
"title": "Web handle_request() is a 348-line monolith",
"file": "src/web/handlers.rs",
"lines": "24-372",
"description": "All HTTP routing in a single match statement with 10+ arms, each with 10-50 lines of nested logic. Changes to one endpoint risk breaking others. Weak error categorization for multi-step operations like source deletion.",
"task": "Extract per-endpoint handler functions: handle_get_api_lines(), handle_post_api_filter(), handle_get_api_events(), etc. The main handle_request() should be a thin router dispatching to these handlers. Target: router < 40 lines, each handler < 60 lines.",
"status": "open"
},
{
"id": "H6",
"severity": "high",
"category": "long-method",
"title": "file_reader::read_line_at() is 102 lines with 4 code paths",
"file": "src/reader/file_reader.rs",
"lines": "268-369",
"description": "Contains 4 separate access strategies (sequential fast path, columnar offsets, columnar tail, sparse index fallback) in a single method. Violates Uncle Bob's rule that methods should be small enough to understand at a glance.",
"task": "Extract into try_sequential_path(), try_columnar_path(), try_columnar_tail_path(), try_sparse_path(). Main method becomes ~20 lines calling each strategy in priority order.",
"status": "open"
},
{
"id": "M1",
"severity": "medium",
"category": "dry-violation",
"title": "Repeated mmap loading pattern in streaming_filter.rs",
"file": "src/filter/streaming_filter.rs",
"lines": "66-661",
"description": "All 5 *_impl() functions repeat identical mmap setup: File::open, metadata check for empty, unsafe Mmap::map. ~15 lines duplicated 5 times.",
"task": "Extract fn load_mmap(path: &Path) -> Result<(Mmap, u64)> that handles open, empty check, and mapping. Use in all 5 impl functions. Saves ~60 lines of duplication.",
"status": "open"
},
{
"id": "M2",
"severity": "medium",
"category": "long-parameter-list",
"title": "stream_filter_range_impl() takes 8 parameters",
"file": "src/filter/streaming_filter.rs",
"lines": "432",
"description": "Parameters: path, filter, start_line, end_line, start_byte_offset, bitmap, tx, cancel. Uncle Bob recommends max 3 parameters.",
"task": "Introduce RangeFilterOptions struct grouping start_line, end_line, start_byte_offset, bitmap. Reduces to 4 params: path, filter, options, cancel (tx moves into options or return type).",
"status": "open"
},
{
"id": "M3",
"severity": "medium",
"category": "long-parameter-list",
"title": "process_filter_shared() takes 7 parameters",
"file": "src/filter/engine.rs",
"lines": "122-129",
"description": "Parameters: reader, filter, tx, batch_size, start_line, end_line, cancel. Also a 106-line method mixing lock acquisition, batch filtering, progress reporting, and synchronization.",
"task": "Extract ProcessFilterOptions { batch_size, start_line, end_line }. Split method into read_batch(), filter_batch(), send_progress(). Target: each sub-method < 30 lines.",
"status": "open"
},
{
"id": "M4",
"severity": "medium",
"category": "tell-dont-ask",
"title": "TUI rendering reaches deep into App/Tab/LogSource chains",
"file": "src/tui/mod.rs",
"lines": "46-54",
"description": "Rendering functions check app.active_tab().source.mode == ViewMode::Aggregation and duplicate tab resolution logic (active_combined vs active_tab). Violates Tell-Don't-Ask principle.",
"task": "Add App accessor methods: current_render_tab() -> &TabState, is_aggregation_active() -> bool. Use these in tui/mod.rs, tui/log_view.rs, and handlers/input.rs instead of reaching into internals.",
"status": "open"
},
{
"id": "M5",
"severity": "medium",
"category": "inconsistent-error-handling",
"title": "Inconsistent error handling patterns across modules",
"file": "multiple",
"lines": "various",
"description": "Five different error handling patterns used inconsistently: silent let _ = send(), silent _ => continue, eprintln + break (capture.rs), poisoned mutex recovery (web/handlers.rs), eprintln + return (watcher/dir.rs). No documented error philosophy.",
"task": "Establish error handling guidelines: (1) propagate via Result at system boundaries, (2) log-and-continue only in background processing with explicit // deliberate: ... comment, (3) never silently recover from poisoned mutex without logging. Add one-line comments to all existing silent-skip patterns.",
"status": "open"
},
{
"id": "M6",
"severity": "medium",
"category": "tech-debt",
"title": "TabState has redundant scroll_position/selected_line fields",
"file": "src/app/tab.rs",
"lines": "87-88",
"description": "scroll_position and selected_line on TabState are manually synced copies of Viewport state via sync_from_viewport(). Comment acknowledges this is 'for compatibility'. Creates risk of desync bugs.",
"task": "Remove scroll_position and selected_line from TabState. Update all callers to use tab.viewport.scroll_position() and tab.viewport.selected_line() directly. Remove sync_from_viewport() method.",
"status": "open"
},
{
"id": "M7",
"severity": "medium",
"category": "misplaced-code",
"title": "451 lines of integration tests in filter/mod.rs",
"file": "src/filter/mod.rs",
"lines": "381-823",
"description": "Module root contains 451 lines of integration tests (index_filter_integration_tests) embedded via #[cfg(test)]. This bloats the production module from ~100 to 823 lines.",
"task": "Move integration tests to tests/filter_integration.rs. Keep only unit tests for FilterMode and FilterHistoryEntry in the module. Target: mod.rs < 200 lines.",
"status": "open"
},
{
"id": "M8",
"severity": "medium",
"category": "extensibility",
"title": "SeverityCounts uses individual fields instead of array",
"file": "src/index/builder.rs",
"lines": "27-36",
"description": "SeverityCounts has individual trace/debug/info/warn/error/fatal fields. add_severity() uses a 6-arm match. Adding a new severity level requires changes in 3+ places.",
"task": "Consider using an array indexed by severity enum variant: counts: [u64; Severity::COUNT]. add_severity becomes counts[severity as usize] += 1. Alternatively, keep current design if no new severity levels are planned.",
"status": "open"
},
{
"id": "M9",
"severity": "medium",
"category": "primitive-obsession",
"title": "Aggregation uses raw tuple instead of named struct",
"file": "src/filter/aggregation.rs",
"lines": "45-51",
"description": "HashMap<Vec<String>, (usize, Vec<usize>)> uses an unnamed tuple where group.0 is count and group.1 is line_indices. Obscures meaning.",
"task": "Replace tuple with struct GroupAccumulator { count: usize, line_indices: Vec<usize> }. Makes code self-documenting: entry.count += 1 instead of entry.0 += 1.",
"status": "open"
},
{
"id": "M10",
"severity": "medium",
"category": "long-method",
"title": "render_log_view() is 171 lines mixing 4 concerns",
"file": "src/tui/log_view.rs",
"lines": "70-240",
"description": "Mixes setup/context building (~40 lines), viewport calculation (~25 lines), main item-building loop (~60 lines), and widget assembly (~20 lines). Closure captures 6+ variables implicitly.",
"task": "Extract build_render_context(), calculate_line_heights() (replace closure with LineHeightCalculator struct), build_list_items(). Target: render_log_view() becomes a 30-line orchestrator.",
"status": "open"
},
{
"id": "M11",
"severity": "medium",
"category": "dry-violation",
"title": "Constructor duplication in tab.rs",
"file": "src/app/tab.rs",
"lines": "150-344",
"description": "Four constructors (new, from_stdin, from_discovered_source, from_config_source) repeat: index reader opening, file size reading, index size calculation, watcher setup, reader Arc<Mutex<>> wrapping.",
"task": "Extract fn open_file_source(path: &Path, watch: bool) -> Result<FileSourceSetup> that returns a struct with reader, index_reader, file_size, index_size, watcher. All constructors use this helper.",
"status": "open"
},
{
"id": "M12",
"severity": "medium",
"category": "coupling",
"title": "Closure over-capture in log_view line height calculation",
"file": "src/tui/log_view.rs",
"lines": "123-141",
"description": "Line height closure captures 6+ variables from outer scope (line_indices, expanded_lines, content_width, line_wrap, raw_mode, reader_guard). Dependencies are implicit, making the code hard to reason about.",
"task": "Replace closure with an explicit LineHeightCalculator struct that takes all dependencies in its constructor. Pass calculator to resolve_with_heights() instead of the closure.",
"status": "open"
},
{
"id": "L1",
"severity": "low",
"category": "dry-violation",
"title": "String truncation logic duplicated across TUI modules",
"file": "src/tui/",
"lines": "various",
"description": "Truncation-with-ellipsis logic appears in log_view.rs (format_source_tag), help.rs (name truncation in confirm dialog), and side_panel.rs (item rendering). Each reimplements width calculation and truncation.",
"task": "Extract a shared utility function truncate_with_ellipsis(s: &str, max_width: usize) -> String in a tui utilities module. Use across all three files.",
"status": "open"
},
{
"id": "L2",
"severity": "low",
"category": "coupling",
"title": "combined_reader::source_info() coupled to ratatui Color",
"file": "src/reader/combined_reader.rs",
"lines": "167-170",
"description": "source_info() takes &[ratatui::style::Color] and returns Option<(&str, Color)>. Domain reader shouldn't depend on TUI framework types.",
"task": "Return Option<(&str, usize)> with source_id index. Let the TUI caller look up the color from the source_id. Removes ratatui dependency from reader module.",
"status": "open"
},
{
"id": "L3",
"severity": "low",
"category": "data-integrity",
"title": "Session context eviction is arbitrary, not LRU",
"file": "src/session.rs",
"lines": "113-121",
"description": "When session contexts exceed MAX_CONTEXTS (100), entries are removed using HashMap iteration order which is arbitrary. No timestamp-based or LRU eviction.",
"task": "Add a last_used timestamp to session entries. When evicting, sort by last_used and remove oldest. Alternatively, use an IndexMap to preserve insertion order.",
"status": "open"
},
{
"id": "L4",
"severity": "low",
"category": "safety",
"title": "Silent u32 overflow in sparse_index.rs",
"file": "src/reader/sparse_index.rs",
"lines": "84",
"description": "line_number as u32 silently truncates if line_number >= 2^32 (4 billion lines). Comment says 'supports up to 4 billion lines' but no runtime check.",
"task": "Add a debug_assert!(line_number <= u32::MAX as usize) or return an error if the line number exceeds u32 range. Document the 4-billion-line limit at the type level.",
"status": "open"
},
{
"id": "L5",
"severity": "low",
"category": "fragile-code",
"title": "Config error message parsing uses magic string offset",
"file": "src/config/error.rs",
"lines": "139-166",
"description": "Parses serde-saphyr error messages with rfind(' at line ') and magic offset + 9. If the YAML library changes its error format, this silently returns (None, None).",
"task": "Add a comment explaining the dependency on serde-saphyr's error format. Consider adding a test that validates the parsing against a known error message to catch format changes.",
"status": "open"
},
{
"id": "L6",
"severity": "low",
"category": "error-handling",
"title": "Capture mode swallows indexing errors",
"file": "src/capture.rs",
"lines": "145-147",
"description": "Index errors during capture are logged via eprintln('Warning: failed to index line') and silently continued. Could cascade into corrupted index that causes later analysis failures.",
"task": "Add a counter for index errors. If errors exceed a threshold (e.g., 10), log a prominent warning that the index may be incomplete. Document the design decision to continue on index failure.",
"status": "open"
},
{
"id": "L7",
"severity": "low",
"category": "srp-violation",
"title": "build_missing_indexes() mixes progress reporting with logic",
"file": "src/source.rs",
"lines": "506-558",
"description": "Function mixes filtering (which sources need indexes), progress reporting (eprintln calls), and index building orchestration. Progress format '[i/count]' duplicated 4 times.",
"task": "Extract progress reporting into a callback parameter or a ProgressReporter trait. Extract the common '[i/count]' format into a helper. Keep build_missing_indexes() focused on orchestration.",
"status": "open"
},
{
"id": "L8",
"severity": "low",
"category": "data-clump",
"title": "Drill-down state fields should be grouped",
"file": "src/log_source.rs",
"lines": "113-117",
"description": "drill_down_aggregation and drill_down_pattern on FilterConfig always travel together but aren't in their own type. Name 'drill_down_aggregation' is also unclear.",
"task": "Group into struct DrillDownState { parent_result: AggregationResult, parent_pattern: String }. Replace the two Option fields with a single Option<DrillDownState>.",
"status": "open"
},
{
"id": "L9",
"severity": "low",
"category": "naming",
"title": "TabState::stream_writer is misleadingly named",
"file": "src/app/tab.rs",
"lines": "100",
"description": "stream_writer is actually the handle to the reader that appends lines, not a writer. The name suggests it writes to a stream, but it writes to the in-memory buffer.",
"task": "Rename to stream_appender or streamable_reader_handle to clarify its purpose.",
"status": "open"
},
{
"id": "L10",
"severity": "low",
"category": "documentation",
"title": "Index builder batch size is undocumented magic number",
"file": "src/index/builder.rs",
"lines": "18",
"description": "const BATCH: usize = 1024 has no comment explaining why 1024 was chosen. Is it empirically tuned? A cache-line optimization? Unclear.",
"task": "Add a comment explaining the rationale for the 1024 batch size (e.g., 'Empirically tuned for balance between flush frequency and I/O overhead').",
"status": "open"
},
{
"id": "L11",
"severity": "low",
"category": "feature-envy",
"title": "TabState directly manipulates marker file directory structure",
"file": "src/app/tab.rs",
"lines": "404-414",
"description": "TabState::refresh_source_status() knows about .lazytail/data/ and .lazytail/sources/ directory structure, navigating parent directories to find marker files. This is source.rs's responsibility.",
"task": "Move this logic to LogSource::refresh_status() or call a source.rs function that encapsulates the directory traversal.",
"status": "open"
},
{
"id": "C5",
"severity": "critical",
"category": "architecture-violation",
"title": "Event-driven architecture bypassed for inactive tabs, combined tabs, and streams",
"file": "src/main.rs",
"lines": "751-1028",
"description": "ADR-001 and CLAUDE.md state 'all state transitions go through App::apply_event()' but main.rs bypasses this in 3 places: (1) collect_file_events() calls tab.apply_file_modification() directly on inactive tabs (line 823) and mutates combined tab fields directly (lines 829-872), (2) collect_filter_progress() calls tab.apply_filter_event() directly on inactive tabs (lines 914-920), (3) collect_stream_events() admits in its docstring it 'modifies tabs directly rather than returning events' (lines 978-1028). AppEvent::StreamData and AppEvent::StreamComplete exist but are #[allow(dead_code)].",
"task": "Document these as intentional fast-path exceptions in CLAUDE.md with performance justification. Optionally wire up AppEvent::StreamData/StreamComplete to unify the stream path. Add explicit '// FAST-PATH: bypasses event system for performance' comments at each bypass site.",
"status": "open"
},
{
"id": "H7",
"severity": "high",
"category": "documentation-mismatch",
"title": "process_event() is a dead pass-through despite documentation claims",
"file": "src/main.rs",
"lines": "1185-1187",
"description": "CLAUDE.md states 'Side effects are in process_event() in main.rs' but process_event() is a single-line function that calls app.apply_event(event) with zero side-effect logic. Side effects actually happen in collect_*() functions and within apply_event(). The documentation describes an idealized architecture that was never implemented or was refactored away.",
"task": "Update CLAUDE.md and ARCHITECTURE.md to accurately describe where side effects occur. Either remove the process_event() wrapper or document that it exists as a future extension point.",
"status": "open"
},
{
"id": "H8",
"severity": "high",
"category": "dependency-inversion",
"title": "Domain core log_source.rs imports from adapter module app/",
"file": "src/log_source.rs",
"lines": "1",
"description": "LogSource is documented as 'domain core shared across TUI/Web/MCP' (ADR-014) but imports FilterState and ViewMode from crate::app. This inverts the hexagonal architecture dependency direction — domain code depends on adapter code. FilterConfig.state: FilterState and LogSource.mode: ViewMode embed adapter-defined types in domain structs. This forces filter_orchestrator.rs and web/mod.rs to also import from app/ to work with these types.",
"task": "Move FilterState and ViewMode definitions from src/app/mod.rs to src/log_source.rs (or src/filter/mod.rs for FilterState). Have app/mod.rs re-export them for backward compatibility. This restores correct dependency direction: adapters import from domain, not vice versa.",
"status": "open"
},
{
"id": "H9",
"severity": "high",
"category": "solid-violation",
"title": "LogReader::as_any() violates LSP and ISP",
"file": "src/reader/mod.rs",
"lines": "21",
"description": "CLAUDE.md documents LogReader as having 3 methods but it has 4 — the undocumented as_any() -> &dyn Any enables runtime downcasting. tui/log_view.rs (lines 255,269,305) downcasts LogReader to CombinedReader to access source_info(), severity(), and renderer_names(). This breaks Liskov Substitution — callers must know the concrete type. It also violates Interface Segregation by forcing all LogReader implementations to carry reflection support they don't need.",
"task": "Option A: Extract as_any() into a separate ReaderReflection trait. Option B: Add source_info() with a default no-op to LogReader trait. Option C: Pass combined-reader metadata through a separate data channel to the rendering layer rather than downcasting. Update CLAUDE.md to document actual method count.",
"status": "open"
},
{
"id": "H10",
"severity": "high",
"category": "data-loss",
"title": "Web adapter missing 'unknown' severity count field",
"file": "src/web/mod.rs",
"lines": "74-82",
"description": "Web's SeverityCountsView struct has 6 fields (trace, debug, info, warn, error, fatal) but is missing the 'unknown' field. MCP's SeverityCountsInfo (src/mcp/types.rs:329-337) correctly includes all 7 fields. The source of truth SeverityCounts (src/index/checkpoint.rs:12-22) has 7 fields including unknown. Web clients silently lose unknown severity count data.",
"task": "Add 'unknown: u64' field to SeverityCountsView in src/web/mod.rs and populate it from the checkpoint data in src/web/state.rs.",
"status": "open"
},
{
"id": "M13",
"severity": "medium",
"category": "adapter-parity",
"title": "Web adapter missing renderer preset support",
"file": "src/web/handlers.rs",
"lines": "98-119",
"description": "MCP adapter (src/mcp/tools.rs:296-355) includes full renderer preset support via PresetRegistry and render_line_info(). Web adapter returns raw line content only — LineRow struct has no 'rendered' field. Same domain data surfaces different capabilities depending on which adapter is used.",
"task": "Either add optional preset rendering to Web's /api/lines endpoint (add rendered field to LineRow, pass PresetRegistry to web handler) or document this as an intentional design decision with rationale.",
"status": "open"
},
{
"id": "M14",
"severity": "medium",
"category": "dry-violation",
"title": "Web adapter duplicates query validation before FilterOrchestrator",
"file": "src/web/handlers.rs",
"lines": "182-193",
"description": "Web handler pre-validates regex and query patterns (calling RegexFilter::new() and query::parse_query()) before passing to FilterOrchestrator::trigger(), which performs the same validation internally during filter construction. MCP does not pre-validate. This violates DRY and means validation error messages may diverge.",
"task": "Remove pre-validation from web handler. Let FilterOrchestrator::trigger() handle all validation and return errors that the web handler can translate to HTTP error responses.",
"status": "open"
},
{
"id": "M15",
"severity": "medium",
"category": "documentation-mismatch",
"title": "Follow-mode jumps don't work for inactive tabs despite ADR-009 claim",
"file": "src/app/mod.rs",
"lines": "1493-1497",
"description": "ADR-009 states 'Follow mode jumps happen for inactive tabs if enabled' but jump_to_end() only fires for the active tab during filter completion in handle_filter_progress_event(). Inactive tabs with follow_mode=true do not auto-scroll when their filter completes or when new lines arrive.",
"task": "Either implement follow-mode jumps for inactive tabs (add jump_to_end() call in the inactive tab branch of collect_filter_progress()) or update ADR-009 to remove the incorrect claim.",
"status": "open"
},
{
"id": "M16",
"severity": "medium",
"category": "side-effect",
"title": "Render function mutates TabState during rendering",
"file": "src/tui/log_view.rs",
"lines": "149-150",
"description": "render_log_view() takes &mut App and mutates tab.scroll_position and tab.selected_line during rendering. Render functions should be pure reads of application state. This makes the render pass side-effectful, complicates reasoning about state transitions, and could cause issues if rendering is called multiple times or cached.",
"task": "Have render_log_view() return the new scroll_position and selected_line as a struct. Apply the updates in the main event loop after rendering completes. Alternatively, resolve this with M6 by removing the redundant fields entirely.",
"status": "open"
},
{
"id": "L12",
"severity": "low",
"category": "misplaced-code",
"title": "FilterOrchestrator lives outside the filter/ module",
"file": "src/filter_orchestrator.rs",
"lines": "1-150",
"description": "FilterOrchestrator is at src/filter_orchestrator.rs (crate root) but logically belongs in src/filter/orchestrator.rs alongside other filter infrastructure. ARCHITECTURE.md lists it under the filter/ module map but it actually lives at the crate root.",
"task": "Move src/filter_orchestrator.rs to src/filter/orchestrator.rs. Add pub mod orchestrator to src/filter/mod.rs. Update imports across the codebase.",
"status": "open"
},
{
"id": "L13",
"severity": "low",
"category": "dead-code",
"title": "Dead AppEvent::StreamData and AppEvent::StreamComplete variants",
"file": "src/app/event.rs",
"lines": "57-62",
"description": "AppEvent::StreamData and AppEvent::StreamComplete exist in the event enum but are marked #[allow(dead_code)] and never constructed. Stream events bypass the event system entirely (see C5). These represent an abandoned attempt to unify stream handling with the event architecture.",
"task": "Either wire these up by having collect_stream_events() return them as events (fixing C5), or remove them to reduce confusion. If removed, add a comment explaining why streams bypass the event system.",
"status": "open"
},
{
"id": "L14",
"severity": "low",
"category": "documentation-mismatch",
"title": "ADR-005 scrolloff documentation says 3 but code has uncommitted change to 0",
"file": "src/app/viewport.rs",
"lines": "13",
"description": "ADR-005 explicitly states 'Edge padding (scrolloff=3)' as a design decision. The working directory shows DEFAULT_EDGE_PADDING = 0, which is an uncommitted modification from the documented value of 3. If the change is intentional, the ADR is stale. If not, the change should be reverted.",
"task": "Either commit the change and update ADR-005 to document scrolloff=0 with the rationale for removing edge padding, or revert the uncommitted change to restore scrolloff=3.",
"status": "open"
}
]
}