Skip to content

fix: v1.6.1 I/O safety, lifecycle recovery and extraction recall - #279

Merged
sunerpy merged 19 commits into
mainfrom
sync/v1.6.1-alignment-3
Oct 1, 2026
Merged

sunerpy merged 19 commits into
mainfrom
sync/v1.6.1-alignment-3

Conversation

@sunerpy

@sunerpy sunerpy commented Oct 1, 2026

Copy link
Copy Markdown
Owner

Summary

Fifth PR of the v1.6.1 alignment, following #278: the I/O-safety, lifecycle-recovery and remaining extraction-recall rows. Dispositions and landing state are tracked in docs/upstream-sync/V1_6_1_AUDIT_2026-09-30.md; tracked parity in UPSTREAM.md stays v1.6.0 until the release gate.

Reading files — extraction version 15 → 16

  • Bounded source reads (#1910): index, sync, migration, the MCP freshness probe and resolution share one reader that stats first and never holds more than the size limit plus one byte. A file over indexing.max_file_size is recorded by a size stamp instead of a content hash, so a same-size rewrite is not a change and crossing the limit is. An MPEG transport stream named .ts is video: not indexed, not counted, and a tracked file that becomes one leaves the index.
  • A Rust-only abort fixed on the way: every pass decoded files strictly, so one Latin-1 or binary file with a source extension failed the whole init/index and left the index unreadable. Bytes that are not UTF-8 now decode with U+FFFD, as upstream's toString('utf8') does.
  • Resolver reads (#1553): resolution and the framework pass read only regular files within the project's limit, with the rejection cached; a FIFO is never opened. The resolver hands the configured limit to every context it builds, so index and sync read the same files.
  • Existence probes (#2072 perf): each file_exists answer is memoized for the pass in both contexts.

Scope

  • JVM packages named build (#1642): src/<sourceSet>/{java,kotlin,scala}/**/build stays indexed and watched; module output and the other defaults inside it stay pruned.
  • The root .gitignore now follows git's rules in scan and watch. The scan matched its lines literally (anchored rules, globs and ** never matched; dir rules stopped at the root) while the watcher matched every dir rule at any depth. One shared matcher on the ignore crate replaces both, so Android's /build and Spring Initializr's build/ + !**/src/main/**/build/ keep a JVM build package, a bare build/ hides it as in git, and the watcher handles exactly what the scan indexes. Behavior change: projects whose root .gitignore uses globs, anchored paths or nested-matching directory rules now index fewer files — the ones git ignores.

Extraction recall

  • MyBatis / iBatis (#1222, plus the rest of #1182): close tags with whitespace no longer swallow the next statement; commented-out statements and includes are ignored (CDATA stays data); attributes may use either quote style; a namespace-less <sqlMap> qualifies statements by their Map.statement ids; iBatis adds <statement>/<procedure>; databaseId joins the signature; a vendor-split pair on one line keeps both nodes through the #1349 allocator. KEEP-RUST: a self-closing <sqlMap resource="…"/> is never a statement-map root (upstream would read the config file's tail as one).
  • Liquid (#1906): tags inside {% liquid %} blocks are extracted; {% comment %}/{% raw %} regions are skipped in both spellings.
  • Explore dynamic boundaries (#1967): import(\./x/${y}`)andimport('./x/' + y)` are runtime imports.

Lifecycle

  • Auto-sync recovery (#1959, with #876 never ported before): lock contention past the budget (a long foreground index) no longer stops the watcher for good. It pauses as RECOVERING, keeps collecting changes and retries a full reconcile every 30 s until one commits. MCP answers carry a RECOVERING or DISABLED banner, status reports Auto-sync:, and while auto-sync is not healthy search/callers/callees/impact name any drifted file instead of answering from the frozen graph.
  • Failed syncs (#1964): a failed sync keeps its batch and any owed full reconcile and retries with backoff. An index-lease timeout now counts as contention rather than a failure (a Rust-only defect recorded in the audit).
  • Sync report (#1360): a sync that heals an interrupted index prints Resolved N pending references (M unresolved).
  • MCP root discovery (#1895): a codegraph.db without a published namespace no longer captures the upward walk; the CLI already used the same state authority.
  • WSL (#995): on /mnt/<drive> with CODEGRAPH_DIR unset, a fresh index goes in .codegraph-wsl/ (an existing .codegraph/codegraph.db is kept); a SQLite I/O error on a shared default index explains the fix.
  • Windows (#2053, #1773): both daemon.pid publishes rename a complete temp record, re-check ownership, and retry sharing violations for up to ~1.6 s; the socket rewrite no longer truncates the lock in place.
  • Installer: install.sh under Git Bash/MSYS2/Cygwin points at the PowerShell installer (#1294); detect() no longer leaves a .backup beside a config it cannot parse (#1870).
  • Prompt hook (#1654): como/wie alone are no longer structural keywords.

Golden impact

All 19 re-indexable corpora regenerated with this build are byte-identical. MCP structural fixtures are unchanged.

Verification

  • Full workspace test suite: 3,824 passed, 0 failed; clippy -D warnings, cargo fmt --check, scripts/guardrail.sh, docs-check and oxfmt --check clean; the pre-push gate re-runs them on the pushed head.
  • Every new test was run against the previous sources first and fails there (negative controls excepted): the .gitignore rule and parity cases, resolution_file_reads, source_reads, mybatis_mappers, liquid_blocks, sync_recovery_report, the MCP root, auto-sync and stamp-freshness cases, the WSL default, the daemon lock replace, the watcher retry and recovery cases, the prompt-hook and dynamic-import cases, and the Windows-shell installer harness.
  • One existing assertion was changed deliberately: multi_segment_dir_rule_matches_on_boundaries expected .gitignore a/b/ to prune x/a/b; an inner / anchors a rule in git, in upstream and in the old scan, so it now expects x/a/b to be watched. MCP root-discovery test fixtures now publish their namespace state, which is what makes a directory an index.

BEGIN_COMMIT_OVERRIDE
fix(scan): keep JVM packages named build under source roots
fix(scan): read the root .gitignore with git's rules in scan and watch
fix(resolve): read no file extraction would skip
fix(extract): bound every source read and skip MPEG-TS clips named .ts
fix(extract): read MyBatis and iBatis mappers as upstream does
fix(extract): read Liquid tags inside {% liquid %} blocks
fix(sync): report the pending references a healing sync resolved
fix(mcp): a database file without a published index is not a root
fix(wsl): keep WSL off Windows' index on /mnt drives and explain the I/O error
fix(daemon): retry a Windows sharing violation when replacing daemon.pid
fix(install): point Windows shell users at the PowerShell installer
fix(installer): keep detection read-only for configs it cannot parse
fix(watch): keep a failed sync's batch and owed full reconcile
fix(watch): recover auto-sync after lock contention and report its health
fix(prompt-hook): require corroboration for everyday como and wie
fix(explore): flag dynamic imports built from a template or concatenation
perf(resolve): answer each file-existence probe once per pass
END_COMMIT_OVERRIDE

🤖 Generated with Claude Code

CodeGraph Test added 18 commits October 1, 2026 09:11
The default build prune hid Java, Kotlin and Scala packages named build
(src/<sourceSet>/{java,kotlin,scala}/**/build). Exempt exactly those
directories in the scan and the watcher's structural tier; module build
output and the other defaults inside them stay pruned (upstream #1642).
The scan matched root .gitignore lines literally: anchored rules,
globs and ** never matched, and dir rules stopped at the root. The
watcher read the same file differently, matching every dir rule at any
depth. Both now share one matcher built on the ignore crate: a leading
or inner / anchors a rule, a slash-less rule applies at any depth, *
and ** glob, and nothing is re-included below an ignored directory.

A JVM package named build therefore stays indexed and watched under
Android's /build and Spring Initializr's build/ plus
!**/src/main/**/build/, while a bare build/ hides it (upstream #1642).
Resolution and the framework pass read files by path with no guard, so
an import that followed package metadata into a dependency archive
decoded it whole, and a FIFO blocked the pass. Stat first: only a
regular file within the project's indexing.max_file_size is read, the
read itself is bounded, and the rejection is cached like any read. The
resolver hands the configured limit to every context it builds, so
index and sync read the same files (upstream #1553).
Every indexing pass read a file in full before checking its size and
decoded it strictly, so one oversize blob was read whole and one byte
that is not UTF-8 failed the entire index, leaving it unreadable.

One shared reader now serves index, sync, migration, the MCP freshness
probe and resolution: it stats before reading and never holds more
than the limit plus one byte; a file over the limit is recorded by a
size stamp in place of its content hash, so a same-size rewrite is no
change and crossing the limit is; an MPEG transport stream named .ts
is video, neither indexed nor counted, and a tracked file that becomes
one leaves the index; other bytes decode with U+FFFD as upstream does.
Extraction version 15 -> 16 (upstream #1910).
A close tag with whitespace before its bracket (</select >) let a
statement run on and swallow the next one (upstream #1222). The rest of
upstream's #1182 forms were also missing: commented-out statements and
includes are now ignored (CDATA stays data), attribute values may use
either quote style, a namespace-less <sqlMap> qualifies statements by
their Map.statement ids, iBatis maps add <statement> and <procedure>,
and databaseId joins the signature. A vendor-split pair written on one
line keeps both nodes through the #1349 id allocator. KEEP-RUST: a
self-closing <sqlMap .../> reference is never a statement-map root.
A {% liquid %} body holds tags without braces of their own, so render,
include, section and assign written there were never extracted. Scan
whole braced tags one at a time and the bare lines of each liquid body,
anchored at the line start, so tag-like strings and inline comments are
not read twice; {% comment %} and {% raw %} regions are skipped in both
spellings (upstream #1906).
The orphan sweep's resolver statistics were dropped, so a sync that
healed an interrupted index on unchanged files read as a no-op. Carry
the attempted, resolved and unresolved counts in SyncOutcome, print
them after the sync line, and add them to the diagnostics payload
(upstream #1360).
MCP root discovery counted any .codegraph/codegraph.db as an index, so a
stray, empty or schema-less file in an ancestor captured the upward walk
for every project below it and the sub-project scan never ran. Require
the published namespace state as well, the authority the CLI's ancestor
walk already uses; it reads only the state slots and never opens
SQLite (upstream #1895). Test fixtures now publish their namespace.
…I/O error

When Windows-native and WSL CodeGraph open the same .codegraph index
under /mnt/<drive>/, SQLite's locking and -shm memory do not hold across
the 9p/DrvFs bridge and WSL fails with a bare disk I/O error. With
CODEGRAPH_DIR unset on a WSL Windows drive, the index root is now chosen
per project: an existing .codegraph-wsl/ wins, an existing
.codegraph/codegraph.db is kept, and a fresh index goes in
.codegraph-wsl/, which init reports. A SQLite I/O error on a shared
default index carries the CODEGRAPH_DIR instruction in the CLI and in
MCP open failures. Every other host is unchanged (upstream #995).
A transient Windows sharing violation (an antivirus scan or indexer
holding daemon.pid open) aborted the lock publish, and the post-bind
socket rewrite truncated the lock in place, so a reader could see a
partial record. Both now rename a complete temp record over the lock,
re-check that the lock still holds this daemon's record before every
attempt, and retry only Windows sharing errors with a 25 ms to 400 ms
backoff over about 1.6 s (upstream #2053, #1773).
Under Git Bash, MSYS2 or Cygwin, install.sh stopped at an unsupported
OS. Recognise those shells, print the exact PowerShell command and exit
non-zero before downloading anything; Linux and Darwin are unchanged
(upstream #1294).
Every target's detect() reads its agent's config, and install --refresh
detects every target to skip the ones codegraph was never installed
into, so an unparseable config of an unmanaged agent got a .backup from
the read alone. Reading is now pure; the backup is made where a config
is replaced (write_json_file) and before a write path skips one it
cannot parse (upstream #1870).
The event loop took the pending paths and the owed full sync before an
attempt and dropped them when it failed, so a directory removal whose
full sync failed never reconciled and its files kept their nodes. Keep
both and retry after the debounce doubled per consecutive failure,
capped at 30 s (upstream #1964). An index-lease timeout behind a long
foreground index is now lock contention, backed off in place, instead
of a sync failure counted toward disabling auto-sync.
…alth

Lock contention past the budget stopped the watcher for good, so after
one long foreground index the daemon answered from a frozen index with
no notice. The watcher now pauses as RECOVERING instead: it keeps
collecting changes and retries a full reconcile every 30 s until one
commits. Persistent sync failures and exhausted watch resources still
stop it as DISABLED.

MCP answers carry a RECOVERING or DISABLED whole-index banner, status
reports the state, and while auto-sync is not healthy search, callers,
callees and impact name any indexed file that changed since its last
sync instead of answering from the frozen graph (upstream #876, #1959).
Portuguese/Spanish como and German wie are everyday words, so treating
them as structural keywords fired the HIGH tier on ordinary prompts.
Drop them from the unconditional list; another structural keyword, a
verified code token or indexed prose segments still decide the tier
(upstream #1654).
…tion

The JS dynamic-import form skipped any argument starting with a quote
or backtick, so import(`./locales/${lang}.js`) and import('./x/' + y)
read as literal imports and explore never noted the runtime boundary.
A call is now dropped only when its argument is exactly one complete
string literal, with escape-aware template scanning (upstream #1967).
Import resolution probes every extension of a specifier from every file
that imports it, and each probe queried the store and stat'd the disk.
Memoize the answer for the pass in both contexts, bounded at 200,000
entries as upstream's fileExistsMemo is; neither the borrowed store nor
the tree changes within a pass.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1fe6c5cd94

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +89 to +90
/// Every occurrence of `tag`, in both spellings Liquid allows (upstream
/// #1906). Inside a `{% liquid %}` tag each body line is a tag without

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Update the canonical extraction reference

Document these shipped extraction changes in docs/embedded-extraction.md: it still says Liquid only recognizes braced regex forms and MyBatis only recognizes namespaced <mapper> roots with five statement types, omitting {% liquid %} blocks, suppressed raw/comment regions, namespace-less iBatis maps, and statement/procedure. Users consulting the canonical reference therefore get behavior that directly contradicts this implementation, and the repository requires extraction-rule changes to update their canonical documentation alongside the code.

AGENTS.md reference: AGENTS.md:L96-L99

Useful? React with 👍 / 👎.

@codecov

codecov Bot commented Oct 1, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 96.19966% with 67 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
crates/codegraph-core/src/wsl.rs 57.57% 14 Missing ⚠️
crates/codegraph-cli/src/main.rs 77.77% 12 Missing ⚠️
crates/codegraph-mcp/src/engine.rs 94.51% 9 Missing ⚠️
crates/codegraph-extract/src/engine.rs 95.00% 8 Missing ⚠️
crates/codegraph-watch/src/watcher.rs 97.56% 7 Missing ⚠️
crates/codegraph-core/src/source_file.rs 97.71% 4 Missing ⚠️
crates/codegraph-store/src/wsl_shared_index.rs 95.52% 3 Missing ⚠️
crates/codegraph-extract/src/embedded/mybatis.rs 98.58% 2 Missing ⚠️
crates/codegraph-watch/src/migrate.rs 84.61% 2 Missing ⚠️
crates/codegraph-watch/src/sync.rs 97.84% 2 Missing ⚠️
... and 4 more

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #279      +/-   ##
==========================================
+ Coverage   95.17%   95.20%   +0.02%     
==========================================
  Files         154      157       +3     
  Lines       93662    95033    +1371     
==========================================
+ Hits        89145    90477    +1332     
- Misses       4517     4556      +39     
Files with missing lines Coverage Δ
crates/codegraph-cli/src/installer/shared.rs 95.00% <100.00%> (+0.14%) ⬆️
crates/codegraph-cli/src/structural_gate.rs 99.32% <ø> (ø)
crates/codegraph-core/src/index_paths.rs 89.77% <100.00%> (+0.51%) ⬆️
crates/codegraph-extract/src/embedded/liquid.rs 99.31% <100.00%> (+0.21%) ⬆️
crates/codegraph-mcp/src/rmcp_handler.rs 89.87% <100.00%> (-0.04%) ⬇️
crates/codegraph-mcp/src/roots.rs 98.77% <100.00%> (+0.06%) ⬆️
crates/codegraph-mcp/src/server.rs 95.12% <100.00%> (-0.05%) ⬇️
crates/codegraph-resolve/src/resolver.rs 96.59% <100.00%> (+0.06%) ⬆️
crates/codegraph-resolve/src/snapshot_context.rs 96.94% <100.00%> (+0.04%) ⬆️
crates/codegraph-store/src/index_state.rs 83.33% <ø> (ø)
... and 16 more

... and 3 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@sunerpy
sunerpy merged commit bc301b9 into main Oct 1, 2026
9 checks passed
@sunerpy
sunerpy deleted the sync/v1.6.1-alignment-3 branch October 1, 2026 07:38
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