Skip to content

Commit ff19e8c

Browse files
Your Nameclaude
andcommitted
feat(server,core): truth-kernel hardening Wave 5 -- close all 7 residuals from Waves 0-4
5.1: gate_prediction now predicts the uncovered-code floor (compute_touch_risk gains real_hunks + any_executable_kind guards), fixing a self-inflicted false-negative in edit_context's own safety prediction. 5.2: DB-Ambiguous candidates are now live-re-verified via verify_live before being reported ambiguous, closing the last untrusted branch of resolve_symbol. 5.3: edit_context/edit_symbol accept qualified_name for unambiguous resolution of globally-common bare names on the write path (parity with the read-only tools from Wave 3.4). 5.4: understand()'s kind fallback now actually defaults to hybrid -- the match statement was missing a "hybrid" arm entirely, so the prior default-flip attempt was a silent no-op; fixed via an extracted, directly-testable parse_understand_kind function. 5.5: added the adversarial test Wave 2.2 left as a DoD gap -- drift injected mid-reconciliation (not just before it) via a #[cfg(test)]-only hook, proving the fence still blocks Reconciled. 5.7: parse_tree now returns Result<Tree, ParseFailure> (UnsupportedLanguage / AbiLoadFailed / Timeout) instead of a bare Option, so verify_live and insertion_hunk_for can surface WHY a re-parse failed instead of a generic "Failed to parse" string. extract_file_data's Tier-0.5 shallow-fallback path deliberately does NOT write file_index.skip_reason -- it still produces a populated ExtractedFile via extract_symbols_shallow, so treating it as a skip would misrepresent a real (if lower-fidelity) index as an empty one. 5.6 (false-hub-rate measurement) intentionally deferred -- data-gathering task, not a code change, per the plan's own sequencing. calm-server 432/432, calm-core 1270/1270 lib tests green, fmt/clippy clean, diff_impact confirms scope. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent 36eafa4 commit ff19e8c

15 files changed

Lines changed: 1570 additions & 78 deletions

File tree

crates/calm-core/src/edit.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -447,7 +447,7 @@ pub fn insertion_hunk(
447447
/// is explicitly meant to also work on files the indexer never parses.
448448
pub fn validate_syntax(new_content: &str, extension: &str) -> Option<bool> {
449449
let language = language_for_extension(extension)?;
450-
let tree = parse_tree(new_content, language)?;
450+
let tree = parse_tree(new_content, language).ok()?;
451451
Some(!tree.root_node().has_error())
452452
}
453453

@@ -526,7 +526,7 @@ pub fn validate_syntax_diff(
526526
touched_new_lines: &[(i64, i64)],
527527
) -> Option<bool> {
528528
let language = language_for_extension(extension)?;
529-
let new_tree = parse_tree(new_content, language)?;
529+
let new_tree = parse_tree(new_content, language).ok()?;
530530
let mut new_errors = Vec::new();
531531
collect_error_line_ranges(new_tree.root_node(), &mut new_errors);
532532
if new_errors.is_empty() {
@@ -549,7 +549,7 @@ pub fn validate_syntax_diff(
549549
return Some(false);
550550
}
551551

552-
let original_errors_all = parse_tree(original, language).map(|t| {
552+
let original_errors_all = parse_tree(original, language).ok().map(|t| {
553553
let mut v = Vec::new();
554554
collect_error_line_ranges(t.root_node(), &mut v);
555555
v

crates/calm-core/src/indexer/csharp_namespace.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ fn rel_file(project_root: &Path, abs_file: &Path) -> Option<String> {
102102
/// (holding either a plain `identifier` or a dotted `qualified_name`; taking
103103
/// the field's raw source text handles either shape without unwrapping).
104104
fn namespaces_declared_in(source: &str, consts: &LangConstants) -> Vec<String> {
105-
let Some(tree) = parse_tree(source, "csharp") else {
105+
let Ok(tree) = parse_tree(source, "csharp") else {
106106
return Vec::new();
107107
};
108108
let root = tree.root_node();

crates/calm-core/src/indexer/imports.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ pub fn extract_imports(source: &str, language: &str) -> Vec<ParsedImport> {
8585
if types.is_empty() {
8686
return Vec::new();
8787
}
88-
let Some(tree) = parse_tree(source, language) else {
88+
let Ok(tree) = parse_tree(source, language) else {
8989
return Vec::new();
9090
};
9191
extract_imports_from_tree(&tree, source, language)

crates/calm-core/src/indexer/parser.rs

Lines changed: 128 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -191,22 +191,68 @@ fn node_kind_to_symbol_kind(node_kind: &str, in_class: bool) -> SymbolKind {
191191
// covers this with no further changes needed.
192192
const PARSE_TIMEOUT_MICROS: u64 = 5_000_000;
193193

194+
/// Why `parse_tree` failed to produce a tree (Wave 5 item 5.7,
195+
/// docs/plans/2026-08-21-truth-kernel-hardening-wave5-residual-closure-plan.md)
196+
/// -- threaded as a `Result` instead of collapsing every cause into one
197+
/// `None`, so a caller that surfaces failure reasons (this module's own
198+
/// `extract_*` wrappers, `verify_live`, `insertion_hunk_for`) can say WHY a
199+
/// re-parse failed, not just THAT it did.
200+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
201+
pub enum ParseFailure {
202+
/// `language` has no registered `LanguageSpec` at all, or the spec
203+
/// exists but its `ts_language` grammar loader returned `None` (the
204+
/// `lang-*` Cargo feature gating that grammar wasn't compiled into this
205+
/// build). The normal, expected outcome for any Tier-0.5 language --
206+
/// callers that fall back to shallow extraction on this variant are not
207+
/// handling an error, just routing around a grammar that was never
208+
/// supposed to exist here.
209+
UnsupportedLanguage,
210+
/// `Parser::set_language` rejected the grammar -- an ABI version
211+
/// mismatch between the `tree-sitter` core crate and this grammar
212+
/// crate. Unlike `UnsupportedLanguage`, this would indicate a real
213+
/// dependency-version bug, not an expected per-language gap.
214+
AbiLoadFailed,
215+
/// `Parser::parse` returned `None` -- the per-call timeout
216+
/// (`PARSE_TIMEOUT_MICROS`) was hit on a pathological/adversarial input.
217+
Timeout,
218+
}
219+
220+
impl ParseFailure {
221+
pub fn as_str(self) -> &'static str {
222+
match self {
223+
ParseFailure::UnsupportedLanguage => "unsupported_language",
224+
ParseFailure::AbiLoadFailed => "abi_load_failed",
225+
ParseFailure::Timeout => "timeout",
226+
}
227+
}
228+
}
229+
230+
impl std::fmt::Display for ParseFailure {
231+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
232+
f.write_str(self.as_str())
233+
}
234+
}
235+
194236
/// Parse `source` for a tier-0 `language` into a tree-sitter tree, or `None` if
195237
/// the language is unsupported or parsing fails. Single source of the per-language
196238
/// grammar mapping.
197-
pub fn parse_tree(source: &str, language: &str) -> Option<tree_sitter::Tree> {
198-
let lang = (crate::indexer::lang_constants::find_spec(language)?.ts_language)()?;
239+
pub fn parse_tree(source: &str, language: &str) -> Result<tree_sitter::Tree, ParseFailure> {
240+
let spec = crate::indexer::lang_constants::find_spec(language)
241+
.ok_or(ParseFailure::UnsupportedLanguage)?;
242+
let lang = (spec.ts_language)().ok_or(ParseFailure::UnsupportedLanguage)?;
199243
let mut parser = tree_sitter::Parser::new();
200-
parser.set_language(&lang).ok()?;
244+
parser
245+
.set_language(&lang)
246+
.map_err(|_| ParseFailure::AbiLoadFailed)?;
201247
parser.set_timeout_micros(PARSE_TIMEOUT_MICROS);
202-
parser.parse(source, None)
248+
parser.parse(source, None).ok_or(ParseFailure::Timeout)
203249
}
204250
pub fn extract_symbols(
205251
source: &str,
206252
language: &str,
207253
path: &str,
208254
) -> Result<Vec<ParsedSymbol>, String> {
209-
let tree = parse_tree(source, language).ok_or("Failed to parse")?;
255+
let tree = parse_tree(source, language).map_err(|e| e.to_string())?;
210256
Ok(extract_symbols_from_tree(&tree, source, language, path))
211257
}
212258

@@ -1968,7 +2014,7 @@ fn selected_callee_byte_span(
19682014
/// Extract call sites from a source file, each attributed to its enclosing function.
19692015
/// Top-level calls (outside any function) are skipped — they have no caller symbol.
19702016
pub fn extract_calls(source: &str, language: &str, _path: &str) -> Result<Vec<RawCall>, String> {
1971-
let tree = parse_tree(source, language).ok_or("Failed to parse")?;
2017+
let tree = parse_tree(source, language).map_err(|e| e.to_string())?;
19722018
Ok(extract_calls_from_tree(&tree, source, language))
19732019
}
19742020

@@ -2031,7 +2077,7 @@ pub fn extract_file_aliases(
20312077
language: &str,
20322078
ctx: &crate::resolver::FileContext,
20332079
) -> std::collections::HashMap<String, String> {
2034-
let Some(tree) = parse_tree(source, language) else {
2080+
let Ok(tree) = parse_tree(source, language) else {
20352081
return std::collections::HashMap::new();
20362082
};
20372083
extract_file_aliases_from_tree(&tree, source, language, ctx)
@@ -2060,7 +2106,7 @@ pub fn extract_file_aliases_from_tree(
20602106
/// JavaScript (dynamic) yields an empty map. Go shares one type across several
20612107
/// names (`x, y Foo`), so each name is mapped.
20622108
pub fn extract_type_map(source: &str, language: &str) -> std::collections::HashMap<String, String> {
2063-
let Some(tree) = parse_tree(source, language) else {
2109+
let Ok(tree) = parse_tree(source, language) else {
20642110
return std::collections::HashMap::new();
20652111
};
20662112
extract_type_map_from_tree(&tree, source, language)
@@ -4259,43 +4305,98 @@ public class FooTest {
42594305
#[test]
42604306
#[cfg(feature = "lang-ruby")]
42614307
fn test_tier0_5_grammar_loads_ruby() {
4262-
assert!(parse_tree("def foo\nend\n", "ruby").is_some());
4308+
assert!(parse_tree("def foo\nend\n", "ruby").is_ok());
42634309
}
42644310

42654311
#[test]
42664312
#[cfg(feature = "lang-php")]
42674313
fn test_tier0_5_grammar_loads_php() {
4268-
assert!(parse_tree("<?php\nfunction foo() {}\n", "php").is_some());
4314+
assert!(parse_tree("<?php\nfunction foo() {}\n", "php").is_ok());
42694315
}
42704316

42714317
#[test]
42724318
#[cfg(feature = "lang-csharp")]
42734319
fn test_tier0_5_grammar_loads_csharp() {
4274-
assert!(parse_tree("class Foo { void Bar() {} }\n", "csharp").is_some());
4320+
assert!(parse_tree("class Foo { void Bar() {} }\n", "csharp").is_ok());
42754321
}
42764322

42774323
#[test]
42784324
#[cfg(feature = "lang-shell")]
42794325
fn test_tier0_5_grammar_loads_shell() {
4280-
assert!(parse_tree("foo() {\n echo hi\n}\n", "shell").is_some());
4326+
assert!(parse_tree("foo() {\n echo hi\n}\n", "shell").is_ok());
42814327
}
42824328

42834329
#[test]
42844330
#[cfg(feature = "lang-c")]
42854331
fn test_tier0_5_grammar_loads_c() {
4286-
assert!(parse_tree("int foo() { return 0; }\n", "c").is_some());
4332+
assert!(parse_tree("int foo() { return 0; }\n", "c").is_ok());
42874333
}
42884334

42894335
#[test]
42904336
#[cfg(feature = "lang-cpp")]
42914337
fn test_tier0_5_grammar_loads_cpp() {
4292-
assert!(parse_tree("int foo() { return 0; }\n", "cpp").is_some());
4338+
assert!(parse_tree("int foo() { return 0; }\n", "cpp").is_ok());
42934339
}
42944340

42954341
#[test]
42964342
#[cfg(feature = "lang-r")]
42974343
fn test_tier0_5_grammar_loads_r() {
4298-
assert!(parse_tree("foo <- function(x) {\n x\n}\n", "r").is_some());
4344+
assert!(parse_tree("foo <- function(x) {\n x\n}\n", "r").is_ok());
4345+
}
4346+
4347+
// Wave 5 item 5.7 (docs/plans/2026-08-21-truth-kernel-hardening-wave5-
4348+
// residual-closure-plan.md): parse_tree's ParseFailure reason must be
4349+
// the correct variant for each real failure mode, and its `as_str`/
4350+
// `Display` output is load-bearing -- verify_live/insertion_hunk_for
4351+
// interpolate it directly into an agent-facing error message via `{e}`.
4352+
#[test]
4353+
fn parse_tree_unrecognized_language_name_reports_unsupported_language() {
4354+
// tree_sitter::Tree implements neither PartialEq nor Debug, so
4355+
// assert_eq!/unwrap_err() can't be used directly on this Result --
4356+
// matches! sidesteps both trait requirements.
4357+
assert!(matches!(
4358+
parse_tree("fn main() {}", "not-a-real-language"),
4359+
Err(ParseFailure::UnsupportedLanguage)
4360+
));
4361+
}
4362+
4363+
#[test]
4364+
fn parse_tree_valid_source_and_supported_language_succeeds() {
4365+
assert!(parse_tree("fn main() {}", "rust").is_ok());
4366+
}
4367+
4368+
#[test]
4369+
fn parse_failure_as_str_and_display_match_for_every_variant() {
4370+
for variant in [
4371+
ParseFailure::UnsupportedLanguage,
4372+
ParseFailure::AbiLoadFailed,
4373+
ParseFailure::Timeout,
4374+
] {
4375+
assert_eq!(variant.to_string(), variant.as_str());
4376+
assert!(!variant.as_str().is_empty());
4377+
}
4378+
// Exact strings are part of the public contract now that
4379+
// extract_symbols/extract_calls thread `.to_string()` of this type
4380+
// straight into REPARSE_FAILED/STALE_SYMBOL error messages shown to
4381+
// an agent -- a silent rename here would silently change those.
4382+
assert_eq!(
4383+
ParseFailure::UnsupportedLanguage.as_str(),
4384+
"unsupported_language"
4385+
);
4386+
assert_eq!(ParseFailure::AbiLoadFailed.as_str(), "abi_load_failed");
4387+
assert_eq!(ParseFailure::Timeout.as_str(), "timeout");
4388+
}
4389+
4390+
#[test]
4391+
fn extract_symbols_unsupported_language_error_names_the_real_reason_not_a_generic_string() {
4392+
// Regression guard for the exact improvement Wave 5.7 makes over the
4393+
// old `.ok_or("Failed to parse")?`: the error string a caller like
4394+
// verify_live surfaces must now say WHY, not just THAT parsing failed.
4395+
// `.err()` (not `.unwrap_err()`) is used deliberately: ParsedSymbol
4396+
// has no Debug impl, and unwrap_err() requires the Ok side (here
4397+
// Vec<ParsedSymbol>) to implement it too.
4398+
let err = extract_symbols("fn main() {}", "not-a-real-language", "a.rs").err();
4399+
assert_eq!(err, Some("unsupported_language".to_string()));
42994400
}
43004401

43014402
/// Regression test for the shell-specific bug this guard suite was born
@@ -4563,7 +4664,7 @@ public class FooTest {
45634664
);
45644665
// Markdown has no tree-sitter grammar, ever — extract_file_data's
45654666
// dedicated branch is the only path that can produce symbols for it.
4566-
assert!(parse_tree("# x", "markdown").is_none());
4667+
assert!(parse_tree("# x", "markdown").is_err());
45674668
}
45684669
#[test]
45694670
fn test_shallow_kotlin() {
@@ -4595,7 +4696,7 @@ public class FooTest {
45954696
#[cfg(feature = "lang-kotlin")]
45964697
fn test_tier0_5_grammar_loads_kotlin() {
45974698
assert!(
4598-
parse_tree("fun main() {}", "kotlin").is_some(),
4699+
parse_tree("fun main() {}", "kotlin").is_ok(),
45994700
"tree-sitter-kotlin-ng grammar should load and parse"
46004701
);
46014702
}
@@ -4604,7 +4705,7 @@ public class FooTest {
46044705
#[cfg(feature = "lang-swift")]
46054706
fn test_tier0_5_grammar_loads_swift() {
46064707
assert!(
4607-
parse_tree("func main() {}", "swift").is_some(),
4708+
parse_tree("func main() {}", "swift").is_ok(),
46084709
"tree-sitter-swift grammar should load and parse"
46094710
);
46104711
}
@@ -4696,7 +4797,7 @@ public class FooTest {
46964797
#[cfg(feature = "lang-scala")]
46974798
fn test_tier0_5_grammar_loads_scala() {
46984799
assert!(
4699-
parse_tree("def main(): Unit = {}", "scala").is_some(),
4800+
parse_tree("def main(): Unit = {}", "scala").is_ok(),
47004801
"tree-sitter-scala grammar should load and parse — locks the pinned ABI (=0.24.1, ABI 14); \
47014802
a caret-range bump to 0.25.0+ (ABI 15) would silently regress this to shallow line-scan"
47024803
);
@@ -5168,7 +5269,7 @@ class Foo {
51685269
#[cfg(feature = "lang-dart")]
51695270
fn test_tier0_5_grammar_loads_dart() {
51705271
assert!(
5171-
parse_tree("void main() {}", "dart").is_some(),
5272+
parse_tree("void main() {}", "dart").is_ok(),
51725273
"tree-sitter-dart grammar should load and parse — locks the pinned ABI \
51735274
(=0.0.4, ABI 14, the newest ABI-14 release since this grammar has no git \
51745275
tags — verified by downloading each published .crate tarball and grepping \
@@ -5283,7 +5384,7 @@ class Foo {
52835384
#[cfg(feature = "lang-lua")]
52845385
fn test_tier0_5_grammar_loads_lua() {
52855386
assert!(
5286-
parse_tree("function main() end", "lua").is_some(),
5387+
parse_tree("function main() end", "lua").is_ok(),
52875388
"tree-sitter-lua grammar should load and parse — locks the pinned ABI \
52885389
(=0.2.0, ABI 14); a caret-range bump to 0.4.1+ (ABI 15) would silently \
52895390
regress this to shallow line-scan"
@@ -5343,7 +5444,7 @@ class Foo {
53435444
#[cfg(feature = "lang-elixir")]
53445445
fn test_tier0_5_grammar_loads_elixir() {
53455446
assert!(
5346-
parse_tree("def main do\nend\n", "elixir").is_some(),
5447+
parse_tree("def main do\nend\n", "elixir").is_ok(),
53475448
"tree-sitter-elixir grammar should load and parse — locks the pinned ABI \
53485449
(=0.3.5, ABI 14)"
53495450
);
@@ -5434,7 +5535,7 @@ class Foo {
54345535
#[cfg(feature = "lang-haskell")]
54355536
fn test_tier0_5_grammar_loads_haskell() {
54365537
assert!(
5437-
parse_tree("main :: IO ()\nmain = putStrLn \"hi\"\n", "haskell").is_some(),
5538+
parse_tree("main :: IO ()\nmain = putStrLn \"hi\"\n", "haskell").is_ok(),
54385539
"tree-sitter-haskell grammar should load and parse — locks the pinned ABI \
54395540
(=0.23.1, ABI 14)"
54405541
);
@@ -5512,7 +5613,7 @@ class Foo {
55125613
#[cfg(feature = "lang-ocaml")]
55135614
fn test_tier0_5_grammar_loads_ocaml() {
55145615
assert!(
5515-
parse_tree("let main () = print_string \"hi\"\n", "ocaml").is_some(),
5616+
parse_tree("let main () = print_string \"hi\"\n", "ocaml").is_ok(),
55165617
"tree-sitter-ocaml grammar should load and parse — locks the pinned ABI \
55175618
(=0.24.2, ABI 14); a caret-range bump to 0.25.0+ (ABI 15) would silently \
55185619
regress this to shallow line-scan"
@@ -5585,7 +5686,7 @@ class Foo {
55855686
#[cfg(feature = "lang-zig")]
55865687
fn test_tier0_5_grammar_loads_zig() {
55875688
assert!(
5588-
parse_tree("pub fn main() void {}\n", "zig").is_some(),
5689+
parse_tree("pub fn main() void {}\n", "zig").is_ok(),
55895690
"tree-sitter-zig grammar should load and parse — locks the pinned ABI \
55905691
(=1.1.2, ABI 14)"
55915692
);
@@ -5664,7 +5765,7 @@ class Foo {
56645765
#[cfg(feature = "lang-powershell")]
56655766
fn test_tier0_5_grammar_loads_powershell() {
56665767
assert!(
5667-
parse_tree("function Main {\n Write-Host \"hi\"\n}\n", "powershell").is_some(),
5768+
parse_tree("function Main {\n Write-Host \"hi\"\n}\n", "powershell").is_ok(),
56685769
"tree-sitter-powershell grammar should load and parse — locks the pinned ABI \
56695770
(=0.25.9, ABI 14); a caret-range bump to 0.25.10+ (ABI 15) would silently \
56705771
regress this to shallow line-scan"
@@ -5739,7 +5840,7 @@ class Foo {
57395840
#[cfg(feature = "lang-groovy")]
57405841
fn test_tier0_5_grammar_loads_groovy() {
57415842
assert!(
5742-
parse_tree("def main() {}\n", "groovy").is_some(),
5843+
parse_tree("def main() {}\n", "groovy").is_ok(),
57435844
"tree-sitter-groovy grammar should load and parse — locks the pinned ABI \
57445845
(=0.1.2, ABI 14)"
57455846
);

crates/calm-core/src/indexer/pipeline/extraction.rs

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -151,9 +151,18 @@ pub(super) fn extract_file_data(
151151
};
152152
}
153153

154-
let Some(tree) = parse_tree(source, lang) else {
155-
// Tier-0.5: no tree-sitter grammar for this language extract symbols
154+
let Ok(tree) = parse_tree(source, lang) else {
155+
// Tier-0.5: no tree-sitter grammar for this language -- extract symbols
156156
// via lightweight line-scan (no calls, no imports, no resolver tiers).
157+
// Wave 5 item 5.7: the `ParseFailure` reason (unsupported language /
158+
// ABI mismatch / timeout) is deliberately discarded here, not threaded
159+
// into `file_index.skip_reason` the way `read_source_capped`'s failure
160+
// is (see `driver.rs`'s `WalkOutcome::Skipped`) -- this path is NOT a
161+
// skip. It still produces a populated `ExtractedFile` below (real
162+
// symbols, just via a lower-fidelity extractor), so writing a
163+
// `skip_reason` here would falsely claim this file has nothing, the
164+
// exact meaning that column already carries for a genuinely-empty
165+
// `file_index` row elsewhere in the pipeline.
157166
let symbols = extract_symbols_shallow(source, lang, rel);
158167
let symbol_count = symbols.len();
159168
let chunks = chunk_pending(source, &symbols);

crates/calm-server/src/__toolsnaps__/edit_context.snap

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,13 @@
2727
],
2828
"format": "int64"
2929
},
30+
"qualified_name": {
31+
"description": "5.3 (Wave 5): exact `qualified_name` from a prior `search`/`locate`\nresult — when set, resolves directly by identity and `path`/`line`\nare ignored, so this can never come back ambiguous even for a\nglobally-common bare `symbol` name. Still flows through the same\nlive-verification every resolution does (Wave 1's `verify_live`).",
32+
"type": [
33+
"string",
34+
"null"
35+
]
36+
},
3037
"if_none_match": {
3138
"description": "`edges_etag` from a prior `edit_context` call on this exact symbol —\nif the caller/callee lists haven't changed since, the response omits\n`callers`/`callees` and sets `edges_not_modified: true`. Every other\nfield (`risk_assessment`, `dead_code_confidence`, `blast_radius`,\n`trend`, `co_changed_files`, `range_checksum`) is always recomputed\nand returned in full regardless — never gated behind this etag, since\nthis is the mandatory pre-edit safety tool and none of those may go\nsilently stale.",
3239
"type": [

0 commit comments

Comments
 (0)