From 4142521d49aeb2f8495591a4e6d9f194aa22647a Mon Sep 17 00:00:00 2001 From: Xuepoo Date: Mon, 24 Aug 2026 20:13:20 +0800 Subject: [PATCH 1/4] fix(ctx-symbol): fold css rule sets at their AST body (css) --- crates/ctx-symbol/src/lib.rs | 5 ++++ crates/ctx-symbol/tests/symbol_test.rs | 40 ++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/crates/ctx-symbol/src/lib.rs b/crates/ctx-symbol/src/lib.rs index 98b7dd1..778e445 100644 --- a/crates/ctx-symbol/src/lib.rs +++ b/crates/ctx-symbol/src/lib.rs @@ -446,6 +446,11 @@ fn fold_at_body_node( out.push_str(parsed.language.comment_prefix()); out.push(' '); out.push_str(&omit_marker(omitted)); + let closer = parsed.language.comment_close(); + if !closer.is_empty() { + out.push(' '); + out.push_str(closer); + } if keep_tail { out.push_str(nl); out.push_str(tail); diff --git a/crates/ctx-symbol/tests/symbol_test.rs b/crates/ctx-symbol/tests/symbol_test.rs index 9849364..4758429 100644 --- a/crates/ctx-symbol/tests/symbol_test.rs +++ b/crates/ctx-symbol/tests/symbol_test.rs @@ -1453,3 +1453,43 @@ fn doc_comment_requires_direct_adjacency() { let close = symbols.iter().find(|s| s.name == "close").unwrap(); assert_eq!(close.doc_comment.as_deref(), Some("Attached docs.")); } + +fn css_path() -> &'static Path { + Path::new("sample.css") +} + +#[test] +fn css_rule_set_fold_keeps_block_comment_closed() { + // The fold marker is a block comment in CSS: it must carry its closing + // `*/`, otherwise the kept `}` line is swallowed by the unterminated + // comment and the compact view no longer re-parses. + let src = " +body { + background-color: #f0f0f8; + color: #000000; +} + +table.form { + border: 1px solid #333; + padding: 2px; +} +"; + let parsed = ctx_symbol::parse(css_path(), src).unwrap(); + let symbols = ctx_symbol::extract_symbols(&parsed); + let names: Vec<&str> = symbols.iter().map(|s| s.name.as_str()).collect(); + assert!(names.contains(&"body"), "symbols: {names:?}"); + assert!(names.contains(&"table.form"), "symbols: {names:?}"); + for sym in &symbols { + let compact = ctx_symbol::compact_symbol(&parsed, sym); + let re = ctx_symbol::parse(css_path(), &compact).unwrap(); + assert!( + !re.tree.root_node().has_error(), + "compact must re-parse cleanly: {compact}" + ); + assert!( + compact.contains(" lines omitted] */"), + "block-comment marker must be closed: {compact}" + ); + assert!(compact.trim_end().ends_with('}'), "closer kept: {compact}"); + } +} From 943afcbcdb12a0dbc61033de2160d686aa770c64 Mon Sep 17 00:00:00 2001 From: Xuepoo Date: Mon, 24 Aug 2026 20:13:21 +0800 Subject: [PATCH 2/4] test: drop needless borrow and elided lifetime flagged by clippy --- crates/ctx-symbol/tests/degenerate_test.rs | 2 +- crates/ctx-symbol/tests/markup_test.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/ctx-symbol/tests/degenerate_test.rs b/crates/ctx-symbol/tests/degenerate_test.rs index d204c6c..8c1d2cd 100644 --- a/crates/ctx-symbol/tests/degenerate_test.rs +++ b/crates/ctx-symbol/tests/degenerate_test.rs @@ -34,7 +34,7 @@ fn assert_deterministic(source: &str, path: &Path) { /// Every byte range must land inside the original source and slice back out /// as valid UTF-8. -fn assert_slices_in_bounds<'a>(source: &str, symbols: &'a [Symbol]) { +fn assert_slices_in_bounds(source: &str, symbols: &[Symbol]) { for s in symbols { let bytes = source .as_bytes() diff --git a/crates/ctx-symbol/tests/markup_test.rs b/crates/ctx-symbol/tests/markup_test.rs index a7f314d..407f7cd 100644 --- a/crates/ctx-symbol/tests/markup_test.rs +++ b/crates/ctx-symbol/tests/markup_test.rs @@ -168,7 +168,7 @@ fn markdown_parent_section_compact_passes_through() { #[test] fn html_self_closing_elements_are_anchors_too() { let src = "\n

end

\n"; - let syms = kinds("icons.html", &src); + let syms = kinds("icons.html", src); let names: Vec<&str> = syms.iter().map(|s| s.0.as_str()).collect(); assert_eq!(names, ["logo", "tail"]); } From 16a450f12c585c83107e0a23d7083cb6d1231987 Mon Sep 17 00:00:00 2001 From: Xuepoo Date: Mon, 24 Aug 2026 20:17:54 +0800 Subject: [PATCH 3/4] test: pin compact output byte-stability across invocations --- crates/ctx-symbol/tests/symbol_test.rs | 49 ++++++++++++++++++++++++++ 1 file changed, 49 insertions(+) diff --git a/crates/ctx-symbol/tests/symbol_test.rs b/crates/ctx-symbol/tests/symbol_test.rs index 4758429..94b989b 100644 --- a/crates/ctx-symbol/tests/symbol_test.rs +++ b/crates/ctx-symbol/tests/symbol_test.rs @@ -1493,3 +1493,52 @@ table.form { assert!(compact.trim_end().ends_with('}'), "closer kept: {compact}"); } } + +#[test] +fn compact_output_is_byte_stable_across_invocations() { + // Byte-stability is sacred: the same input parsed twice must yield the + // exact same compact bytes for every symbol, on every path (AST-anchored + // brace fold, indentation fold, line-heuristic macro fold). + let cases: &[(&'static Path, &str)] = &[ + ( + c_path(), + "#define LOG(t) \\\n log_write(t)\nint add(int a, int b) {\n int r = a + b;\n return r;\n}\ntypedef struct Point {\n int x;\n int y;\n} Point;\n", + ), + ( + py_path(), + "class Point:\n def norm(self):\n x = self.x\n y = self.y\n return x * x + y * y\n", + ), + ( + go_path(), + "type Point struct {\n\tx int\n\ty int\n}\n\nfunc Add(a, b int) int {\n\treturn a + b\n}\n", + ), + ( + css_path(), + "table.form {\n border: 1px solid #333;\n padding: 2px;\n}\n", + ), + ( + rust_path(), + "pub fn add(a: i32, b: i32) -> i32 {\n let r = a + b;\n r\n}\n", + ), + ]; + for (path, src) in cases { + let first = { + let parsed = ctx_symbol::parse(path, src).unwrap(); + let symbols = ctx_symbol::extract_symbols(&parsed); + symbols + .iter() + .map(|s| ctx_symbol::compact_symbol(&parsed, s)) + .collect::>() + }; + assert!(!first.is_empty(), "no symbols for {path:?}"); + let second = { + let parsed = ctx_symbol::parse(path, src).unwrap(); + let symbols = ctx_symbol::extract_symbols(&parsed); + symbols + .iter() + .map(|s| ctx_symbol::compact_symbol(&parsed, s)) + .collect::>() + }; + assert_eq!(first, second, "compact bytes must be stable for {path:?}"); + } +} From 02950d29b624301b0a6bb6feed7717821e2d9dba Mon Sep 17 00:00:00 2001 From: Xuepoo Date: Mon, 24 Aug 2026 20:19:27 +0800 Subject: [PATCH 4/4] docs: record M1 corpus verification --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a834a79..9b4f76d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,16 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +## [Unreleased] + +### Fixed + +- **CSS rule-set folds re-parse** — the AST-anchored fold marker in + `compact_symbol` now carries its closing `*/` for block-comment languages, + so folded CSS rule sets keep a parseable `}` closer. Corpus re-parse + regressions on `/usr/include`, `/usr/lib/go/src`, and `/usr/lib/python3.14` + (55,498 files, 1,879,020 raw-parse-ok slices): 458 → 0. + ## [0.3.1] - 2026-08-24 ### Security