Skip to content

chore: bump version to v0.8.3#117

Merged
jahala merged 1 commit into
mainfrom
chore/bump-0.8.3
May 17, 2026
Merged

chore: bump version to v0.8.3#117
jahala merged 1 commit into
mainfrom
chore/bump-0.8.3

Conversation

@jahala

@jahala jahala commented May 17, 2026

Copy link
Copy Markdown
Owner

Summary

Patch release. Bumps `Cargo.toml` and `npm/package.json` to 0.8.3.

Included since v0.8.2

PR Type Description
#109 docs Community standards + Scorecard badge
#110 fix Walker stops at mount boundaries (NFS narrow)
#111 fix `--full` raises search match cap from 10 to 100 (issue #65)
#113 chore(deps) actions/checkout v4 → v6
#114 chore(deps) tree-sitter-scala 0.24 → 0.26
#115 chore(deps) tree-sitter-python 0.23.6 → 0.25
#116 chore(ci) Pin all GitHub Actions by commit SHA (Scorecard fix)

Test plan

  • `cargo build --release` clean
  • `cargo test --release` — 357 + 4 passing
  • `cargo clippy --release -- -D warnings` clean
  • `cargo fmt --check` clean
  • Benchmark spot-check (sonnet × 2 hard tasks × baseline+tilth): 100% correct, tilth −28% cost vs baseline
  • After merge: tag `v0.8.3` and push to trigger release workflow

🤖 Generated with Claude Code

Patch release: search cap fix, NFS walker narrow, dep refreshes,
community standards, SHA-pinned workflows.

Includes (since v0.8.2):
- fix(search): --full raises match cap from 10 to 100 (#65 / #111)
- fix(walker): stop at mount boundaries, recognize composer.json (#110)
- chore(deps): tree-sitter-scala 0.24 → 0.26 (#114)
- chore(deps): tree-sitter-python 0.23.6 → 0.25 (#115)
- chore(deps): actions/checkout v4 → v6 (#113)
- chore(ci): pin all GitHub Actions by commit SHA (#116)
- docs: community standards files + Scorecard badge (#109)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@jahala jahala merged commit c2334ee into main May 17, 2026
7 checks passed
@jahala jahala deleted the chore/bump-0.8.3 branch May 17, 2026 13:27
paulnsorensen referenced this pull request in paulnsorensen/tilth May 18, 2026
* perf(diff): parallelise overlay construction across files

Extracted from #61 part 4 (rayon parallelisation, diff overlay half).
Independent of the timeout (#82) and batch-edit work — touches only
src/diff/mod.rs.

`compute_overlay` is pure per-`FileDiff` work: each file independently
fetches old/new content, runs tree-sitter, and matches symbols. Switching
the overlay build from `.iter()` to `.par_iter()` lets multi-file diffs
fan out across cores — a meaningful win on large PRs where each file does
non-trivial AST work.

Thread safety: `compute_overlay` constructs its own
`tree_sitter::Parser::new()` per call inside
`lang::outline::get_outline_entries` — no shared mutable state crosses
worker boundaries. `FileDiff` and `DiffSource` are Send+Sync via auto
trait derivation.

Scope deliberately limited to `diff::diff()` — left the per-commit
overlay loop in `diff_log()` alone since each commit's overlay set is
typically small. Also left the `tool_read` batch parallelisation from
the original #61 out of this PR pending the deadline-preservation
discussion (review item #5).

Test plan
- cargo build — clean
- cargo test — 344 pass
- cargo clippy -- -D warnings — clean
- cargo fmt --check — clean

* feat(mcp): batch tilth_edit — accept files: [{path, edits}]

Extracted from #61 part 3 (batch tilth_edit). Independent of the timeout
(#82) and diff-overlay (#83) extractions; touches src/edit.rs (the new
apply_batch + FileEditTask), src/mcp.rs (new tool_edit + parsing helpers
+ schema), and AGENTS.md (user-facing docs).

What changes for callers
- tilth_edit now takes {"files": [{"path", "edits"}, ...]} instead of
  {"path", "edits"}. Each file is processed independently in parallel —
  a hash mismatch on one file no longer blocks its siblings.
- Per-file results render as Markdown sections (## <path>) joined by ---
  separators. Returns isError only when every file failed.
- Cap of 20 files per call, surfaced both in the JSON Schema (maxItems)
  and at runtime.

Why the schema break is safe
- MCP serves tools/list dynamically on every session; clients always get
  the current inputSchema. The LLM sees the new shape on next connect
  and adapts. There is no cached-schema problem to migrate around.

Review fixes from #61 layered in here
- #4 (duplicate paths → data corruption): detect_duplicate_paths now
  rejects the whole batch up front when two Ready tasks resolve to the
  same canonical path. Falls back to the literal path for files that
  don't exist yet, so the dedup key is still well-defined.
- Medium (parse_file_edit short-circuit): parse_edit_entry returns the
  failing edit index in its error message, so the LLM can fix exactly
  the right entry instead of guessing which edit was malformed.
- Medium (tests use tempfile::tempdir): new batch tests use tempdir
  rather than fixed-name paths in std::env::temp_dir.

Implementation shape
- src/edit.rs: pub apply_batch + pub FileEditTask::{Ready, ParseError},
  with apply_one / render_applied keeping the parallel closure trivial.
  apply_edits and EditResult are now private — callers go through
  apply_batch.
- src/mcp.rs: tool_edit shrinks from ~85 lines of inline JSON parsing to
  parse → cap-check → dedup-check → record_read → apply_batch. JSON
  parsing stays at the MCP wire boundary; parallel I/O and blast radius
  live next to apply_edits.

Test plan
- cargo build — clean
- cargo test — 348 pass (4 new in edit::tests covering two-file success,
  partial failure, all-failed, and parse-error surfacing — all using
  tempfile::tempdir)
- cargo clippy -- -D warnings — clean
- cargo fmt --check — clean

* refactor(error): adopt thiserror for Display/Error derives

NIH audit finding — replace 25 lines of mechanical write! formatting
in `src/error.rs` with `thiserror::Error` derives. Each variant gets
an inline `#[error(...)]` attribute; the manual `impl Display` and
empty `impl std::error::Error` blocks are removed.

Net: +11 / -40 in src/error.rs; one new dependency (thiserror v2).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* refactor(diff): adopt strsim::sorensen_dice for fuzzy symbol matching

NIH audit finding — replace hand-rolled `jaccard_similarity`
(whitespace-set Jaccard) in `src/diff/matching.rs` Phase 3 fuzzy
matcher with `strsim::sorensen_dice` (character-bigram). Same
[0.0, 1.0] range, same 0.8 threshold, no rescaling needed.

Net: +1 / -15 in src/diff/matching.rs; one new dependency (strsim).

Existing tests `fuzzy_match` and `below_fuzzy_threshold` still pass
at the unchanged 0.8 threshold.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* refactor(install): adopt home crate for cross-platform home dir lookup

Replace the cfg-split `$HOME` / `$USERPROFILE` block in `home_dir()` with
a one-liner around `home::home_dir()`. The `home` crate is maintained by
the cargo team itself (rust-lang/cargo) and handles a corner case the
hand-rolled version misses: an empty `$HOME` is treated as missing rather
than silently producing an empty `PathBuf` that joins onto relative paths.

Behavior preserved:
- Same env-var precedence ($HOME on unix, $USERPROFILE on windows).
- Wrapper kept (4 call sites) with an actionable error message that names
  both env vars.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* refactor(mcp): adopt percent-encoding crate for file:// URI decoding

Replace the hand-rolled `percent_decode` + `hex_val` helpers
(`src/mcp.rs`) with `percent_encoding::percent_decode_str`. Single call
site in `extract_root_from_response`, so the helpers are inlined rather
than wrapped.

The fallback contract on invalid UTF-8 is preserved: rather than
substituting U+FFFD replacement characters via `decode_utf8_lossy`,
fall back to the raw undecoded input so the subsequent `is_dir()` check
rejects mangled paths cleanly.

The unit-test for the deleted helper (`percent_decode_basic`) is
removed; end-to-end coverage of the same code path lives in
`extract_root_percent_encoded_uri`, which exercises a real `file://`
URI with `%20` resolving to a real directory.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* refactor(read): adopt strsim::levenshtein for filename / heading suggestions

Replace the 16-LOC hand-rolled Wagner-Fischer implementation in
`src/read/mod.rs` with a thin wrapper around `strsim::levenshtein`. Two
call sites — `suggest_similar` (filename "did you mean?") and
`suggest_headings` (markdown heading suggestion) — so the wrapper is
kept rather than inlined.

`strsim::levenshtein` wraps `generic_levenshtein` over `StringWrapper`,
which iterates `.chars()`. Same Unicode-scalar semantics as the
hand-rolled version: a single CJK or emoji glyph counts as one edit
unit, not 3-4 bytes. The `edit_distance_is_unicode_aware` regression
test (CJK + emoji + ASCII) is updated to lock in this contract — if
`strsim` ever switches to byte-level distance, the test fails loudly.

Note: PR #88 (`refactor-strsim` for diff fuzzy matching) also adds
`strsim = "0.11"`. Whichever lands first, the second can drop the
duplicate Cargo.toml line in a trivial rebase.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix: char-boundary panic in is_minified_by_name with multi-byte UTF-8

stem.len() - 4 is a byte offset, not a char offset — it can land
inside a CJK or emoji character, causing a panic. Use
char_indices().nth_back(3) to find the byte offset of the 4th
character from the end instead.

* Update dependencies in Cargo.toml

Fixed an error I did when manually resolving a conflict

* chore: bump version to v0.8.1

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(bloom): adopt fastbloom for BloomFilter implementation

NIH audit finding — drop ~105 LOC of hand-rolled bloom filter math
in `src/index/bloom.rs` (`double_hash`, `combined_hash`,
`hash_with_seed`, optimal-bit/hash sizing) in favor of `fastbloom`,
the SIMD-optimized de facto Rust crate.

The `extract_identifiers` byte state machine — the unique-to-tilth
piece — stays. `BloomFilterCache` now wraps `fastbloom::BloomFilter`
constructed via `with_false_pos(0.01).expected_items(n)`.

Drop `test_bloom_filter_sizing` — it asserted private fields
(`num_bits`, `num_hashes`) of the removed type. Equivalent guarantees
are part of fastbloom's own test suite.

Net: +5 / -133 in src/index/bloom.rs; one new dependency (fastbloom).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* refactor(mcp): extract per-request timeout into its own module

Extracted from #61 part 2 (timeout module). Depends on the Services
struct from #63 (already merged) for the per-instance ThreadTracker.

Moves the inline mpsc::recv_timeout machinery in handle_tool_call into
src/timeout.rs as spawn_with_timeout, backed by crossbeam-channel's
select! { default(timeout) => ... } — the sync-world equivalent of
Future.get(timeout, unit).

Two correctness changes vs. the original #61 timeout draft, addressing
the review feedback there:

- ThreadTracker::is_at_cap uses Ordering::Acquire (was Relaxed). Pairs
  with the Release in record_timeout / record_finish_after_timeout, so
  a load that observes the new count also observes any state the
  incrementing thread published before it.
- The deadline arm now CAS-claims the timeout *before* incrementing the
  tracker (was: increment, then CAS, then conditionally roll back). The
  worker, if it lost the CAS, spins on a new timeout_acked AtomicBool
  before decrementing — so the tracker can never go negative and a
  concurrent is_at_cap() can never observe an inflated count that gets
  rolled back. Closes the false-rejection window the reviewer flagged
  near the hard cap.

Also wires an upfront tracker.is_at_cap() check in handle_tool_call so
the server refuses new work cleanly under sustained pressure instead of
piling on more abandoned threads.

ThreadTracker is owned by Services rather than a static global, so unit
tests instantiate their own and don't serialise on shared state.

No behaviour change for the happy path: same 90s default, same
TILTH_TIMEOUT env override, same client-visible "tool timed out" /
"tool panicked" messages.

Test plan
- cargo build — clean
- cargo test — 349 pass (5 new in timeout::tests covering the CAS
  roundtrip, fast path, panic surfacing, saturated tracker, and env
  parsing)
- cargo clippy -- -D warnings — clean
- cargo fmt --check — clean

* fix(timeout): address PR 82 review feedback

- Switch tracker RMWs to AcqRel — canonical pessimistic ordering for an
  atomic counter read from another thread. Release/Acquire was sufficient
  for the counter value alone, but AcqRel documents the cross-thread
  contract more explicitly so the next reader doesn't have to re-derive
  that the value composes with the rest of the spawn state machine.
- Add std::thread::yield_now() after spin_loop() in wait_for_timeout_ack
  so a worker scheduled before the main thread on a single-CPU container
  surrenders the rest of its quantum instead of burning ~10ms before the
  ack becomes visible.
- Doc-comment ABANDONED_THREAD_WARN as a deliberate warn-once policy so a
  future maintainer doesn't switch the deadline arm's `==` check back to
  `>=`. The hard cap at MAX_ABANDONED_THREADS bounds the silence past the
  threshold.
- Mark SpawnFailure as #[non_exhaustive] so a future failure mode
  (e.g. OS-level thread spawn failure) can be added without churning
  every call site.
- Relax abandoned_counter_roundtrips_through_cas deadline from 2s to 5s
  to reduce flake risk on contended CI runners.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(edit): address PR 84 review — path dedup, empty edits, encapsulation

- Moves normalize_path_key and detect_duplicate_paths from mcp.rs to edit.rs
- Adds macOS case-insensitive APFS dedup (ASCII-lowercasing on cfg target_os)
- Adds runtime validation for empty edits array (was schema-only; now parse error)
- Moves dedup gate into apply_batch to prevent wire-layer bypass
- Simplifies apply_one/render_applied signatures (&Arc<T> → &T with deref-coercion)
- Updates AGENTS.md documentation (isError semantics, parse-error behavior)
- Adds 5 tests (case aliases, empty edits, integration gate)

Co-Authored-By: Claude <noreply@anthropic.com>

* chore: bump version to v0.8.2

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: oss-hygiene — supply-chain workflows and harden permissions

Add three supply-chain hygiene workflows and tighten GITHUB_TOKEN
permissions on existing workflows. All free for public repos.

New:
- .github/dependabot.yml — weekly version updates for cargo, npm, github-actions
- .github/workflows/dependency-review.yml — block PRs that introduce
  vulnerable or disallowed-license deps (free on public repos)
- .github/workflows/scorecard.yml — OpenSSF Scorecard analysis and
  badge publication

Hardened:
- ci.yml — add top-level `permissions: contents: read` (least privilege)
- release.yml — drop top-level `contents: write`; scope it to the `build`
  job that actually needs it for softprops/action-gh-release. Publish
  jobs use CARGO_REGISTRY_TOKEN / NPM_TOKEN secrets and don't need any
  GITHUB_TOKEN write scope.

Generated via the /oss-hygiene skill from skillz-that-grillz.

* chore(deps): bump github/codeql-action from 3 to 4

Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3 to 4.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](github/codeql-action@v3...v4)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore(deps): bump softprops/action-gh-release from 2 to 3

Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 2 to 3.
- [Release notes](https://github.com/softprops/action-gh-release/releases)
- [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md)
- [Commits](softprops/action-gh-release@v2...v3)

---
updated-dependencies:
- dependency-name: softprops/action-gh-release
  dependency-version: '3'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore(deps): bump actions/upload-artifact from 4 to 7

Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 7.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](actions/upload-artifact@v4...v7)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore(deps): bump actions/dependency-review-action from 4 to 5

Bumps [actions/dependency-review-action](https://github.com/actions/dependency-review-action) from 4 to 5.
- [Release notes](https://github.com/actions/dependency-review-action/releases)
- [Commits](actions/dependency-review-action@v4...v5)

---
updated-dependencies:
- dependency-name: actions/dependency-review-action
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore(deps): bump actions/setup-node from 4 to 6

Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4 to 6.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](actions/setup-node@v4...v6)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* fix(scorecard): pin to v2.4.3 — @v2 ref doesn't exist

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(deps): bump the cargo-minor-and-patch group with 7 updates

Bumps the cargo-minor-and-patch group with 7 updates:

| Package | From | To |
| --- | --- | --- |
| [clap](https://github.com/clap-rs/clap) | `4.5.60` | `4.6.1` |
| [clap_complete](https://github.com/clap-rs/clap) | `4.5.66` | `4.6.5` |
| [tree-sitter-rust](https://github.com/tree-sitter/tree-sitter-rust) | `0.24.0` | `0.24.2` |
| [tree-sitter-c](https://github.com/tree-sitter/tree-sitter-c) | `0.24.1` | `0.24.2` |
| [tree-sitter-c-sharp](https://github.com/tree-sitter/tree-sitter-c-sharp) | `0.23.1` | `0.23.5` |
| [tree-sitter-swift](https://github.com/alex-pinkus/tree-sitter-swift) | `0.7.1` | `0.7.2` |
| [rayon](https://github.com/rayon-rs/rayon) | `1.11.0` | `1.12.0` |


Updates `clap` from 4.5.60 to 4.6.1
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](clap-rs/clap@clap_complete-v4.5.60...clap_complete-v4.6.1)

Updates `clap_complete` from 4.5.66 to 4.6.5
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](clap-rs/clap@clap_complete-v4.5.66...clap_complete-v4.6.5)

Updates `tree-sitter-rust` from 0.24.0 to 0.24.2
- [Release notes](https://github.com/tree-sitter/tree-sitter-rust/releases)
- [Commits](tree-sitter/tree-sitter-rust@v0.24.0...v0.24.2)

Updates `tree-sitter-c` from 0.24.1 to 0.24.2
- [Release notes](https://github.com/tree-sitter/tree-sitter-c/releases)
- [Commits](tree-sitter/tree-sitter-c@v0.24.1...v0.24.2)

Updates `tree-sitter-c-sharp` from 0.23.1 to 0.23.5
- [Release notes](https://github.com/tree-sitter/tree-sitter-c-sharp/releases)
- [Commits](tree-sitter/tree-sitter-c-sharp@v0.23.1...v0.23.5)

Updates `tree-sitter-swift` from 0.7.1 to 0.7.2
- [Release notes](https://github.com/alex-pinkus/tree-sitter-swift/releases)
- [Commits](alex-pinkus/tree-sitter-swift@0.7.1...0.7.2)

Updates `rayon` from 1.11.0 to 1.12.0
- [Changelog](https://github.com/rayon-rs/rayon/blob/main/RELEASES.md)
- [Commits](rayon-rs/rayon@rayon-core-v1.11.0...rayon-core-v1.12.0)

---
updated-dependencies:
- dependency-name: clap
  dependency-version: 4.6.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: cargo-minor-and-patch
- dependency-name: clap_complete
  dependency-version: 4.6.5
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: cargo-minor-and-patch
- dependency-name: tree-sitter-rust
  dependency-version: 0.24.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo-minor-and-patch
- dependency-name: tree-sitter-c
  dependency-version: 0.24.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo-minor-and-patch
- dependency-name: tree-sitter-c-sharp
  dependency-version: 0.23.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo-minor-and-patch
- dependency-name: tree-sitter-swift
  dependency-version: 0.7.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo-minor-and-patch
- dependency-name: rayon
  dependency-version: 1.12.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: cargo-minor-and-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore(deps): bump toml from 0.8.23 to 1.1.2+spec-1.1.0

Bumps [toml](https://github.com/toml-rs/toml) from 0.8.23 to 1.1.2+spec-1.1.0.
- [Commits](toml-rs/toml@toml-v0.8.23...toml-v1.1.2)

---
updated-dependencies:
- dependency-name: toml
  dependency-version: 1.1.2+spec-1.1.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore(deps): bump tree-sitter-javascript from 0.23.1 to 0.25.0

Bumps [tree-sitter-javascript](https://github.com/tree-sitter/tree-sitter-javascript) from 0.23.1 to 0.25.0.
- [Release notes](https://github.com/tree-sitter/tree-sitter-javascript/releases)
- [Commits](tree-sitter/tree-sitter-javascript@v0.23.1...v0.25.0)

---
updated-dependencies:
- dependency-name: tree-sitter-javascript
  dependency-version: 0.25.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore(deps): bump tree-sitter-go from 0.23.4 to 0.25.0

Bumps [tree-sitter-go](https://github.com/tree-sitter/tree-sitter-go) from 0.23.4 to 0.25.0.
- [Release notes](https://github.com/tree-sitter/tree-sitter-go/releases)
- [Commits](tree-sitter/tree-sitter-go@v0.23.4...v0.25.0)

---
updated-dependencies:
- dependency-name: tree-sitter-go
  dependency-version: 0.25.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* fix(structured): use toml::from_str for document parsing; add regression tests

The `content.parse::<toml::Value>()` path relied on toml 0.8's FromStr
parsing a full document. In toml 1.1, FromStr instead parses a single
value and rejects further content, so any TOML file rendered by
toml_outline would show "[parse error: unexpected content]" instead of
the section/scalar tree.

Switching to `toml::from_str(content)` is forward-and-backward compatible
(works on both 0.8 and 1.1) and matches the deserialize path already used
in `overview.rs` for Cargo.toml/pyproject.toml.

Pinned four regression tests on `toml_outline` covering the failure mode
this fixed:
- Top-level table renders as `[section]` headers (Value::Table arm)
- Tables beyond max_depth collapse to `{N keys}` summary
- Arrays render as `[N items]` (Value::Array arm)
- Parse errors surface as `[parse error: ...]` markers

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(deps): bump tree-sitter from 0.25.10 to 0.26.8

Bumps [tree-sitter](https://github.com/tree-sitter/tree-sitter) from 0.25.10 to 0.26.8.
- [Release notes](https://github.com/tree-sitter/tree-sitter/releases)
- [Commits](tree-sitter/tree-sitter@v0.25.10...v0.26.8)

---
updated-dependencies:
- dependency-name: tree-sitter
  dependency-version: 0.26.8
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* fix: stop walker at mount boundaries, recognize composer.json

Two minimal hardening fixes for the issues @quanhavn surfaced in #62:

1. `same_file_system(true)` on the WalkBuilder in both `walker()` and
   `find_basename_fallback()`. Stops traversal at mount-point boundaries
   so NFS / OrbStack / external-volume directories accessible from a
   project root don't get walked into. Pairs with the existing
   `follow_links(true)` — symlinks are still followed, just not across
   device boundaries.

2. `composer.json` added to `MANIFESTS` in `package_root()`. PHP /
   Laravel projects use composer.json as their manifest; without this,
   scope resolution falls through to a parent and pulls in unrelated
   trees.

Scope deliberately narrower than #62 — drops `follow_links(false)`
(would break pnpm, npm/yarn workspaces, macOS `/tmp` symlinks) and the
`.git`-as-manifest fallback (would expand scope to the entire git repo
for any project lacking a manifest).

Closes #62.

Co-Authored-By: quanhavn <anhquanhh4@gmail.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(deps): bump actions/checkout from 4 to 6

Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](actions/checkout@v4...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore(deps): bump tree-sitter-scala from 0.24.0 to 0.26.0

Bumps [tree-sitter-scala](https://github.com/tree-sitter/tree-sitter-scala) from 0.24.0 to 0.26.0.
- [Release notes](https://github.com/tree-sitter/tree-sitter-scala/releases)
- [Commits](tree-sitter/tree-sitter-scala@v0.24.0...v0.26.0)

---
updated-dependencies:
- dependency-name: tree-sitter-scala
  dependency-version: 0.26.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore(deps): bump tree-sitter-python from 0.23.6 to 0.25.0

Bumps [tree-sitter-python](https://github.com/tree-sitter/tree-sitter-python) from 0.23.6 to 0.25.0.
- [Release notes](https://github.com/tree-sitter/tree-sitter-python/releases)
- [Commits](tree-sitter/tree-sitter-python@v0.23.6...v0.25.0)

---
updated-dependencies:
- dependency-name: tree-sitter-python
  dependency-version: 0.25.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* docs: add community standards files + Scorecard badge (#109)

Closes #97 (scaffold CoC / CONTRIBUTING / SECURITY / issue + PR templates).
Closes half of #98 (Scorecard badge; Best Practices badge pending registration).

- CODE_OF_CONDUCT.md — references Contributor Covenant 2.1, brief
- CONTRIBUTING.md — light tone, points at CLAUDE.md and the three local gates
- SECURITY.md — private vuln reporting via GitHub advisory flow
- .github/ISSUE_TEMPLATE/{bug_report,feature_request,config}.yml — minimal,
  blank issues disabled
- .github/PULL_REQUEST_TEMPLATE.md — summary + 3-line test plan checklist
- README.md — OpenSSF Scorecard badge near the title

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(search): --full raises match cap from 10 to 100 (#65) (#111)

Without --full, search/callers cap at 10 matches and show "... and N
more matches" — users asking for the full picture were getting a
truncated view. With --full, the cap is raised to 100 and the walker's
early-quit threshold is raised proportionally (300), so all real matches
become visible up to a generous bound.

The cap-bump is gated strictly on the parsed --full flag, never on the
piped-derived `full = cli.full || !is_tty`. This preserves the existing
contract (`piped_invocation_does_not_auto_expand` test): subprocess /
pipeline callers must keep the concise outline by default. To wire this
through cleanly, run_inner and run_expanded now take an explicit
cli_full parameter alongside the existing full (which stays as the
file-content semantic for FilePath queries).

Symbol, content, callers, and regex search all honor --full. The new
symbol::full_flag_raises_match_cap test pins the cap behavior on a
synthetic 15-file repo.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(ci): pin all GitHub Actions by commit SHA (#116)

Closes the 16 open `PinnedDependenciesID` Scorecard alerts. Each
`uses:` reference now carries a 40-char commit SHA plus a
`# ratchet:org/repo@<tag>` comment marking the original constraint
so the version remains greppable.

Generated with `ratchet pin .github/workflows/*.yml` (sethvargo/ratchet
v0.11.4). Dependabot continues to bump SHAs based on the trailing
tag comment; `ratchet upgrade` can re-sync any drift later.

Workflows affected: ci, dependency-review, release, scorecard.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: bump version to v0.8.3 (#117)

Patch release: search cap fix, NFS walker narrow, dep refreshes,
community standards, SHA-pinned workflows.

Includes (since v0.8.2):
- fix(search): --full raises match cap from 10 to 100 (#65 / #111)
- fix(walker): stop at mount boundaries, recognize composer.json (#110)
- chore(deps): tree-sitter-scala 0.24 → 0.26 (#114)
- chore(deps): tree-sitter-python 0.23.6 → 0.25 (#115)
- chore(deps): actions/checkout v4 → v6 (#113)
- chore(ci): pin all GitHub Actions by commit SHA (#116)
- docs: community standards files + Scorecard badge (#109)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(fuzz): cargo-fuzz harness + nightly CI + OSS-Fuzz prep (#118)

* feat(fuzz): scaffold cargo-fuzz with three targets

Addresses the OpenSSF Scorecard FuzzingID alert. Adds the fuzz/ workspace
with three initial targets covering tilth's primary input surfaces:

- outline — feeds bytes through `read::outline::code::outline` against
  all 18 Lang variants. Exercises tree-sitter parsing + traversal +
  signature extraction + formatter. ~1900 exec/sec; primary attack
  surface ("any file on disk in any language").
- strip — feeds bytes through `search::strip::strip_noise` across 6
  representative paths. ~26k exec/sec. Past bugs have lived in multi-
  byte UTF-8 handling.
- diff_parse — feeds bytes through `diff::parse::parse_unified_diff`.
  Our own parser (not a crate boundary). ~78k exec/sec.

All three pass 20s smoke runs with no findings. Plan and design notes
in docs/research/fuzzing-plan.md (gitignored).

The `__fuzz` module re-exports the fuzz surface without widening
pub(crate) visibility elsewhere. Marked `#[doc(hidden)]` and documented
as unstable.

Tasks tracked: FUZZ-A1, FUZZ-B1, FUZZ-B2, FUZZ-B3 (done).
Next: corpus seeding, CI nightly workflow, OSS-Fuzz application.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(fuzz): nightly CI, SECURITY.md, OSS-Fuzz artifacts

Builds on the scaffolding commit:

- .github/workflows/fuzz.yml — nightly cron + workflow_dispatch, runs
  each target for 5 min (configurable), caches corpus across runs,
  uploads crash artifacts on failure. Does NOT block PR merges.
- SECURITY.md — documents the security testing surface (unit tests,
  CodeQL, Scorecard, fuzz). Includes local repro recipe.
- oss-fuzz/projects/tilth/ — ready-to-submit OSS-Fuzz artifacts
  (project.yaml, Dockerfile, build.sh). README explains the
  submission workflow.

Also: fmt fix on lib.rs (single-line wrapper signature), and
`#[must_use]` on outline / strip_noise wrappers (now reachable
publicly via __fuzz, so clippy::must_use_candidate fires under
the pedantic lint group).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fuzz: preserve OOM finding as tracked seed

5-min outline target run surfaced one OOM finding: a 9KB Rust-shaped
input (real source from ripgrep's globset crate) causes tree-sitter
to allocate >2GB inside ts_parser_parse. Likely one of the 14 grammars
we iterate is choking on input that's the wrong language for it.

Preserved as fuzz/seeds/outline/oom-rust-imports-9kb so future runs
have it as a permanent seed. Diagnostics + fix tracked separately
(task FUZZ-FIX-1).

The two other targets (strip, diff_parse) completed 1.82M and 7.92M
runs respectively with no findings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(edit): remove cwd race in normalize_path_key (root-cause flaky test)

normalize_path_key used std::path::absolute() to resolve `./foo` vs `foo`
aliases for nonexistent files. absolute() reads current_dir() — process-
global mutable state. Another test (mcp::tests::scope_handoff_when_cwd_is_root)
calls set_current_dir("/") and restores it; under parallel test execution,
the two normalize_path_key calls inside detect_duplicate_paths could see
different cwds, producing different keys, breaking the dedup invariant.

Surfaced as a flaky CI failure on PR #118:
  thread 'edit::tests::dedup_catches_nonexistent_alias_spellings' panicked:
  alias spellings should collide

Fix: replace std::path::absolute() with pure-lexical normalization
(strip CurDir components, walk ParentDir against the in-memory stack).
No filesystem touch, no current_dir() call → deterministic under
parallel tests.

Trade-off: `/abs/foo.rs` and `foo.rs` (cwd=/abs) no longer dedup when
neither exists. Acceptable — agents typically send all-relative or
all-absolute paths in one batch; canonicalize still handles the common
case (when at least one of the paths exists, canonicalize succeeds for it).

Three new lexical_normalize unit tests pin the CurDir-strip, ParentDir-pop,
and absolute-preserve invariants. A new normalize_path_key_is_cwd_independent
test pins the race-free property as a load-bearing guarantee.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(edit): preserve unresolved ParentDir in lexical_normalize

v1 of lexical_normalize had a real bug surfaced during edge-case audit:
`../foo.rs` and `foo.rs` would normalize to the same key, but they
point to DIFFERENT files (one in parent dir, one in cwd). v1 popped
`..` at the empty stack, silently collapsing the path.

Fixed: track whether the path is absolute; on `ParentDir`, only pop if
the tail is a real (Normal) segment, else preserve `..` (relative
paths) or no-op (absolute paths at root, matching Linux's `/.. == /`).

New test pins the regression:
  assert_ne!(normalize_path_key("../foo.rs"), normalize_path_key("foo.rs"))

Plus tests for the multi-level case (foo/bar/../../../baz.rs → ../baz.rs),
absolute /foo/../bar.rs → /bar.rs, and /.. → /.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(edit): track normal_count instead of peeking with next_back

Replaces `out.components().next_back()` in lexical_normalize (which
iterates the whole PathBuf to inspect the tail, O(N) per ParentDir hit)
with a `normal_count: usize` running counter. The decision becomes
O(1) per component, dropping the function from O(N·K) to O(N) overall.

Behavior identical — all 23 edit tests still pass without modification.
The smell flagged in the previous audit (iterator allocation just to
peek at the tail) is gone; the intent (\"can I pop a real segment?\")
is now expressed directly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: bump version to v0.8.4 (#119)

Patch release. Adds cargo-fuzz infrastructure (3 fuzz targets, nightly CI,
OSS-Fuzz artifacts) and root-causes the cwd-race in normalize_path_key
that was producing flaky dedup test failures on Linux CI.

Includes (since v0.8.3):
- feat(fuzz): cargo-fuzz harness + nightly CI + OSS-Fuzz prep (#118)
- fix(edit): normalize_path_key is now race-free + lexically correct
  for `..` (handled as part of #118 audit)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(bench): align rg_trait_implementors prompt with ground truth

The task asserted `find_at` was in the agent's final answer, but the
prompt never mentioned methods — only \"where each implementor is
defined and what crate it lives in.\" Sonnet tended to mention method
names anyway (more verbose answers); haiku tended to answer literally
and omit them, producing a ~33% spurious failure rate.

Fixed by updating the prompt to ask explicitly for the trait's
required methods, with `find_at` as an example. Verified: 3/3
haiku reps pass after the change (vs 2/3 before).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(search): --full raises match cap from 10 to 100

Re-adds upstream PR #111's behavior on top of the current SymbolMode
signature deferred during the sync (PR #34). The CLI's `--full` flag
now raises search/callers cap from 10 to 100 with proportional walker
early-quit thresholds (300 = 100 × 3). Gated strictly on the *parsed*
cli.full — piped-derived `full = cli.full || !is_tty` does NOT bump
the cap. MCP and library `_raw` callsites pass `false` to preserve
token budgets.

Adds `full_flag_raises_match_cap` test in symbol search plus content,
regex, callers cap-raise tests and a source-level MCP `full=false`
regression guard.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* refactor(search,cli,ci): apply age findings on the sync PR

Curing /age findings on PR #34 (sync upstream jahala v0.8.4 + adapted PR
111 cap raise):

Security (workflows)
- fuzz.yml: move duration_seconds input to job env so it cannot inject
  shell at the run step; add a seed-copy step so the committed corpus
  seeds (e.g. fuzz/seeds/outline/oom-rust-imports-9kb) are actually fed
  to the fuzzer instead of sitting unreachable.
- release.yml: explicit `permissions: {}` on publish-crate / publish-npm
  so they cannot inadvertently elevate to contents:write.

Encapsulation + complexity (search slice)
- Add search::callers::find_callers_batch_default that hardcodes the
  default BATCH_EARLY_QUIT threshold internally. Demote BATCH_EARLY_QUIT
  back to private const. External callers in diff/, search::blast,
  search::deps, and search::mod tests now use the wrapper instead of
  reaching into callers internals.
- Hoist FULL_MAX_MATCHES and FULL_EARLY_QUIT_THRESHOLD to one location
  in search::mod (pub(crate)); drop the three duplicates from
  symbol/content/callers. The definitions-vs-usages thresholds are no
  longer a hardcoded 300 — they share the FULL_MAX_MATCHES * 3 form.

Library API (lib.rs)
- Collapse pub run / run_full / run_expanded into a single
  pub fn run(query, scope, cache, RunConfig). Introduce MatchCap enum
  (Default | Extended) to replace the cli_full: bool parameter so the
  public API no longer carries a CLI-flag-named bool. run_inner drops
  from 10 args to 4; the #[allow(too_many_arguments)] suppression is
  gone. ExpandedCtx.full_search renamed to cap: MatchCap.
- main.rs updated to construct RunConfig and pass MatchCap.

Gates: cargo build --release, cargo test (497 pass), cargo clippy
--release -- -D warnings, cargo fmt --check all clean. CLI smoke test
confirms `tilth fn` → 10 matches default, `tilth fn --full` → 100.

Deferred (not silently dropped; rationale in cure report):
- path-clean crate migration of lexical_normalize: skipped pending an
  intentional supply-chain trade-off decision.
- fuzz/Cargo.lock dual maintenance: belongs in a dedicated supply-chain
  PR alongside the existing oss-hygiene work.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(edit): preserve leading `..` on Windows drive-relative paths

`lexical_normalize` flipped `is_absolute = true` on any `Component::Prefix(_)`,
but on Windows a path like `C:foo` parses as `[Prefix("C:"), Normal("foo")]`
with no `RootDir` — it is drive-relative (anchored to that drive's cwd), not
absolute. With the old code, a leading `..` in such a path was silently dropped
instead of being preserved as unresolved, which could produce incorrect dedup
keys / collisions in `detect_duplicate_paths`.

Split the match arm so only `RootDir` flips the flag. `Prefix(_)` is still
pushed onto the output but does not change the absoluteness state.

Caught by Copilot review on PR #34.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: jensenojs <jensenojs@qq.com>
Co-authored-by: jahala <jahala@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: quanhavn <anhquanhh4@gmail.com>
paulnsorensen referenced this pull request in paulnsorensen/tilth May 22, 2026
* perf(diff): parallelise overlay construction across files

Extracted from #61 part 4 (rayon parallelisation, diff overlay half).
Independent of the timeout (#82) and batch-edit work — touches only
src/diff/mod.rs.

`compute_overlay` is pure per-`FileDiff` work: each file independently
fetches old/new content, runs tree-sitter, and matches symbols. Switching
the overlay build from `.iter()` to `.par_iter()` lets multi-file diffs
fan out across cores — a meaningful win on large PRs where each file does
non-trivial AST work.

Thread safety: `compute_overlay` constructs its own
`tree_sitter::Parser::new()` per call inside
`lang::outline::get_outline_entries` — no shared mutable state crosses
worker boundaries. `FileDiff` and `DiffSource` are Send+Sync via auto
trait derivation.

Scope deliberately limited to `diff::diff()` — left the per-commit
overlay loop in `diff_log()` alone since each commit's overlay set is
typically small. Also left the `tool_read` batch parallelisation from
the original #61 out of this PR pending the deadline-preservation
discussion (review item #5).

Test plan
- cargo build — clean
- cargo test — 344 pass
- cargo clippy -- -D warnings — clean
- cargo fmt --check — clean

* feat(mcp): batch tilth_edit — accept files: [{path, edits}]

Extracted from #61 part 3 (batch tilth_edit). Independent of the timeout
(#82) and diff-overlay (#83) extractions; touches src/edit.rs (the new
apply_batch + FileEditTask), src/mcp.rs (new tool_edit + parsing helpers
+ schema), and AGENTS.md (user-facing docs).

What changes for callers
- tilth_edit now takes {"files": [{"path", "edits"}, ...]} instead of
  {"path", "edits"}. Each file is processed independently in parallel —
  a hash mismatch on one file no longer blocks its siblings.
- Per-file results render as Markdown sections (## <path>) joined by ---
  separators. Returns isError only when every file failed.
- Cap of 20 files per call, surfaced both in the JSON Schema (maxItems)
  and at runtime.

Why the schema break is safe
- MCP serves tools/list dynamically on every session; clients always get
  the current inputSchema. The LLM sees the new shape on next connect
  and adapts. There is no cached-schema problem to migrate around.

Review fixes from #61 layered in here
- #4 (duplicate paths → data corruption): detect_duplicate_paths now
  rejects the whole batch up front when two Ready tasks resolve to the
  same canonical path. Falls back to the literal path for files that
  don't exist yet, so the dedup key is still well-defined.
- Medium (parse_file_edit short-circuit): parse_edit_entry returns the
  failing edit index in its error message, so the LLM can fix exactly
  the right entry instead of guessing which edit was malformed.
- Medium (tests use tempfile::tempdir): new batch tests use tempdir
  rather than fixed-name paths in std::env::temp_dir.

Implementation shape
- src/edit.rs: pub apply_batch + pub FileEditTask::{Ready, ParseError},
  with apply_one / render_applied keeping the parallel closure trivial.
  apply_edits and EditResult are now private — callers go through
  apply_batch.
- src/mcp.rs: tool_edit shrinks from ~85 lines of inline JSON parsing to
  parse → cap-check → dedup-check → record_read → apply_batch. JSON
  parsing stays at the MCP wire boundary; parallel I/O and blast radius
  live next to apply_edits.

Test plan
- cargo build — clean
- cargo test — 348 pass (4 new in edit::tests covering two-file success,
  partial failure, all-failed, and parse-error surfacing — all using
  tempfile::tempdir)
- cargo clippy -- -D warnings — clean
- cargo fmt --check — clean

* refactor(error): adopt thiserror for Display/Error derives

NIH audit finding — replace 25 lines of mechanical write! formatting
in `src/error.rs` with `thiserror::Error` derives. Each variant gets
an inline `#[error(...)]` attribute; the manual `impl Display` and
empty `impl std::error::Error` blocks are removed.

Net: +11 / -40 in src/error.rs; one new dependency (thiserror v2).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* refactor(diff): adopt strsim::sorensen_dice for fuzzy symbol matching

NIH audit finding — replace hand-rolled `jaccard_similarity`
(whitespace-set Jaccard) in `src/diff/matching.rs` Phase 3 fuzzy
matcher with `strsim::sorensen_dice` (character-bigram). Same
[0.0, 1.0] range, same 0.8 threshold, no rescaling needed.

Net: +1 / -15 in src/diff/matching.rs; one new dependency (strsim).

Existing tests `fuzzy_match` and `below_fuzzy_threshold` still pass
at the unchanged 0.8 threshold.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* refactor(install): adopt home crate for cross-platform home dir lookup

Replace the cfg-split `$HOME` / `$USERPROFILE` block in `home_dir()` with
a one-liner around `home::home_dir()`. The `home` crate is maintained by
the cargo team itself (rust-lang/cargo) and handles a corner case the
hand-rolled version misses: an empty `$HOME` is treated as missing rather
than silently producing an empty `PathBuf` that joins onto relative paths.

Behavior preserved:
- Same env-var precedence ($HOME on unix, $USERPROFILE on windows).
- Wrapper kept (4 call sites) with an actionable error message that names
  both env vars.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* refactor(mcp): adopt percent-encoding crate for file:// URI decoding

Replace the hand-rolled `percent_decode` + `hex_val` helpers
(`src/mcp.rs`) with `percent_encoding::percent_decode_str`. Single call
site in `extract_root_from_response`, so the helpers are inlined rather
than wrapped.

The fallback contract on invalid UTF-8 is preserved: rather than
substituting U+FFFD replacement characters via `decode_utf8_lossy`,
fall back to the raw undecoded input so the subsequent `is_dir()` check
rejects mangled paths cleanly.

The unit-test for the deleted helper (`percent_decode_basic`) is
removed; end-to-end coverage of the same code path lives in
`extract_root_percent_encoded_uri`, which exercises a real `file://`
URI with `%20` resolving to a real directory.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* refactor(read): adopt strsim::levenshtein for filename / heading suggestions

Replace the 16-LOC hand-rolled Wagner-Fischer implementation in
`src/read/mod.rs` with a thin wrapper around `strsim::levenshtein`. Two
call sites — `suggest_similar` (filename "did you mean?") and
`suggest_headings` (markdown heading suggestion) — so the wrapper is
kept rather than inlined.

`strsim::levenshtein` wraps `generic_levenshtein` over `StringWrapper`,
which iterates `.chars()`. Same Unicode-scalar semantics as the
hand-rolled version: a single CJK or emoji glyph counts as one edit
unit, not 3-4 bytes. The `edit_distance_is_unicode_aware` regression
test (CJK + emoji + ASCII) is updated to lock in this contract — if
`strsim` ever switches to byte-level distance, the test fails loudly.

Note: PR #88 (`refactor-strsim` for diff fuzzy matching) also adds
`strsim = "0.11"`. Whichever lands first, the second can drop the
duplicate Cargo.toml line in a trivial rebase.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix: char-boundary panic in is_minified_by_name with multi-byte UTF-8

stem.len() - 4 is a byte offset, not a char offset — it can land
inside a CJK or emoji character, causing a panic. Use
char_indices().nth_back(3) to find the byte offset of the 4th
character from the end instead.

* Update dependencies in Cargo.toml

Fixed an error I did when manually resolving a conflict

* chore: bump version to v0.8.1

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(bloom): adopt fastbloom for BloomFilter implementation

NIH audit finding — drop ~105 LOC of hand-rolled bloom filter math
in `src/index/bloom.rs` (`double_hash`, `combined_hash`,
`hash_with_seed`, optimal-bit/hash sizing) in favor of `fastbloom`,
the SIMD-optimized de facto Rust crate.

The `extract_identifiers` byte state machine — the unique-to-tilth
piece — stays. `BloomFilterCache` now wraps `fastbloom::BloomFilter`
constructed via `with_false_pos(0.01).expected_items(n)`.

Drop `test_bloom_filter_sizing` — it asserted private fields
(`num_bits`, `num_hashes`) of the removed type. Equivalent guarantees
are part of fastbloom's own test suite.

Net: +5 / -133 in src/index/bloom.rs; one new dependency (fastbloom).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* refactor(mcp): extract per-request timeout into its own module

Extracted from #61 part 2 (timeout module). Depends on the Services
struct from #63 (already merged) for the per-instance ThreadTracker.

Moves the inline mpsc::recv_timeout machinery in handle_tool_call into
src/timeout.rs as spawn_with_timeout, backed by crossbeam-channel's
select! { default(timeout) => ... } — the sync-world equivalent of
Future.get(timeout, unit).

Two correctness changes vs. the original #61 timeout draft, addressing
the review feedback there:

- ThreadTracker::is_at_cap uses Ordering::Acquire (was Relaxed). Pairs
  with the Release in record_timeout / record_finish_after_timeout, so
  a load that observes the new count also observes any state the
  incrementing thread published before it.
- The deadline arm now CAS-claims the timeout *before* incrementing the
  tracker (was: increment, then CAS, then conditionally roll back). The
  worker, if it lost the CAS, spins on a new timeout_acked AtomicBool
  before decrementing — so the tracker can never go negative and a
  concurrent is_at_cap() can never observe an inflated count that gets
  rolled back. Closes the false-rejection window the reviewer flagged
  near the hard cap.

Also wires an upfront tracker.is_at_cap() check in handle_tool_call so
the server refuses new work cleanly under sustained pressure instead of
piling on more abandoned threads.

ThreadTracker is owned by Services rather than a static global, so unit
tests instantiate their own and don't serialise on shared state.

No behaviour change for the happy path: same 90s default, same
TILTH_TIMEOUT env override, same client-visible "tool timed out" /
"tool panicked" messages.

Test plan
- cargo build — clean
- cargo test — 349 pass (5 new in timeout::tests covering the CAS
  roundtrip, fast path, panic surfacing, saturated tracker, and env
  parsing)
- cargo clippy -- -D warnings — clean
- cargo fmt --check — clean

* fix(timeout): address PR 82 review feedback

- Switch tracker RMWs to AcqRel — canonical pessimistic ordering for an
  atomic counter read from another thread. Release/Acquire was sufficient
  for the counter value alone, but AcqRel documents the cross-thread
  contract more explicitly so the next reader doesn't have to re-derive
  that the value composes with the rest of the spawn state machine.
- Add std::thread::yield_now() after spin_loop() in wait_for_timeout_ack
  so a worker scheduled before the main thread on a single-CPU container
  surrenders the rest of its quantum instead of burning ~10ms before the
  ack becomes visible.
- Doc-comment ABANDONED_THREAD_WARN as a deliberate warn-once policy so a
  future maintainer doesn't switch the deadline arm's `==` check back to
  `>=`. The hard cap at MAX_ABANDONED_THREADS bounds the silence past the
  threshold.
- Mark SpawnFailure as #[non_exhaustive] so a future failure mode
  (e.g. OS-level thread spawn failure) can be added without churning
  every call site.
- Relax abandoned_counter_roundtrips_through_cas deadline from 2s to 5s
  to reduce flake risk on contended CI runners.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(edit): address PR 84 review — path dedup, empty edits, encapsulation

- Moves normalize_path_key and detect_duplicate_paths from mcp.rs to edit.rs
- Adds macOS case-insensitive APFS dedup (ASCII-lowercasing on cfg target_os)
- Adds runtime validation for empty edits array (was schema-only; now parse error)
- Moves dedup gate into apply_batch to prevent wire-layer bypass
- Simplifies apply_one/render_applied signatures (&Arc<T> → &T with deref-coercion)
- Updates AGENTS.md documentation (isError semantics, parse-error behavior)
- Adds 5 tests (case aliases, empty edits, integration gate)

Co-Authored-By: Claude <noreply@anthropic.com>

* chore: bump version to v0.8.2

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: oss-hygiene — supply-chain workflows and harden permissions

Add three supply-chain hygiene workflows and tighten GITHUB_TOKEN
permissions on existing workflows. All free for public repos.

New:
- .github/dependabot.yml — weekly version updates for cargo, npm, github-actions
- .github/workflows/dependency-review.yml — block PRs that introduce
  vulnerable or disallowed-license deps (free on public repos)
- .github/workflows/scorecard.yml — OpenSSF Scorecard analysis and
  badge publication

Hardened:
- ci.yml — add top-level `permissions: contents: read` (least privilege)
- release.yml — drop top-level `contents: write`; scope it to the `build`
  job that actually needs it for softprops/action-gh-release. Publish
  jobs use CARGO_REGISTRY_TOKEN / NPM_TOKEN secrets and don't need any
  GITHUB_TOKEN write scope.

Generated via the /oss-hygiene skill from skillz-that-grillz.

* chore(deps): bump github/codeql-action from 3 to 4

Bumps [github/codeql-action](https://github.com/github/codeql-action) from 3 to 4.
- [Release notes](https://github.com/github/codeql-action/releases)
- [Changelog](https://github.com/github/codeql-action/blob/main/CHANGELOG.md)
- [Commits](github/codeql-action@v3...v4)

---
updated-dependencies:
- dependency-name: github/codeql-action
  dependency-version: '4'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore(deps): bump softprops/action-gh-release from 2 to 3

Bumps [softprops/action-gh-release](https://github.com/softprops/action-gh-release) from 2 to 3.
- [Release notes](https://github.com/softprops/action-gh-release/releases)
- [Changelog](https://github.com/softprops/action-gh-release/blob/master/CHANGELOG.md)
- [Commits](softprops/action-gh-release@v2...v3)

---
updated-dependencies:
- dependency-name: softprops/action-gh-release
  dependency-version: '3'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore(deps): bump actions/upload-artifact from 4 to 7

Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 4 to 7.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](actions/upload-artifact@v4...v7)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: '7'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore(deps): bump actions/dependency-review-action from 4 to 5

Bumps [actions/dependency-review-action](https://github.com/actions/dependency-review-action) from 4 to 5.
- [Release notes](https://github.com/actions/dependency-review-action/releases)
- [Commits](actions/dependency-review-action@v4...v5)

---
updated-dependencies:
- dependency-name: actions/dependency-review-action
  dependency-version: '5'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore(deps): bump actions/setup-node from 4 to 6

Bumps [actions/setup-node](https://github.com/actions/setup-node) from 4 to 6.
- [Release notes](https://github.com/actions/setup-node/releases)
- [Commits](actions/setup-node@v4...v6)

---
updated-dependencies:
- dependency-name: actions/setup-node
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* fix(scorecard): pin to v2.4.3 — @v2 ref doesn't exist

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(deps): bump the cargo-minor-and-patch group with 7 updates

Bumps the cargo-minor-and-patch group with 7 updates:

| Package | From | To |
| --- | --- | --- |
| [clap](https://github.com/clap-rs/clap) | `4.5.60` | `4.6.1` |
| [clap_complete](https://github.com/clap-rs/clap) | `4.5.66` | `4.6.5` |
| [tree-sitter-rust](https://github.com/tree-sitter/tree-sitter-rust) | `0.24.0` | `0.24.2` |
| [tree-sitter-c](https://github.com/tree-sitter/tree-sitter-c) | `0.24.1` | `0.24.2` |
| [tree-sitter-c-sharp](https://github.com/tree-sitter/tree-sitter-c-sharp) | `0.23.1` | `0.23.5` |
| [tree-sitter-swift](https://github.com/alex-pinkus/tree-sitter-swift) | `0.7.1` | `0.7.2` |
| [rayon](https://github.com/rayon-rs/rayon) | `1.11.0` | `1.12.0` |


Updates `clap` from 4.5.60 to 4.6.1
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](clap-rs/clap@clap_complete-v4.5.60...clap_complete-v4.6.1)

Updates `clap_complete` from 4.5.66 to 4.6.5
- [Release notes](https://github.com/clap-rs/clap/releases)
- [Changelog](https://github.com/clap-rs/clap/blob/master/CHANGELOG.md)
- [Commits](clap-rs/clap@clap_complete-v4.5.66...clap_complete-v4.6.5)

Updates `tree-sitter-rust` from 0.24.0 to 0.24.2
- [Release notes](https://github.com/tree-sitter/tree-sitter-rust/releases)
- [Commits](tree-sitter/tree-sitter-rust@v0.24.0...v0.24.2)

Updates `tree-sitter-c` from 0.24.1 to 0.24.2
- [Release notes](https://github.com/tree-sitter/tree-sitter-c/releases)
- [Commits](tree-sitter/tree-sitter-c@v0.24.1...v0.24.2)

Updates `tree-sitter-c-sharp` from 0.23.1 to 0.23.5
- [Release notes](https://github.com/tree-sitter/tree-sitter-c-sharp/releases)
- [Commits](tree-sitter/tree-sitter-c-sharp@v0.23.1...v0.23.5)

Updates `tree-sitter-swift` from 0.7.1 to 0.7.2
- [Release notes](https://github.com/alex-pinkus/tree-sitter-swift/releases)
- [Commits](alex-pinkus/tree-sitter-swift@0.7.1...0.7.2)

Updates `rayon` from 1.11.0 to 1.12.0
- [Changelog](https://github.com/rayon-rs/rayon/blob/main/RELEASES.md)
- [Commits](rayon-rs/rayon@rayon-core-v1.11.0...rayon-core-v1.12.0)

---
updated-dependencies:
- dependency-name: clap
  dependency-version: 4.6.1
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: cargo-minor-and-patch
- dependency-name: clap_complete
  dependency-version: 4.6.5
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: cargo-minor-and-patch
- dependency-name: tree-sitter-rust
  dependency-version: 0.24.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo-minor-and-patch
- dependency-name: tree-sitter-c
  dependency-version: 0.24.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo-minor-and-patch
- dependency-name: tree-sitter-c-sharp
  dependency-version: 0.23.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo-minor-and-patch
- dependency-name: tree-sitter-swift
  dependency-version: 0.7.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: cargo-minor-and-patch
- dependency-name: rayon
  dependency-version: 1.12.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: cargo-minor-and-patch
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore(deps): bump toml from 0.8.23 to 1.1.2+spec-1.1.0

Bumps [toml](https://github.com/toml-rs/toml) from 0.8.23 to 1.1.2+spec-1.1.0.
- [Commits](toml-rs/toml@toml-v0.8.23...toml-v1.1.2)

---
updated-dependencies:
- dependency-name: toml
  dependency-version: 1.1.2+spec-1.1.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore(deps): bump tree-sitter-javascript from 0.23.1 to 0.25.0

Bumps [tree-sitter-javascript](https://github.com/tree-sitter/tree-sitter-javascript) from 0.23.1 to 0.25.0.
- [Release notes](https://github.com/tree-sitter/tree-sitter-javascript/releases)
- [Commits](tree-sitter/tree-sitter-javascript@v0.23.1...v0.25.0)

---
updated-dependencies:
- dependency-name: tree-sitter-javascript
  dependency-version: 0.25.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore(deps): bump tree-sitter-go from 0.23.4 to 0.25.0

Bumps [tree-sitter-go](https://github.com/tree-sitter/tree-sitter-go) from 0.23.4 to 0.25.0.
- [Release notes](https://github.com/tree-sitter/tree-sitter-go/releases)
- [Commits](tree-sitter/tree-sitter-go@v0.23.4...v0.25.0)

---
updated-dependencies:
- dependency-name: tree-sitter-go
  dependency-version: 0.25.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* fix(structured): use toml::from_str for document parsing; add regression tests

The `content.parse::<toml::Value>()` path relied on toml 0.8's FromStr
parsing a full document. In toml 1.1, FromStr instead parses a single
value and rejects further content, so any TOML file rendered by
toml_outline would show "[parse error: unexpected content]" instead of
the section/scalar tree.

Switching to `toml::from_str(content)` is forward-and-backward compatible
(works on both 0.8 and 1.1) and matches the deserialize path already used
in `overview.rs` for Cargo.toml/pyproject.toml.

Pinned four regression tests on `toml_outline` covering the failure mode
this fixed:
- Top-level table renders as `[section]` headers (Value::Table arm)
- Tables beyond max_depth collapse to `{N keys}` summary
- Arrays render as `[N items]` (Value::Array arm)
- Parse errors surface as `[parse error: ...]` markers

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(deps): bump tree-sitter from 0.25.10 to 0.26.8

Bumps [tree-sitter](https://github.com/tree-sitter/tree-sitter) from 0.25.10 to 0.26.8.
- [Release notes](https://github.com/tree-sitter/tree-sitter/releases)
- [Commits](tree-sitter/tree-sitter@v0.25.10...v0.26.8)

---
updated-dependencies:
- dependency-name: tree-sitter
  dependency-version: 0.26.8
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* fix: stop walker at mount boundaries, recognize composer.json

Two minimal hardening fixes for the issues @quanhavn surfaced in #62:

1. `same_file_system(true)` on the WalkBuilder in both `walker()` and
   `find_basename_fallback()`. Stops traversal at mount-point boundaries
   so NFS / OrbStack / external-volume directories accessible from a
   project root don't get walked into. Pairs with the existing
   `follow_links(true)` — symlinks are still followed, just not across
   device boundaries.

2. `composer.json` added to `MANIFESTS` in `package_root()`. PHP /
   Laravel projects use composer.json as their manifest; without this,
   scope resolution falls through to a parent and pulls in unrelated
   trees.

Scope deliberately narrower than #62 — drops `follow_links(false)`
(would break pnpm, npm/yarn workspaces, macOS `/tmp` symlinks) and the
`.git`-as-manifest fallback (would expand scope to the entire git repo
for any project lacking a manifest).

Closes #62.

Co-Authored-By: quanhavn <anhquanhh4@gmail.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(deps): bump actions/checkout from 4 to 6

Bumps [actions/checkout](https://github.com/actions/checkout) from 4 to 6.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](actions/checkout@v4...v6)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-version: '6'
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore(deps): bump tree-sitter-scala from 0.24.0 to 0.26.0

Bumps [tree-sitter-scala](https://github.com/tree-sitter/tree-sitter-scala) from 0.24.0 to 0.26.0.
- [Release notes](https://github.com/tree-sitter/tree-sitter-scala/releases)
- [Commits](tree-sitter/tree-sitter-scala@v0.24.0...v0.26.0)

---
updated-dependencies:
- dependency-name: tree-sitter-scala
  dependency-version: 0.26.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* chore(deps): bump tree-sitter-python from 0.23.6 to 0.25.0

Bumps [tree-sitter-python](https://github.com/tree-sitter/tree-sitter-python) from 0.23.6 to 0.25.0.
- [Release notes](https://github.com/tree-sitter/tree-sitter-python/releases)
- [Commits](tree-sitter/tree-sitter-python@v0.23.6...v0.25.0)

---
updated-dependencies:
- dependency-name: tree-sitter-python
  dependency-version: 0.25.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>

* docs: add community standards files + Scorecard badge (#109)

Closes #97 (scaffold CoC / CONTRIBUTING / SECURITY / issue + PR templates).
Closes half of #98 (Scorecard badge; Best Practices badge pending registration).

- CODE_OF_CONDUCT.md — references Contributor Covenant 2.1, brief
- CONTRIBUTING.md — light tone, points at CLAUDE.md and the three local gates
- SECURITY.md — private vuln reporting via GitHub advisory flow
- .github/ISSUE_TEMPLATE/{bug_report,feature_request,config}.yml — minimal,
  blank issues disabled
- .github/PULL_REQUEST_TEMPLATE.md — summary + 3-line test plan checklist
- README.md — OpenSSF Scorecard badge near the title

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(search): --full raises match cap from 10 to 100 (#65) (#111)

Without --full, search/callers cap at 10 matches and show "... and N
more matches" — users asking for the full picture were getting a
truncated view. With --full, the cap is raised to 100 and the walker's
early-quit threshold is raised proportionally (300), so all real matches
become visible up to a generous bound.

The cap-bump is gated strictly on the parsed --full flag, never on the
piped-derived `full = cli.full || !is_tty`. This preserves the existing
contract (`piped_invocation_does_not_auto_expand` test): subprocess /
pipeline callers must keep the concise outline by default. To wire this
through cleanly, run_inner and run_expanded now take an explicit
cli_full parameter alongside the existing full (which stays as the
file-content semantic for FilePath queries).

Symbol, content, callers, and regex search all honor --full. The new
symbol::full_flag_raises_match_cap test pins the cap behavior on a
synthetic 15-file repo.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore(ci): pin all GitHub Actions by commit SHA (#116)

Closes the 16 open `PinnedDependenciesID` Scorecard alerts. Each
`uses:` reference now carries a 40-char commit SHA plus a
`# ratchet:org/repo@<tag>` comment marking the original constraint
so the version remains greppable.

Generated with `ratchet pin .github/workflows/*.yml` (sethvargo/ratchet
v0.11.4). Dependabot continues to bump SHAs based on the trailing
tag comment; `ratchet upgrade` can re-sync any drift later.

Workflows affected: ci, dependency-review, release, scorecard.

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: bump version to v0.8.3 (#117)

Patch release: search cap fix, NFS walker narrow, dep refreshes,
community standards, SHA-pinned workflows.

Includes (since v0.8.2):
- fix(search): --full raises match cap from 10 to 100 (#65 / #111)
- fix(walker): stop at mount boundaries, recognize composer.json (#110)
- chore(deps): tree-sitter-scala 0.24 → 0.26 (#114)
- chore(deps): tree-sitter-python 0.23.6 → 0.25 (#115)
- chore(deps): actions/checkout v4 → v6 (#113)
- chore(ci): pin all GitHub Actions by commit SHA (#116)
- docs: community standards files + Scorecard badge (#109)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(fuzz): cargo-fuzz harness + nightly CI + OSS-Fuzz prep (#118)

* feat(fuzz): scaffold cargo-fuzz with three targets

Addresses the OpenSSF Scorecard FuzzingID alert. Adds the fuzz/ workspace
with three initial targets covering tilth's primary input surfaces:

- outline — feeds bytes through `read::outline::code::outline` against
  all 18 Lang variants. Exercises tree-sitter parsing + traversal +
  signature extraction + formatter. ~1900 exec/sec; primary attack
  surface ("any file on disk in any language").
- strip — feeds bytes through `search::strip::strip_noise` across 6
  representative paths. ~26k exec/sec. Past bugs have lived in multi-
  byte UTF-8 handling.
- diff_parse — feeds bytes through `diff::parse::parse_unified_diff`.
  Our own parser (not a crate boundary). ~78k exec/sec.

All three pass 20s smoke runs with no findings. Plan and design notes
in docs/research/fuzzing-plan.md (gitignored).

The `__fuzz` module re-exports the fuzz surface without widening
pub(crate) visibility elsewhere. Marked `#[doc(hidden)]` and documented
as unstable.

Tasks tracked: FUZZ-A1, FUZZ-B1, FUZZ-B2, FUZZ-B3 (done).
Next: corpus seeding, CI nightly workflow, OSS-Fuzz application.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(fuzz): nightly CI, SECURITY.md, OSS-Fuzz artifacts

Builds on the scaffolding commit:

- .github/workflows/fuzz.yml — nightly cron + workflow_dispatch, runs
  each target for 5 min (configurable), caches corpus across runs,
  uploads crash artifacts on failure. Does NOT block PR merges.
- SECURITY.md — documents the security testing surface (unit tests,
  CodeQL, Scorecard, fuzz). Includes local repro recipe.
- oss-fuzz/projects/tilth/ — ready-to-submit OSS-Fuzz artifacts
  (project.yaml, Dockerfile, build.sh). README explains the
  submission workflow.

Also: fmt fix on lib.rs (single-line wrapper signature), and
`#[must_use]` on outline / strip_noise wrappers (now reachable
publicly via __fuzz, so clippy::must_use_candidate fires under
the pedantic lint group).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fuzz: preserve OOM finding as tracked seed

5-min outline target run surfaced one OOM finding: a 9KB Rust-shaped
input (real source from ripgrep's globset crate) causes tree-sitter
to allocate >2GB inside ts_parser_parse. Likely one of the 14 grammars
we iterate is choking on input that's the wrong language for it.

Preserved as fuzz/seeds/outline/oom-rust-imports-9kb so future runs
have it as a permanent seed. Diagnostics + fix tracked separately
(task FUZZ-FIX-1).

The two other targets (strip, diff_parse) completed 1.82M and 7.92M
runs respectively with no findings.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(edit): remove cwd race in normalize_path_key (root-cause flaky test)

normalize_path_key used std::path::absolute() to resolve `./foo` vs `foo`
aliases for nonexistent files. absolute() reads current_dir() — process-
global mutable state. Another test (mcp::tests::scope_handoff_when_cwd_is_root)
calls set_current_dir("/") and restores it; under parallel test execution,
the two normalize_path_key calls inside detect_duplicate_paths could see
different cwds, producing different keys, breaking the dedup invariant.

Surfaced as a flaky CI failure on PR #118:
  thread 'edit::tests::dedup_catches_nonexistent_alias_spellings' panicked:
  alias spellings should collide

Fix: replace std::path::absolute() with pure-lexical normalization
(strip CurDir components, walk ParentDir against the in-memory stack).
No filesystem touch, no current_dir() call → deterministic under
parallel tests.

Trade-off: `/abs/foo.rs` and `foo.rs` (cwd=/abs) no longer dedup when
neither exists. Acceptable — agents typically send all-relative or
all-absolute paths in one batch; canonicalize still handles the common
case (when at least one of the paths exists, canonicalize succeeds for it).

Three new lexical_normalize unit tests pin the CurDir-strip, ParentDir-pop,
and absolute-preserve invariants. A new normalize_path_key_is_cwd_independent
test pins the race-free property as a load-bearing guarantee.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(edit): preserve unresolved ParentDir in lexical_normalize

v1 of lexical_normalize had a real bug surfaced during edge-case audit:
`../foo.rs` and `foo.rs` would normalize to the same key, but they
point to DIFFERENT files (one in parent dir, one in cwd). v1 popped
`..` at the empty stack, silently collapsing the path.

Fixed: track whether the path is absolute; on `ParentDir`, only pop if
the tail is a real (Normal) segment, else preserve `..` (relative
paths) or no-op (absolute paths at root, matching Linux's `/.. == /`).

New test pins the regression:
  assert_ne!(normalize_path_key("../foo.rs"), normalize_path_key("foo.rs"))

Plus tests for the multi-level case (foo/bar/../../../baz.rs → ../baz.rs),
absolute /foo/../bar.rs → /bar.rs, and /.. → /.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(edit): track normal_count instead of peeking with next_back

Replaces `out.components().next_back()` in lexical_normalize (which
iterates the whole PathBuf to inspect the tail, O(N) per ParentDir hit)
with a `normal_count: usize` running counter. The decision becomes
O(1) per component, dropping the function from O(N·K) to O(N) overall.

Behavior identical — all 23 edit tests still pass without modification.
The smell flagged in the previous audit (iterator allocation just to
peek at the tail) is gone; the intent (\"can I pop a real segment?\")
is now expressed directly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* chore: bump version to v0.8.4 (#119)

Patch release. Adds cargo-fuzz infrastructure (3 fuzz targets, nightly CI,
OSS-Fuzz artifacts) and root-causes the cwd-race in normalize_path_key
that was producing flaky dedup test failures on Linux CI.

Includes (since v0.8.3):
- feat(fuzz): cargo-fuzz harness + nightly CI + OSS-Fuzz prep (#118)
- fix(edit): normalize_path_key is now race-free + lexically correct
  for `..` (handled as part of #118 audit)

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test(bench): align rg_trait_implementors prompt with ground truth

The task asserted `find_at` was in the agent's final answer, but the
prompt never mentioned methods — only \"where each implementor is
defined and what crate it lives in.\" Sonnet tended to mention method
names anyway (more verbose answers); haiku tended to answer literally
and omit them, producing a ~33% spurious failure rate.

Fixed by updating the prompt to ask explicitly for the trait's
required methods, with `find_at` as an example. Verified: 3/3
haiku reps pass after the change (vs 2/3 before).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(mcp): split server into modules

Extract the monolithic `src/mcp.rs` into `src/mcp/{mod,tools/*}.rs`:

- `mcp/mod.rs` keeps the JSON-RPC scaffolding (request/response wire
  format, dispatch, `Services` shared deps).
- `mcp/tools/mod.rs` re-exports each tool's entry point and hosts
  shared helpers (`resolve_scope`, `apply_budget`).
- One file per MCP tool (`tools/{definitions,deps,diff,edit,files,
  read,search,session}.rs`).

No behavior changes — inline prompts and tool surfaces are intact, so
follow-up MCP prompt / v2-surface work can be reviewed separately.

Forwarded from #33.

Co-authored-by: paulnsorensen <429793+paulnsorensen@users.noreply.github.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>

* refactor(mcp): tighten module visibility and co-locate tests

Three cleanup items from the /age review of the module split:

1. Tool entry points (tool_definitions, tool_deps, tool_diff, tool_edit,
   tool_files, tool_read, tool_search, tool_session) plus parse_file_edit
   narrowed from pub(crate) to pub(in crate::mcp). Their pub(super) use
   re-exports in src/mcp/tools/mod.rs likewise narrowed. The originals in
   the monolithic src/mcp.rs were truly private; pub(in crate::mcp) is the
   tightest visibility that keeps src/mcp/mod.rs's `use tools::tool_*`
   imports working.

2. Services struct, its impl methods, and dispatch_tool in src/mcp/mod.rs
   dropped pub(crate) — only callers live in mod.rs itself, so default
   (module-private) visibility is the right scope.

3. Tests co-located with the code they exercise:
   - resolve_scope_* and scope_flag_overrides_bad_cwd → src/mcp/tools/mod.rs
   - tool_files_* (+ scratch_project helper) → src/mcp/tools/files.rs
   - parse_file_edit_rejects_empty_edits_array → src/mcp/tools/edit.rs
   The extract_root_* tests and package_root_finds_project_from_subdirectory
   stay in src/mcp/mod.rs since they exercise mod.rs functions. The
   #[cfg(test)] use tools::{parse_file_edit, resolve_scope} import in
   mod.rs and the matching pub(super) use re-export in tools/mod.rs are
   removed since neither name is referenced from mod.rs anymore.

No behavior changes; 366 tests pass.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>

* refactor(mcp): extract server instructions to prompts/*.md

SERVER_INSTRUCTIONS and EDIT_MODE_EXTRA move out of inline string
literals in src/mcp/mod.rs into versionable markdown sources at
prompts/mcp-base.md and prompts/mcp-edit.md, wired back at compile time
via include_str!. AGENTS.md becomes a generated artifact via
scripts/regen-agents-md.sh and a CI step that fails if anyone edits the
prompts without re-running it -- replacing the prior "both should stay
in sync" manual discipline that had already drifted.

Runtime MCP `instructions` is byte-identical to pre-refactor and locked
by three new tests:

  - server_instructions_byte_lock (length + boundaries + interior
    markers + no triple newlines)
  - edit_mode_extra_byte_lock (same shape; preserves the leading "\n\n"
    that format!("{S}{E}") relies on)
  - instructions_compose_with_single_blank_line_between_sections (pins
    the exact concat boundary between the two sections)

Reconciles the pre-existing SERVER_INSTRUCTIONS <-> AGENTS.md drift by
canonicalising on SERVER_INSTRUCTIONS. Supersedes #85 (closed: wrong
fork target) and re-shapes slice 1 of #112 against the current v1 tool
surface so it stands alone on origin/main.

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>

* feat(grok): tilth_grok — one-call structural understanding (#120)

Collapses the search → expand → search-callers → read-context dance into one
call. Returns everything structural about a symbol: definition, doc, body,
signature, callees (internal + external), callers, siblings, and tests.

  - src/search/grok.rs — target resolution (symbol name or path:line),
    bundle assembly with seeded caps (5 callees / 5 callers / 8 siblings /
    8 tests / 60 body lines by default; widen via full)
  - src/lib.rs run_grok + src/main.rs CLI subcommand
  - src/mcp/tools/grok.rs + src/mcp/tools/definitions.rs — MCP tool surface
  - prompts/mcp-base.md + AGENTS.md — instructions describing tilth_grok
    between tilth_deps and tilth_diff
  - benchmark/tasks/grok_tasks.py — 3 grok-shaped benchmark variants
  - byte-lock test updated 2952 → 3466 with a contains() assertion so
    accidental deletion of the grok description fails the test loudly

Shares the underlying primitives (find_callers_batch, extract_callee_names,
get_outline_entries) with tilth_deps but answers a symbol-level question
where deps answers a file-level one — different granularity, different
intent. See PR description for benchmark rationale.

Reviewed by Opus 4.7.

* fix: begin post-merge compatibility fixes

Agent-Logs-Url: https://github.com/paulnsorensen/tilth/sessions/1eeb7e02-1a23-4cbd-914d-5c4b03e3c93d

Co-authored-by: paulnsorensen <429793+paulnsorensen@users.noreply.github.com>

* fix: align conflicted files with upstream main

Agent-Logs-Url: https://github.com/paulnsorensen/tilth/sessions/1eeb7e02-1a23-4cbd-914d-5c4b03e3c93d

Co-authored-by: paulnsorensen <429793+paulnsorensen@users.noreply.github.com>

* fix(prompts): satisfy MD031/MD032 on stranded prompts/tools docs

The new .markdownlint.json adopted from upstream uses numeric-rule keys
and no longer disables MD031 (blanks-around-fences) or MD032
(blanks-around-lists), so the fork's pre-merge prompts/tools/*.md docs
now fail the markdown CI gate. Add the missing blank lines around the
fenced blocks and list start; AGENTS.md regen remains idempotent.

These docs describe a tool surface (tilth_list, tilth_write-hash) that
no longer exists after the upstream merge — flagged as a follow-up
cleanup in .cheese/age/pr-35.md, out of scope for this sync.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Paul Sorensen <paulnsorensen@gmail.com>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
Co-authored-by: jensenojs <jensenojs@qq.com>
Co-authored-by: jahala <jahala@gmail.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: quanhavn <anhquanhh4@gmail.com>
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: paulnsorensen <429793+paulnsorensen@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant