Skip to content

Commit 34738d2

Browse files
committed
docs: update
1 parent 888a122 commit 34738d2

6 files changed

Lines changed: 396 additions & 201 deletions

File tree

README.md

Lines changed: 306 additions & 184 deletions
Large diffs are not rendered by default.

apps/tui-cli/src/daemon.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,8 @@ pub struct SerializableStats {
148148
pub tokens_estimated: bool,
149149
pub units_cached: usize,
150150
pub units_unparseable: usize,
151+
#[serde(default)]
152+
pub units_too_large: usize,
151153
pub files_skipped_by_triage: usize,
152154
pub suppressed: usize,
153155
pub below_confidence: usize,
@@ -164,6 +166,7 @@ impl From<&AnalysisStats> for SerializableStats {
164166
tokens_estimated: s.tokens_estimated,
165167
units_cached: s.units_cached,
166168
units_unparseable: s.units_unparseable,
169+
units_too_large: s.units_too_large,
167170
files_skipped_by_triage: s.files_skipped_by_triage,
168171
suppressed: s.suppressed,
169172
below_confidence: s.below_confidence,
@@ -182,6 +185,7 @@ impl From<SerializableStats> for AnalysisStats {
182185
tokens_estimated: s.tokens_estimated,
183186
units_cached: s.units_cached,
184187
units_unparseable: s.units_unparseable,
188+
units_too_large: s.units_too_large,
185189
files_skipped_by_triage: s.files_skipped_by_triage,
186190
suppressed: s.suppressed,
187191
below_confidence: s.below_confidence,

apps/tui-cli/src/output.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,6 +161,14 @@ pub fn print_footer(shown: usize, gated: usize, stats: &AnalysisStats) {
161161
plural(stats.units_unparseable)
162162
);
163163
}
164+
if stats.units_too_large > 0 {
165+
eprintln!(
166+
" {} {} unit{} too large for the context window — skipped, not reviewed",
167+
"!".yellow(),
168+
stats.units_too_large,
169+
plural(stats.units_too_large)
170+
);
171+
}
164172
if stats.suppressed > 0 {
165173
eprintln!(
166174
" {} {} finding{} suppressed (inline comments or baseline)",
@@ -255,6 +263,7 @@ pub fn render(
255263
"units": stats.units_total,
256264
"units_cached": stats.units_cached,
257265
"units_unparseable": stats.units_unparseable,
266+
"units_too_large": stats.units_too_large,
258267
"inference_ms": stats.inference_ms,
259268
"prompt_tokens": stats.prompt_tokens,
260269
"completion_tokens": stats.completion_tokens,

apps/tui-cli/src/tui.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
//! The reviewer's cockpit.
1+
//! Interactive review — the terminal UI behind `diffmind --tui`.
22
//!
33
//! This is the surface the whole tool is for: not a report, but a place to sit
44
//! while deciding what to say about someone else's branch. Three things follow

packages/core-engine/src/analyzer.rs

Lines changed: 67 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,14 @@ const MAX_CHUNK_LINES: usize = 1200;
3232
/// How many times a chunk may be halved when its prompt overruns the window.
3333
const MAX_SPLIT_DEPTH: u32 = 4;
3434

35+
/// Ceiling on either optional prompt section. Past this the model's attention is
36+
/// the binding constraint rather than the window.
37+
const MAX_SECTION_BYTES: usize = 12_000;
38+
/// Floor for an optional section, below which it is not worth carrying.
39+
const MIN_SECTION_BYTES: usize = 400;
40+
/// Ticket text is a fixed brief, not something that grows with the diff.
41+
const MAX_REQUIREMENTS_BYTES: usize = 2000;
42+
3543
/// Triage only pays for itself once a diff spans enough files that skipping
3644
/// some saves more than the extra inference pass costs.
3745
const TRIAGE_MIN_FILES: usize = 6;
@@ -63,6 +71,9 @@ pub struct AnalysisStats {
6371
pub units_cached: usize,
6472
/// Units whose output could not be parsed even after repair.
6573
pub units_unparseable: usize,
74+
/// Units skipped because no amount of splitting fit them in the window.
75+
/// Distinct from `units_unparseable`: the model was never asked.
76+
pub units_too_large: usize,
6677
pub files_skipped_by_triage: usize,
6778
pub suppressed: usize,
6879
pub below_confidence: usize,
@@ -312,25 +323,55 @@ impl ReviewAnalyzer {
312323
(available / TOKENS_PER_DIFF_LINE).clamp(MIN_CHUNK_LINES, MAX_CHUNK_LINES)
313324
}
314325

315-
fn build_prompt(&self, diff: &str, context: &str, rulebooks: &[&Rulebook]) -> Prompt {
326+
fn build_prompt(
327+
&self,
328+
diff: &str,
329+
context: &str,
330+
rulebooks: &[&Rulebook],
331+
max_new_tokens: usize,
332+
depth: u32,
333+
) -> Prompt {
334+
let budget = self.section_budget_bytes(max_new_tokens, depth);
316335
review_prompt(&ReviewPromptInput {
317336
diff,
318337
context,
319338
languages: self.languages.as_deref(),
320339
requirements: self.requirements.as_deref(),
321340
rulebooks,
322-
max_context_bytes: self.context_budget_bytes(),
323-
max_requirements_bytes: 2000,
324-
max_rules_bytes: self.context_budget_bytes(),
341+
max_context_bytes: budget,
342+
max_requirements_bytes: budget.min(MAX_REQUIREMENTS_BYTES),
343+
max_rules_bytes: budget,
325344
})
326345
}
327346

328-
/// Byte budget for the RAG/context section, scaled to the window rather
329-
/// than pinned at the 2 KB a 4K window demanded.
330-
fn context_budget_bytes(&self) -> usize {
331-
let ctx = self.backend.context_tokens();
332-
// Roughly a sixth of the window, three bytes per token.
333-
((ctx / 6) * 3).clamp(1500, 12_000)
347+
/// Byte budget for each of the prompt's optional sections — the symbol
348+
/// context and the project rules — at recursion `depth`.
349+
///
350+
/// Two properties, both learned the hard way.
351+
///
352+
/// **The diff keeps at least half the window.** These sections used to be
353+
/// sized from the window alone, so a large `.diffmind/rules/` could claim
354+
/// 12 KB and the context another 12 KB regardless of what was left for the
355+
/// thing actually under review.
356+
///
357+
/// **The budget shrinks with depth.** When a prompt overruns, the analyzer
358+
/// halves the *diff* and retries — which cannot help when the fixed sections
359+
/// are what overran. A big enough rule set therefore failed identically at
360+
/// every recursion level and gave up at the depth cap, having never had a
361+
/// chance. Shrinking here is what makes the retry mean something.
362+
/// [`crate::prompt`] drops whole rule sets that no longer fit rather than
363+
/// truncating one mid-sentence, so this degrades to "fewer rules", then to
364+
/// "no rules" — never to half a rule that reads like a complete one.
365+
fn section_budget_bytes(&self, max_new_tokens: usize, depth: u32) -> usize {
366+
let available = self
367+
.backend
368+
.context_tokens()
369+
.saturating_sub(max_new_tokens + SYSTEM_PROMPT_TOKENS);
370+
// Half of what is left, shared by the two sections, at ~3 bytes a token.
371+
let share = ((available / 4) * 3).min(MAX_SECTION_BYTES);
372+
// The floor never exceeds the share, or a window too small to afford it
373+
// would be pushed further over by the very thing meant to protect it.
374+
(share >> depth.min(16)).max(MIN_SECTION_BYTES.min(share))
334375
}
335376

336377
/// True when the prompt fits the window with room for the response.
@@ -458,6 +499,16 @@ impl ReviewAnalyzer {
458499
}
459500
stats.units_unparseable += 1;
460501
}
502+
// Counted and skipped, not fatal. A hunk nobody can fit in the
503+
// window is a fact about that hunk; failing the run over it
504+
// would throw away every other unit's findings — including the
505+
// deterministic ones, which never needed a model at all.
506+
Err(EngineError::UnitTooLarge(why)) => {
507+
if self.debug {
508+
eprintln!("[debug] unit {} ({}) skipped: {why}", i + 1, unit.file());
509+
}
510+
stats.units_too_large += 1;
511+
}
461512
Err(e) => return Err(e),
462513
}
463514
}
@@ -573,15 +624,15 @@ impl ReviewAnalyzer {
573624
}
574625

575626
let context = context_for(chunk);
576-
let prompt = self.build_prompt(chunk, &context, rulebooks);
627+
let prompt = self.build_prompt(chunk, &context, rulebooks, max_tokens as usize, depth);
577628

578629
if !self.prompt_fits(&prompt, max_tokens as usize) {
579630
if depth >= MAX_SPLIT_DEPTH {
580-
return Err(EngineError::ForwardError(
581-
"a single diff hunk is too large for the model's context window even after \
582-
splitting; review that file on its own"
583-
.into(),
584-
));
631+
return Err(EngineError::UnitTooLarge(format!(
632+
"still {} bytes of diff after {MAX_SPLIT_DEPTH} splits, with every \
633+
optional section already at its minimum",
634+
chunk.len()
635+
)));
585636
}
586637
// Halve and recurse rather than truncate: silently dropping half a
587638
// hunk means silently not reviewing it. Each half re-derives its own

packages/core-engine/src/error.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,15 @@ pub enum EngineError {
1818
SamplingError(String),
1919
#[error("serialization error: {0}")]
2020
SerializationError(String),
21+
/// A review unit could not be made to fit the model's context window, even
22+
/// after splitting and after shrinking everything optional in the prompt.
23+
///
24+
/// Reported as an error so the analyzer can tell it apart from a chunk that
25+
/// merely came back unparseable, but it is *counted and skipped* rather than
26+
/// failing the run: one unreviewable hunk must not discard the findings
27+
/// every other hunk produced.
28+
#[error("review unit does not fit the context window: {0}")]
29+
UnitTooLarge(String),
2130
#[error("io error: {0}")]
2231
Io(String),
2332
/// A remote backend (Ollama, OpenAI-compatible) failed to answer.

0 commit comments

Comments
 (0)