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
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/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"]); } diff --git a/crates/ctx-symbol/tests/symbol_test.rs b/crates/ctx-symbol/tests/symbol_test.rs index 9849364..94b989b 100644 --- a/crates/ctx-symbol/tests/symbol_test.rs +++ b/crates/ctx-symbol/tests/symbol_test.rs @@ -1453,3 +1453,92 @@ 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}"); + } +} + +#[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::