From 912020c1b6cf30ce2f54e650e78409afcf0afe9d Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Mon, 5 Oct 2026 07:58:10 -0400 Subject: [PATCH 01/21] Test that brace groups follow bash expansion in path-tables --- packages/rust/tests/path_table_cli_e2e.rs | 25 +++++++++++ packages/rust/tests/path_table_globs.rs | 55 +++++++++++++++++++++++ 2 files changed, 80 insertions(+) diff --git a/packages/rust/tests/path_table_cli_e2e.rs b/packages/rust/tests/path_table_cli_e2e.rs index 7919a1b05..b62c83239 100644 --- a/packages/rust/tests/path_table_cli_e2e.rs +++ b/packages/rust/tests/path_table_cli_e2e.rs @@ -291,3 +291,28 @@ fn an_ordinary_syntax_error_carries_no_quoting_hint() { "a syntax error with no path in it must stay unhinted, got: {stderr}" ); } + +fn braces() -> TempDir { + let root = TempDir::new().unwrap(); + fs::create_dir_all(root.path().join("br")).unwrap(); + for name in ["q.md", "{q}.md", "a1.md", "a2.md", "a3.md"] { + fs::write(root.path().join("br").join(name), name).unwrap(); + } + root +} + +#[test] +fn a_brace_group_without_a_comma_matches_literally() { + let dir = braces(); + let out = run(&dir, "SELECT path FROM './br/{q}.md'"); + + assert_eq!(paths(&out), vec!["br/{q}.md"]); +} + +#[test] +fn a_brace_sequence_expands_like_bash() { + let dir = braces(); + let out = run(&dir, "SELECT path FROM './br/a{1..3..2}.md'"); + + assert_eq!(paths(&out), vec!["br/a1.md", "br/a3.md"]); +} diff --git a/packages/rust/tests/path_table_globs.rs b/packages/rust/tests/path_table_globs.rs index 49274b35c..384d35c61 100644 --- a/packages/rust/tests/path_table_globs.rs +++ b/packages/rust/tests/path_table_globs.rs @@ -399,3 +399,58 @@ fn a_prefixed_glob_starts_its_walk_at_the_named_directory() { "the walk must begin at docs/, not list the index root" ); } + +/// Files whose names a brace group could plausibly expand to, including one +/// whose name holds the braces themselves. +fn brace_fixture() -> TempDir { + let root = TempDir::new().unwrap(); + fs::create_dir_all(root.path().join("br")).unwrap(); + for name in ["q.md", "{q}.md", "a1.md", "a2.md", "a3.md", "a{1..2}.md"] { + fs::write(root.path().join("br").join(name), name).unwrap(); + } + root +} + +#[test] +fn a_brace_group_without_a_comma_is_literal_text() { + let root = brace_fixture(); + let db = open(&root); + + assert_eq!( + paths(&db, "SELECT path FROM './br/{q}.md'"), + vec!["br/{q}.md"] + ); +} + +#[test] +fn a_brace_group_with_a_comma_is_an_alternation() { + let root = brace_fixture(); + let db = open(&root); + + assert_eq!( + paths(&db, "SELECT path FROM './br/{q,a1}.md'"), + vec!["br/a1.md", "br/q.md"] + ); +} + +#[test] +fn a_brace_sequence_expands_to_each_value() { + let root = brace_fixture(); + let db = open(&root); + + assert_eq!( + paths(&db, "SELECT path FROM './br/a{1..2}.md'"), + vec!["br/a1.md", "br/a2.md"] + ); +} + +#[test] +fn a_literal_brace_group_nested_in_an_alternation_stays_literal() { + let root = brace_fixture(); + let db = open(&root); + + assert_eq!( + paths(&db, "SELECT path FROM './br/{a3,{q}}.md'"), + vec!["br/a3.md", "br/{q}.md"] + ); +} From cd8c60dce85e4d60b9d7bd99bec351ba0603494c Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Mon, 5 Oct 2026 08:08:59 -0400 Subject: [PATCH 02/21] Expand path-table braces as bash does A brace group with neither a top-level comma nor a .. range is literal text; ranges expand to each value. --- PARITY.md | 7 + docs/reference/path-tables.md | 7 + .../2026-10-05-brace-groups-follow-bash.md | 6 + .../2026-10-05-brace-groups-follow-bash.md | 39 ++ .../2026-10-05-brace-groups-follow-bash.md | 6 + .../2026-10-05-brace-groups-follow-bash.md | 40 ++ packages/rust/src/brace.rs | 443 ++++++++++++++++++ packages/rust/src/lib.rs | 1 + packages/rust/src/scanner.rs | 4 +- .../2026-10-05-brace-groups-follow-bash.md | 6 + .../2026-10-05-brace-groups-follow-bash.md | 39 ++ 11 files changed, 597 insertions(+), 1 deletion(-) create mode 100644 packages/python/changelog.d/2026-10-05-brace-groups-follow-bash.md create mode 100644 packages/python/migrations.d/2026-10-05-brace-groups-follow-bash.md create mode 100644 packages/rust/changelog.d/2026-10-05-brace-groups-follow-bash.md create mode 100644 packages/rust/migrations.d/2026-10-05-brace-groups-follow-bash.md create mode 100644 packages/rust/src/brace.rs create mode 100644 packages/ts/changelog.d/2026-10-05-brace-groups-follow-bash.md create mode 100644 packages/ts/migrations.d/2026-10-05-brace-groups-follow-bash.md diff --git a/PARITY.md b/PARITY.md index ff504f1dd..5ab65ddd0 100644 --- a/PARITY.md +++ b/PARITY.md @@ -257,6 +257,13 @@ the skip rules (configured `ignore` plus the built-in `node_modules/**` and by `db::query` and the `dirsql_path` module). All three SDKs and the CLI get the same answers with no per-binding surface and no SDK signature changes. +**Path-table braces follow bash brace expansion (#1262) — parity by +construction, no drift.** A comma group alternates, a `..` range expands, and +any other brace is literal text. The expansion runs in the shared Rust core +(`brace::expand`, called by `scanner::compile_glob`), so all three SDKs and the +CLI match the same files with no per-binding surface and no SDK signature +changes. Config `[[table]] glob` does not use it yet. + **Gitignore-by-default in path-tables (#742) — at parity, opt-out restored (#745/#746).** Path-table scans respect `.gitignore` files by default (hierarchical, traversal-pruned, hidden files still scanned). The *behavior* diff --git a/docs/reference/path-tables.md b/docs/reference/path-tables.md index be847093c..31cef083e 100644 --- a/docs/reference/path-tables.md +++ b/docs/reference/path-tables.md @@ -52,6 +52,13 @@ depth, as in the shell. A path containing `*`, `?`, `[` or `{` is a glob and is used exactly as written: `*` matches within a single directory, `**` crosses directories. +Braces expand as bash expands them. A group holding a comma is an +alternation: `'./docs/{a,b}.md'` is `docs/a.md` and `docs/b.md`, and groups +nest. A group holding a `..` range counts: `'./log/{1..3}.txt'`, +`'./{a..c}.md'`, `'./{01..10}.md'` (zero-padded) and `'./{1..10..2}.md'` +(every second value). Any other brace is literal text, so `'./br/{q}.md'` +names the file `br/{q}.md`, and `\{` is always a literal brace. + The scan starts at the last directory named outright before the first glob component -- `'./small/*.md'` walks `small/` and nothing else -- so a query over one directory costs what `find ./small` costs, however large the diff --git a/packages/python/changelog.d/2026-10-05-brace-groups-follow-bash.md b/packages/python/changelog.d/2026-10-05-brace-groups-follow-bash.md new file mode 100644 index 000000000..27cefd4ab --- /dev/null +++ b/packages/python/changelog.d/2026-10-05-brace-groups-follow-bash.md @@ -0,0 +1,6 @@ +**Changed** Braces in a path-table follow bash brace expansion. A group with +a top-level comma (`{a,b}`, nested allowed) is an alternation, and a `..` +range (`{1..3}`, `{a..c}`, `{01..10}`, `{1..10..2}`) expands to each value. +Any other brace is literal text: `'./br/{q}.md'` names the file `br/{q}.md` +where it used to match `br/q.md`, and `{` or `}` without a partner no longer +fails to compile. Config `[[table]] glob` is unchanged. diff --git a/packages/python/migrations.d/2026-10-05-brace-groups-follow-bash.md b/packages/python/migrations.d/2026-10-05-brace-groups-follow-bash.md new file mode 100644 index 000000000..ca63b5613 --- /dev/null +++ b/packages/python/migrations.d/2026-10-05-brace-groups-follow-bash.md @@ -0,0 +1,39 @@ +### Core: path-table braces follow bash brace expansion + +#### Summary + +A path-table's braces now expand as bash expands them before globbing: a +group with a top-level comma is an alternation, a `..` range expands to each +value, and every other brace is literal text. `'./br/{q}.md'` used to match +`br/q.md` and now matches only a file named `br/{q}.md`; `'./a{1..3}.md'` +used to match nothing and now matches `a1.md`, `a2.md`, `a3.md`. Comma groups +behave as before. Every surface that resolves a path-table is affected (CLI, +REPL, HTTP server, SDKs, `--on-file`); config `[[table]] glob` is not. + +#### Required changes + +| Surface | Before | After | +| ------- | ------ | ----- | +| One-option group | `SELECT * FROM './br/{q}.md'` (matched `br/q.md`) | `SELECT * FROM './br/q.md'` | + +#### Deprecations removed + +_None._ + +#### Behavior changes without code changes + +- A brace group with neither a top-level comma nor a valid `..` range is + literal text. +- `{1..3}`, `{a..c}`, `{01..10}` and `{1..10..2}` expand to their values + instead of matching the literal text between the braces. +- An unpaired `{` or `}` is literal text instead of a glob compile error. + +#### Verification + +```python +# root holds q.md, {q}.md, a1.md and a2.md +rows = await DirSQL(root).query("SELECT path FROM './{q}.md'") +# expected: [{"path": "{q}.md"}] +rows = await DirSQL(root).query("SELECT path FROM './a{1..2}.md' ORDER BY path") +# expected: [{"path": "a1.md"}, {"path": "a2.md"}] +``` diff --git a/packages/rust/changelog.d/2026-10-05-brace-groups-follow-bash.md b/packages/rust/changelog.d/2026-10-05-brace-groups-follow-bash.md new file mode 100644 index 000000000..27cefd4ab --- /dev/null +++ b/packages/rust/changelog.d/2026-10-05-brace-groups-follow-bash.md @@ -0,0 +1,6 @@ +**Changed** Braces in a path-table follow bash brace expansion. A group with +a top-level comma (`{a,b}`, nested allowed) is an alternation, and a `..` +range (`{1..3}`, `{a..c}`, `{01..10}`, `{1..10..2}`) expands to each value. +Any other brace is literal text: `'./br/{q}.md'` names the file `br/{q}.md` +where it used to match `br/q.md`, and `{` or `}` without a partner no longer +fails to compile. Config `[[table]] glob` is unchanged. diff --git a/packages/rust/migrations.d/2026-10-05-brace-groups-follow-bash.md b/packages/rust/migrations.d/2026-10-05-brace-groups-follow-bash.md new file mode 100644 index 000000000..4fc0b91d6 --- /dev/null +++ b/packages/rust/migrations.d/2026-10-05-brace-groups-follow-bash.md @@ -0,0 +1,40 @@ +### Core: path-table braces follow bash brace expansion + +#### Summary + +A path-table's braces now expand as bash expands them before globbing: a +group with a top-level comma is an alternation, a `..` range expands to each +value, and every other brace is literal text. `'./br/{q}.md'` used to match +`br/q.md` and now matches only a file named `br/{q}.md`; `'./a{1..3}.md'` +used to match nothing and now matches `a1.md`, `a2.md`, `a3.md`. Comma groups +behave as before. Every surface that resolves a path-table is affected (CLI, +REPL, HTTP server, SDKs, `--on-file`); config `[[table]] glob` is not. + +#### Required changes + +| Surface | Before | After | +| ------- | ------ | ----- | +| One-option group | `SELECT * FROM './br/{q}.md'` (matched `br/q.md`) | `SELECT * FROM './br/q.md'` | + +#### Deprecations removed + +_None._ + +#### Behavior changes without code changes + +- A brace group with neither a top-level comma nor a valid `..` range is + literal text. +- `{1..3}`, `{a..c}`, `{01..10}` and `{1..10..2}` expand to their values + instead of matching the literal text between the braces. +- An unpaired `{` or `}` is literal text instead of a glob compile error. + +#### Verification + +```bash +cd "$(mktemp -d)" +touch q.md '{q}.md' a1.md a2.md +dirsql query "SELECT path FROM './{q}.md'" +# expected: [{"path":"{q}.md"}] +dirsql query "SELECT path FROM './a{1..2}.md' ORDER BY path" +# expected: [{"path":"a1.md"},{"path":"a2.md"}] +``` diff --git a/packages/rust/src/brace.rs b/packages/rust/src/brace.rs new file mode 100644 index 000000000..d4f10e54c --- /dev/null +++ b/packages/rust/src/brace.rs @@ -0,0 +1,443 @@ +//! Bash brace expansion for path globs. +//! +//! A pattern expands the way bash expands a word before globbing it: a brace +//! group holding a top-level comma is an alternation, one holding a `..` +//! sequence is a range, and any other brace is literal text. The expansions +//! come back as globset patterns with every remaining brace escaped, so +//! globset never sees an alternation of its own. + +/// Expand `pattern`'s brace groups as bash does, one globset pattern per word. +pub(crate) fn expand(pattern: &str) -> Vec { + let chars: Vec = pattern.chars().collect(); + expand_word(&chars) + .iter() + .map(|word| escape_braces(word)) + .collect() +} + +#[derive(Clone, Copy, PartialEq)] +enum Stop { + Open, + Close, + Comma, +} + +/// Index of the first unescaped `stop` at nesting level zero, scanning from +/// `i`; `text.len()` when there is none. A `}` only closes once a comma or a +/// `..` has been seen, which is why `{q}` never closes. +fn gobble(text: &[char], mut i: usize, stop: Stop) -> usize { + let target = match stop { + Stop::Open => '{', + Stop::Close => '}', + Stop::Comma => ',', + }; + let mut level = 0usize; + let mut separators = usize::from(stop != Stop::Close); + while i < text.len() { + let c = text[i]; + if c == '\\' { + i += 2; + continue; + } + if c == target && level == 0 && separators > 0 { + if stop == Stop::Open && is_shell_brace(text, i) { + i += 1; + continue; + } + return i; + } + if c == '{' { + level += 1; + } else if c == '}' && level > 0 { + level -= 1; + } else if stop == Stop::Close + && level == 0 + && (c == ',' || (text[i..].starts_with(&['.', '.']) && text.get(i + 2) != Some(&'}'))) + { + separators += 1; + } + i += 1; + } + text.len() +} + +/// Bash never opens a group at a `{` that starts a word-like token and is +/// followed by whitespace or `}`, so `{}` stays intact for `find -exec`. +fn is_shell_brace(text: &[char], i: usize) -> bool { + let after_space = i == 0 || text[i - 1].is_ascii_whitespace(); + let before_space_or_close = text + .get(i + 1) + .is_some_and(|c| c.is_ascii_whitespace() || *c == '}'); + after_space && before_space_or_close +} + +fn expand_word(text: &[char]) -> Vec { + let mut i = 0; + let (open, close) = loop { + i = gobble(text, i, Stop::Open); + if i >= text.len() { + return vec![text.iter().collect()]; + } + let close = gobble(text, i + 1, Stop::Close); + if close < text.len() { + break (i, close); + } + i += 1; + }; + let preamble: String = text[..open].iter().collect(); + let amble = &text[open + 1..close]; + let middle = if has_comma(amble) { + expand_alternatives(amble) + } else { + let amble: String = amble.iter().collect(); + sequence(&amble).unwrap_or_else(|| vec![format!("{{{amble}}}")]) + }; + let rest = expand_word(&text[close + 1..]); + middle + .iter() + .flat_map(|m| { + rest.iter() + .map(|r| format!("{preamble}{m}{r}")) + .collect::>() + }) + .collect() +} + +fn has_comma(amble: &[char]) -> bool { + let mut i = 0; + while i < amble.len() { + match amble[i] { + '\\' => i += 2, + ',' => return true, + _ => i += 1, + } + } + false +} + +fn expand_alternatives(amble: &[char]) -> Vec { + let mut words = Vec::new(); + let mut start = 0; + loop { + let end = gobble(amble, start, Stop::Comma); + words.extend(expand_word(&amble[start..end])); + if end >= amble.len() { + return words; + } + start = end + 1; + } +} + +/// The `..` range bash expands `{lo..hi}` / `{lo..hi..step}` to, or `None` +/// when the text is not a range bash accepts. +fn sequence(amble: &str) -> Option> { + let (lhs, rhs) = amble.split_once("..")?; + if lhs.is_empty() || rhs.is_empty() { + return None; + } + let digits = number_prefix_len(rhs); + let (end, rhs_len) = if let Ok(n) = rhs[..digits].parse() { + (Term::Int(n), digits) + } else { + let c = rhs.as_bytes()[0]; + if !c.is_ascii_alphabetic() || !(rhs.len() == 1 || rhs.as_bytes()[1] == b'.') { + return None; + } + (Term::Char(c), 1) + }; + let step = match &rhs[rhs_len..] { + "" => 1, + tail => tail + .strip_prefix("..") + .filter(|s| !s.is_empty())? + .parse() + .ok()?, + }; + let start = if let Ok(n) = lhs.parse::() { + Term::Int(n) + } else if lhs.len() == 1 && lhs.as_bytes()[0].is_ascii_alphabetic() { + Term::Char(lhs.as_bytes()[0]) + } else { + return None; + }; + match (start, end) { + (Term::Int(lo), Term::Int(hi)) => { + let width = pad_width(lhs, &rhs[..rhs_len]); + Some( + steps(lo, hi, step)? + .map(|n| format!("{n:0width$}")) + .collect(), + ) + } + (Term::Char(lo), Term::Char(hi)) => Some( + steps(i64::from(lo), i64::from(hi), step)? + .filter_map(|n| u8::try_from(n).ok()) + .map(|b| char::from(b).to_string()) + .collect(), + ), + _ => None, + } +} + +enum Term { + Int(i64), + Char(u8), +} + +fn number_prefix_len(s: &str) -> usize { + let sign = usize::from(matches!(s.as_bytes()[0], b'+' | b'-')); + sign + s[sign..].bytes().take_while(u8::is_ascii_digit).count() +} + +/// Bash zero-pads every term to the widest end written with a leading zero. +fn pad_width(lhs: &str, rhs: &str) -> usize { + let padded = + |s: &str| (s.len() > 1 && s.starts_with('0')) || (s.len() > 2 && s.starts_with("-0")); + if padded(lhs) || padded(rhs) { + lhs.len().max(rhs.len()) + } else { + 0 + } +} + +/// `lo` to `hi` inclusive in steps of `|step|` (0 counts as 1), walking down +/// when `hi < lo`. `None` past the element count bash refuses to generate. +fn steps(lo: i64, hi: i64, step: i64) -> Option> { + let stride = i128::from(step).abs().max(1); + let distance = i128::from(hi) - i128::from(lo); + if distance.abs() / stride > i128::from(i32::MAX) - 3 { + return None; + } + let count = distance.abs() / stride + 1; + let signed = stride * distance.signum(); + Some((0..count).map(move |k| i128::from(lo) + k * signed)) +} + +/// Escape the braces bash left as literal text, so globset matches them +/// rather than reading an alternation. A complete `[...]` class is copied as +/// written; an unclosed one ends escaping so globset reports it as before. +fn escape_braces(word: &str) -> String { + let chars: Vec = word.chars().collect(); + let mut out = String::with_capacity(word.len()); + let mut i = 0; + while i < chars.len() { + match chars[i] { + '\\' if i + 1 < chars.len() => { + match chars[i + 1] { + '{' => out.push_str("[{]"), + '}' => out.push_str("[}]"), + ',' => out.push(','), + c => { + out.push('\\'); + out.push(c); + } + } + i += 2; + } + '[' => match class_end(&chars, i) { + Some(end) => { + out.extend(&chars[i..=end]); + i = end + 1; + } + None => { + out.extend(&chars[i..]); + return out; + } + }, + '{' => { + out.push_str("[{]"); + i += 1; + } + '}' => { + out.push_str("[}]"); + i += 1; + } + c => { + out.push(c); + i += 1; + } + } + } + out +} + +/// Index of the `]` closing the class globset opens at `open`, where a `]` +/// first in the class (after any `!` / `^`) is a member, not the close. +fn class_end(chars: &[char], open: usize) -> Option { + let mut i = open + 1; + if matches!(chars.get(i), Some('!' | '^')) { + i += 1; + } + if chars.get(i) == Some(&']') { + i += 1; + } + (i..chars.len()).find(|&j| chars[j] == ']') +} + +#[cfg(test)] +mod tests { + use super::*; + + fn words(pattern: &str) -> Vec { + expand_word(&pattern.chars().collect::>()) + } + + #[test] + fn a_group_without_a_comma_or_sequence_is_literal() { + for pattern in [ + "x{q}y", "x{}y", "x{a,b", "xa,b}y", "x{}}y", "x{{}}y", "{},a}", + ] { + assert_eq!(words(pattern), vec![pattern], "{pattern}"); + } + } + + #[test] + fn a_comma_group_alternates_in_order() { + assert_eq!(words("x{a,b}y"), vec!["xay", "xby"]); + assert_eq!(words("{a,b}{c,d}"), vec!["ac", "ad", "bc", "bd"]); + } + + #[test] + fn empty_alternatives_are_kept() { + assert_eq!(words("x{,a}y"), vec!["xy", "xay"]); + assert_eq!(words("x{a,}y"), vec!["xay", "xy"]); + assert_eq!(words("x{,}y"), vec!["xy", "xy"]); + } + + #[test] + fn nested_groups_expand_inside_out_as_bash_does() { + assert_eq!(words("x{a,{b,c}}y"), vec!["xay", "xby", "xcy"]); + assert_eq!(words("x{a{b,c}}y"), vec!["x{ab}y", "x{ac}y"]); + assert_eq!(words("x{{a,b}}y"), vec!["x{a}y", "x{b}y"]); + assert_eq!(words("x{a,{b}}y"), vec!["xay", "x{b}y"]); + assert_eq!(words("x{{},a}y"), vec!["x{}y", "xay"]); + assert_eq!(words("x{}a,b}y"), vec!["x}ay", "xby"]); + assert_eq!(words("x{..{a,b}}y"), vec!["x..ay", "x..by"]); + } + + #[test] + fn stray_braces_beside_a_group_are_literal() { + assert_eq!(words("x{{a,b}y"), vec!["x{ay", "x{by"]); + assert_eq!(words("x{a,b}}y"), vec!["xa}y", "xb}y"]); + assert_eq!(words("x{a,}}y"), vec!["xa}y", "x}y"]); + assert_eq!(words("xx{a..}y,b}"), vec!["xxa..}y", "xxb"]); + } + + #[test] + fn an_escaped_brace_or_comma_does_not_count() { + assert_eq!(words(r"x\{a,b}y"), vec![r"x\{a,b}y"]); + assert_eq!(words(r"x{a\,b}y"), vec![r"x{a\,b}y"]); + assert_eq!(words(r"x{a\,b,c}y"), vec![r"xa\,by", "xcy"]); + assert_eq!(words(r"x{\}a,b}y"), vec![r"x\}ay", "xby"]); + } + + #[test] + fn a_brace_before_whitespace_or_a_close_after_whitespace_never_opens() { + assert_eq!(words("x {},a}y"), vec!["x {},a}y"]); + assert_eq!(words("{ a,b}"), vec!["{ a,b}"]); + assert_eq!(words("x{ a,b}"), vec!["x a", "xb"]); + } + + #[test] + fn integer_sequences_count_either_way() { + assert_eq!(words("x{1..3}y"), vec!["x1y", "x2y", "x3y"]); + assert_eq!(words("x{3..1}"), vec!["x3", "x2", "x1"]); + assert_eq!(words("{-2..1}"), vec!["-2", "-1", "0", "1"]); + assert_eq!(words("{+1..3}"), vec!["1", "2", "3"]); + assert_eq!(words("{1..-1}"), vec!["1", "0", "-1"]); + assert_eq!(words("{1..1}"), vec!["1"]); + } + + #[test] + fn a_step_is_taken_by_magnitude() { + assert_eq!(words("{1..10..3}"), vec!["1", "4", "7", "10"]); + assert_eq!(words("{10..1..3}"), vec!["10", "7", "4", "1"]); + assert_eq!(words("{1..3..-1}"), vec!["1", "2", "3"]); + assert_eq!(words("{1..3..0}"), vec!["1", "2", "3"]); + } + + #[test] + fn a_leading_zero_pads_every_term() { + assert_eq!(words("{08..10}"), vec!["08", "09", "10"]); + assert_eq!(words("{1..03}"), vec!["01", "02", "03"]); + assert_eq!(words("{001..2}"), vec!["001", "002"]); + assert_eq!(words("{-01..1}"), vec!["-01", "000", "001"]); + assert_eq!(words("{0..10..5}"), vec!["0", "5", "10"]); + assert_eq!(words("{-0..10..5}"), vec!["0", "5", "10"]); + } + + #[test] + fn letter_sequences_walk_the_alphabet() { + assert_eq!(words("{a..c}"), vec!["a", "b", "c"]); + assert_eq!(words("{e..a..2}"), vec!["e", "c", "a"]); + assert_eq!(words("{Y..[}"), vec!["{Y..[}"]); + } + + #[test] + fn a_malformed_sequence_is_literal() { + for pattern in [ + "{1..a}", + "{ab..c}", + "{1...3}", + "{a..}", + "{..a}", + "{1..3..}", + "{ab..cd}", + "{1..3...}", + "{1..3.}", + "{1..3..x}", + "{é..f}", + "{9223372036854775807..9223372036854775808}", + ] { + assert_eq!(words(pattern), vec![pattern], "{pattern}"); + } + } + + #[test] + fn a_malformed_sequence_leaves_later_groups_expanding() { + assert_eq!(words("x{1..z}{a,b}"), vec!["x{1..z}a", "x{1..z}b"]); + assert_eq!(words("x{q}{1..2}"), vec!["x{q}1", "x{q}2"]); + } + + #[test] + fn a_comma_beside_dots_makes_an_alternation_not_a_range() { + assert_eq!(words("{a..c,d}"), vec!["a..c", "d"]); + assert_eq!(words("{1..3,}"), vec!["1..3", ""]); + } + + #[test] + fn a_range_bash_refuses_to_generate_stays_literal() { + assert!(steps(0, i64::from(i32::MAX), 1).is_none()); + let widest = steps(0, i64::from(i32::MAX) - 4, 1).unwrap(); + assert_eq!(widest.size_hint().0, i32::MAX as usize - 3); + } + + #[test] + fn literal_braces_are_escaped_for_globset() { + assert_eq!(escape_braces("x{q}y"), "x[{]q[}]y"); + assert_eq!(escape_braces(r"x\{a,b\}"), "x[{]a,b[}]"); + assert_eq!(escape_braces(r"a\,b"), "a,b"); + assert_eq!(escape_braces(r"a\*b\"), r"a\*b\"); + } + + #[test] + fn a_class_is_copied_as_written() { + assert_eq!(escape_braces("[{}]x"), "[{}]x"); + assert_eq!(escape_braces("[]{]{"), "[]{][{]"); + assert_eq!(escape_braces("[!]{]{"), "[!]{][{]"); + assert_eq!(escape_braces("[^}]}"), "[^}][}]"); + } + + #[test] + fn an_unclosed_class_ends_escaping() { + assert_eq!(escape_braces("x[{q}"), "x[{q}"); + assert_eq!(escape_braces("x[]{"), "x[]{"); + } + + #[test] + fn expand_escapes_every_word() { + assert_eq!(expand("{a,{b}}.md"), vec!["a.md", "[{]b[}].md"]); + assert_eq!(expand("{q}"), vec!["[{]q[}]"]); + assert_eq!(expand("plain/*.md"), vec!["plain/*.md"]); + } +} diff --git a/packages/rust/src/lib.rs b/packages/rust/src/lib.rs index 15ad54e91..d4bae4cea 100644 --- a/packages/rust/src/lib.rs +++ b/packages/rust/src/lib.rs @@ -7,6 +7,7 @@ //! bindings in this workspace can reach them, but they are not part of the //! stable public API. +mod brace; /// Reusable command runner backing the command-backed events. pub mod command; #[doc(hidden)] diff --git a/packages/rust/src/scanner.rs b/packages/rust/src/scanner.rs index 002f3d8da..085de1ac5 100644 --- a/packages/rust/src/scanner.rs +++ b/packages/rust/src/scanner.rs @@ -135,7 +135,9 @@ fn is_wanted(glob: &GlobSet, ignore: &TableMatcher, rel_path: &Path) -> bool { /// silently recurse. `**` still crosses separators. pub fn compile_glob(pattern: &str) -> Result { let mut builder = GlobSetBuilder::new(); - builder.add(GlobBuilder::new(pattern).literal_separator(true).build()?); + for word in crate::brace::expand(pattern) { + builder.add(GlobBuilder::new(&word).literal_separator(true).build()?); + } builder.build() } diff --git a/packages/ts/changelog.d/2026-10-05-brace-groups-follow-bash.md b/packages/ts/changelog.d/2026-10-05-brace-groups-follow-bash.md new file mode 100644 index 000000000..27cefd4ab --- /dev/null +++ b/packages/ts/changelog.d/2026-10-05-brace-groups-follow-bash.md @@ -0,0 +1,6 @@ +**Changed** Braces in a path-table follow bash brace expansion. A group with +a top-level comma (`{a,b}`, nested allowed) is an alternation, and a `..` +range (`{1..3}`, `{a..c}`, `{01..10}`, `{1..10..2}`) expands to each value. +Any other brace is literal text: `'./br/{q}.md'` names the file `br/{q}.md` +where it used to match `br/q.md`, and `{` or `}` without a partner no longer +fails to compile. Config `[[table]] glob` is unchanged. diff --git a/packages/ts/migrations.d/2026-10-05-brace-groups-follow-bash.md b/packages/ts/migrations.d/2026-10-05-brace-groups-follow-bash.md new file mode 100644 index 000000000..aa4fd8b87 --- /dev/null +++ b/packages/ts/migrations.d/2026-10-05-brace-groups-follow-bash.md @@ -0,0 +1,39 @@ +### Core: path-table braces follow bash brace expansion + +#### Summary + +A path-table's braces now expand as bash expands them before globbing: a +group with a top-level comma is an alternation, a `..` range expands to each +value, and every other brace is literal text. `'./br/{q}.md'` used to match +`br/q.md` and now matches only a file named `br/{q}.md`; `'./a{1..3}.md'` +used to match nothing and now matches `a1.md`, `a2.md`, `a3.md`. Comma groups +behave as before. Every surface that resolves a path-table is affected (CLI, +REPL, HTTP server, SDKs, `--on-file`); config `[[table]] glob` is not. + +#### Required changes + +| Surface | Before | After | +| ------- | ------ | ----- | +| One-option group | `SELECT * FROM './br/{q}.md'` (matched `br/q.md`) | `SELECT * FROM './br/q.md'` | + +#### Deprecations removed + +_None._ + +#### Behavior changes without code changes + +- A brace group with neither a top-level comma nor a valid `..` range is + literal text. +- `{1..3}`, `{a..c}`, `{01..10}` and `{1..10..2}` expand to their values + instead of matching the literal text between the braces. +- An unpaired `{` or `}` is literal text instead of a glob compile error. + +#### Verification + +```ts +// root holds q.md, {q}.md, a1.md and a2.md +const literal = await db.query("SELECT path FROM './{q}.md'"); +// expected: [{ path: "{q}.md" }] +const range = await db.query("SELECT path FROM './a{1..2}.md' ORDER BY path"); +// expected: [{ path: "a1.md" }, { path: "a2.md" }] +``` From c43d3702dd549ecf0eaf9f2c517d951fb98ba433 Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Mon, 5 Oct 2026 08:14:37 -0400 Subject: [PATCH 03/21] e2e attestation for claude/1262-brace-literal --- .../python/e2e-attestations/claude-1262-brace-literal.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 packages/python/e2e-attestations/claude-1262-brace-literal.json diff --git a/packages/python/e2e-attestations/claude-1262-brace-literal.json b/packages/python/e2e-attestations/claude-1262-brace-literal.json new file mode 100644 index 000000000..deee1d4ce --- /dev/null +++ b/packages/python/e2e-attestations/claude-1262-brace-literal.json @@ -0,0 +1,7 @@ +{ + "command": "just test-e2e", + "ran_at": 1791202477, + "exit_code": 0, + "commit": "cd8c60dce85e4d60b9d7bd99bec351ba0603494c", + "branch": "claude/1262-brace-literal" +} From 671b75c132a209971961e5e6881cfb8394e28b07 Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Mon, 5 Oct 2026 08:15:04 -0400 Subject: [PATCH 04/21] e2e attestation for claude/1262-brace-literal --- .../ts/e2e-attestations/claude-1262-brace-literal.json | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 packages/ts/e2e-attestations/claude-1262-brace-literal.json diff --git a/packages/ts/e2e-attestations/claude-1262-brace-literal.json b/packages/ts/e2e-attestations/claude-1262-brace-literal.json new file mode 100644 index 000000000..bc00e65ad --- /dev/null +++ b/packages/ts/e2e-attestations/claude-1262-brace-literal.json @@ -0,0 +1,7 @@ +{ + "command": "pnpm test:e2e", + "ran_at": 1791202504, + "exit_code": 0, + "commit": "c43d3702dd549ecf0eaf9f2c517d951fb98ba433", + "branch": "claude/1262-brace-literal" +} From cf516e957cfb5cfb0e8227b2343588ae2b683238 Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Mon, 5 Oct 2026 08:57:14 -0400 Subject: [PATCH 05/21] Scan braces with iterators so every scan terminates --- packages/rust/src/brace.rs | 130 +++++++++++++++---------------------- 1 file changed, 52 insertions(+), 78 deletions(-) diff --git a/packages/rust/src/brace.rs b/packages/rust/src/brace.rs index d4f10e54c..e00354869 100644 --- a/packages/rust/src/brace.rs +++ b/packages/rust/src/brace.rs @@ -17,46 +17,48 @@ pub(crate) fn expand(pattern: &str) -> Vec { #[derive(Clone, Copy, PartialEq)] enum Stop { - Open, Close, Comma, } +/// The characters of `text` from `from` on that no backslash escapes, with +/// their indices; the backslashes themselves are dropped too. +fn unescaped(text: &[char], from: usize) -> impl Iterator + '_ { + let mut escaped = false; + text.iter() + .copied() + .enumerate() + .skip(from) + .filter(move |&(_, c)| { + let keep = !escaped && c != '\\'; + escaped = !escaped && c == '\\'; + keep + }) +} + /// Index of the first unescaped `stop` at nesting level zero, scanning from -/// `i`; `text.len()` when there is none. A `}` only closes once a comma or a -/// `..` has been seen, which is why `{q}` never closes. -fn gobble(text: &[char], mut i: usize, stop: Stop) -> usize { +/// `from`; `text.len()` when there is none. A `}` only closes once a comma or +/// a `..` has been seen, which is why `{q}` never closes. +fn gobble(text: &[char], from: usize, stop: Stop) -> usize { let target = match stop { - Stop::Open => '{', Stop::Close => '}', Stop::Comma => ',', }; let mut level = 0usize; - let mut separators = usize::from(stop != Stop::Close); - while i < text.len() { - let c = text[i]; - if c == '\\' { - i += 2; - continue; - } - if c == target && level == 0 && separators > 0 { - if stop == Stop::Open && is_shell_brace(text, i) { - i += 1; - continue; - } + let mut closable = stop != Stop::Close; + for (i, c) in unescaped(text, from) { + if c == target && level == 0 && closable { return i; } if c == '{' { level += 1; } else if c == '}' && level > 0 { level -= 1; - } else if stop == Stop::Close - && level == 0 + } else if level == 0 && (c == ',' || (text[i..].starts_with(&['.', '.']) && text.get(i + 2) != Some(&'}'))) { - separators += 1; + closable = true; } - i += 1; } text.len() } @@ -72,21 +74,18 @@ fn is_shell_brace(text: &[char], i: usize) -> bool { } fn expand_word(text: &[char]) -> Vec { - let mut i = 0; - let (open, close) = loop { - i = gobble(text, i, Stop::Open); - if i >= text.len() { - return vec![text.iter().collect()]; - } - let close = gobble(text, i + 1, Stop::Close); - if close < text.len() { - break (i, close); - } - i += 1; + let group = unescaped(text, 0) + .filter(|&(i, c)| c == '{' && !is_shell_brace(text, i)) + .find_map(|(open, _)| { + let close = gobble(text, open + 1, Stop::Close); + (close < text.len()).then_some((open, close)) + }); + let Some((open, close)) = group else { + return vec![text.iter().collect()]; }; let preamble: String = text[..open].iter().collect(); let amble = &text[open + 1..close]; - let middle = if has_comma(amble) { + let middle = if unescaped(amble, 0).any(|(_, c)| c == ',') { expand_alternatives(amble) } else { let amble: String = amble.iter().collect(); @@ -103,29 +102,13 @@ fn expand_word(text: &[char]) -> Vec { .collect() } -fn has_comma(amble: &[char]) -> bool { - let mut i = 0; - while i < amble.len() { - match amble[i] { - '\\' => i += 2, - ',' => return true, - _ => i += 1, - } - } - false -} - fn expand_alternatives(amble: &[char]) -> Vec { - let mut words = Vec::new(); - let mut start = 0; - loop { - let end = gobble(amble, start, Stop::Comma); - words.extend(expand_word(&amble[start..end])); - if end >= amble.len() { - return words; - } - start = end + 1; + let end = gobble(amble, 0, Stop::Comma); + let mut words = expand_word(&amble[..end]); + if let Some(tail) = amble.get(end + 1..) { + words.extend(expand_alternatives(tail)); } + words } /// The `..` range bash expands `{lo..hi}` / `{lo..hi..step}` to, or `None` @@ -219,43 +202,34 @@ fn steps(lo: i64, hi: i64, step: i64) -> Option> { fn escape_braces(word: &str) -> String { let chars: Vec = word.chars().collect(); let mut out = String::with_capacity(word.len()); - let mut i = 0; - while i < chars.len() { - match chars[i] { - '\\' if i + 1 < chars.len() => { - match chars[i + 1] { + let mut resume = 0; + for (i, &c) in chars.iter().enumerate() { + if i < resume { + continue; + } + match (c, chars.get(i + 1)) { + ('\\', Some(&next)) => { + match next { '{' => out.push_str("[{]"), '}' => out.push_str("[}]"), ',' => out.push(','), - c => { - out.push('\\'); - out.push(c); - } + _ => out.extend(['\\', next]), } - i += 2; + resume = i + 2; } - '[' => match class_end(&chars, i) { + ('[', _) => match class_end(&chars, i) { Some(end) => { out.extend(&chars[i..=end]); - i = end + 1; + resume = end + 1; } None => { out.extend(&chars[i..]); return out; } }, - '{' => { - out.push_str("[{]"); - i += 1; - } - '}' => { - out.push_str("[}]"); - i += 1; - } - c => { - out.push(c); - i += 1; - } + ('{', _) => out.push_str("[{]"), + ('}', _) => out.push_str("[}]"), + _ => out.push(c), } } out From 6ae735eb444ea5aa39ed8fc7bc10c11eec116971 Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Mon, 5 Oct 2026 09:06:09 -0400 Subject: [PATCH 06/21] e2e attestation for claude/1262-brace-literal --- .../python/e2e-attestations/claude-1262-brace-literal.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/python/e2e-attestations/claude-1262-brace-literal.json b/packages/python/e2e-attestations/claude-1262-brace-literal.json index deee1d4ce..34ba7d7a8 100644 --- a/packages/python/e2e-attestations/claude-1262-brace-literal.json +++ b/packages/python/e2e-attestations/claude-1262-brace-literal.json @@ -1,7 +1,7 @@ { "command": "just test-e2e", - "ran_at": 1791202477, + "ran_at": 1791205569, "exit_code": 0, - "commit": "cd8c60dce85e4d60b9d7bd99bec351ba0603494c", + "commit": "cf516e957cfb5cfb0e8227b2343588ae2b683238", "branch": "claude/1262-brace-literal" } From 6809016e5b21b3ef3fd72c5363489836b3f11d94 Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Mon, 5 Oct 2026 09:07:26 -0400 Subject: [PATCH 07/21] e2e attestation for claude/1262-brace-literal --- packages/ts/e2e-attestations/claude-1262-brace-literal.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ts/e2e-attestations/claude-1262-brace-literal.json b/packages/ts/e2e-attestations/claude-1262-brace-literal.json index bc00e65ad..b9c4edc6c 100644 --- a/packages/ts/e2e-attestations/claude-1262-brace-literal.json +++ b/packages/ts/e2e-attestations/claude-1262-brace-literal.json @@ -1,7 +1,7 @@ { "command": "pnpm test:e2e", - "ran_at": 1791202504, + "ran_at": 1791205646, "exit_code": 0, - "commit": "c43d3702dd549ecf0eaf9f2c517d951fb98ba433", + "commit": "6ae735eb444ea5aa39ed8fc7bc10c11eec116971", "branch": "claude/1262-brace-literal" } From 38fe559ccef8f94beae2559586f0a6ee3c919c0f Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Mon, 5 Oct 2026 09:42:38 -0400 Subject: [PATCH 08/21] Pin the sequence guards and bash's element-count limit --- packages/rust/src/brace.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/packages/rust/src/brace.rs b/packages/rust/src/brace.rs index e00354869..dde21d15e 100644 --- a/packages/rust/src/brace.rs +++ b/packages/rust/src/brace.rs @@ -382,8 +382,16 @@ mod tests { #[test] fn a_range_bash_refuses_to_generate_stays_literal() { assert!(steps(0, i64::from(i32::MAX), 1).is_none()); - let widest = steps(0, i64::from(i32::MAX) - 4, 1).unwrap(); - assert_eq!(widest.size_hint().0, i32::MAX as usize - 3); + assert!(steps(0, i64::from(i32::MAX) - 2, 1).is_none()); + let widest = steps(0, i64::from(i32::MAX) - 3, 1).unwrap(); + assert_eq!(widest.size_hint().0, i32::MAX as usize - 2); + assert!(steps(0, i64::from(i32::MAX), 2).is_some()); + } + + #[test] + fn a_range_missing_either_end_is_not_a_sequence() { + assert!(sequence("a..").is_none()); + assert!(sequence("..a").is_none()); } #[test] From c48199ef8a77388d010d60bcc2c7fe820b37fa7b Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Mon, 5 Oct 2026 12:04:54 -0400 Subject: [PATCH 09/21] e2e attestation for claude/1262-brace-literal --- .../python/e2e-attestations/claude-1262-brace-literal.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/python/e2e-attestations/claude-1262-brace-literal.json b/packages/python/e2e-attestations/claude-1262-brace-literal.json index 34ba7d7a8..acab5454a 100644 --- a/packages/python/e2e-attestations/claude-1262-brace-literal.json +++ b/packages/python/e2e-attestations/claude-1262-brace-literal.json @@ -1,7 +1,7 @@ { "command": "just test-e2e", - "ran_at": 1791205569, + "ran_at": 1791216294, "exit_code": 0, - "commit": "cf516e957cfb5cfb0e8227b2343588ae2b683238", + "commit": "84bebad9f3348dfcba0736c6416f0a0556bda766", "branch": "claude/1262-brace-literal" } From 14cd9912da88c50ce9b540bfecfda3e2bbaf5817 Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Mon, 5 Oct 2026 12:05:42 -0400 Subject: [PATCH 10/21] e2e attestation for claude/1262-brace-literal --- packages/ts/e2e-attestations/claude-1262-brace-literal.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ts/e2e-attestations/claude-1262-brace-literal.json b/packages/ts/e2e-attestations/claude-1262-brace-literal.json index b9c4edc6c..a7d445f3e 100644 --- a/packages/ts/e2e-attestations/claude-1262-brace-literal.json +++ b/packages/ts/e2e-attestations/claude-1262-brace-literal.json @@ -1,7 +1,7 @@ { "command": "pnpm test:e2e", - "ran_at": 1791205646, + "ran_at": 1791216342, "exit_code": 0, - "commit": "6ae735eb444ea5aa39ed8fc7bc10c11eec116971", + "commit": "c48199ef8a77388d010d60bcc2c7fe820b37fa7b", "branch": "claude/1262-brace-literal" } From 08dc87da8c076cd6d9edc546e22f457301f3cce5 Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Mon, 5 Oct 2026 13:30:26 -0400 Subject: [PATCH 11/21] e2e attestation for claude/1262-brace-literal --- .../python/e2e-attestations/claude-1262-brace-literal.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/python/e2e-attestations/claude-1262-brace-literal.json b/packages/python/e2e-attestations/claude-1262-brace-literal.json index acab5454a..1be58d698 100644 --- a/packages/python/e2e-attestations/claude-1262-brace-literal.json +++ b/packages/python/e2e-attestations/claude-1262-brace-literal.json @@ -1,7 +1,7 @@ { "command": "just test-e2e", - "ran_at": 1791216294, + "ran_at": 1791221426, "exit_code": 0, - "commit": "84bebad9f3348dfcba0736c6416f0a0556bda766", + "commit": "5e74090355d2d6aca7a45bd02e81868e6149f9a7", "branch": "claude/1262-brace-literal" } From b327ac7f75335e1ac8de661e8b978848a0da0103 Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Mon, 5 Oct 2026 13:31:21 -0400 Subject: [PATCH 12/21] e2e attestation for claude/1262-brace-literal --- packages/ts/e2e-attestations/claude-1262-brace-literal.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ts/e2e-attestations/claude-1262-brace-literal.json b/packages/ts/e2e-attestations/claude-1262-brace-literal.json index a7d445f3e..fc8023eda 100644 --- a/packages/ts/e2e-attestations/claude-1262-brace-literal.json +++ b/packages/ts/e2e-attestations/claude-1262-brace-literal.json @@ -1,7 +1,7 @@ { "command": "pnpm test:e2e", - "ran_at": 1791216342, + "ran_at": 1791221481, "exit_code": 0, - "commit": "c48199ef8a77388d010d60bcc2c7fe820b37fa7b", + "commit": "08dc87da8c076cd6d9edc546e22f457301f3cce5", "branch": "claude/1262-brace-literal" } From 494fcdde007621442eb44d2734967da331e2a825 Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Mon, 5 Oct 2026 14:28:04 -0400 Subject: [PATCH 13/21] e2e attestation for claude/1262-brace-literal --- .../python/e2e-attestations/claude-1262-brace-literal.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/python/e2e-attestations/claude-1262-brace-literal.json b/packages/python/e2e-attestations/claude-1262-brace-literal.json index 1be58d698..a7052c154 100644 --- a/packages/python/e2e-attestations/claude-1262-brace-literal.json +++ b/packages/python/e2e-attestations/claude-1262-brace-literal.json @@ -1,7 +1,7 @@ { "command": "just test-e2e", - "ran_at": 1791221426, + "ran_at": 1791224884, "exit_code": 0, - "commit": "5e74090355d2d6aca7a45bd02e81868e6149f9a7", + "commit": "84f532654ed2a2581ec7843863233638ba22ac36", "branch": "claude/1262-brace-literal" } From 5ea17afc7d3a7204ba2a15f5e1cdec98a4293584 Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Mon, 5 Oct 2026 14:28:42 -0400 Subject: [PATCH 14/21] e2e attestation for claude/1262-brace-literal --- packages/ts/e2e-attestations/claude-1262-brace-literal.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ts/e2e-attestations/claude-1262-brace-literal.json b/packages/ts/e2e-attestations/claude-1262-brace-literal.json index fc8023eda..1c674ec75 100644 --- a/packages/ts/e2e-attestations/claude-1262-brace-literal.json +++ b/packages/ts/e2e-attestations/claude-1262-brace-literal.json @@ -1,7 +1,7 @@ { "command": "pnpm test:e2e", - "ran_at": 1791221481, + "ran_at": 1791224922, "exit_code": 0, - "commit": "08dc87da8c076cd6d9edc546e22f457301f3cce5", + "commit": "494fcdde007621442eb44d2734967da331e2a825", "branch": "claude/1262-brace-literal" } From ee1eb2a155cfc4eb05527e13ad12409aefe96dab Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Mon, 5 Oct 2026 17:09:56 -0400 Subject: [PATCH 15/21] e2e attestation for claude/1262-brace-literal --- .../python/e2e-attestations/claude-1262-brace-literal.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/python/e2e-attestations/claude-1262-brace-literal.json b/packages/python/e2e-attestations/claude-1262-brace-literal.json index a7052c154..d18deeb6e 100644 --- a/packages/python/e2e-attestations/claude-1262-brace-literal.json +++ b/packages/python/e2e-attestations/claude-1262-brace-literal.json @@ -1,7 +1,7 @@ { "command": "just test-e2e", - "ran_at": 1791224884, + "ran_at": 1791234595, "exit_code": 0, - "commit": "84f532654ed2a2581ec7843863233638ba22ac36", + "commit": "b3643163aa5fb94ef8a15e642cd0767ecd3588e0", "branch": "claude/1262-brace-literal" } From 42a8a9024aca38d35b3a77e5f693dc4d1a7017aa Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Mon, 5 Oct 2026 17:10:33 -0400 Subject: [PATCH 16/21] e2e attestation for claude/1262-brace-literal --- packages/ts/e2e-attestations/claude-1262-brace-literal.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ts/e2e-attestations/claude-1262-brace-literal.json b/packages/ts/e2e-attestations/claude-1262-brace-literal.json index 1c674ec75..aa65f7dd1 100644 --- a/packages/ts/e2e-attestations/claude-1262-brace-literal.json +++ b/packages/ts/e2e-attestations/claude-1262-brace-literal.json @@ -1,7 +1,7 @@ { "command": "pnpm test:e2e", - "ran_at": 1791224922, + "ran_at": 1791234633, "exit_code": 0, - "commit": "494fcdde007621442eb44d2734967da331e2a825", + "commit": "ee1eb2a155cfc4eb05527e13ad12409aefe96dab", "branch": "claude/1262-brace-literal" } From 47fcbcd9842bdf693a5c803fa67579671d0fbc6e Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Mon, 5 Oct 2026 17:10:53 -0400 Subject: [PATCH 17/21] Drop history from the unclosed-class comment --- packages/rust/src/brace.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/rust/src/brace.rs b/packages/rust/src/brace.rs index dde21d15e..b96fe195f 100644 --- a/packages/rust/src/brace.rs +++ b/packages/rust/src/brace.rs @@ -198,7 +198,7 @@ fn steps(lo: i64, hi: i64, step: i64) -> Option> { /// Escape the braces bash left as literal text, so globset matches them /// rather than reading an alternation. A complete `[...]` class is copied as -/// written; an unclosed one ends escaping so globset reports it as before. +/// written; an unclosed one ends escaping so globset reports the error. fn escape_braces(word: &str) -> String { let chars: Vec = word.chars().collect(); let mut out = String::with_capacity(word.len()); From 14262a3dfcae19b15904669a0c3d4592db0b802f Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Mon, 5 Oct 2026 17:12:39 -0400 Subject: [PATCH 18/21] e2e attestation for claude/1262-brace-literal --- .../python/e2e-attestations/claude-1262-brace-literal.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/python/e2e-attestations/claude-1262-brace-literal.json b/packages/python/e2e-attestations/claude-1262-brace-literal.json index d18deeb6e..3761bbb2a 100644 --- a/packages/python/e2e-attestations/claude-1262-brace-literal.json +++ b/packages/python/e2e-attestations/claude-1262-brace-literal.json @@ -1,7 +1,7 @@ { "command": "just test-e2e", - "ran_at": 1791234595, + "ran_at": 1791234759, "exit_code": 0, - "commit": "b3643163aa5fb94ef8a15e642cd0767ecd3588e0", + "commit": "47fcbcd9842bdf693a5c803fa67579671d0fbc6e", "branch": "claude/1262-brace-literal" } From 1efa33292f9c6f72df80290e08d2002a1f02a540 Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Mon, 5 Oct 2026 17:13:13 -0400 Subject: [PATCH 19/21] e2e attestation for claude/1262-brace-literal --- packages/ts/e2e-attestations/claude-1262-brace-literal.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ts/e2e-attestations/claude-1262-brace-literal.json b/packages/ts/e2e-attestations/claude-1262-brace-literal.json index aa65f7dd1..66ee7f980 100644 --- a/packages/ts/e2e-attestations/claude-1262-brace-literal.json +++ b/packages/ts/e2e-attestations/claude-1262-brace-literal.json @@ -1,7 +1,7 @@ { "command": "pnpm test:e2e", - "ran_at": 1791234633, + "ran_at": 1791234793, "exit_code": 0, - "commit": "ee1eb2a155cfc4eb05527e13ad12409aefe96dab", + "commit": "14262a3dfcae19b15904669a0c3d4592db0b802f", "branch": "claude/1262-brace-literal" } From f6ce938426f1c162ad69d5c52254660d2f0f416d Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Mon, 5 Oct 2026 18:48:29 -0400 Subject: [PATCH 20/21] e2e attestation for claude/1262-brace-literal --- .../python/e2e-attestations/claude-1262-brace-literal.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/python/e2e-attestations/claude-1262-brace-literal.json b/packages/python/e2e-attestations/claude-1262-brace-literal.json index 3761bbb2a..7e583b400 100644 --- a/packages/python/e2e-attestations/claude-1262-brace-literal.json +++ b/packages/python/e2e-attestations/claude-1262-brace-literal.json @@ -1,7 +1,7 @@ { "command": "just test-e2e", - "ran_at": 1791234759, + "ran_at": 1791240509, "exit_code": 0, - "commit": "47fcbcd9842bdf693a5c803fa67579671d0fbc6e", + "commit": "42886c665ab14434f259593bf7868455cf206eda", "branch": "claude/1262-brace-literal" } From c5c0cafa77b06747612bcb84a7e751d4c242bd24 Mon Sep 17 00:00:00 2001 From: thekevinbot <248217931+thekevinbot@users.noreply.github.com> Date: Mon, 5 Oct 2026 18:49:05 -0400 Subject: [PATCH 21/21] e2e attestation for claude/1262-brace-literal --- packages/ts/e2e-attestations/claude-1262-brace-literal.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ts/e2e-attestations/claude-1262-brace-literal.json b/packages/ts/e2e-attestations/claude-1262-brace-literal.json index 66ee7f980..1d04e2332 100644 --- a/packages/ts/e2e-attestations/claude-1262-brace-literal.json +++ b/packages/ts/e2e-attestations/claude-1262-brace-literal.json @@ -1,7 +1,7 @@ { "command": "pnpm test:e2e", - "ran_at": 1791234793, + "ran_at": 1791240545, "exit_code": 0, - "commit": "14262a3dfcae19b15904669a0c3d4592db0b802f", + "commit": "f6ce938426f1c162ad69d5c52254660d2f0f416d", "branch": "claude/1262-brace-literal" }